* ci(landing): split landing deploy into staging gate + manual production A merge to `main` previously published the landing page straight to production (open-design.ai) via `landing-page-deploy`. There was no buffer to review the rendered site, so a bad merge was live instantly. Split deploys across two Cloudflare Pages projects so production is only ever reached by an explicit human action: - `landing-page-staging` (push to main) -> staging project `open-design-landing-staging` -> staging.open-design.ai. - `landing-page-production` (manual workflow_dispatch only) -> production project `open-design-landing` -> open-design.ai. Only this workflow names the production project; gate it with required reviewers on the `production` GitHub environment. - `landing-page-ci` now also deploys a per-PR preview into the staging project (`--branch=pr-<n>`) for same-repo branches and comments the URL. Fork PRs (no secrets / read-only token) skip the deploy and keep just the build validation. Path filters already scope this to landing edits. Decouple search-engine indexing from staging: - `blog-indexing-on-deploy` now triggers on `landing-page-production` (not every main push), so the test environment is never submitted to Google/IndexNow. - It diffs from a new `blog-indexed-prod` tag (the last indexed prod commit) instead of `HEAD^`, and force-advances the tag after a successful run, so a manual promotion bundling several merged posts indexes all of them rather than only the last commit. Staging and PR-preview builds drop `PUBLIC_GA_MEASUREMENT_ID` so test traffic does not pollute the production GA property. * ci(landing): keep staging + PR previews out of the search index staging.open-design.ai mirrors production and is exposed via cert transparency logs, so search engines can discover it. Indexing the mirror competes with open-design.ai for the same content. Emit `<meta name="robots" content="noindex, nofollow">` whenever OD_LANDING_NOINDEX=1, and set that flag on the staging and PR-preview builds (production leaves it unset and stays indexable). noindex is used rather than a robots.txt Disallow so crawlers can still fetch the page and read both the tag and the canonical, which already points at the production origin. * fix(landing): make staging noindex actually take effect The previous commit read `process.env.OD_LANDING_NOINDEX` directly in `seo-head.astro`, but `.astro` frontmatter is transformed by Vite and does not see process.env, so the meta never rendered. Two fixes: - Inject the flag as the compile-time constant `__OD_LANDING_NOINDEX__` via `vite.define` in astro.config.ts (config runs in Node and can read process.env); SeoHead consumes that constant. - The homepage (`index.astro`) and `og.astro` build their own <head> and never use SeoHead, so a per-component meta can miss pages. Add an `astro:build:done` integration that appends a catch-all `/* X-Robots-Tag: noindex, nofollow` to the Cloudflare Pages `_headers` on staging/preview builds, covering every response (homepage, assets, any custom-head page) at the HTTP layer. Production builds leave `_headers` untouched. Verified: build with OD_LANDING_NOINDEX=1 emits the _headers block and the SeoHead <meta>; build without the flag emits neither; astro check clean. * fix(landing): address review — pin prod checkout to main, defer index pointer Two blockers from review: - landing-page-production: workflow_dispatch can be launched from any ref via the Actions "Use workflow from" dropdown, so an operator could ship an arbitrary branch to open-design.ai. Pin the checkout to `ref: main` so the deployed artifact always equals reviewed main. - blog-indexing-on-deploy: the `blog-indexed-prod` pointer was advanced right after sitemap submission, before Inspect / Search Analytics / Render status / Open status PR. A failure in any of those still moved the pointer, so the next production run skipped those posts. Move the advance to the very end, gated on `success()`, so a failure leaves the tag in place and the range is re-processed next run (submissions are idempotent). * fix(landing): gate production promotion to the main ref only Follow-up to the production-path review note: pinning checkout to main fixed the deployed content, but the workflow was still dispatchable from any ref, which records a non-main production run and would dodge blog-indexing's `workflow_run` `branches: [main]` filter. Gate the whole job on `github.ref == 'refs/heads/main'` so a dispatch from any other branch/tag is skipped outright.
7.4 KiB
apps/landing-page/AGENTS.md
Follow the root AGENTS.md and apps/AGENTS.md first. This file only
records module-level boundaries for apps/landing-page/.
Purpose
apps/landing-page is a stand-alone static Astro site that renders
the Open Design marketing surface in the Atelier Zero style and
ships per-facet catalog pages for every skill, design system, craft
principle, and live-artifact template in the repo root.
Tightly coupled with:
- Design template:
design-templates/open-design-landing/— agent workflow + the source-of-truthexample.htmlknown-good rendering for the homepage hero. - Design system:
design-systems/atelier-zero/DESIGN.md— token spec. - Image assets:
design-templates/open-design-landing/assets/*.pngare uploaded to Cloudflare R2 (open-design-static) and served throughstatic.open-design.aiwith Image Resizing (format=auto). Do not commit local mirrored PNGs intoapps/landing-page/public/assets/.
What it is
- Astro static output. The site has multiple route groups:
/— Atelier Zero homepage (app/pages/index.astro)./skills/+/skills/<slug>/— everySKILL.mdinskills/./skills/mode/<slug>/and/skills/scenario/<slug>/— facet pages generated from frontmatter viagetStaticPaths./systems/+/systems/<slug>/+/systems/category/<slug>/— everyDESIGN.mdindesign-systems/./craft/+/craft/<slug>/— every*.mdincraft/./templates/+/templates/<slug>/— Live Artifacts intemplates/live-artifacts/plus skills withod.mode: template.
- Content sources are never mirrored into this app. Astro content
collections (
app/content.config.ts) glob the canonical Markdown bundles in the repo root at build time. When a contributor adds or edits aSKILL.md/DESIGN.md, the next build picks it up — no intermediate "register your skill here" step. - The shaped data layer lives in
app/_lib/catalog.ts. Page templates import shaped records from there and never re-parse Markdown in JSX. - React is used only at build time (
renderToStaticMarkup) forapp/page.tsxand the sharedHeader. The output ships CDN-ready HTML/CSS plus a small inline enhancement script; no React runtime ships to browsers. - All styles split between
app/globals.css(homepage, kept in lockstep withdesign-templates/open-design-landing/example.html) andapp/sub-pages.css(catalog/facet/detail pages). - All page imagery is referenced through
app/image-assets.ts, which builds Cloudflare Image Resizing URLs for the R2 originals. - Per-skill / per-template thumbnails are rendered offline by
scripts/generate-previews.ts(Playwright). Output lives inpublic/previews/<bucket>/<slug>.<ext>and is gitignored — CI regenerates on every deploy. The script preserves the actual file extension so a future sharp/webp post-processor will work without touching the data layer.
What it is NOT
- Not part of
apps/web. The web app is the product surface; the landing page is a marketing surface. They share design tokens but not state, routes, or runtime. - Not connected to
apps/daemon. There is no/api, no/artifacts, no/frames— no proxy to set up. - Not a CMS. Content authors edit Markdown in
skills/,design-systems/,craft/, ortemplates/live-artifacts/at the repo root; the landing page rebuilds against those globs and ships to Cloudflare Pages automatically.
Boundary constraints
- Must remain a static Astro output.
- Must not import from
@open-design/web,@open-design/daemon,@open-design/desktop,@open-design/sidecar*, or@open-design/contracts. Those are product runtime concerns. - Must not introduce a
src/shell — keep all source underapp/. Component bundles live inapp/_components/<name>.{tsx,astro}. - Must not depend on any non-Google web font.
- Visible "X skills" / "Y systems" claims must read from
getCatalogCounts()— never hardcode. The hero, capabilities cards, labs pills, selected-work fractions, footer Library, and<meta name="description">all derive from the same call so a fresh content edit can never publish contradictory totals. - When the canonical
design-templates/open-design-landing/example.htmlchanges, the corresponding section JSX inapp/page.tsxand rules inapp/globals.cssmust be updated to match. Those two files are kept in lockstep; the rest of the landing-page sources are not. - Content-collection schemas in
app/content.config.tsstay loose (passthrough()). Validation lives at render time so vendored upstream Markdown (e.g.,guizang-ppt) doesn't break the build when an author uses a slightly differentod:key.
Deploy contract (staging → manual production)
Deploys are split across two Cloudflare Pages projects so a merge to
main can never publish to the live site on its own:
- Production project
open-design-landing→open-design.ai. - Staging project
open-design-landing-staging→staging.open-design.ai.
The safety gate is project separation: only the manual production workflow ever names the production project.
.github/workflows/landing-page-staging.ymlruns on push tomainand deploys to the staging project (open-design-landing-staging,staging.open-design.ai)..github/workflows/landing-page-production.ymlis manual (workflow_dispatch) and is the only workflow that names the production project (open-design-landing,open-design.ai). Gate it with required reviewers on the GitHubproductionenvironment..github/workflows/landing-page-ci.ymlruns on PRs: it validates the build and, for same-repo branches, deploys a per-PR preview into the staging project (--branch=pr-<number>→pr-<number>.open-design-landing-staging.pages.dev) and comments the URL.
The staging workflow triggers when any of these change:
apps/landing-page/**design-templates/open-design-landing/**skills/**design-systems/**craft/**templates/**package.json,pnpm-lock.yaml,pnpm-workspace.yaml- the workflow files themselves
A push that only edits a SKILL.md MUST trigger the staging workflow — if it
doesn't, the paths: filter has drifted from the content-collection glob and
the staged site will fall behind silently. Treat that as a regression, not a
feature.
Common commands
pnpm --filter @open-design/landing-page dev # http://127.0.0.1:17574
pnpm --filter @open-design/landing-page typecheck
pnpm --filter @open-design/landing-page previews # render thumbnails
pnpm --filter @open-design/landing-page build # static export → out/
When to update this app
- Added/edited a
SKILL.md,DESIGN.md, craft*.md, or live-artifact template at the repo root → no landing-page edit required; CI rebuilds and re-renders thumbnails on the next push tomain. - Adding a new top-level route group (e.g.
/playbooks/) → add an Astro page directory underapp/pages/, a content collection inapp/content.config.ts, a shaping function inapp/_lib/catalog.ts, and route entries that match the existing index/detail/facet pattern. - New section added to the canonical landing page → port it into
app/page.tsxandapp/globals.csskeeping lockstep withdesign-templates/open-design-landing/example.html. - Brand re-keying for a non-Open-Design tenant → fork the app, update copy, swap PNGs. Do not parameterize this app for multi-tenancy.