Skip to main content

Unified Docusaurus Website (Landing + Docs) — Design

Date: 2026-10-07 Status: Draft, awaiting review Supersedes: the hosting, framework and deployment parts of 2026-10-06-homepage-design.md (Astro in landing/, Netlify Git builds). Everything else in that spec (audience, page structure, brand rules, copy rules, the landing-only wording rules) still applies and carries over.

Goal​

One website for ZIO Blocks, built as a single Docusaurus app in website/: the landing page at / and the reference docs under /docs/, sharing one navbar, footer and brand look. The Astro site in landing/ is removed.

Success criteria:

  • / is the landing page described in the homepage spec; /docs/... is the current docs, themed with the brand.
  • A reader moving between the landing page and the docs sees one site: same navbar, footer, fonts, colours, light and dark mode.
  • The landing content stays generated from docs/index.md; a docs change that breaks the contract fails the build naming the row.
  • docs/ content, docs/sidebars.js, the README generation and the npm package for zio.dev are unchanged.
  • The site is built and deployed by GitHub Actions; PR previews show landing and docs together.
  • Lighthouse 95+ on / (see Risks), no copy buttons on the landing page, all brand rules still enforced.

Context and constraints​

  • website/ is already a Docusaurus 3.10 app: a stub today (default theme, url: localhost), built by the buildDocs CI job and published as a Netlify preview by deploy-preview.yml. Its docs are the mdoc output of docs/ (generated into website/docs by sbt docs/mdoc, not checked in), with the sidebar from docs/sidebars.js. Docs are currently served at / (routeBasePath: '/').
  • zio.dev renders the docs from the npm package @zio.dev/zio-blocks (sbt docs/publishToNpm). That is untouched, so docs stay available there as well. This site is a second home for them, now with the landing page.
  • docs/index.md is also the README source (PR #1717) and the zio.dev docs home, so its content and slug must not change. The move to /docs/ is configuration in website/ only.
  • Building the docs needs a JVM and sbt (mdoc), so Netlify cannot build this site from the repo; GitHub Actions builds it and deploys the artifact.
  • Existing CI: ci.yml is generated by zio-sbt-ci from project/CiWorkflow.scala and runs on push to main, on release, and on pull requests. deploy-preview.yml is generated by the plugin (ciEnableDeployPreview) and uses the secrets NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID.
  • Brand (assets/logo/README.md): flat and square, no gradient/shadow/stroke/rotation/radius, fixed palette (Ultramarine #2D3F8F, Ink #141A2E, Lifted blue #4B5FC4, Paper ink #F3F5FC, white, hairline #E4E7F0, muted #7C859E), Manrope, wordmark only as the logo image.

Decisions​

DecisionChoice
FrameworkDocusaurus 3.10 (the existing website/ app); Astro and landing/ removed
Brand reachWhole site: navbar, footer, docs pages and landing page (option A)
URLs/ landing page; docs under /docs/ via routeBasePath: '/docs'; no edits under docs/
Data pipelineGenerate website/src/data/site.json before each build (yarn build, yarn start) with the existing scripts, moved to website/scripts/
Build and deployGitHub Actions builds (mdoc, generator, yarn build) and deploys to Netlify; Netlify is only a host
Landing links"Learn More" and other docs links point to internal /docs/... routes, verified by Docusaurus

Defaults chosen by the author where the maintainer did not answer; each is a one-line change if wrong:

  • Admonitions (note, tip, warning, danger) keep their standard semantic colours, flat and square. They are status colours, not brand marks.
  • Production deploys to a separate Netlify site with its own secret NETLIFY_PRODUCTION_SITE_ID (reusing NETLIFY_AUTH_TOKEN), so previews and production cannot interfere.
  • Docs code blocks keep Docusaurus's copy button. Only the landing page has no copy buttons.

Architecture​

website/
docusaurus.config.js brand navbar and footer, colour-mode toggle, docs at /docs, Scala for Prism, env switches
src/pages/index.js the landing page
src/components/landing/ Hero, ModuleField, Principles, DeepDives, Catalog (+ one global landing.css)
src/lib/ pure helpers: titleCase, inlineMarkdown (renderInline), rows packing
src/css/custom.css brand tokens mapped onto Infima; Manrope @font-face
src/data/site.json GENERATED, gitignored
scripts/ parse-index, markdown, collapse, split, rename, override, version, build-catalog, check-links
test/unit/*.test.mjs unit tests (moved from landing/)
test/dist.test.mjs static-output tests against build/index.html
static/fonts/ static/brand/ Manrope (+OFL), brand SVGs, social card, favicon (brand/ is copied by build-catalog); static/_headers (cache headers)
lighthouserc.json
docs/ unchanged
landing/ deleted
URLContent
/landing page
/docs/ and belowcurrent docs; docs/index.md is the docs home

Environment switches in docusaurus.config.js:

  • URL / SITE_URL: the site's canonical origin (Netlify domain, chosen by the maintainer).
  • LANDING_ONLY=1: turns the docs plugin off and downgrades broken-link failures to warnings, so the landing page can be built and tested with Node only (no sbt, no website/docs). The full build, which CI deploys, never sets it.

Data flow​

  1. website/scripts/build-catalog.mjs reads docs/index.md, parses it (as today), applies the landing-only rules (one tile per artifact, the Codecs split, category renames, principle text overrides), resolves the release version (GitHub API with the pinned fallback, token from GITHUB_TOKEN in CI), and writes src/data/site.json.
  2. Docs links in the data become site-relative: ./reference/schema/index.md -> /docs/reference/schema/. The offline "docs file exists" check stays, so a typo fails the build early.
  3. React components import site.json; they hold no content of their own.
  4. The brand SVGs are copied from assets/logo/ into static/brand/ at build time (not duplicated in git).

All parser failures keep naming the row, as in the Astro version.

Components and theming​

Landing page (React function components, one global stylesheet src/components/landing/landing.css for layout; the catalog tiles are rendered inside Catalog.js, there is no separate BlockTile component):

  • Hero: ink band under the navbar; headline titleCase(tagline up to the dash) with "Building Blocks" joined by a non-breaking space; lead; the install line and one-line JSON result highlighted with prism-react-renderer directly (no copy button); Get started / GitHub; stack strip. ModuleField keeps the CSS-only animation and the module-field pattern.
  • Principles, DeepDives (Schema, Scope, Async, SQL tabs), Catalog (category headings, tiles, Learn More buttons, three-column row packing, equal heights in shared rows): same content, order and rules as the Astro site.
  • Tabs are React state. All panels are in the server-rendered HTML, and non-selected panels are hidden only after hydration, so the content stays readable with JavaScript off.
  • Semantic structure: one h1, ARIA tabs, named landmarks, ids and data-* hooks that tests use instead of hashed class names.

Chrome and docs (configuration, no swizzling):

  • Navbar: ink in both colour modes, on-dark badge lockup, Docs, Blocks (/#catalog), GitHub, light/dark toggle that follows the system preference. Footer: ink with the mono-white lockup and Docs, Reference, GitHub links.
  • custom.css maps the palette onto Infima: Manrope, ultramarine primary in light mode and lifted blue in dark mode, ink dark background and #1A2140 surfaces, --ifm-global-radius: 0, all shadow variables none, hairline borders, flat Prism theme for code blocks with Scala added through additionalLanguages.
  • Colour rules from the earlier spec still hold: #7C859E only as a rule colour or on ink surfaces; text on light surfaces #5B6580 or darker.

Quality gates​

  • Unit tests (node:test): parser, collapse, split, rename, override, version, title case, inline markdown, row packing, link checker, brand-rule guard (scans src/**, including custom.css and landing.css).
  • Static-output tests (about 13, ported): head and meta, hero headline, install snippet, principles, deep-dive panels visible without JavaScript, catalog tiles and Learn More buttons, row grouping, title case everywhere, page order, anchors, images with alt text, no copy buttons or filter controls. They read website/build/index.html from a LANDING_ONLY=1 build and select by id, role and data-*.
  • Internal links are verified by Docusaurus (onBrokenLinks: 'throw' in full builds). The external link checker stays for the few external links.
  • Lighthouse 95+ in all four categories on /, via the pinned @lhci/cli@0.15.1 run with npx.
  • Keyboard and screen-reader basics: Docusaurus skip link, visible focus, tab keyboard handling, reduced motion honoured, contrast AA in both modes.

CI and deploy​

  • project/CiWorkflow.scala is the only place CI changes; ci.yml is regenerated with sbt ciGenerateGithubWorkflow and ciCheckGithubWorkflow must pass.
  • buildDocs keeps its steps (yarn install, sbt docs/mdoc, yarn build, checkDocsOnFreshWebsite, preview artifact). yarn build now also runs the generator. PR previews therefore show landing and docs.
  • New step in buildDocs: deploy website/build to the production Netlify site with nwtgck/actions-netlify and production-deploy: true, only on push to main and on release events, using NETLIFY_AUTH_TOKEN and NETLIFY_PRODUCTION_SITE_ID. A release rebuilds and redeploys the site, so the install line shows the new version right away (this removes the stale-version problem of the Astro setup).
  • landing job keeps its id and runs on Node only: yarn install, unit tests, LANDING_ONLY=1 yarn build, static-output tests, link check, Lighthouse. It stays outside the pull-request approval gate unless the maintainer decides otherwise (unchanged open decision from PR #1719).
  • Removed: the landing/ directory, its Netlify config and Git-integration instructions, the Astro dependency. website/static/_headers carries the cache headers.

Migration order​

  1. Theme, navbar, footer, colour mode, docs at /docs, Manrope and brand assets.
  2. Move the scripts and unit tests; switch docs links to internal routes; add the LANDING_ONLY build and the generator hook in yarn build / yarn start.
  3. Port the landing components one at a time, each with its static-output test.
  4. Lighthouse, accessibility, and a visual check of several docs pages (restyling risk).
  5. CI and deploy changes; regenerate ci.yml.
  6. Delete landing/; update AGENTS.md, the READMEs and the earlier spec/plan notes; update draft PR #1719 on the same branch.

Risks​

  • Lighthouse performance. A React/Docusaurus page may score about 90 on mobile performance. The plan measures first; if it is below 95 the options are optimization (lazy hydration, trimming the bundle) or an explicitly agreed lower performance threshold. It is never lowered silently.
  • Docs restyling. Infima variables restyle every docs page; the plan includes a visual pass over representative pages (long reference page, tables, code, admonitions, tabs, the home page) in both modes.
  • No-JS regression. Docusaurus needs JavaScript for navigation and the colour toggle; only the landing content is guaranteed readable without it.
  • Build duplication of docs. Docs now live on zio.dev and on this site; canonical URLs and search-engine duplicates are the maintainer's call (possible later: noindex or a canonical link to zio.dev).

Out of scope​

Changes to docs/ content, the docs sidebar structure, zio.dev, search (Algolia or local), versioned docs, i18n, a blog.

Open items for the maintainer​

  1. Create the production Netlify site; add the repository secret NETLIFY_PRODUCTION_SITE_ID (NETLIFY_AUTH_TOKEN already exists) and choose the domain; set it as the SITE_URL repository variable.
  2. Decide whether the landing job becomes a required check (and add it to ciPullRequestApprovalJobs).
  3. Decide the canonical-URL policy between this site's docs and zio.dev's copy.
  4. Confirm or change the three defaults listed under Decisions.