Documentation

Quayside documentation.

Everything a buyer needs to install, deploy, rebrand, edit content, wire forms, audit accessibility, optimize performance, upgrade, and ship to a marketplace. Edit the matching Markdown files in docs/ if you want to fork the docs.

Quick start

This template is a static Astro 5 site. Clone it, install dependencies, and run the dev server. The default brand is Quayline; switch brand, theme or home variant from the floating demo pill in the bottom-right corner.

git clone  quayside
cd quayside
npm install
npm run dev

Open http://localhost:4321/. Edit data in src/data/*.json or components in src/components/*.astro; the page hot-reloads.

Install & build

Requires Node 20+ and npm 10+. npm ci installs pinned dependencies from the lockfile; npm run build generates the static site into dist/. npm run preview serves the built site locally on port 4321.

npm ci
npm run build
npm run preview

The build also regenerates the 14-day vessel schedule in src/data/vessel-schedule.json from scripts/gen-schedule.mjs. To pin the schedule to a different anchor, edit the seed at the top of that script.

Deploy

The output is fully static — deploy dist/ to any static host.

Cloudflare Pages

Build command npm run build, output directory dist. No environment variables required.

Netlify

Build command npm run build, publish directory dist. For Netlify Forms, set forms.adapter = 'netlify' and forms.netlifyFormName in src/data/site.config.ts; the Form component automatically emits the hidden form-name input that Netlify expects.

Vercel

Framework preset: Astro. Output directory dist. No environment variables required.

Brand & tokens

Three presets ship: Quayline, Tidewatch, Vesselmark. To change the default brand for a clean build, edit one line in src/data/brand.config.ts:

export const activeBrand: BrandId = 'tidewatch';

Each preset lives in src/brands/<name>.ts with: id, name, legal name, tagline, hero headline and body, fonts (display/body/mono), radius scale, illustration style, and the full light + dark palette (canvas, surface, surface-2, ink, muted, accent, accent-weak, border, success, warning, danger).

CSS tokens are emitted from src/styles/tokens.css using [data-brand='...'] attribute selectors. The floating demo pill (or your own runtime code) sets document.documentElement.dataset.brand to switch palette without a rebuild.

Make a fourth brand

  1. Create src/brands/fourth.ts by copying quayline.ts and changing every field.
  2. Add fourth to BrandId in src/data/brand.config.ts and to the brandIds array.
  3. Import it in src/brands/index.ts and add it to brandList.
  4. Add a button to the demo pill in src/components/DemoPill.astro: <button role="radio" aria-checked="false" data-brand="fourth">Fourth</button>.
  5. If you used a new font family, install its @fontsource/<name> package and import the weights you need at the top of src/layouts/Base.astro.

Content & data

