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 thebuildDocsCI job and published as a Netlify preview bydeploy-preview.yml. Its docs are the mdoc output ofdocs/(generated intowebsite/docsbysbt docs/mdoc, not checked in), with the sidebar fromdocs/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.mdis 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 inwebsite/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.ymlis generated by zio-sbt-ci fromproject/CiWorkflow.scalaand runs onpushtomain, onrelease, and on pull requests.deploy-preview.ymlis generated by the plugin (ciEnableDeployPreview) and uses the secretsNETLIFY_AUTH_TOKENandNETLIFY_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
| Decision | Choice |
|---|---|
| Framework | Docusaurus 3.10 (the existing website/ app); Astro and landing/ removed |
| Brand reach | Whole site: navbar, footer, docs pages and landing page (option A) |
| URLs | / landing page; docs under /docs/ via routeBasePath: '/docs'; no edits under docs/ |
| Data pipeline | Generate website/src/data/site.json before each build (yarn build, yarn start) with the existing scripts, moved to website/scripts/ |
| Build and deploy | GitHub 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(reusingNETLIFY_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
| URL | Content |
|---|---|
/ | landing page |
/docs/ and below | current 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, nowebsite/docs). The full build, which CI deploys, never sets it.
Data flow
website/scripts/build-catalog.mjsreadsdocs/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 fromGITHUB_TOKENin CI), and writessrc/data/site.json.- 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. - React components import
site.json; they hold no content of their own. - The brand SVGs are copied from
assets/logo/intostatic/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; headlinetitleCase(tagline up to the dash)with "Building Blocks" joined by a non-breaking space; lead; the install line and one-line JSON result highlighted withprism-react-rendererdirectly (no copy button); Get started / GitHub; stack strip.ModuleFieldkeeps 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 anddata-*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.cssmaps the palette onto Infima: Manrope, ultramarine primary in light mode and lifted blue in dark mode, ink dark background and#1A2140surfaces,--ifm-global-radius: 0, all shadow variablesnone, hairline borders, flat Prism theme for code blocks with Scala added throughadditionalLanguages.- Colour rules from the earlier spec still hold:
#7C859Eonly as a rule colour or on ink surfaces; text on light surfaces#5B6580or darker.
Quality gates
- Unit tests (
node:test): parser, collapse, split, rename, override, version, title case, inline markdown, row packing, link checker, brand-rule guard (scanssrc/**, includingcustom.cssandlanding.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.htmlfrom aLANDING_ONLY=1build and select by id, role anddata-*. - 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.1run withnpx. - 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.scalais the only place CI changes;ci.ymlis regenerated withsbt ciGenerateGithubWorkflowandciCheckGithubWorkflowmust pass.buildDocskeeps its steps (yarn install,sbt docs/mdoc,yarn build,checkDocsOnFreshWebsite, preview artifact).yarn buildnow also runs the generator. PR previews therefore show landing and docs.- New step in
buildDocs: deploywebsite/buildto the production Netlify site withnwtgck/actions-netlifyandproduction-deploy: true, only onpushtomainand onreleaseevents, usingNETLIFY_AUTH_TOKENandNETLIFY_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). landingjob 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/_headerscarries the cache headers.
Migration order
- Theme, navbar, footer, colour mode, docs at
/docs, Manrope and brand assets. - Move the scripts and unit tests; switch docs links to internal routes; add the
LANDING_ONLYbuild and the generator hook inyarn build/yarn start. - Port the landing components one at a time, each with its static-output test.
- Lighthouse, accessibility, and a visual check of several docs pages (restyling risk).
- CI and deploy changes; regenerate
ci.yml. - Delete
landing/; updateAGENTS.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:
noindexor 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
- Create the production Netlify site; add the repository secret
NETLIFY_PRODUCTION_SITE_ID(NETLIFY_AUTH_TOKENalready exists) and choose the domain; set it as theSITE_URLrepository variable. - Decide whether the
landingjob becomes a required check (and add it tociPullRequestApprovalJobs). - Decide the canonical-URL policy between this site's docs and zio.dev's copy.
- Confirm or change the three defaults listed under Decisions.