All editable content is in src/data/*.json:

  • site.config.ts — site name, legal name, URL, contact, social, forms adapter, demo pill on/off.
  • terminals.json — 5 terminals with berths, capacity, coordinates.
  • vessel-schedule.json — generated; do not edit by hand.
  • services.json — services with body, capabilities, related terminals.
  • tariffs.json — per-TEU charges, surcharges, terminal adjustments.
  • sustainability.json — 5-year CO₂/TEU, shore-power share, electrification.
  • throughput.json — monthly TEU per terminal (12 months).
  • projects.json — expansion projects with milestones.
  • customers.json — partner list (replace placeholders before launch).
  • insights.json — articles.
  • investors.json — bond listings, reports, KPIs, governance.
  • team.json — leadership.
  • roles.json — open job roles.
  • forms.json — form variant definitions.

Aggregates (group totals, KPIs, status counts) are computed in src/lib/metrics.ts so the same number is consistent everywhere. To add a new aggregate, export it from metrics.ts and use it in any component.

Data viz, hero & charts

The living hero on the home page is a hand-built SVG animation in src/components/LivingHarbor.astro. It pauses via IntersectionObserver when scrolled off-screen, and there is a Pause button with aria-pressed. prefers-reduced-motion and prefers-reduced-data swap to a static SVG composition.

Charts are SVG built from the same JSON data, with HTML overlay labels (CSS px, do not scale with viewBox). Each chart has an adjacent <details> with a table of the same numbers — the table is the canonical data source for screen readers.

  • BerthGantt.astro — 14-day schedule, per berth per hour, "now" line, mobile list fallback.
  • PortMap.astro — interactive SVG map with cargo filter and keyboard navigation.
  • ThroughputChart.astro — monthly TEU bars with hover/focus tooltips.
  • SustainabilityDashboard.astro — three charts (line, donut, stacked bar).
  • Estimator.astro — interactive cost estimator from the tariff table.

Forms

Forms are demo by default. To wire a form to a real backend, set the adapter in src/data/site.config.ts:

forms: {
  adapter: 'formspree',
  formspreeEndpoint: 'https://formspree.io/f/xxxxxxxx',
}

Supported adapters: formspree, netlify, custom. When the adapter is configured correctly, the Form component lets the native submit go through to the configured action; the URL does not change. When no adapter is configured, the form shows an honest post-submit notice that nothing was sent — pre-submit, the page contains no demo/buyer notes. See tariff formula for the cost-estimator math.

Remove the demo pill & home variants

To remove the floating demo pill before going live, set demoPill: false in src/data/site.config.ts:

export const site = {
  // ...
  demoPill: false,
};

The DemoPill component checks this flag at build time and renders nothing when it is false. The CSS, JS, and the floating element are all omitted from the build. See Remove the demo pill for the full guide.

If you want to ship only one home variant, edit src/pages/index.astro and remove the <article data-home-variant="b"> and <article data-home-variant="c"> blocks. Then remove the variant switcher from DemoPill.astro (the data-variant buttons block).

Accessibility

  • lang="en" on <html>, one h1 per page, ordered headings.
  • Skip link to #main in every layout.
  • Focus-visible rings on every interactive element (3 px accent outline with 2 px offset).
  • Keyboard: full nav, drawer focus trap, Gantt bars are buttons, port map terminals are buttons.
  • AA contrast in light and dark for all three brands (verified by the contrast checker shipped with the design repo — 47 checks pass).
  • Charts have an adjacent table as the canonical data source for screen readers.
  • Forms: labelled inputs, aria-invalid on errors, role="alert" on error messages, aria-busy on submit.
  • Reduced motion: every animation flattens to opacity-only or instant via prefers-reduced-motion: reduce.

Performance

  • Static output, no runtime CDN, fonts self-hosted via Fontsource (latin subset only).
  • JS is shipped only to pages that need it; see docs/performance.md for the per-page JS budget.
  • Animations are transform/opacity only; durations 100–300 ms for UI, max 500 ms for page transitions.
  • Lighthouse targets: a11y 100, best practices 100, SEO 100, performance ≥ 95, LCP ≤ 2.5 s, CLS 0.
  • The living hero animation pauses when off-screen via IntersectionObserver.

Upgrade between versions

This template follows semantic versioning. Patch releases (1.0.x) are bug fixes; minor releases (1.x) add backward-compatible features; major releases (x.0.0) may break APIs.

To upgrade, pull the new version into a fresh branch and re-apply your changes to src/data/*.json and src/brands/<your-brand>.ts. The CHANGELOG.md in the repo root lists every change between versions.

Troubleshooting

Build fails with "Cannot find package 'lightningcss'"

Run npm install lightningcss. It is required by Vite's CSS minifier.

The schedule looks empty

The vessel schedule is generated at build time around the current date. Run npm run gen:schedule to regenerate it. The schedule covers 7 days before and 7 days after the build anchor, so the "now" line is always visible.

The brand switch does not persist

The demo pill writes the brand to localStorage['quayside'].brand and to ?brand=. If your browser blocks third-party storage or you are in a private window, the persistence may fail — the URL parameter still works.

The hero is not animating

Check prefers-reduced-motion and prefers-reduced-data in your OS settings. Both flatten the hero to a static SVG. Also verify the IntersectionObserver is firing — the hero pauses when off-screen.

Credits & licenses

Quayside is released under the MIT License. The full text is in LICENSE. A full credits and license inventory lives in docs/credits.md and at the credits section above.

Fonts (all OFL 1.1)

  • Fraunces — by Phaedra Charles & Flavia Zimbardi, distributed via Fontsource.
  • IBM Plex Sans, IBM Plex Mono — by Mike Abbink & Bold Monday, distributed via Fontsource.
  • Space Grotesk — by Florian Karsten, distributed via Fontsource.
  • Public Sans — by the United States Web Design System, distributed via Fontsource.
  • Bricolage Grotesque — by Mathieu Triay, distributed via Fontsource.
  • Atkinson Hyperlegible — by Braille Institute, distributed via Fontsource.

The demo ships with SVG illustrations rather than photography, so there are no third-party photo credits. To use real photography, drop WebP/AVIF files ≤ 1600 px into src/assets/photos/ and import them in components; record credits in docs/credits.md.

Icons

The icon set is hand-drawn for this template (24×24, currentColor, stroke 1.75 px). The full set lives in src/components/Icon.astro. No third-party icon library is used.

Libraries

  • Astro 5 — MIT.
  • @astrojs/sitemap — MIT.
  • @fontsource/* — MIT (the package wrappers; the fonts themselves are OFL).
  • lightningcss — MPL-2.0.