Commit graph

33 commits

Author SHA1 Message Date
Jane
3f4fd58937
feat(landing-page): surface Discord + X in header, restructure site footer (#3230)
Some checks failed
ci / Detect CI change scopes (push) Successful in 0s
visual-baseline / Capture visual baselines (push) Waiting to run
landing-page-ci / Validate landing page (push) Failing after 2s
landing-page-staging / Deploy landing page to staging (push) Has been skipped
nix-check / build (push) Failing after 2s
ci / Validate Nix flake (push) Has been skipped
ci / Preflight (push) Failing after 2s
ci / Workspace unit tests (push) Failing after 2s
ci / Daemon workspace tests (push) Failing after 1s
ci / Web workspace tests (push) Failing after 1s
ci / Browser tests (push) Failing after 1s
ci / Build workspaces (push) Failing after 2s
ci / Validate workspace (push) Failing after 0s
ci / Runtime trace (push) Has been skipped
* feat(landing-page): surface Discord + X in header, restructure site footer

Two related public-chrome adjustments:

- **Header gains compact Discord + X icon buttons.** Both community
  channels were previously buried in the footer, so the typical
  visitor never saw them on a page-deep scroll. They now sit before
  the Download / Star CTAs in `nav-side`, share the ghost-button
  outline language, and stay icon-only with `aria-label` so they
  read as social affordances rather than competing with the text
  CTAs. At ≤1080px the icon buttons hide alongside the existing
  ghost CTA, so the bar still collapses cleanly into the hamburger
  panel — Star stays in the bar at every breakpoint.

- **Footer restructured into 4 columns: Products / Plugins /
  Resources / Connect.** The old `Plugins / Open Design / Connect`
  three-column layout muddled three different things — sister
  products, the artifact catalogue, and contributor channels —
  under one roof, so visitors hunting for "the other thing this
  team makes" had nowhere obvious to go.
  - **Products** (new) lists the team's apps: Open Design (links
    to homepage) and HTML Anything. Two entries by design — adding
    more products without an editorial pass would dilute the
    column.
  - **Plugins** mirrors the topbar `Plugins` dropdown verbatim:
    Templates / Skills / Systems / Craft, with no count prefix on
    Systems / Craft so it reads identically to the nav.
  - **Resources** (renamed from `Open Design`) carries the
    docs-style links: Official source / Quickstart / Agents locaux
    / Compare / Claude Design alternative. The old column heading
    was confusing because the OD logo + brand name already sit
    under the column.
  - **Connect** gains an X / Twitter row pointing at
    `@nexudotio`. The brand entries on this column are
    contributor / community surfaces only — code, releases,
    chat, social, RSS, contact form.

Implementation:

- `_components/header.tsx` — `DISCORD` and `X_TWITTER` consts at
  the top alongside `REPO`. Two `<a class="nav-icon">` blocks with
  inline SVG before the existing Download / Star CTAs.
- `_components/site-footer.astro` — `HTML_ANYTHING` and `NEXU_IO`
  consts. `<div class="sub-footer-col">` re-ordered to put
  Products first, Plugins second (no longer carries `counts.*`
  values), Resources third, Connect fourth (with the new X / Twitter
  row).
- `globals.css` — `.nav-icon` rule cloned from the ghost CTA's
  visual language (transparent + 1px line, fills on hover) but
  square (36×36 round) so it reads as a social-icon affordance.
  Added `display: none` for `.nav-side .nav-icon` to the existing
  ≤1080px and ≤880px media queries so the icons follow the same
  collapse behaviour as the Download CTA.
- `sub-pages.css` — `.sub-footer-grid` switches from
  `1.6fr 1fr 1fr 1fr` to `1.4fr 1fr 1fr 1fr 1fr` (brand + 4
  columns). At ≤1080px it falls back to a 3-column shape so each
  column has room to breathe; at ≤720px it stays a single column
  (existing behaviour).
- `i18n.ts` — adds `products`, `resources`, `xTwitter`,
  `sisterProjects`, `htmlAnything`, `nexuIo` to `LandingUiCopy.footer`
  (the last three are kept around even though `sisterProjects` is no
  longer rendered after the column was renamed Products — they're
  harmless and avoid churning the type if a future iteration brings
  the Sister-projects framing back). All 17 non-English landing
  locales gain translations for the new keys via the existing
  `LOCALIZED_LANDING_FOOTER_COPY` map (and the `LANDING_UI_COPY_OVERRIDES`
  block for `zh` / `zh-tw`). Translations were generated with
  `claude-haiku-4-5` over OpenRouter, with explicit instructions
  to keep "Open Design", "HTML Anything", and "X / Twitter" in
  English and to render "Products" / "Resources" in sentence case
  per locale convention. Spot-checked against rendered pages on
  `/zh/`, `/zh-tw/`, `/ja/`, `/ko/`, `/de/`, `/fr/` (and `/ar/` for
  RTL) for natural phrasing.

Validation: `pnpm --filter @open-design/landing-page typecheck` ->
0 errors / 0 warnings; local dev server smoke-tested on en root
(`/html-anything/`) and 5 locale variants (`/zh/`, `/zh-tw/`,
`/ja/`, `/de/`, `/fr/`) — header renders 2 nav-icon buttons,
footer renders 4 localized column headings in the correct order
with the right link targets.

* fix(landing-page): address PR #3230 review — locale-aware HTML Anything link + drop unused const

Two non-blocking inline review points from @PerishCode on PR #3230:

- The HTML Anything entry in the new Products column hardcoded
  `https://open-design.ai/html-anything/` via a top-level
  `HTML_ANYTHING` const, but `/html-anything/` is a real localized
  route in this app (`pages/[locale]/html-anything/index.astro`)
  and `open-design.ai` is the same site's live domain. A visitor
  on `/zh/…` clicking through landed on the English route and lost
  locale context, and hardcoding the production domain meant a
  preview build would surface a link that bounces visitors back
  to prod. Switch to `href('/html-anything/')` so the locale prefix
  + the current site's domain (resolved by `localizedHref`) are
  honored, matching every other footer link.

- `NEXU_IO` was declared at the top of the component but never
  referenced — leftover from an earlier iteration that listed
  `nexu.io` as a Sister-projects entry before the column was
  renamed Products and reduced to OD + HTML Anything. Removed.

No behavior change beyond the locale routing fix; the i18n keys
and column structure stay as they landed in the original commit.

* fix(landing-page): correct nav-icon comment to match actual responsive behaviour

The JSX comment introduced for the new Discord + X icon buttons in
PR #3230 claimed the icons "survive at narrow widths while text-only
nav items get pushed off". The CSS that shipped in the same PR does
the opposite: both `@media (max-width: 1080px)` and `@media (max-width:
880px)` blocks add `.nav-side .nav-icon { display: none; }`, so at
narrow widths the icons collapse alongside the ghost Download CTA
while the text nav <ul> moves into the hamburger panel — only the
Star CTA remains visible in the bar.

Rewrite the comment to describe the actual responsive contract so
the next reader of `header.tsx` doesn't have to cross-reference
`globals.css` to figure out which surface stays. Reviewer flag from
@PerishCode on PR #3230.

No code-path change; comment-only.

* fix(landing-page): correct sub-footer 1080px comment to describe actual 3-column grid

The CSS comment introduced for the new sub-footer grid claimed the
≤1080px breakpoint drops to "brand + 2x2 grid of columns" — but the
rule produces a 3-column grid, not a 2x2.

`.sub-footer-grid` has 5 children at this breakpoint (the brand
block + the four footer columns) and `.sub-footer-brand` carries
no `grid-column` span, so with `grid-template-columns: 1.6fr
repeat(2, 1fr)` they flow as: row 1 = brand · Products · Plugins,
row 2 = Resources · Connect · empty cell. The brand sits inline
with two columns rather than on its own, and the four content
columns are not a clean 2x2.

The layout itself is fine; only the comment misleads the next
reader about how the columns wrap. Same flavor as the `header.tsx`
icon comment fixed in 744daec — describe what the rule actually
does so the comment doesn't drift from the CSS. Reviewer flag
from @PerishCode on PR #3230.

Comment-only change.

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-29 05:59:24 +00:00
Jane
9d65e26c0f
feat(landing-page): card grid + share popover for /plugins/templates/ (#3185)
* feat(landing-page): YouMind-style grid + share popover for /plugins/templates/

The list-style catalog rows that landed in PR #3010 read as a long
table of items rather than a discoverable grid. Product feedback (after
benchmarking against youmind.com/zh-CN/seedance-2-0-prompts) wanted:

- A YouMind-shape card with a top accent band, video / poster preview
  area, author + attribution row, an excerpt frame, and a primary CTA
  paired with a share button.
- Hover-autoplay on the 46 video templates whose manifest carries a
  Cloudflare Stream MP4. The data was already there since PR #3010;
  the catalog row just rendered the poster as a static `<img>`.
- A counter chip on the right of the hero that surfaces the live total
  (`Total · 231`) instead of baking the number into the H1
  ("231 runnable templates."). The hero now reads as
  `OPEN SOURCE CLAUDE DESIGN` eyebrow + `Templates.` static H1, which
  also threads the brand keyword into the page's SEO surface.
- A six-question FAQ block below the grid covering license, BYOK keys,
  contribution, and the "open source Claude Design alternative"
  positioning explicitly.

Implementation:

- `_components/template-card.astro` — new card component. Accent band
  hue is derived from `od.mode` so artifacts of the same kind get a
  consistent color (video green, prototype blue, deck mustard, image
  wisteria, hyperframes coral, audio amber, live-artifact teal),
  falling back to a stable per-index hue for unrecognized modes.
  Featured tag (yellow, on-brand) is visible when the manifest tag
  list contains `featured`; the rest of the card is locale-resolved
  via the same `resolveBundledTitle` / `resolveBundledDescription`
  helpers PR #3010 added.

- `pages/plugins/templates/index.astro` + `[kind]/index.astro` — grid
  layout (`.tpl-grid`, `repeat(auto-fill, minmax(340px, 1fr))`),
  hero with counter chip, FAQ section on the parent only. Adjacent
  filter strips share a single divider rather than drawing one each,
  so the kind + scene chip block reads as one filter unit instead of
  three stacked horizontal cuts.

- Hover-autoplay observer + share button click handler bundled into
  one `<script>` per page so they share the same boot lifecycle. The
  earlier split version dispatched `astro:page-load` from the autoplay
  block before the share block's listener attached, which dropped
  the share click on the floor; the merged init() runs eagerly when
  DOM is ready, re-runs idempotently on `astro:page-load` (Astro view
  transition), and uses `data-tpl-init` / `data-tpl-share-bound`
  markers to prevent double-binding.

- Card share is a popover, not a system share sheet. The detail page's
  `<dialog class="detail-share-dialog">` UI is reused (single instance
  per page populated per click), but `<dialog>.show()` runs in
  non-modal mode and JS positions it via `getBoundingClientRect()` to
  unfold above-right of the trigger button. Outside-click and Escape
  close the popover; the existing `data-share-copy` / `data-copy-link`
  handlers in `header-enhancer.astro` wire Copy text + Copy link
  automatically. Width tuned to 420px so it fits next to a 340px-wide
  card without spilling onto the next column.

- `_redirects` already covers retired Skills + Craft routes (PR #3010)
  so this grid pivot doesn't need new redirects.

Out of scope for this PR (kept lean):
- Multi-locale hero + FAQ copy. Hero / FAQ render in English on every
  locale right now; the `pcopy.tileTemplates` chip rail and per-card
  title/description still localize per PR #3010. Locale rollout for
  the hero + FAQ is a follow-up.
- Sort + filter buttons in the YouMind reference top-right (we still
  show artifact-kind chips only). Sort by featured weight is the
  most likely next step.
- `od.featured` weight as a featured proxy. We currently key off
  `tags?.includes('featured')` which is 0-match across the catalog
  today; promoting the numeric weight into `BundledPluginRecord` is a
  separate small commit.

`pnpm --filter @open-design/landing-page typecheck` clean (0 errors).

* feat(landing-page): localize templates chrome + FAQPage JSON-LD + hover-only autoplay

Three follow-ups Looper flagged on the YouMind-style grid (PR #3185):

- **Localizable hero / FAQ / card chrome.** PR #3185 wired the grid
  through `pcopy` for record titles + descriptions but hard-coded the
  surrounding chrome — hero eyebrow / lead / counter label, FAQ head,
  Featured tag, "Read full prompt", "Use this template", and the
  share-button `aria-label` — to English. `/ja/plugins/templates/`,
  `/zh-CN/plugins/templates/video/`, etc. now ship those strings via
  `pcopy.*` keys (`templatesHeroEyebrow`, `templatesHeroLead`,
  `templatesCounterLabel`, `cardFeaturedTag`, `cardReadFullPrompt`,
  `cardUseTemplate`, `cardShareAria`, `faqHead`, `faqItems`). English
  is the base; per-locale overrides for hero copy + 6 FAQ Q&A pairs
  remain a follow-up (the PR-#3185 "Out of scope" item), so the 17
  non-English locales fall back to English chrome instead of showing
  undefined values.

- **`FAQPage` JSON-LD entity.** The visible accordion was a SEO
  surface but `jsonLd` was still a single `CollectionPage`. Switched
  it to an array and appended a `FAQPage` whose `mainEntity` is each
  question + answer from `pcopy.faqItems`, so the structured-data
  payload search engines see and the visible <details> share one
  source of truth — drift between them is now mechanical, not
  editorial.

- **Hover-only autoplay (not viewport autoplay).** The previous
  observer played every video the moment its card scrolled into the
  viewport, which contradicted the PR's stated hover-autoplay
  contract and spawned N simultaneous decoders on a casual scroll.
  The IntersectionObserver now hydrates `data-src` -> `src` lazily
  (one-shot, then unobserve) at a 300px rootMargin; `play()` and
  `pause()` are gated to `pointerenter` / `pointerleave` (plus
  `focusin` / `focusout` for keyboard users) on the parent
  `.tpl-media` host so hovering anywhere on the preview frame
  triggers playback. Same change applied to the `[kind]` route so
  faceted pages behave identically.

Validation: pnpm --filter @open-design/landing-page typecheck -> 0
errors / 0 warnings; local dev (port 3061) renders 231 cards / 46
data-tpl-autoplay markers / FAQPage entity present in jsonLd / 6 FAQ
summaries; zh-CN locale falls back to English chrome (expected, the
locale routes themselves remain out of scope per PR #3185).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:17:56 +00:00
koki
ee1eab77c6
feat(landing): add Community link + first-party Ambassadors page (#3066)
* feat(landing): add Community link to top nav

Adds a 'Community' entry to the landing-page nav between Blog and the
Star CTA, linking to /community/ which Cloudflare Pages 302-redirects to
the contributors honor-cards page (currently a Vercel deploy).

Translations added for all 18 locales. The nav slot was previously empty
after Blog because Contact had been intentionally pulled from the bar
(left as a footer + #contact anchor).

* fix(landing): use literal /community/ href so non-default locales don't 404

PerishCode review caught that href('/community/') routes through
localizedHref and produces /zh/community/, /ja/community/, etc. The
_redirects rule only matches the literal /community/ and the
[locale]/[...path].astro catch-all does not generate community pages,
so 17 of the 18 translated locales would have hit a Cloudflare 404.

The destination is a single non-locale-aware external page, so skip
the locale prefix entirely — same shape as the GitHub Star and
Download CTAs.

* feat(landing): host community + ambassadors page first-party

Lands the contributors / ambassadors page as a static asset at
apps/landing-page/public/community/index.html, served at /community/
on open-design.ai. Drops the temporary 302 to the Vercel preview URL
(d5458c46-…vercel.app) — that hostname was a deploy-time UUID Vercel
could recycle, which the reviewer correctly flagged as a follow-up.

The page now opens with an Ambassadors section: vocation, patronage,
covenant — three columns of the program in Renaissance-atelier voice,
with a single Apply on Discord CTA pointing at the ambassador channel
(discord.gg/2p7Ajbxw3h). Maintainers / leaderboards / good-first-issues
sit below as before. Header.tsx comment updated to point at the new
source of truth instead of the deleted redirect rule.

* fix(community): drop time-bound claims, tighten bot heuristic, drop dead CORE_TEAM entry

PerishCode review on ff1cd44b flagged three correctness issues with
the static community page. Addressing each:

* The 'This week's signal' / 'This week's leader' / 'Last 7 days' /
  'PRs · 7d' framing made promises a frozen RANKING_SNAPSHOT can't
  keep — three weeks from build, the page would be calling the
  2026-05-26 leaderboard 'this week's leader.' Renamed to time-neutral
  copy ('Recent signal', 'A recent leader', 'Snapshot', 'Recent PRs')
  and dropped the snapshot's 'since' field so we don't pin a window
  we can't honour. Real refresh pipeline is a follow-up.

* 'Showing first N · resets every 30 minutes' didn't describe the code
  (no caching of any kind exists; each page load re-hits /search/issues
  and /users/:login). Replaced with a truthful 'Showing first N open
  good-first-issues.'

* The bot exclusion heuristic used substring match on bot/cursor/agent,
  which would silently drop real logins like 'agentina', 'cursorsmith',
  'robothai'. Tightened to a whole-token regex (/(?:^|[-_])(bot|cursor|
  agent)(?:$|[-_])/) and dropped 'leon wang' from CORE_TEAM — it had
  an embedded space, which GitHub logins never do, so the entry was
  unreachable dead code.

---------

Co-authored-by: koki yanlai xu <koki@kokideMacBook-Air.local>
2026-05-27 11:33:45 +00:00
Jane
f8c860a505
feat(landing-page): localize plugins library across 18 locales (#3010)
* feat(landing-page): localize plugins library across 18 locales

PR #2926 shipped the new `/plugins/` library hub + four kind sub-routes
+ detail pages, but the chrome was English-only — visitors landing on
`/zh/plugins/` saw the old marketplace registry placeholder rendered
by the catch-all instead, and detail pages rendered identical English
copy regardless of locale prefix. This PR brings the plugin surface
to feature parity with `/zh/skills/`, `/zh/templates/`, `/zh/systems/`,
`/zh/craft/`.

## What changes

- New `app/_lib/plugins-i18n.ts` — single source for all plugin chrome
  copy (hub, list pages, chip rails, share dialog, detail-page meta
  labels). English baseline + 17 locale overrides keyed on
  `LandingLocaleCode` (the same short-code shape `localeFromPath()`
  returns). Missing keys per locale fall back to English so a
  partially-translated locale still renders sensibly. Translations
  cover hub copy, four tile titles + blurbs, seven artifact-kind
  labels + descriptions, 23 scene-subcategory labels, 18 detail-page
  chrome strings, and a six-key share-dialog table with a
  per-locale `shareTemplate({title, url})` function (translated for
  every locale where `_lib/i18n.ts` already had one — same voice).
- `app/pages/plugins/{,templates/,templates/[kind]/,skills/,systems/,
  craft/,[slug]/}/index.astro` — every hardcoded English string now
  reads `getPluginsCopy(locale)` keys. Page logic and routing
  unchanged.
- New short-code wrappers under `app/pages/[locale]/plugins/` — six
  files (hub + three sub-routes + `[kind]/` and `[slug]/`) following
  the same pattern `[locale]/skills/index.astro` already uses: each
  re-exports the canonical page component and adds a per-locale
  `getStaticPaths()` so the build emits 17 locale prefixes per
  plugin route. Total plugin-route prerender count goes from ~390 to
  ~7 000, matching the existing skill/template scaling.
- Catch-all (`[locale]/[...path].astro`) — old `getPublicPlugins` /
  `getRegistryCounts` registry rendering removed (placeholder UI
  that was never wired to a real marketplace data source). Plugin
  routes now live exclusively under `[locale]/plugins/...` short-code
  wrappers, so the catch-all stops claiming `'plugins'` as a route
  root. The dead-code path also drops a `pluginCounts.all` reference
  the title row was reading.
- `.plugins-tile-grid` styles promoted from a scoped `<style>` in the
  default-locale hub to global `app/sub-pages.css` so the
  short-code wrapper renders the same hub markup without re-mounting
  per-page CSS — `display: contents`-style scoping pitfalls in
  Astro's per-component CSS scoping made this the cleanest fix.

## Surface area

- [ ] **UI** — new page / dialog / panel / menu item / setting / empty state in `apps/web` or `apps/desktop`
- [ ] **Keyboard shortcut** — new or changed
- [ ] **CLI / env var** — new `od` subcommand or flag, new `tools-dev` flag, or new `OD_*` env var
- [ ] **API / contract** — new `/api/*` endpoint, new SSE event, or changed shape in `packages/contracts`
- [ ] **Extension point** — new entry under `skills/`, `design-systems/`, `design-templates/`, or `craft/`, or change to the skills protocol
- [ ] **i18n keys** — new translation keys (full plugin chrome added across all 18 locales)
- [ ] **New top-level dependency** — adding any new entry to the **root** `package.json`
- [ ] **Default behavior change** — changes what existing users experience without opting in
- [x] **None** — landing-page-only restoration of i18n parity for the plugin surface

## Validation

- `pnpm --filter @open-design/landing-page typecheck` → 0 errors
- `pnpm --filter @open-design/landing-page build:static` → 16 127 pages
  built (+6 584 over current main: ~388 plugin detail pages × 17
  locale prefixes plus the hub + four sub-routes × 17 locales).
- `copy-example-html.ts` reports `266 entry files + 65 referenced
  files`, identical to before — no regression in the asset-mirroring
  pipeline.
- Local Playwright smoke (`/zh/plugins/...`):
  - `/zh/plugins/` renders `<title>插件库 · Open Design</title>`,
    label `插件库`, h1 `407 个可组合的构件。`, four tiles labelled
    `模板 / 技能 / 设计系统 / 工艺`.
  - `/zh/plugins/templates/video/` renders h1 `48 视频`, scene chips
    `全部 / 动效 / 短视频 / 营销 / 产品 / 数据讲解`.
  - `/zh/plugins/example-article-magazine/` share dialog renders
    `复制下面的文案、然后跳到你想分享的平台粘贴即可` etc., share
    template auto-interpolates plugin title + URL into Chinese voice.
  - All 18 locale prefixes (`/zh`, `/zh-tw`, `/ja`, `/ko`, `/de`,
    `/fr`, `/ru`, `/es`, `/pt-br`, `/it`, `/vi`, `/pl`, `/id`, `/nl`,
    `/ar`, `/tr`, `/uk`) → 200 across hub + four sub-routes + sample
    detail page.
  - English `/plugins/` unchanged (default-locale path bypasses the
    `[locale]/...` wrapper).

* feat(landing-page): finish plugins i18n chrome across 18 locales

The first localization pass shipped a partial fix: hub headings, lead
copy, two-level page chrome, detail-page metadata labels, the share
dialog, and the chip rail were still falling back to English on every
non-English locale because plugins-i18n.ts only filled a chrome slice
for `zh` and the file header even claimed "7 artifact-kind labels and
25 scene-subcategory labels are translated" for every locale that did
not yet have those blocks.

Three changes close the visible gap:

1. plugins-i18n.ts: fills the 27 still-missing chrome fields per locale
   for zh-tw / ja / ko / de / fr / ru / es / pt-br / it / vi / pl / id /
   nl / ar / tr / uk. Includes the 7-key category map, the 23-key
   subcategory map, hubHeading / hubLead, the 4 *Label / *Heading /
   *Lead triples for the templates / skills / systems / craft hub
   pages, the 4 tile blurbs, the 4 browse buttons, sceneLabel, allChip,
   the 12 detail-page metadata labels (mode / scenario / platform /
   surface / author / manifest id / tags / preview caption / find on
   GitHub / homepage / open in new tab) and bucket label map, the
   detail share dialog (title / copy link / jump-to), and the
   header-side nav.plugins entry. zh receives the same 11 detail-page
   and share-dialog labels it was also missing.

2. header.tsx + site-footer.astro: routes the hardcoded "Plugins /
   Templates / Skills / Systems / Craft" labels through `nav.*` from
   HeaderCopy, so every locale gets its own dropdown trigger and
   footer column. Adds `nav.plugins` to HeaderCopy and fills it in 18
   locales with the local form ("插件" / "プラグイン" / "Plugins" /
   "Plug-ins" / "Plaginy" / "الإضافات" / etc).

3. plugin-row.astro + content-i18n.ts: chip rail. The bundled-plugin
   branch now runs raw `mode` / `scenario` slugs through the shared
   localizeTaxonomyValue, and that helper now also consults the
   plugins-i18n subcategory map before giving up. localizeTaxonomyValue
   now returns undefined on a true miss instead of the unknownTag
   placeholder, so chips drop quietly instead of showing "Category" /
   "分類" / "Categoría" for taxonomy slugs we have not localized yet.
   Callers that genuinely want the placeholder (`localizeContentTag`,
   blog `category`, system noun) still keep the explicit fallback.

Out of scope and tracked separately: per-plugin title and description
in plugins/_official/* (author-supplied English metadata, ~401 plugins
without an i18n schema in the manifest yet — needs RFC + tooling
before the manifests can be expanded), and adding the long tail of
mode / scenario / category slugs (`code-migration`, `plugin-sharing`,
`tune-collab`, `live-artifacts`, `engineering`, ...) to TAXONOMY_TERMS
so chips render localized labels for every taxonomy value rather than
dropping silently.

* feat(landing-page): cover plugins chip rail long-tail taxonomy slugs

PR #3010's first round localized the high-frequency mode/scenario
chips (prototype, video, image, marketing, design, ...) but left the
~37 mode/scenario and 14 category slugs that show up in real `od.*`
metadata — code-migration, plugin-sharing, design-system, planning,
scenario, refine, discovery, handoff, token-map, tune-collab, orbit,
live-artifacts, engineering, healthcare, hr, sales, support,
default-router, downstream-export, figma-migration, media-generation,
plugin-authoring, validation, 3d-shaders, animation-motion,
audio-music, creative-direction, design-systems, diagrams, documents,
image-generation, marketing-creative, screenshots, slides,
video-generation, web-artifacts, ... — falling through to undefined
and dropping their chip silently on every non-English locale.

The data layer is the source of truth here, so this expansion lands
in `content-i18n.ts:TAXONOMY_TERMS` / `CATEGORY_LABELS` rather than
the plugins-i18n catalog: a single dictionary entry per slug fans out
to every chip-rail consumer (catalog rows, detail metadata, the
templates/[kind] facets) without each consumer touching its own copy.

Translations cover all 17 non-`en` locales. Brand and product nouns
(Figma, Open Design, BYOK, plugin) stay literal; technical taxonomy
slugs get short equivalents that read as chips rather than full
prose. The result on `/ja/plugins/skills/` matches `/plugins/skills/`
chip-for-chip (30 chips both sides) instead of dropping 27 of them
the way the previous iteration did.

* feat(landing-page): read manifest title_i18n / description_i18n on bundled plugins

PR #3010's prior rounds localized chrome and chip rails but the
catalog's most prominent text — each row's plugin name and blurb —
stayed English on every non-English locale. The plugin manifest
schema (`packages/contracts/src/plugins/manifest.ts`) has supported
`title_i18n` and `description_i18n` (Record<locale, string>) on every
manifest from spec v1; ~24 of the 401 first-party manifests already
carry one for `zh-CN`. The reader was just never wired to use them.

This change does the reader half: bundled-plugins.ts captures the
two i18n maps off each `open-design.json`, plugin-row.astro and the
detail page resolve them at render time via two new helpers
(`resolveBundledTitle`, `resolveBundledDescription`) that mirror the
short→long fallback chain documented in the manifest spec
(`htmlLang` like `zh-CN` → short `LandingLocaleCode` like `zh` →
primary tag → `en` → English baseline). The static-paths pass still
runs once for all locales — it has to, since each manifest produces
one URL — but the title/description shown on the rendered page now
reads the locale off `Astro.url.pathname` and picks the right entry
out of the maps.

Verified locally: `/zh/plugins/example-card-twitter/` now reads
"Twitter 分享卡 / 推特金句 / 数据卡, 适合配推文" from the manifest's
existing `zh-CN` block instead of the English baseline.

Plugin-data half follows in a separate commit. The 17 non-English
locales × 401 manifests need backfilling so the reader has something
to resolve to; that's data, not schema, and lands as a sequence of
manifest patches rather than tangled with this code change.

* feat(plugins): translate scenarios bucket title/description across 17 locales

Closes the first chunk of #3028. Eleven scenarios plugins (the
default-scenario bundle for each taskKind: code-migration,
figma-migration, media-generation, new-generation, tune-collab,
plugin-authoring; the default design router; the React / Vue /
Next.js downstream-export starters; and the Refine baseline) get
title_i18n + description_i18n filled for all 17 non-English locales
the landing page serves (zh-CN, zh-TW, ja, ko, de, fr, ru, es,
pt-BR, it, vi, pl, id, nl, ar, tr, uk).

The reader landed in 7ddfe36; this commit is data-only. taskKind
slugs that other docs reference by name (`code-migration`,
`figma-migration`, `tune-collab`, etc.) stay literal in the
descriptions so cross-references still resolve. Brand nouns —
Open Design, Next.js, React, Vue, Figma — also stay literal.

`/ja/plugins/od-code-migration/` now reads
"コードマイグレーション(デフォルトシナリオ)" instead of the English
baseline; `/zh/plugins/skills/` shows "代码迁移(默认场景)" in the
catalog row.

Remaining buckets (image-templates 45, video-templates 50,
examples 140, design-systems 142 = 377 plugins) follow in
subsequent commits in this PR.

* fix(landing-page): drop CJK template wrap when source name is still English

The Chinese / Japanese / Korean fallback templates for craft, skill,
template, system, plugin, and blog text splice the source `name` /
`title` into a CJK sentence frame: ``${name}工艺规则``,
``Open Design 指南:${topic}``, ``${name} は…のスキルです``. When the
underlying SKILL.md / craft markdown / blog frontmatter still ships
an English name (true for ~95% of the catalog today), that produces
mid-sentence script straddling on `/zh/...`, `/zh-tw/...`, `/ja/...`,
`/ko/...` like:

  H1   : "Editorial typography hierarchy工艺规则"
  Lead : "这条 Open Design 工艺规则定义 Editorial typography hierarchy
          的执行标准…"
  Plug : "video 插件 · 3D Animated Boy Building Lego"

That reads worse than the all-English fallback, because the visitor
parses the page in two scripts at once.

Adds a `nameNeedsEnglishFallback` guard that fires for the four CJK
locales whenever the spliced-in name has no CJK characters of its
own, and threads it through every `localizeXxxText` helper:
craft, template, system, plugin, skill, blog. When it fires the
helper returns the raw English content untouched, so the section
renders end-to-end in one language. Chrome (header, footer, breadcrumb,
buttons, share dialog) keeps its CJK rendering — only the
title-and-lead block falls back.

Side benefit: the same guard kicks in on the long tail of plugin
manifests still pending `title_i18n` / `description_i18n` backfill
(tracked in #3028), so `/zh/plugins/<bundled>/` no longer pairs a
"video 插件 · 3D Animated Boy Building Lego" title with a Chinese
breadcrumb. The page reads "3D Animated Boy Building Lego" + the
English manifest description, while header / footer / breadcrumbs
stay localized. Once a manifest ships its i18n maps, the chrome and
body re-converge automatically.

Non-CJK non-Latin scripts (ar, vi, ...) keep the previous behavior —
their templates already read tolerably with English names. If that
turns out to be wrong on a real audit, the same guard generalizes by
adding the matching Unicode range and locale set.

* feat(plugins): translate image-templates bucket title/description across 17 locales

44 of 45 image-templates plugins get title_i18n + description_i18n
filled for all 17 non-English locales (zh-CN, zh-TW, ja, ko, de, fr,
ru, es, pt-BR, it, vi, pl, id, nl, ar, tr, uk). Generated via Claude
Sonnet 4.5 over the OpenRouter gateway, ~$1.38 in API spend, 156s
wall-clock. Brand and cultural references stay literal (Open Design,
Lego, Hanfu, Showa, Pokémon, Black Myth: Wukong). Long AI generation
prompts collapse to a 1-2 sentence summary capturing what the plugin
does — the description doubles as catalog blurb on the landing site,
not as the actual generation prompt (which lives in example.html /
the manifest's preview entry).

Skipped: `profile-avatar-realistically-imperfect-ai-selfie` returned
malformed JSON on three retries; will rerun with a tighter prompt in
a follow-up commit. Catalog rows for that plugin keep falling back to
the raw English fields per #3010's reader change, so nothing breaks.

Tracking: closes the image-templates row in #3028.

* feat(plugins): translate video-templates bucket title/description across 17 locales

49 of 50 video-templates plugins get title_i18n + description_i18n
filled for the 17 non-English landing locales. Generated via Claude
Sonnet 4.5 over OpenRouter, ~$1.47 in API spend, 177s wall-clock.
HyperFrames templates, the Three Kingdoms cinematic series, the
Seedance/short-film prompts, and the K-pop / wuxia / anime variants
all get a 1-2 sentence catalog blurb in each locale; brand and
cultural tokens (Black Myth: Wukong, Hanfu, Showa, Pokémon, Three
Kingdoms / 三国志, Lego, Disney, K-pop, HyperFrames) stay literal.

Skipped: `live-action-anime-adaptation-water-vs-thunder-breathing-duel`
returned malformed JSON on three retries; will rerun in followup.
Falls back to the raw English fields per the reader landed in 7ddfe36.

Tracking: closes the video-templates row in #3028.

* feat(plugins): translate examples bucket (117/140) title/description across 17 locales

117 of 140 examples plugins get title_i18n + description_i18n filled
for the 17 non-English landing locales. Generated via Claude Sonnet
4.5 over OpenRouter, $3.94 in API spend, ~13 min wall-clock at
8-way concurrency. Existing zh-CN translations on 24 manifests are
preserved (the merge keeps author-supplied entries and only adds
missing locales).

23 of 140 returned malformed JSON on three retries — the output
likely hit the 4000 max_tokens ceiling on plugins whose description
balloons across 17 locales. Those manifests fall back to English on
non-`en` rendering per the reader landed in 7ddfe36, and will rerun
in a follow-up commit with a larger token budget and a stricter
output schema.

Tracking: closes 117/140 of the examples row in #3028; the remaining
23 stay open in that issue's failure list.

* feat(plugins): translate design-systems bucket (141/142) title/description across 17 locales

141 of 142 design-systems plugins get title_i18n + description_i18n
filled for the 17 non-English landing locales. Generated via Claude
Sonnet 4.5 over OpenRouter, $2.55 in API spend, 301s wall-clock at
8-way concurrency.

Translator script gained two improvements between examples and this
bucket:
- max_tokens bumped from 4000 to 8000 so 17-locale outputs stop
  truncating on the long-tail manifests with verbose descriptions
- a balanced-brace JSON extractor that pulls the outermost `{ ... }`
  from the response, tolerating trailing prose Claude occasionally
  appends after the JSON object.

Result: only 1 manifest (`totality-festival`) failed parse this
batch, down from ~16% on the examples bucket. The next commit
re-runs the prior buckets' failures with the improved script.

Tracking: closes 141/142 of the design-systems row in #3028.

* fix(plugins): backfill 4 plugins that retried green after JSON extractor improvement

dcf-valuation, social-media-dashboard, wireframe-sketch (examples
bucket) and live-action-anime-adaptation-water-vs-thunder-breathing-duel
(video-templates bucket) parse cleanly under the balanced-brace
extractor introduced for the design-systems batch. The remaining
22 failures from the prior runs hit a different parse mode (Claude
emitting unescaped double quotes inside string values when the source
description contains its own English quotes like 'make it professional');
those will need a tighter prompt and rerun.

* fix(plugins): translate the last 22 plugins with quote-handling prompt fix

The 22 stuck plugins all carried English / Chinese double-quoted
phrases inside their description (\"make it professional\",
\"What's inside\", \"电子杂志 × 电子墨水\") that Claude was emitting
back inside JSON string values without escaping, breaking the parse.

Added one rule to the translator prompt — never use a straight double
quote inside a translated string, prefer single quotes / curly quotes
/ CJK 『 』 / 《 》 — and the previously stuck batch sailed through
clean: 22/22 ok, 0 retries, $0.85.

This closes the long tail of #3028:
- scenarios   11/11   ✓
- image-templates 45/45 ✓
- video-templates 50/50 ✓
- examples    140/140  ✓
- design-systems 142/142 ✓
- atoms       N/A (filtered from public catalog)

All 388 catalog-visible plugins now ship title_i18n + description_i18n
for all 17 non-English locales the landing page serves.

* fix(plugins): clean up four review-flagged i18n data issues

- apps/landing-page/app/_lib/plugins-i18n.ts:759 — Polish bucket
  label `examples: 'Przyklad'` was missing the diacritic; every
  other Polish string in the same block uses proper diacritics.
  Restore to 'Przykład'. (Reviewer: looper #4364985878.)

- video-templates/cinematic-route-navigation-guide — German
  title_i18n.de was a byte-for-byte copy of en ("Cinematic Route
  Navigation Guide") while the German description was already
  translated. Replace with "Cinematischer Routen-Navigationsleitfaden"
  to match the German voice the description sets.

- video-templates/hollywood-haute-couture-fantasy-video-prompt —
  Dutch title_i18n.nl was identical to en for the same reason.
  Translate the trailing noun phrase: "Hollywood Haute Couture
  Fantasy Videoprompt" (mirrors the Dutch description's compound
  word style).

- video-templates/video-seedance-three-kingdoms-guanyu-slaying-yanliang —
  Korean Hangul `돌진` had leaked into the Turkish description (a
  translation-pipeline artifact where the model copied the verb
  from the Korean output without translating it). Replace
  "saflarına돌진 eder" with the idiomatic Turkish "saflarına dalar".

All four are data-only fixes against existing manifests; no schema
changes, no reader changes. typecheck stays at 0 errors.

* fix(landing-page): localize aria-labels, alt text and BreadcrumbList JSON-LD on plugin detail page

The PR's prior rounds left six accessibility / structured-data
surfaces on `/{locale}/plugins/<slug>/` either entirely English or
mixing English chrome with the localized plugin title. Reviewer
flagged each one across multiple loops; this commit clears them all:

1. `aria-label` on the open-in-new-tab popout no longer reuses the
   visible label `pcopy.detailOpenInNewTab` (which carries the
   decorative `↗`). Added `detailOpenInNewTabAria` — same wording,
   no glyph — and the `<a aria-label>` consumes that key. The
   visible link text still ends in `↗`.

2. `<nav class="breadcrumb" aria-label="Breadcrumb">` now reads
   `aria-label={pcopy.breadcrumbLabel}`. Eighteen locales filled
   ("面包屑导航", "パンくずリスト", "Brotkrumen-Navigation",
   "Fil d'Ariane", "مسار التنقل", "İçerik haritası", ...).

3. Share-dialog `<button aria-label="Close">` now reads
   `aria-label={pcopy.shareDialogClose}`. Eighteen locales filled
   ("关闭", "閉じる", "Cerrar", "Закрыть", "إغلاق", ...).

4. Three template-literal a11y strings (`${pluginTitle} preview`,
   `Open interactive preview for ${pluginTitle}`, `${pluginTitle}
   interactive preview`) become function calls
   (`pcopy.previewImageAlt(t)`, `previewSummaryAria(t)`,
   `previewIframeTitle(t)`) so the sentence frame around the
   plugin title rotates with the page locale. Two `<img alt>` call
   sites (the static preview at line 210 and the click-to-expand
   thumbnail at line 179) both consume `previewImageAlt`.

5. `BreadcrumbList` JSON-LD position-2 now reads
   `name: pcopy.hubLabel` instead of hardcoded English `"Plugins"`.
   The visible breadcrumb at line 105 already renders
   `pcopy.hubLabel`; this aligns the structured data with the
   rendered chrome on every locale.

The new function-typed keys deliberately interpolate `pluginTitle`
(which is itself locale-resolved via `resolveBundledTitle`) so the
mixed-language guard from commit 002d457 is preserved: a manifest
without a per-locale title still flows through to a coherent
single-language a11y string because `pluginTitle` falls back to
English along with the rest of the section.

apps/landing-page typecheck stays at 0 errors.

Closes reviewer threads:
- #pullrequestreview-4364985878 (Open in new tab aria)
- #pullrequestreview-4368926224 (Polish typo + plus mixed-language alt/aria)
- #4373... (BreadcrumbList JSON-LD)
- #4374... (aria-label="Close" + aria-label="Breadcrumb")

* fix(landing-page): redirect legacy fa/hu/th /plugins/ paths to canonical

When the new `/{locale}/plugins/...` short-code wrappers landed, the
legacy catch-all `pages/[locale]/[...path].astro` dropped `'plugins'`
from its `paths` list. That intentionally avoids serving stale
marketplace-registry placeholder routes for the modern landing
locales — but it also takes `/fa/plugins/`, `/hu/plugins/`, and
`/th/plugins/` from 200 to 404, because those three legacy locales
live only in the old `_lib/i18n.ts:LOCALES` set and are not part of
`LANDING_LOCALES` (the modern 18-locale list the new wrappers serve).

Three `301`s in `_redirects` send those legacy URLs to the canonical
English `/plugins/...` so SEO and inbound links keep working until
the legacy locale set is retired entirely.

Reviewer thread (#pullrequestreview-4364052045) flagged this as a
non-blocking regression across multiple loops; this commit closes it.

* ci(landing-page): add merge_group trigger so the queue can clear PRs

`landing-page-ci.yml` only fired on `pull_request` and `push:main`,
which meant the required `Validate landing page` and
`Strict PR visual tests` checks never dispatched against the
`merge_group` ref the merge queue creates. The queue then sat at
"awaiting checks" until it timed out and ejected the PR (the
deadlock observed during the 5/26 release window).

Adding a `merge_group: { types: [checks_requested] }` trigger to
the same workflow lets the queued ref reuse the existing job graph,
matching the pattern in `ci.yml` which already wires `merge_group`.

Also drops `plugins/**` into the same paths filter as `pull_request`
since the new bundled-plugins reader (commit 7ddfe364) consumes
those manifests' `title_i18n` / `description_i18n` maps and the
landing-page CI must rerun when manifest data changes.

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-27 09:30:59 +00:00
lefarcen
7312c64580
ci(landing): split landing deploy into staging gate + manual production (#2994)
* 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.
2026-05-26 14:05:04 +00:00
Jane
40ae0836dd
feat(landing-page): rebuild plugins library to mirror in-app taxonomy (#2926)
* feat(landing-page): synthesize fallback preview cards for instruction skills

The skill catalog renders a diagonal-stripe placeholder for any skill
without a runnable example.html, which leaves ~70% of /skills/ as a
field of bare grey thumbs (instruction skills like copywriting,
creative-director, color-expert, brainstorming have no static demo
because their output depends on the agent's input).

Synthesize a typographic editorial card from each SKILL.md frontmatter
and screenshot it through the same Playwright pipeline that handles
real demos, so every catalog row carries a thumbnail. Cards include:

  - OPEN DESIGN · SKILL top label + Nº NNN index (1..96 over the
    instruction subset, sorted by od.featured then alphabetical)
  - Big Playfair Display slug with a coral dot accent
  - Italic serif description clamped to 3 lines
  - mode/category chips + "Curated from <author>" attribution
  - Warm-paper background with a subtle 135° stripe to thread the
    landing's existing visual language

Bundle a few related improvements caught while building this:

  - SkillRecord gains a `kind: 'instruction' | 'template'` field so
    the detail page can render differently per kind (instruction
    skills now render the SKILL.md body inline as "About this skill",
    template skills keep the click-to-expand iframe demo).
  - Catalog row thumbnails switch from the bespoke IntersectionObserver
    pipeline to native `loading="lazy"` (with eager + fetchpriority=high
    on the first 3). The observer's swap latency stranded mid-list
    rows on the SVG placeholder during fast scrolls; native lazy uses
    the browser's 1250-3000px lookahead so the placeholder flash is
    gone.
  - precise-lazyload rootMargin bumped to 1500px for any remaining
    data-precise-src callers.
  - CI cache key for generated previews now folds in
    fallback-preview-card.ts so a template tweak invalidates the cache.

* feat(landing-page): rebuild plugins library to mirror in-app taxonomy

The marketing site's `/skills/`, `/templates/`, `/systems/`, `/craft/`
top-level entries were organized around author-supplied `od.mode` /
`od.scenario` taxonomies that visitors never see inside Open Design
itself. The in-app Plugins home (`apps/web/src/components/plugins-home/`)
groups every bundled plugin by the artifact it produces — Prototype,
Live Artifact, Slides, Image, Video, HyperFrames, Audio — and that's
the language users encounter the moment they open the product.

This PR rebuilds the public library around the same taxonomy and the
same data source so a visitor reading "Templates · 231" on the
marketing site sees the same 231 inside the app.

## What changes

- New top-level `/plugins/` hub: four tiles (Templates, Skills,
  Systems, Craft) with live counts pulled straight from
  `plugins/_official/<bucket>/<slug>/open-design.json` — the daemon's
  bundled-plugin registry.
- `/plugins/templates/` lists every bundled plugin that lands in one
  of the seven artifact kinds. Seven sub-routes
  (`/plugins/templates/prototype/`, `/deck/`, `/image/`, `/video/`,
  `/hyperframes/`, `/audio/`, `/live-artifact/`) carry the same chip
  rail with an active state, so visitors can switch artifact kinds
  with one click without losing the rail.
- Each artifact-kind sub-route shows a Scene chip rail when the kind
  has scene buckets (Prototype / Slides / Image / Video each get
  five-six). The Scene filter runs client-side via inline `style.display`
  toggles; URLs stay one-per-kind so we don't multiply 25 × 18 locales
  worth of static pages just for filter combinations.
- `/plugins/skills/` collects the instruction-only entries (mode
  doesn't fit any of the seven kinds) — copywriting, color theory,
  creative direction, brainstorming, etc.
- `/plugins/systems/` lists the 150 bundled design systems via the
  legacy SystemCard renderer (palette swatches, tagline) so the
  visual treatment matches the in-product library.
- `/plugins/craft/` keeps the existing craft principles list.
- `/plugins/<manifest-id>/` detail pages built from manifest metadata:
  hero (poster image or playable Cloudflare Stream MP4 for video
  templates), author / mode / scenario / tags, GitHub source link.
  Author URLs pointing at the `nexu-io` org redirect to the
  `nexu-io/open-design` repo so the attribution is actionable.
- Header dropdown labelled "Plugins" with the four sub-routes; footer
  Library column updated to match.
- Old marketplace registry pages under `/plugins/` and
  `/[locale]/plugins/` removed (they were a dormant placeholder UI;
  the actual manifests it tried to load lived nowhere). The rest of
  the legacy plugin-registry loader stays intact for any other
  consumer.

## Preview generation

Bundled plugins ship `od.preview.poster` URLs on R2 for image and
video templates; those are used directly. The other 293 entries
(html-mode examples, design-systems, scenarios) had no poster, so
`generate-previews.ts` was extended to:

1. Screenshot a local `example.html` referenced by `od.preview.entry`
   when present (134 examples).
2. Synthesize the same typographic editorial card the SKILL.md
   fallback uses, sourced from manifest title / description / mode /
   author (159 systems / scenarios / misc).

Output lands at `public/previews/plugins/<manifest-id>.png`. The
catalog loader checks for the local file when the manifest carries no
poster URL, so the row's `<img src>` always has something to point at.

Result: every catalog row and every detail page has a thumbnail;
visiting `/plugins/templates/video/` shows the same 48 entries the
in-app Plugins home shows, hyperframes the same 13, etc.

## Counts

- Templates: 231 (Prototype 59 + Slides 59 + Image 46 + Video 48 +
  HyperFrames 13 + Audio 1 + Live Artifact 5)
- Skills: 15
- Systems: 150
- Craft: 11

Atoms (13 infrastructure plugins, `od.kind === 'atom'`) are filtered
to mirror the in-app behaviour.

* fix(landing-page): use Astro 6 render() helper for SKILL.md body

Astro 6 dropped `entry.render()` in favour of a top-level `render(entry)`
helper imported from `astro:content`. The instruction-kind skill detail
page was still using the legacy method, which compiled locally on Astro
6 only because tsx ignored the missing prototype method, but `astro
check` (run in CI) flagged it as ts(2551) and broke the workflow.

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-26 02:49:58 +00:00
Jane
829fc01c1c
feat(landing-page): detail pages — interactive preview, share row, dual CTAs (#2679)
* feat(landing-page): detail pages — interactive preview, share row, dual CTAs

Joey requested three additions to every `/skills/<slug>/` and
`/templates/<slug>/` detail page, with opendesigner.io's skills
catalog and youmind.com's seedance prompt page as references.

What
- **Interactive preview**: a `<details>` toggle below the static thumb
  reveals an `<iframe sandbox>` rendering the canonical artifact
  (`/skills/<slug>/example.html` for skill-template origins,
  `/templates/<slug>/template.html` for live-artifact origins). The
  iframe loads lazily — only on first toggle — so the page stays fast.
  An "Open in new tab ↗" pill on top-right of the frame links to the
  same URL standalone.
- **Six-channel share row**: Reddit, X, LinkedIn, Facebook, Email,
  Copy-link. Each anchor is a vendor "intent" URL (no tracker SDKs);
  the copy-link button uses the Clipboard API with a `prompt()`
  fallback for older Safari / embedded webviews. Wired by a small
  handler appended to `header-enhancer.astro`.
- **Two primary CTAs** in the detail-actions row:
  - "Use this skill →" / "Use this template →" routes to
    `/quickstart/?skill=<slug>` (or `?template=<slug>`). The OD
    desktop client has no public protocol handler yet, so a
    `od://skill/<slug>` deep link would 404. Quickstart is the v1
    pivot; once the client registers a scheme, the anchor flips to
    a JS try-`od://`-then-fallback without changing the page surface.
  - "Find on GitHub →" deep-links into the source folder.

Share copy keeps "open-source Claude Design alternative" front and
center across every channel — same brand keyword Google associates
with the homepage and `/alternatives/claude-design/`, so each social
click reinforces the same entity claim. Per-skill name + summary
follow so a reader who lands on a friend's tweet has a concrete
reason to click.

  - X intent: "I'm using <skill> from @opendesignai — the open-source
    Claude Design alternative.\n\n<description>"
  - Reddit submit title: "<skill> — open-source Claude Design alternative"
  - Email subject: same as Reddit; body: "I thought you'd like this —
    <skill>, an open-source Claude Design alternative skill from Open
    Design.\n\n<description>\n\n<url>"
  - LinkedIn / Facebook: URL-only (those vendors auto-fetch OG meta,
    so they read the existing canonical title + image).

Surface area
- Marketing site only. `apps/landing-page/app/pages/skills/[slug].astro`,
  `pages/templates/[slug].astro`, `_components/header-enhancer.astro`,
  `sub-pages.css`.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI surfaces.
- No new top-level dependencies. WeChat QR was dropped from the v1
  scoping in favor of Joey's revised channel set; brings Reddit and
  Facebook in instead.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: `/skills/deck-swiss-international/` shows all six share
  buttons, both CTAs, and the iframe `<details>` toggle. Same on
  `/templates/magazine-poster/`.
- Local dev: Reddit submit URL contains the SEO keyword in the title
  param; X intent URL contains the @opendesignai mention + keyword
  in the tweet body; Email mailto: subject + body wired correctly.

Followups
- Once OD desktop client registers a `od://` scheme, flip the "Use
  this skill" anchor to JS-driven try + fallback so installed users
  bypass /quickstart/.
- Translate the share copy + CTA labels across the 18 landing
  locales (currently English-only).
- `i18n.ts` `ui.catalog.skills` keys could absorb the share-copy
  template if we want per-locale share text in the future.

* fix(landing-page): preview clicks the thumb; CTA goes to releases

Two follow-ups to #2679 against Joey's review.

1. Preview UX: the thumb is the trigger
   The previous shape rendered a static thumb followed by a separate
   "View interactive preview ▸" disclosure row underneath. Joey wanted
   one composed unit: click the thumb itself to open the live frame.
   Wraps the existing `<details>` so that `<summary>` IS the thumb
   image (with a hover overlay revealing "Click for live preview ↗"),
   and once open the summary hides so the iframe lands in the same
   visual slot. The figcaption moves below the open/closed unit so it
   labels both states identically.

2. "Use this skill" / "Use this template" → /releases
   Sends users straight to the desktop-app release page rather than
   pivoting through /quickstart/. The flow is now concrete (download
   the binary now) instead of asking users to read an install doc as
   step 0. Once the desktop client registers a `od://skill/<slug>`
   protocol handler, this anchor flips to a JS try-deep-link-then-
   fallback without changing the page surface.

Note on the other two issues Joey raised:
- example.html 404: production has all 4 example files at HTTP 200
  (verified with curl). The 404 in his screenshot was production
  serving the previous deploy that pre-dates this PR; the fix is in
  flight, not a missing route. Once #2679 deploys, the iframe will
  resolve cleanly.
- Empty share copy: same root cause. Production HTML still rendered
  the pre-#2679 share row (no copy at all). Local dev confirms the
  X intent URL contains the full "I'm using <skill> from
  @opendesignai — the open-source Claude Design alternative…"
  string in the `text` param; Reddit submit URL contains the
  "<skill> — open-source Claude Design alternative" title; Email
  mailto: subject and body are wired. LinkedIn and Facebook are
  URL-only by their vendor design — those platforms read the OG
  meta tags from the destination page itself.

Surface area
- Marketing site only. `pages/skills/[slug].astro`,
  `pages/templates/[slug].astro`, `sub-pages.css`.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI.
- No new dependencies.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: skill detail thumb shows the live-preview overlay on
  hover; click opens the iframe in the same frame. Use this skill
  → opens https://github.com/nexu-io/open-design/releases. Same on
  the templates detail page.

* fix(landing-page): example.html copy step + share dialog with copy-then-paste flow

Two follow-ups against Joey's review of #2679.

1. example.html 404 — production was SPA-falling back to the homepage

The "404" Joey screenshotted on
`/skills/deck-guizang-editorial/example.html` was a Cloudflare Pages
SPA fallback: the URL returned HTTP 200 but the body was the
homepage HTML, so the iframe loaded "the homepage inside the iframe"
which the browser displays as broken-page. Root cause: the build
artifact never contained `out/skills/<slug>/example.html`. Astro
generates `<slug>/index.html` for the detail page from `[slug].astro`,
but the canonical `example.html` next to the SKILL.md file in the
repo root never gets copied into `out/`.

Adds `scripts/copy-example-html.ts` and chains it into the
`build` script. After `astro build`, the script walks:

  - `skills/<slug>/example.html` → `out/skills/<slug>/example.html`
  - `design-templates/<slug>/example.html` → `out/skills/<slug>/example.html`
    (design-templates surface as skill-template-origin records in the
    catalog and the iframe targets the `/skills/<slug>/example.html`
    path for those.)
  - `templates/live-artifacts/<slug>/template.html` → `out/templates/<slug>/template.html`
    (live-artifact-origin records — the iframe targets template.html.)

Source files that don't exist are silently skipped. The script
prints a summary line so the build log makes the count visible.

2. Share UX — modal with copy-then-paste flow

The previous inline 6-button row had two problems Joey called out:
  - Position was below the meta block, not prominent enough.
  - LinkedIn and Facebook ignore `text` pre-fill params, so users
    landing on those platforms saw an empty composer with no idea
    what to write. X / Reddit pre-fill works but truncates Chinese
    unpredictably.

Replaces the row with a `<dialog>` modal:
  - A `Share ↗` button sits inside `.detail-actions` next to the
    primary CTAs, so it has equal visual weight.
  - Clicking opens the dialog with the canonical share copy
    (containing the brand SEO keyword "open-source Claude Design
    alternative") in a readonly `<textarea>`.
  - `Copy text` button writes the textarea contents to the clipboard
    (with a `prompt()` fallback for older browsers) and flashes the
    coral confirmation state.
  - `Copy link only` writes just the URL.
  - Below: a row of platform jump buttons (X · LinkedIn · Reddit ·
    Facebook · Email). Each opens the vendor's compose URL in a new
    tab. The user pastes the already-copied text — uniformly
    reliable across every platform.
  - Modal closes via the × button (form method="dialog") or Escape.

Native `<dialog>` element + `showModal()` API. No new dependencies;
the JS handler lives in the existing `header-enhancer.astro`
inline script alongside the headroom + stars + hamburger handlers.

Surface area
- Marketing site only. `pages/skills/[slug].astro`,
  `pages/templates/[slug].astro`, `_components/header-enhancer.astro`,
  `sub-pages.css`, plus the new `scripts/copy-example-html.ts` and
  one-line `package.json` build script change.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI.
- No new dependencies.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: `/skills/<slug>/` shows the `Share ↗` trigger inside
  detail-actions; clicking opens the modal with the readonly
  textarea pre-filled with the canonical share copy. Copy text /
  Copy link only both flash coral on click and write to clipboard.
  Platform buttons open compose pages in new tabs.
- After deploy: `/skills/<slug>/example.html` will resolve to the
  actual canonical example output rather than SPA-falling back to
  the homepage. Same for templates.

* fix(landing-page): example.html endpoint routes + locale-aware share + brand logos

Three follow-ups against Joey's review of #2679 round 2.

1. example.html 404 — root cause + proper fix
   The 404 Joey kept seeing was real, not a deploy lag: nothing in
   the build pipeline copied `skills/<slug>/example.html` from the
   repo root into the landing-page output. Astro generated only the
   detail-page `index.html`; Cloudflare Pages SPA-fell-back to the
   homepage on requests for `example.html`, which the browser
   rendered as "wrong page in iframe" and Joey read as 404.

   Replaces the post-build copy script (`scripts/copy-example-html.ts`,
   removed) with two Astro endpoint routes:

   - `pages/skills/[slug]/example.html.ts` — streams the canonical
     example for skill-template-origin records, including the
     design-templates passthrough
     (`design-templates/<slug>/example.html` → same URL).
   - `pages/templates/[slug]/template.html.ts` — streams the canonical
     artifact for live-artifact-origin templates.

   Both use `getStaticPaths` so Astro pre-renders into the static
   build artifact under `out/`. Works in dev (Astro dev server runs
   the endpoint live) and prod (file is on disk after `astro build`).

   Required moving `pages/skills/[slug].astro` →
   `pages/skills/[slug]/index.astro` (and same for templates) because
   Astro can't have BOTH a `[slug].astro` file AND a `[slug]/`
   directory with dynamic param children at the same level. The
   `[locale]/skills/[slug].astro` re-exporters were updated to point
   at the new index files.

   `trailingSlash: 'always'` rewrites endpoint URLs to `path/`, so the
   iframe `src` and "Open in new tab" anchor now use
   `example.html/` and `template.html/` (with trailing slash). Tested
   locally: HTTP 200 + real example HTML in the body.

2. Share copy now per-locale; description dropped
   The previous template hardcoded the framing in English ("I'm using
   X from @opendesignai…") with the description following from
   `skill.description`. Joey's catch: when the SKILL.md description is
   in one language and the page locale is another, the share text
   reads as a forced bilingual mash-up.

   Adds an inline `SHARE_COPY` table per landing locale (18 entries,
   one per locale). Drops the description from the share template
   entirely — the framing + URL is enough to prompt a click, and
   removes any chance of a bilingual mismatch when SKILL.md
   frontmatter happens to be in a non-matching language.

   The brand keyword "open-source Claude Design alternative" stays
   English because that's the canonical search query Google
   associates with the domain — translating it would split the
   entity claim. Surrounding sentence translates per locale so the
   message reads as one voice.

   Same template added for templates/[slug]/index.astro.

3. Share dialog UI: brand logos for the 4 platform jump buttons; Email dropped
   Replaces the previous text labels (`X` / `LinkedIn` / `Reddit` /
   `Facebook` / `Email`) with inline-SVG brand logos. Per Joey's
   revision the Email channel was dropped — Gmail / Outlook
   pre-fill is reliable but the audience reach is much smaller than
   the four social platforms, and removing it tightens the row.

   Logos are SimpleIcons-style SVG paths inlined directly (no font
   dependency, no external icon library). Each button keeps an
   `aria-label` plus a visually-hidden `<span class="sr-only">`
   for screen readers.

Surface area
- Marketing site only. `pages/skills/[slug]/index.astro`,
  `pages/skills/[slug]/example.html.ts`,
  `pages/templates/[slug]/index.astro`,
  `pages/templates/[slug]/template.html.ts`,
  `_components/header-enhancer.astro`, `sub-pages.css`,
  `package.json` (build script revert), and the two `[locale]/...`
  re-exporters.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI surfaces.
- No new top-level dependencies.
- The two restructured detail pages keep their existing route URLs
  and existing static-paths logic — only the file location changed.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: `/skills/deck-guizang-editorial/example.html/` returns
  HTTP 200 with a 4942-byte body that's the actual canonical
  example output (not the homepage SPA fallback).
- Local dev: `/skills/deck-swiss-international/` share dialog shows
  4 brand-logo platform buttons (no Email); textarea contains the
  English-only framing + URL. `/zh/skills/...` shows the Chinese
  framing + URL with no English bleed-through.

* fix(landing-page): punchier share copy with emojis across 18 locales

The previous share template ("I'm using <name> from @opendesignai —
the open-source Claude Design alternative.\n\n<url>") was too flat to
spark a click — Joey called it out as 平淡 with the keyword
front-and-center but no hook.

New shape: three-line punchy block with emojis as visual anchors.

Skills surface (`/skills/<slug>/`):

  🎨 Just discovered <name> on @opendesignai — the open-source
     Claude Design alternative.
   Local-first · BYOK · your agent does the design.

  → <url>

Templates surface (`/templates/<slug>/`):

  🎨 Just forked <name> from @opendesignai — the open-source
     Claude Design alternative.
   Templates as files, not vendor docs. Fork → swap → ship.

  → <url>

Pattern per locale:
  - Line 1: action verb hook (`Just discovered` / `Just forked` /
    locale equivalent like `安利一个` / `推薦一個` / `Gerade entdeckt` /
    `Découvert` / etc) + skill name + brand keyword.
  - Line 2: tight value-prop with `·` separators — Local-first ·
    BYOK · agent does the design (skills) or Templates as files,
    not vendor docs (templates).
  - Line 3: → URL.

Both lines lead with an emoji (🎨 then ) so the post visually pops
in a feed. The brand keyword "open-source Claude Design alternative"
stays English in every locale (canonical search query for the
domain); surrounding sentence translates per locale.

All 18 landing locales rewritten — ar, de, en, es, fr, id, it, ja,
ko, nl, pl, pt-br, ru, tr, uk, vi, zh, zh-tw. Skills and templates
each have their own `SHARE_COPY` table; the templates variant has
fork-flavored framing because the user action there is fork-and-ship,
not run-once.

Surface area
- Marketing site only. `pages/skills/[slug]/index.astro` and
  `pages/templates/[slug]/index.astro`.
- No other files touched. No new dependencies.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: en / zh / ja all render with emojis intact and
  language-specific framing; X intent URL preserves the multiline
  breaks via `\n` in the `text` query param.

* fix(landing-page): restore post-build copy step for preview iframes

The detail-page interactive preview iframe pointed at endpoint routes
(`pages/skills/[slug]/example.html.ts`,
`pages/templates/[slug]/template.html.ts`) introduced in 0d9c9a5, but
Astro 6 silently drops `pages/<dir>/[slug]/<file>.<ext>.ts` routes
under dynamic segments at build time — even with `export const
prerender = true` — so the URLs returned 404 in both `pnpm dev` and
the production build.

Verified locally: dev server `curl /skills/<slug>/example.html` → 404,
`find apps/landing-page/out -name 'example.html'` → 0 files after a
clean `pnpm build`.

Restore the post-build copy step that 138cbd2 had: an `astro build`
postscript that mirrors `skills/<slug>/example.html` and
`design-templates/<slug>/example.html` into the static output. While
re-introducing the script, also address the live-artifact preview
mismatch flagged by review:

  - Live-artifact records carry a `live-` slug prefix from
    `shapeLiveArtifactTemplate()` in `_lib/catalog.ts`, so the iframe
    URL is `/templates/live-<slug>/preview.html` — copy the source
    file into `out/templates/live-<slug>/preview.html` to match.
  - Serve `index.html` (the rendered preview) rather than
    `template.html` (which still contains `{{data.*}}` placeholders).
    The iframe is for visitors and reviewers, not the template
    runtime.

Detail-page iframe `src` and "Open in new tab" link in
`pages/templates/[slug]/index.astro` already use `/preview.html`;
sub-pages.css comment kept aligned.

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-22 20:47:59 +08:00
ashleyashli
558fedd207
fix(landing): wire GA4 rollout config (#2615)
Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 14:56:58 +08:00
lefarcen
5f7d65d513
perf(landing): preconnect api.github.com + rAF-throttle scroll listener (#2666)
Two PSI-targeted wins (split from #2599 follow-up).

1. New `resource-hints.astro` mounted in every page's <head> declares
   `<link rel="preconnect" href="https://api.github.com" crossorigin>`.
   The inline enhancer script on /` issues 3 fetch() calls to
   api.github.com right after DOMContentLoaded (stars, latest release,
   contributors). Without preconnect each pays a full DNS + TCP + TLS
   handshake (~150-300ms) inline with the fetch. With preconnect those
   handshakes happen in parallel with HTML parse and all three share one
   warmed HTTP/2 connection.

2. Wrap the scroll listener's read + classList write in
   requestAnimationFrame. Trackpads and high-rate wheels fire scroll
   faster than display refresh, and every callback that hits classList
   triggers layout recalc. PSI was attributing ~700ms of "forced reflow"
   to the un-throttled version. The rAF gate collapses each burst to one
   DOM mutation per frame; `{ passive: true }` is preserved so the
   listener still doesn't block the scroll thread.

   Same throttling pattern mirrored to `header-enhancer.astro` (used by
   every sub-page) and `home-enhancer.astro` (kept in lockstep even
   though /` currently uses its own inline copy).

Expected PSI delta:
  - "Preconnect to required origins" hint: cleared
  - "Forced reflow" diagnostic 700ms → near zero
  - LCP: small bonus from earlier GH fetch warm-up (~100-300ms)
2026-05-22 14:06:39 +08:00
lefarcen
7f03030f3f
perf(landing): self-host fonts + inline critical CSS (#2599)
* perf(landing): self-host fonts + inline critical CSS

PageSpeed Insights flagged ~2.3s of render-blocking on /:
  globals.css   12.9 KB external link, 160ms
  fonts CSS     2.2 KB  fonts.googleapis.com, 750ms
  + 4 woff2     ~1200ms each from fonts.gstatic.com

Two changes drop that whole chain:

1. Self-host fonts via @fontsource-variable/{inter,inter-tight,
   playfair-display,jetbrains-mono}. Each family ships a single variable
   woff2 (covers all weights we use) that Astro bundles into /_astro/*
   alongside the rest of the build, served same-origin through CF Pages —
   no separate TLS handshake, no Google Fonts CSS round-trip. The CSS
   variable names get an extra alias in front (`'Inter Tight Variable',
   'Inter Tight', ...`) so a system fallback still works if the package
   ever ships under a different family name.

2. `astro.config.ts: build.inlineStylesheets: 'always'` inlines every
   emitted <style> into the HTML <head> instead of emitting a separate
   /_astro/*.css link. The HTML grows from ~13KB to ~28KB (gzip) but
   loses one stylesheet round-trip + the entire @font-face chain that
   used to gate text rendering.

Component cleanup: the `<FontStylesheet>` component (preconnect + link to
fonts.googleapis.com) is no longer needed and is deleted, removed from
all 7 places that mounted it. og.astro keeps its own font setup since
it renders to a screenshot.

Expected effect (from PageSpeed Insights "Render-blocking requests"
diagnostic on the previous build):
  FCP  1.9s → ~1.2s
  LCP  2.2s → ~1.5s

Verified: pnpm typecheck 0 errors, pnpm build 1853 pages 78s, preview
serves /_astro/*.woff2 as font/woff2 same-origin, 0 fonts.googleapis or
fonts.gstatic references in the built HTML.

* perf(landing): include Playfair italic + bump nix pnpm-deps hash

Two follow-ups on the self-host fonts PR:

1. globals.css imported only `@fontsource-variable/playfair-display`,
   which ships @font-face for font-style: normal only. The previous
   Google Fonts URL included the italic axis (`ital,wght@0,500;1,400;
   ...`) and several rules (.roman, .work-rule .roman, .sec-rule .roman,
   plus 8 other italics across globals.css + sub-pages.css) render
   Playfair italics via `font-family: var(--serif); font-style: italic`.
   Without the italic face self-hosted, those would fall through to
   Times New Roman italic or browser synthesis. Adding
   `wght-italic.css` keeps the typography visually equivalent.

2. nix/pnpm-deps.nix uses a fixed-output derivation hash that has to
   match the pnpm vendored store; adding the four fontsource packages
   changed pnpm-lock.yaml so the hash has to be bumped to the value Nix
   reported in CI.

Codex (Looper reviewer) flagged #1 as non-blocking.

* perf(landing): pin fontsource versions exactly per repo guard

`pnpm add` defaulted to caret ranges (`^5.2.8`) but repo guard rejects
non-exact specs ("dependency specs must be exact versions like 1.2.3 or
workspace:*"). That was the actual cause of the Preflight + Validate
workspace failures — pinning to the locked versions Codex reviewer
called out:

  @fontsource-variable/inter             5.2.8
  @fontsource-variable/inter-tight       5.2.7
  @fontsource-variable/jetbrains-mono    5.2.8
  @fontsource-variable/playfair-display  5.2.8

`pnpm guard` now passes locally (6/6 tests).
2026-05-22 11:49:16 +08:00
Jane
3db1e27b81
fix(landing-page): translate Library/Tutorials, drop dropdown numbers, dedupe Product menu (#2610)
* fix(landing-page): translate Library/Tutorials, drop dropdown numbers, dedupe Product menu

Three small UX issues surfaced after #2605 landed Tom's full i18n
bundle. None of them block deploy; all of them affect every visitor
who lands on the site, especially the non-English ones.

Issues
1. The new Library / Tutorials nav labels in #2605's header
   restructure were hardcoded English. On `/zh/`, `/ja/`,
   `/de/`, etc. the rest of the nav was localized but those two
   labels stayed "Library" / "Tutorials" inside otherwise-translated
   chrome.
2. The four catalog facets inside the Library dropdown carried the
   live count badges from the previous top-row treatment ("Skills132",
   "设计系统150", etc.). Inside a dropdown they read as a glued-on
   suffix rather than a tag, and Joey called them out as "weird —
   drop them entirely".
3. The Product dropdown still listed Tutorials as a child item
   alongside Open Design and HTML Anything. After #2605 made
   Tutorials a standalone top-level link, the duplicate was
   confusing — same URL appearing in two places in the same nav row.
4. Chinese (zh and zh-tw) had `skills: 'Skill'` — the singular
   English word, untranslated in an otherwise fully Chinese nav.

Fixes
- Extends `HeaderCopy.nav` interface with `library` and `tutorials`
  keys; populates both across all 18 landing locales (en, zh, zh-tw,
  ja, ko, de, fr, ru, es, pt-br, it, vi, pl, id, nl, ar, tr, uk).
- Updates `header.tsx` to read `headerCopy.nav.library` and
  `headerCopy.nav.tutorials` instead of literal strings.
- Removes the four `<span class="dropdown-num">{count}</span>` badges
  from inside the Library dropdown items. The numbers still sit on
  the catalog index pages themselves (header counts, hero) and on
  the homepage; the dropdown is for navigation, not status.
- Removes the Tutorials `<li>` from the Product dropdown — Tutorials
  lives at the top-row level only. Updates the Product trigger's
  `is-active` set so it no longer highlights when on `/tutorials/`.
- zh/zh-tw `nav.skills` switches to `技能` (the actual translation
  every other Chinese localized site uses for "skills" in this
  context).

Bonus: catalog-row horizontal padding unified at 24px to match the
fix already applied across `featured-card` / `template-card` /
`system-card` / `source-card` (see PR #2600). The skill list grid
on `/skills/` was the last family still on `padding: 22px 0`.

Surface area
- Marketing site only. `apps/landing-page/app/_components/header.tsx`,
  `app/i18n.ts`, `app/sub-pages.css`, `app/globals.css`.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI surfaces.
- No new dependencies.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: `/zh/` shows `资源库 / 教程 / 博客`; `/zh/` Library
  dropdown shows `技能 / 设计系统 / 模板 / 工艺` without count badges.
- Local dev: `/` Product dropdown shows only Open Design + HTML
  Anything (no Tutorials child); top-row Tutorials link is the
  single canonical entry to that page.

* fix(landing-page): keep catalog-row horizontal padding at narrow widths

#2610's catalog-row 24 px fix only addressed the desktop rule.
The `@media (max-width: 720px)` override block still set
`.catalog-row a { padding: 18px 0 }` — zero horizontal — which is
what Joey's screenshot was actually showing on narrow viewports.

Switches the narrow padding to `18px 20px`. Matches the same
narrow inset `.source-card` already uses (line 1058 of this file)
so the catalog-row family stays in lockstep with the rest of the
catalog tile families at narrow widths.

Also drops the now-stale hover override (`padding-left: 8px;
padding-right: 8px`) — that rule existed only to compensate for
the zero-horizontal default and now produces a visible jolt
against the new 20 px base.

* fix(landing-page): featured-card description honors the 24px gutter

Same shorthand fragility pattern that #2600 fixed for template-card,
just one card family further down the file.

`.featured-card p` (the description line on each featured strip
tile, e.g. "Nº 0.001 deck-swiss-international" with the 16-列网格
blurb under it) had `margin: 0 0 12px` — three-arg shorthand,
which expands to TOP RIGHT BOTTOM = LEFT, so margin-left and
margin-right both got reset to 0.

The group rule `.featured-card a > * + *` had set the horizontal
margin to 24px so every text child sits inside a clean 24px gutter
from the card border. The shorthand on `<p>` overrode the group
rule and the description text bled right up against the card's
right edge.

Switches to `margin-bottom: 12px` only. Horizontal margin stays
owned by the group rule, matching the fix template-summary already
got in #2600.

Surfaced by Joey on `/skills/` featured strip — the exact symptom
he's been calling out for the last few rounds. Same shape, different
file.

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-22 01:57:53 +08:00
Jane
439f071cb0
feat(landing-page): replicate #2469 SEO content with deploy + regression fixes (#2605)
* chore(landing-page): bring PR #2469 content wholesale onto post-revert main

Step 1 of replicating @pftom's #2469 work without the deploy-blocking
issues that forced #2603. This commit copies the full \`apps/landing-page/\`
diff from #2469's HEAD (`9d2a4f1`) onto current main verbatim — every
i18n bundle, every page rewrite, every \`[locale]/\` wrapper. Subsequent
commits on this branch then surgically restore the SEO fixes that
#2469 silently regressed and configure the sitemap to survive the
Cloudflare Pages 25 MiB limit, so deploy is healthy when this lands.

What's in this commit
- Tom's i18n bundle: \`i18n.ts\` (5377 lines), \`home-page-i18n.ts\`,
  \`info-page-i18n.ts\`, \`landing-ui-i18n.ts\`, \`content-i18n.ts\`
  (~10K lines total of locale data)
- 18 landing-page locales: en, zh, zh-tw, ja, ko, de, fr, ru, es,
  pt-br, it, vi, pl, id, nl, ar, tr, uk
- All existing pages rewritten to consume the new i18n bundle
- Full \`[locale]/<route>/\` wrapper tree for every catalog page
- \`plugin-registry.ts\` rewrite, \`catalog.ts\` adjustments
- \`astro.config.ts\` route + sitemap reconfiguration
- \`public/_headers\`, \`public/_redirects\`, \`public/favicon.svg\` adds
- \`_components/locale-switcher-script.astro\` add

What's intentionally NOT done in this commit (handled in follow-ups
on this same branch):
- Restore brand mark 44px + rounded corners (was lost from #2588)
- Restore HA SoftwareApplication \`alternateName\` array (was lost from #2566)
- Restore HA \`url\` canonical pointing at the landing page (was lost from #2586)
- Restore Product/Library/Tutorials/Blog nav grouping (was lost from #2588)
- Restore catalog-card padding 24px (was lost from #2600)
- Configure sitemap to filter \`[locale]/\` routes so the generated XML
  stays under 25 MiB and Cloudflare Pages accepts the deploy
- Add \`/zh-CN/* → /zh/*\` redirects for backwards-compatibility with
  any externally-linked OD-canonical locale URLs

Validation so far
- \`pnpm --filter @open-design/landing-page typecheck\` — 0 errors

* fix(landing-page): unblock deploy + restore SEO regressions on top of #2469

Step 2 of replicating @pftom's #2469. The previous commit on this
branch brings #2469's content wholesale; this commit applies the
surgical fixes that make the result actually deploy and preserves
the SEO improvements that #2469 silently regressed.

Fix 1 — sitemap stays under Cloudflare Pages 25 MiB upload limit
- `astro.config.ts` `filter` now drops every `/{locale}/...` route
  so the sitemap only emits canonical English URLs.
- Locale variants are still discoverable via the
  `<xhtml:link rel="alternate" hreflang="...">` annotations the
  `namespaces.xhtml: true` option emits inside each canonical entry.
  This is Google's recommended pattern for a multi-language site.
- Verified: post-fix `out/sitemap-0.xml` = 179 KB (was 38.4 MiB
  on the prior attempt that forced #2603's revert).

Fix 2 — header brand block restored to the polished version
- Logo `width/height` 36 → 44 (matches PR #2588's brand-mark refresh
  for visual weight against the new black speech-bubble glyph)
- `.brand-meta` block ("Studio Nº 01 · Berlin / Open / Earth") removed
  from the header bar; the same editorial flourish still lives on the
  rotated `.side-rail .rail-text` pseudo-elements at page edges.

Fix 3 — header nav grouped into Library + standalone Tutorials/Blog
- Skills / Systems / Templates / Craft are now children of a Library
  dropdown (matches PR #2588's grouping). Each row keeps its count
  badge inline; the trigger highlights when any of the four facet
  pages is active.
- Tutorials and Blog stay as standalone top-row items (PR #2588's
  original decision after Joey's review on the Learn dropdown).
- Contact removed from the header — it was a same-page anchor that
  the footer already surfaces.
- Hardcoded "Library" / "Tutorials" labels match the brand-name
  pattern: unlocalized across all 18 landing-page locales.

Fix 4 — HA SoftwareApplication entity canonicalized on the LP again
- `alternateName` is back to an explicit array of real query
  variants `["html anything", "html-anything", "htmlanything",
  "HTML Anything Editor", "The agentic HTML editor"]`. #2469
  re-routed it through `copy.schemaAlternateName` which dropped
  the literal alias declarations Google needs for spaced-vs-
  hyphenated-vs-joined matching. (Restores PR #2566.)
- `url` flips back from `HA_URL` (the GitHub repo) to the LP URL
  itself, matching the `BreadcrumbList` block on the same page.
  GitHub repo lives in `sameAs` as a peer surface. (Restores PR
  #2586. Without this, Google credits the GitHub repo as canonical
  for the entity, which is the opposite of what this surface
  exists for.)

Fix 5 — catalog-card horizontal padding unified at 24 px
- featured-card 22 → 24, template-card 20 → 24,
  system-card 18 → 24, source-card 28 → 24.
- For template-card, also moved horizontal padding into the group
  rule exclusively so future siblings join without re-asserting
  margin shorthands. (Restores PR #2600.)

Fix 6 — `_redirects` for the locale-code rename
- This bundle uses `zh` / `zh-tw` / `pt-br` / `es` (the codes Tom's
  i18n.ts ships). The previous OD landing-page used `zh-CN` /
  `zh-TW` / `pt-BR` / `es-ES`. Externally-indexed and inbound-linked
  URLs against the old prefixes now 301 to the new canonical.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- `pnpm --filter @open-design/landing-page build` — completed
  successfully; 18,204 pages built; sitemap-0.xml is 179 KB
  (well under the 25 MiB Cloudflare Pages limit).

* docs: promote 'open-source alternative to Claude Design' to README H1

Brings the missing README and .gitignore changes from #2469 that the
first wholesale-checkout in this branch missed (the auto-pulled diff
scope was filtered to apps/landing-page/ initially).

What
- Every README.*.md (13 locale variants) now leads with the
  "open-source alternative to Claude Design" tagline as a subtitle to
  the project name in the H1 / first paragraph. This was @pftom's
  brand-positioning commit (`ee851dc`) on the original #2469 branch.
- `.gitignore` adds `growth/**` to keep growth-research scratch out of
  the repo.

Why
- The README is one of the highest-PageRank surfaces a GitHub project
  exposes to Google. Promoting the "alternative to Claude Design"
  framing into the H1/subtitle position makes the project surface for
  exactly the query the SEO work in this PR is trying to capture.
- Without this commit, the replicated #2469 in this branch would still
  rank against the previous H1 ("Open Design") on GitHub crawls,
  letting the SEO win at the LP fall short on the GitHub surface.

This is a strict subset of #2469's content — pure docs, no code,
no behavior change beyond what GitHub renders on the repo overview.

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-22 00:59:11 +08:00
Jane
eefaf4504a
Revert "Enhance landing page with SEO-focused content and FAQ section (#2469)" (#2603)
This reverts commit 26ee030b4c.

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-22 00:23:37 +08:00
Tom Huang
26ee030b4c
Enhance landing page with SEO-focused content and FAQ section (#2469)
* Enhance landing page with SEO-focused content and FAQ section

- Updated `.gitignore` to include growth directory.
- Modified `astro.config.ts` to prioritize high-intent landing pages for SEO.
- Added new FAQ styles and layout in `globals.css` for better user experience.
- Implemented FAQ section in `page.tsx`, ensuring it aligns with structured data requirements.
- Created dedicated pages for agents and alternatives to Claude Design, enhancing SEO and user navigation.
- Introduced comparison page for evaluating Open Design against competitors.
- Added favicon links component for consistent branding across all pages.

* Add SVG favicon and update favicon links for improved branding

* Enhance landing page with official source pillars for improved branding and navigation

- Added five canonical "official source" pillars to the homepage, reinforcing key links: official site, GitHub repository, releases, documentation, and Discord community.
- Updated URLs for releases, issues, documentation, and license to streamline access and improve user experience.

* Add locale support and enhance landing page with language switcher

- Introduced locale management with a new i18n module, defining multiple languages for the landing page.
- Implemented a locale switcher in the topbar and header, allowing users to select their preferred language.
- Updated global styles for the locale selector and adjusted layout for better responsiveness.
- Enhanced SEO by ensuring localized content is served based on user selection.
- Added a script for automatic locale detection and persistence in local storage.

* Implement localized routing and enhance navigation with href utility

- Added `stripLocaleFromPath` and `localizedHref` functions to manage locale-based URL paths.
- Updated `localePath` to normalize paths based on the detected locale.
- Refactored links in the header, footer, and main page components to utilize the new `localizedHref` function for improved navigation.
- Introduced locale-aware routing for new pages, ensuring consistent user experience across different languages.
- Enhanced SEO with alternate links for localized content in the sub-page layout.

* Update header component to use new logo format

- Replaced favicon image with a new logo in WebP format for improved performance and quality.
- Ensured consistent branding across the landing page with the updated logo.

* Enhance landing page with localization and new UI components

- Introduced a comprehensive localization schema for content management, allowing for multilingual support across various sections.
- Updated the blog and collection schemas to include internationalization (i18n) fields for better content localization.
- Implemented a new official source strip for improved navigation and branding, linking to key resources like the official site and documentation.
- Enhanced the locale switcher functionality, allowing users to select their preferred language with improved UX.
- Updated styles for the locale switcher and added new components for better responsiveness and accessibility.
- Refactored existing components to utilize localized URLs, ensuring a consistent user experience across different languages.

* Implement comprehensive localization and enhance landing page UI

- Introduced new localization features, including `EXTRA_LOCALIZED_HOME_BODY_COPY` and `EXTRA_LOCALIZED_LANDING_UI_COPY`, to support multilingual content across the landing page.
- Updated `astro.config.ts` to integrate internationalization (i18n) settings for the sitemap, improving SEO for localized content.
- Created new files for home page and info page internationalization, defining structured content for various languages.
- Enhanced the locale switcher functionality with improved UX, allowing users to easily select their preferred language.
- Refactored existing components to utilize localized content and URLs, ensuring a consistent experience across different languages.
- Made CSS adjustments for better responsiveness and accessibility in the UI components.

* Enhance landing page with new design templates and localization improvements

- Added support for design templates in the content management system, allowing for better organization and access to design resources.
- Implemented comprehensive localization for blog topics, enhancing multilingual support across various sections of the landing page.
- Updated the header component to include new product menu items, improving navigation and user experience.
- Refactored CSS for improved responsiveness and accessibility, including a new sticky chrome bar for better navigation.
- Enhanced the locale switcher functionality, ensuring a seamless experience for users selecting their preferred language.

* docs(readme): promote 'open-source alternative to Claude Design' tagline to subtitle across locales
2026-05-21 23:40:58 +08:00
Jane
e5bea2c134
feat(landing-page): SEO surfaces, schema upgrades, manifest — cherry-pick from #2469 (#2596)
Some checks failed
visual-baseline / Capture visual baselines (push) Waiting to run
ci / Detect CI change scopes (push) Successful in 0s
landing-page-ci / Validate landing page (push) Failing after 1s
landing-page-deploy / Deploy landing page (push) Has been skipped
nix-check / build (push) Failing after 1s
ci / Validate Nix flake (push) Failing after 1s
ci / Preflight (push) Failing after 1s
ci / Core package tests (push) Failing after 1s
ci / Tools workspace tests (push) Failing after 1s
ci / Daemon workspace tests (1/2) (push) Failing after 1s
ci / Daemon workspace tests (2/2) (push) Failing after 1s
ci / Web workspace tests (push) Failing after 1s
ci / E2E vitest (push) Failing after 1s
ci / Playwright critical (starters) (push) Failing after 2s
ci / Playwright critical (core) (push) Failing after 1s
ci / Build workspaces (push) Failing after 2s
ci / App workspace tests (push) Failing after 0s
ci / Validate workspace (push) Failing after 0s
ci / Runtime trace (push) Has been skipped
Pulls the high-value, low-conflict slices of @pftom's #2469 onto
current main. That PR's branch was based on `af63af3` (May 20) and
diverged from a fast-moving main, so its Tier 1 SEO content is shipped
here as a fresh PR rather than rebased.

What's included
- **5 new SEO landing pages**, English-only for v1:
  - `/official/` — brand-authority hub naming every alias the project
    is searched as (Open Design, OpenDesign, open-design, opendesign,
    Open Design AI, OD); Organization JSON-LD with the same alternateName.
  - `/quickstart/` — three-command install path with HowTo JSON-LD,
    requirements, expected output, troubleshooting, next steps.
  - `/agents/` — 17 BYOK adapters across three tiers, ItemList JSON-LD,
    BYOK explainer.
  - `/compare/` — evaluation-stage comparison hub vs Claude Design,
    Figma Make, v0, Lovable/Bolt, Open CoDesign; mandatory honest-limits
    block as FAQPage JSON-LD.
  - `/alternatives/claude-design/` — primary commercial-intent page;
    TL;DR, why-people-search, BYOK explainer, feature-comparison table,
    who-should-pick-which, migration steps, FAQPage JSON-LD.
- **`[locale]/` wrappers** for each of the 5 new pages, generated via
  `PREFIXED_LOCALES` from OD's existing `_lib/i18n.ts`. Each wrapper is
  a thin re-export of the canonical English page; per-locale translations
  are a follow-up.
- **`seo-head.astro` schema upgrades** — `inLanguage` on Article /
  WebSite / Blog JSON-LD, `availableLanguage` on the WebSite block,
  `og:locale:alternate` for non-current locales, hreflang `x-default`
  link.
- **`favicon-links.astro` extracted as a shared component** so every
  page emits the same icon set; new sizes (favicon-16x16, favicon-32x32,
  android-chrome-192x192, android-chrome-512x512) generated from the
  current brand mark; `site.webmanifest` published for PWA / Android
  install affordances.
- **`llms.txt`** restructured to lead with brand-alias declaration and
  list every new official entry point. The Sister Projects section
  introduced in #2566 stays.

What's intentionally NOT included
- @pftom's parallel i18n bundle (`i18n.ts` / `home-page-i18n.ts` /
  `info-page-i18n.ts` / `landing-ui-i18n.ts` / `content-i18n.ts`,
  ~10K lines of locale data). It duplicates and conflicts with OD's
  existing `_lib/i18n.ts` + `_lib/home-copy.ts`. Adapting the new
  pages to OD's existing system instead keeps the codebase on a
  single source of truth; per-locale translations of the new pages
  can land as a separate PR.
- Mass-rewrites of existing pages (homepage, HA, Skills/Systems
  catalogs, blog detail). #2469's branch base predates a number of
  in-flight PRs that touched those files heavily; cherry-picking the
  rewrites would re-litigate already-merged work.
- The `[locale]/<existing-route>/` routing tree for Skills, Systems,
  Templates, etc. Without translations, mirroring 26 routes per locale
  produces duplicate-content signals; that lift belongs with a
  per-locale copy push.

Surface area
- Marketing site only. No `apps/web`, no `apps/daemon`, no contracts,
  no CLI surfaces. No new dependencies. No env vars.
- New favicon assets are static PNGs generated from the canonical
  brand-mark source via `magick`; committed as files, no pipeline.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev server: every new English route returns 200; every
  `[locale]/<route>/` variant (e.g. `/zh-CN/agents/`) returns 200;
  `/favicon.ico` and `/site.webmanifest` resolve.
- Local dev server: `seo-head.astro` emits the new `inLanguage`,
  `availableLanguage`, `og:locale:alternate`, and `hreflang=x-default`
  signals; `<FaviconLinks />` renders the consolidated icon set
  including the new manifest link.

Followups
- Translate the 5 new pages and replace the English-content wrappers
  under `[locale]/`.
- Swap `sub-page-layout.astro` to use `<SeoHead />` so the new schema
  upgrades reach all sub-pages, not just the homepage and any future
  pages that opt into SeoHead directly.
- Mass-update the homepage and HA page along the lines of #2469's
  rewrite, but rebased onto current main rather than off `af63af3`.

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-21 23:03:21 +08:00
Jane
682a9c9a9a
feat(landing-page): group header nav into Product / Library / Learn (#2588)
* feat(landing-page): group header nav into Product / Library / Learn

Reduces the top-level nav from 7 link slots (Product, Skills, Systems,
Templates, Craft, Tutorials, Blog, Contact — plus the locale switcher
and two CTAs) to 3 dropdown groups, eliminating the row-overflow
that forced the brand sub-meta to truncate at narrow desktop widths
and cramming the hamburger panel on tablet.

Why
- The four catalog pages — Skills, Systems, Templates, Craft — share
  the same shape: a list page that links into per-record details.
  They are facets of one library, not competing peer destinations.
  Letting each occupy its own top-row slot was a policy of last
  resort when there were only four; with Product newly added and
  Tutorials newly added since #2452 + #2266, the row no longer fits.
- Tutorials and Blog are both editorial reading surfaces (videos
  vs articles), so they pair naturally under one Learn group.
- Contact in the top nav was always misleading — it links to
  `#contact`, a same-page anchor on the homepage CTA section. The
  footer already lists it. Promoting it to top-row real estate while
  the row was overflowing made no sense.

What
- New `Library` dropdown holds Skills (132) / Systems (150) /
  Templates (111) / Craft (11). Each row inside the panel keeps its
  count badge inline beside the label, plus a one-line blurb so the
  panel reads as a directory rather than an undifferentiated list.
  The trigger highlights `is-active` whenever any of the four facet
  pages is the active route, so users still see "you are inside
  Library" feedback.
- New `Learn` dropdown holds Tutorials and Blog. Same pattern: inline
  blurbs, parent-highlight when either child page is active.
- `Contact` removed from the header. The footer surface still lists
  it; the top-row capacity it occupied is reclaimed.
- `.dropdown-num` CSS class added for the inline count badge inside
  dropdown rows, distinct from the existing `.num` (which floats
  above top-row links as a superscript).

Active-state semantics are preserved — every sub-page that already
passes `active="skills"`, `active="systems"`, etc. keeps working
without changes; the new Library trigger reads those same values
to decide whether to highlight itself.

Surface area
- Marketing site only. No `apps/web`, no `apps/daemon`, no contracts,
  no CLI surfaces. No new dependencies. No i18n keys (existing
  `copy.navSkills` / `copy.navSystems` / `copy.navTemplates` /
  `copy.navCraft` / `copy.navBlog` keep working; "Library" and
  "Learn" are universal English labels that don't need translation
  tables for this iteration).

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev server at narrow breakpoints (≤1080px) — hamburger panel
  flat-expands the new Library / Learn dropdowns; existing handler
  closes the panel on link click.
- Local dev server at desktop widths (≥1180px) — top row now reads
  Product · Library · Learn, matching the design intent.

* feat(landing-page): polish brand mark — bigger glyph, single-line wordmark

Three small refinements to the header brand block, requested after
the new logo + grouped-nav landed:

1. The "Open Design" wordmark was wrapping to two lines once the new
   nav groups (Product · Library · Learn) sat alongside it on narrow
   desktop widths. The hamburger fallback already hides the entire
   nav at ≤1080px so the wordmark has plenty of horizontal room
   there — wrapping was a layout glitch, not a width constraint.
   Applies `white-space: nowrap` on `.brand` and adds an explicit
   `.brand-name` span so the rule is intent-bearing rather than
   relying on the parent inline-flex behaviour.

2. The brand mark felt undersized against the wordmark's optical
   weight, especially with the new black mark where the negative
   space inside the speech bubble visually shrinks the glyph. Bumps
   the displayed size from 36×36 to 44×44 (+22%) in both the header
   and the footer, so the proportion matches what users see in the
   logo.webp source.

3. Removes the `<span class="brand-meta">` block ("STUDIO Nº 01 ·
   BERLIN / OPEN / EARTH"). It was decorative metadata that earned a
   third of the brand block's horizontal real estate without
   carrying any user-facing function. Drops the JSX, drops the
   styles, drops the now-stale `display:none` overrides at the 1180
   and 880 breakpoints. The same rotated brand-string still lives on
   the side-rail pseudo-elements at the page edges (`.side-rail
   .rail-text`) — that surface is the canonical home for that
   editorial flourish, not the nav.

Surface area
- Marketing site only. `_components/header.tsx`, `page.tsx` (footer
  brand block), and `globals.css`. No `apps/web`, no `apps/daemon`,
  no contracts, no CLI. No new dependencies, no i18n changes.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev server: header HTML no longer contains `brand-meta`;
  the brand block on `/` and any sub-page renders the bigger 44px
  glyph + single-line "Open Design" wordmark.

* refactor(landing-page): unbundle Learn dropdown and round the brand mark

Two small follow-ups after looking at the rendered nav:

1. Reverts the Learn dropdown back to two standalone top-row links
   (Tutorials, Blog). Rolling exactly two items into a dropdown adds
   a click without reclaiming any horizontal space — the Library
   grouping is what genuinely cleared the row, and Tutorials + Blog
   can live side by side. Active-state semantics are preserved
   automatically since both pages already pass `active="tutorials"`
   and `active="blog"`.

2. Adds `border-radius: 10px` to the brand-mark image. The source
   PNG is a solid-fill square; clipping the corners gives the mark
   the modern app-icon silhouette (~22% of side length, matching
   the iOS / macOS convention) so it reads as a brand glyph rather
   than a raw screenshot next to the wordmark. Applies to both the
   header and footer brand blocks via the shared `.brand-mark img`
   selector.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev server: top row reads `Product ▾ · Library ▾ · Tutorials · Blog`
- Local dev server: brand mark renders with rounded corners on `/`
  and any sub-page (header + footer reuse the same `.brand-mark`
  styling)

---------

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-21 21:15:48 +08:00
Eli-tangerine
c37c691d82
[codex] Add landing page Google Analytics (#2582)
* Add Claude-style design system workflow

* Merge design system workflow into main

* Restore design system workflow UI styles

* Fix design system setup scrolling

* Fix design system setup connector button

* Preserve connector auth link after popup block

* Simplify connected GitHub setup state

* Open generated design system workspace project

* Summarize design system auto prompt in chat

* Add bounded GitHub connector design intake

* Prefer path-scoped GitHub intake tools

* Restore branch GitHub design context intake

* Restore design system review workspace

* Restore design system manager tab

* Let design system workflow routes own details

* Open editable design systems as projects

* Restore design system workspace coverage

* Fix bounded GitHub connector intake

* Hide design system review while generating

* Suppress design system generation questions

* Constrain GitHub design intake to bounded command

* Tolerate oversized GitHub metadata during intake

* Rebuild daemon CLI when sources change

* Fallback when GitHub connector snapshots are rate limited

* Allow GitHub intake without Composio

* Use native GitHub auth for design intake

* Remove design system review group heading

* Improve design system extraction evidence

* Align design system scaffold with Claude output

* Add evidence inventory for design system intake

* Add local design system evidence intake

* Add design system package audit gate

* Allow auditing Claude Design reference packages

* Audit design system package content quality

* Migrate legacy design system artifacts

* Clean migrated design system artifacts

* Require modular design system UI kits

* Reject thin design system UI kits

* Prioritize core design evidence intake

* Require role-based design system UI kits

* Clean stale design system manifest references

* Require representative preserved design assets

* Warn on generic design system visuals

* Enforce design system quality warnings

* Audit connected design system UI kits

* Require mounted design system UI kits

* Require composed design system app shells

* Require runnable JSX design system kits

* Require browser globals for design system components

* Infer design system names from source URLs

* Require source examples in design system packages

* Bind preserved fonts in design system tokens

* Require skill frontmatter in design system packages

* Preserve build icons in design system packages

* Require real assets in brand previews

* Require substantive source examples

* Require product overview in design system README

* Require reusable UI kit README

* Require reusable design system skill docs

* Seed Claude-style UI kit entry contract

* Preserve runtime build assets in design packages

* Audit design system packages after generation

* Audit design system first-run output

* Audit source-backed preview cards

* Align design system UI kit scaffolds

* Materialize design evidence package artifacts

* Show project chat during design system setup

* Hand off design system setup to project chat

* Auto-repair design system audit failures

* Harden design system evidence preservation

* Tighten design system package guidance

* Add targeted design system repair guidance

* Bound design system audit auto repair

* Use connector statuses in design system setup

* Audit design system preview manifests

* Require README preview manifests for design systems

* Add landing page Google Analytics

* Keep GA PR scoped to landing page

* Cover new landing routes with Google Analytics

* Load Google Analytics without static script src

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-05-21 20:38:09 +08:00
Jane
af63af3951
feat(landing-page): refresh brand mark and publish a real favicon.ico (#2561)
Replaces the four icon assets in `apps/landing-page/public/` with
renders of the new brand mark — black-fill speech bubble + white
pointer arrow — and adds a real multi-resolution `favicon.ico` at
the path SEO crawlers actually probe.

Why
- The brand mark was refreshed on 2026-05-21 (canonical source:
  black 2988×2988 PNG of the speech-bubble + pointer logo). The
  marketing site needed the matching favicon, apple-touch-icon, and
  header brand mark refreshed in lockstep so the browser tab, iOS
  home-screen tile, and the in-page nav glyph all line up with the
  new identity.
- `/favicon.ico` did not exist on the published site. The Astro head
  declares `<link rel="icon" href="/favicon.png">`, which modern
  browsers honor, but a long tail of SEO crawlers, link-preview
  services (Slack, Discord, third-party SEO tools), and older
  clients hard-probe `/favicon.ico` regardless of the link tag. Hits
  to that URL were falling through to the SPA fallback HTML
  (200 with `content-type: text/html`), so those clients rendered
  an empty/broken favicon. Several SEO surfaces showed an empty
  black circle instead of the brand mark.
- Adding a real `favicon.ico` plus an explicit
  `<link rel="icon" type="image/x-icon" href="/favicon.ico">` is
  the smallest defensive fix that covers both well-behaved and
  hard-probing clients.

What
- Regenerated icon assets from the new logo source:
  - `favicon.ico` — multi-resolution ICO with 16/32/48/64 PNG-encoded
    entries. The 16/32 entries are what browser tabs, bookmarks, and
    most crawlers sample; 48/64 cover high-DPI tabs and Windows
    pinned-tile sampling.
  - `favicon.png` — 32×32 PNG (existing slot).
  - `apple-touch-icon.png` — 180×180 PNG (existing slot, iOS
    home-screen).
  - `logo.webp` — 144×144 WebP, 4× the 36px logical size used by
    the header brand mark for crisp retina rendering.
- Added `<link rel="icon" type="image/x-icon" href="/favicon.ico" sizes="any">`
  to both `app/pages/index.astro` and the shared `sub-page-layout`
  so every route under `open-design.ai` advertises the ICO. Existing
  PNG and apple-touch links are preserved — modern browsers will
  still pick the PNG, the ICO catches the hard-probing tail.

Surface area
- Marketing site only. No `apps/web`, `apps/daemon`, contracts, or
  CLI surfaces touched.
- No new dependencies; assets generated locally from the canonical
  source via `magick` + `cwebp` and committed as static files.

Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors.
- File integrity:
  - `favicon.ico` — `MS Windows icon resource - 4 icons,
    16x16, 32x32, 48x48, 64x64`
  - `logo.webp` — `RIFF Web/P image, VP8 encoding, 144x144`
- Manual: `/favicon.ico` will return `image/x-icon` once deployed,
  not the SPA fallback HTML it returns today.

Followup
- Once Cloudflare's edge cache rolls (or is purged), third-party
  favicon caches (Google SERP, Slack link-preview) take days-to-weeks
  to refresh on their own; that lag is expected and not a deploy
  problem.

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-21 17:39:43 +08:00
ashleyashli
86dafa9be8
feat(landing): add 19-locale URL routing with full home translations (#2408)
* feat(landing): add 19-locale URL routing with full home translations

Adds locale-prefixed routes (/zh-CN/, /ja/, /de/, …) for 18 non-default
locales while keeping English as the only unprefixed canonical. Generates
proper hreflang + og:locale, points hreflang="en" / x-default at the
unprefixed canonical, and serves localized RSS and plugin search JSON
under each prefix.

Adds a visible language switcher (globe pill + native names) on every
page, replacing the small topbar dropdown. Native-name menu, current
locale marked aria-current, closes on outside click / Escape, only one
open at a time.

Adds app/_lib/home-copy.ts as the source of truth for marketing copy
on the landing page, with full translations for zh-CN, zh-TW, ja, ko,
de, fr, es-ES, pt-BR. Remaining locales (it, pl, hu, ru, uk, tr, ar, fa,
th, id) fall back to English for marketing copy while still getting
fully localized chrome.

Extracts the IntersectionObserver reveal + GitHub stats + wire ticker
script into _components/home-enhancer.astro so localized homepages
animate in the same way as the canonical home.

- New: app/_lib/i18n.ts, app/_lib/home-copy.ts
- New: app/_components/home-enhancer.astro, locale-switcher-enhancer.astro
- New: app/pages/[locale]/{index,[...path]}.astro, blog/rss.xml.ts,
  plugins/search.json.ts

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(landing): bound localized routing to listing pages only

Detail pages (skills/<slug>, blog/<id>, systems/<slug>, templates/<slug>,
craft/<slug>, plugins/<slug>) no longer fan out across 18 prefixed
locales — they stay at their canonical English URLs. Localized chrome
on listing pages links straight to those English detail URLs.

Generated page count drops from ~6,000+ to ~1,800 and the landing-page
CI build returns to ~50s. Localized homes, listings, and filter index
pages (skills/mode/*, skills/scenario/*, systems/category/*) are all
still produced per locale.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 11:36:15 +08:00
Tuola-waj
b1aa62e63c
fix(landing-page): restore 'tutorials' in Header active union (#2458) 2026-05-20 21:20:56 +08:00
Tuola-waj
69469c639e
feat(landing-page): add HTML Anything page and responsive header (#2452) 2026-05-20 20:37:34 +08:00
ashleyashli
93dd7066fd
feat(landing): refresh templates and add tutorials channel (#2409)
* feat(landing): rebuild /templates/ catalog from design-templates

Source the public templates page from `design-templates/*/SKILL.md` so
the catalog reflects the renderable template registry (111 entries with
mode, platform, and scenario metadata) instead of the previous handful
of live-artifact + skill-mode shims. Render per-template thumbnails by
shooting each design template's `example.html`, surface mode/platform/
scenario chips on cards and detail pages, and bump the preview
navigation timeout so heavier examples no longer flake under
concurrency.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(landing-page): add /tutorials/ channel for YouTube content

Adds an editorial tutorials channel to open-design.ai. Each video maps
to a Markdown entry in `app/content/tutorials/` and renders inline via
`youtube-nocookie.com` so the page does not require a cookie consent
banner. The list page mirrors the magazine-style /blog/ layout and
shares the same category-filter UX.

Seeds the channel with 12 community-produced videos (English + Chinese)
sourced from YouTube as of 2026-05-19, spanning getting-started,
tutorial, demo, and review categories. Header now exposes a "Tutorials"
nav item between Craft and Blog.

* fix(landing-page): switch tutorial player to click-through to YouTube

The original inline `youtube-nocookie.com/embed/<id>` iframe failed
to play in practice. YouTube's embedder identity check rejected the
request with `PLAYABILITY_ERROR_CODE_EMBEDDER_IDENTITY_MISSING_REFERRER`
and the in-frame "Sign in to confirm you're not a bot" challenge.
Removing the explicit `referrerpolicy` attribute did not resolve it,
and the alternative — providing a Referrer-Policy that always sends
a full referrer to a third party — is a regression we don't want
to ship.

Replace the iframe with a click-through facade: high-resolution
thumbnail + YouTube-style red play button + "Watch on YouTube ↗"
pill, wrapped in an anchor that opens the canonical
`youtube.com/watch?v=<id>` page in a new tab.

The detail page still renders all of the editorial content — title,
summary, author, date, duration, chapter notes — so it remains a
useful in-site landing for SEO and social shares. Only the playback
itself moves to YouTube proper.

* fix(landing-page): correct chase-ai tutorial chapter map

The seeded chapter map for the Chase AI tutorial mirrored the
typo in the original YouTube description: it listed
`14:06 — Dashboard` (past the 13:47 runtime) and put
`12:40 — Final verdict` after it, breaking strictly-ascending
order. YouTube's auto-detected chapter list for this video
(`yt-dlp --dump-json` `chapters` field) shows only four
sections — Open Design, Install + Demo, Design Systems, Final
Verdict — with no Dashboard segment, so drop that line and
keep the remaining four chapters in order.

---------

Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-20 17:44:28 +08:00
kami
c85da3eb40
fix: sync landing source-of-truth paths (#2066) 2026-05-20 11:44:04 +08:00
lefarcen
7fc4362ba8
perf(landing): edge-cache HTML and precise-load thumbnails (#2235)
* perf(landing): edge-cache HTML and precise-load thumbnails

Without `public/_headers` Cloudflare Pages serves every HTML with
`cf-cache-status: DYNAMIC` so each request roundtrips to the Pages
origin — observed TTFB 660–900ms from Seattle, worse from Asia.
With `s-maxage=3600, stale-while-revalidate=86400` HTML stays cached
at the edge between deploys (CF Pages auto-purges on every deploy so
freshness is unchanged in practice), and `_astro/` hash bundles flip
to `immutable` so the existing 4h+must-revalidate roundtrips go away.

For thumbnails, native `loading="lazy"` is browser-decided —
Chrome over-prefetches (1250–3000px), Safari fires near in-viewport.
A new `<LazyImg>` Astro component and global IntersectionObserver
(rootMargin 300px for images, 600px for videos) replaces all 10
site-wide `loading="lazy"` usages with precise control. Above-the-fold
slots (first 4 rows, detail-page hero previews) opt into `eager` or
`priority` to skip the IO roundtrip.

Homepage hero LCP gets `<link rel="preload" imagesrcset>`, a 4-step
`srcset` (768/1280/1920/2560) plus `fetchpriority="high"` so retina
devices stop repainting from the 1024-only variant — was the P99 long
tail.

Verified: `pnpm guard` 6/6, `pnpm typecheck` 0 errors, `pnpm build`
865 pages 28s, generated `out/index.html` contains the preload link
and 15 `data-precise-src` thumbnails, `out/plugins/index.html` has
95 precise-loaded thumbnails plus the IO script.

* perf(landing): logo to webp + parallelize Google Fonts load

Two HAR-validated wins on top of the edge-cache / precise-load commit:

logo: 500x500 192KB PNG → 200x200 7.5KB WebP. Footer/header actually
render at 36x36, so the source is 5x larger than necessary at the
display size and ships RGBA PNG bytes for what reads as a flat
graphic. WebP at q=85 keeps the gradient ring crisp at every DPR we
care about.

fonts: globals.css used `@import url(...)` for Google Fonts, which
serialized HTML → CSS → fonts.googleapis.com/css2 → fonts.gstatic.com/
woff2. HAR measured 953ms for the fonts CSS plus 400–800ms per woff2
× 4 — close to 3s before text could render in the intended family,
even with display=swap. Moving to `<link>` + `<link rel=preconnect>`
in each page's <head> lets the fonts CSS fetch race the HTML body
parse, and warms the TLS handshake to gstatic.com so woff2 requests
don't pay DNS+TLS at request time.

A shared `font-stylesheet.astro` keeps the four-family URL canonical
across all five entry points (index, sub-page-layout, plugins/index,
plugins/[slug], blog/index, blog/[slug]). og.astro already had this
treatment.
2026-05-19 19:14:25 +08:00
ashleyashli
07659b7272
feat(seo): add Search Console reporting workflows (#2229)
* feat(blog): daily 3-day Search Console traffic digest

Adds `blog-3day-report.yml` (cron 09:00 Asia/Shanghai) and a
companion `report-3day.ts` script that refreshes
`docs/blog-traffic-digest.md` once per day. The digest has two
sections:

- T-3 spotlight: posts published exactly three days ago, with their
  3-day Search Analytics window plus current URL Inspection coverage
  state.
- Rolling 30-day cohort: every post 1–30 days old with its latest
  3-day Search Analytics window, sorted by impressions descending.

The workflow is read-only against Google APIs (no Indexing API,
no "request indexing" automation) and mirrors the secret / config
plumbing already used by `blog-indexing-monitor.yml`. Output lands
in a reviewable `automation/blog-traffic-digest` PR opened by the
open-design bot.

Also widens `querySearchAnalytics` to accept `windowDays: 3 | 7 | 28`
and updates `docs/blog-indexing-automation.md` with the new pipeline.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(seo): post daily Search Console report to Feishu

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(blog): push traffic digest to Feishu

Emit a compact JSON summary from the daily 3-day traffic digest and add a Feishu custom bot sender for the summary card. Wire the workflow to send the card when `FEISHU_BLOG_DIGEST_WEBHOOK` is configured while keeping Markdown PR output as the source of truth.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(landing-page): add Discord routing CTAs

Add a lightweight Discord pill to the landing hero and Discord links in the landing and blog footers so community routing is visible without displacing the primary GitHub and download CTAs.

Add a blog-ending conversion card that points guide and use-case readers to the internal workflows library, while keeping Discord as a secondary support path.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 18:09:44 +08:00
Yuhao Chen
2e1dd497aa
fix(landing): expose blog RSS feed alias (#1859) 2026-05-16 21:52:11 +08:00
ashleyashli
e3c7c3c611
fix(landing): unify blog chrome and star counts (#1811)
* fix(landing): unify blog chrome and stars

Ensure blog and catalog pages share the same header/footer behavior, with safe GitHub star fallbacks and RSS discovery for the refreshed blog.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(nix): update pnpm dependency hash

Keep Nix fixed-output dependency hashes aligned with the landing page lockfile changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 16:56:19 +08:00
ashleyashli
0a5403883c
Refresh landing page blog (#1711)
* feat(landing): editorial blog UI + Blog nav entry

Adds the magazine-style /blog/ index and post detail pages backed by an
Astro content collection of long-form posts (Product, Guides, Use cases,
Community), and threads a Blog entry through the shared site Header so
blog readers see the same Skills/Systems/Templates/Craft nav as the
home page.

What's in:
- header.tsx: add Blog item to nav-links + 'blog' active highlight key
- pages/blog/index.astro: editorial list with featured card, category
  filter chips, and shared <Header counts={...} active="blog" />
- pages/blog/[slug].astro: long-form post template with SeoHead article
  JSON-LD, post-topline kicker (← Back to Blog + category · date),
  and 'View source on GitHub ↗' footer link
- _components/seo-head.astro: shared SeoHead helper used by every page
  for canonical, OpenGraph, Twitter, and Article JSON-LD
- image-assets.ts: export ogDefaultImage for the SeoHead default card
- content.config.ts: tighten blog schema to enum category +
  numeric readingTime, exclude underscore-prefixed files (_topics.md
  is the blog-factory topic backlog, not a public post)
- content/blog/*.md: 5 launch posts (4 published + 1 internal _topics
  backlog)

What's out:
- _components/blog-layout.astro: the placeholder layout with its own
  mini-header was only used by the placeholder posts being removed;
  drop it instead of leaving dead code
- 4 placeholder posts under content/blog/*.mdx that documented blog
  scaffolding (test-post, atelier-zero-for-articles,
  blog-routes-and-post-template, shipping-the-latest-note)

* feat(landing): refresh blog editorial layout

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 15:06:42 +08:00
Joey-nexu
e3a848a33a
feat(landing-page): replace Ø wordmark with PNG logo across nav/footer/favicon (#1449)
* feat(landing-page): replace Ø wordmark with PNG logo across nav/footer/favicon

Switches the brand mark from the Unicode 'Ø' glyph to the new circular
gradient paper-plane PNG. Header nav and footer share the same image, and
the browser tab + iOS home screen icons are regenerated from the same
500x500 source.

- public/logo.png (500x500, brand source)
- public/favicon.png (32x32, replaces favicon.svg)
- public/apple-touch-icon.png (180x180, regenerated)
- header.tsx + page.tsx footer: <span>Ø</span> -> <img src=/logo.png />
- globals.css: simplify .brand-mark (drop Ø-era border/font, add object-fit
  contain on child img)
- index.astro: link rel=icon now points at favicon.png

* fix(landing-page): apply logo + favicon swap to sub-page layout too

Review on #1449 caught two cross-page consistency issues:

- P1: sub-page-layout.astro still linked /favicon.svg, which this PR
  deletes — every Skills/Systems/Templates/Craft page would request a
  missing asset. Updated to /favicon.png to match index.astro.
- P2: sub-page-layout.astro still rendered the Ø wordmark in its footer
  brand block, leaving the public site with mixed brand marks. Replaced
  with the same <img src=/logo.png /> wrapper pattern used on the
  homepage header and footer.

Repo-wide grep now shows 0 favicon.svg references and 0 Ø brand-mark
spans. typecheck still 25 files / 0 errors / 0 warnings.

---------

Co-authored-by: Joey-nexu <236967869+joeylee12629-star@users.noreply.github.com>
2026-05-13 12:30:32 +08:00
Prantik Medhi
0244a769cb
feat(landing-page): add blog routes (#1444)
* fix(landing-page): register blog collection config

* fix(landing-page): restore blog content config

* fix(blog): use content-layer ids
2026-05-13 12:20:45 +08:00
Joey-nexu
5077a1cd38
feat(landing-page): split catalog into per-facet pages + auto-deploy on content changes (#1158)
* feat(landing-page): split catalog into per-facet pages + auto-deploy on content changes

Convert the single-page landing into a content-driven multi-page site
sourced directly from the canonical Markdown bundles in the repo root,
and close the deploy loop so contributor edits go live without manual
follow-up.

## What's new

- `/skills/`, `/systems/`, `/craft/`, `/templates/` index + detail
  pages, generated from `skills/<slug>/SKILL.md`,
  `design-systems/<slug>/DESIGN.md`, `craft/*.md`, and
  `templates/live-artifacts/<slug>/README.md` via Astro content
  collections (`app/content.config.ts`). No mirroring of content into
  the landing-page package — `glob` re-scans on every build.
- Faceted sub-routes generated from frontmatter:
    - `/skills/mode/<slug>/`     — 8 pages (deck, prototype, image, …)
    - `/skills/scenario/<slug>/` — 18 pages after alias collapse
    - `/systems/category/<slug>/` — 21 pages
  Each page owns its own `<title>`, meta description, and
  `CollectionPage` JSON-LD; chips on the parent index pages are now
  real anchors that link to these facet routes.
- Updated top-bar nav (`_components/header.tsx`) to point at the new
  internal routes with live counts pulled from the catalog. Counts in
  the homepage hero meta description likewise driven by
  `getCatalogCounts()` so they never drift.
- Per-skill / per-template thumbnails. A Playwright generator
  (`scripts/generate-previews.ts`) walks every `example.html` and
  `templates/live-artifacts/<slug>/index.html`, screenshots them at
  1440×900@2x, and writes PNGs to `public/previews/`. The catalog
  data layer auto-detects presence and degrades gracefully when an
  artifact has no renderable HTML.

## Plumbing the auto-update loop

- `landing-page-deploy.yml` and `landing-page-ci.yml` now trigger on
  changes under `skills/`, `design-systems/`, `craft/`, and
  `templates/`. Without this, a contributor adding a new SKILL.md to
  `main` would silently skip the deploy and the published site would
  fall behind.
- Both workflows now install Playwright Chromium (cached by version)
  and run `pnpm previews` before `astro build`, so generated
  thumbnails ship in `out/previews/` automatically. Preview generation
  is `continue-on-error: true` — a single broken example.html should
  not block the deploy of the rest of the catalog.
- `apps/landing-page/public/previews/` is gitignored: the directory
  is owned by CI and would otherwise add ~70MB of binary churn to the
  repo on every regeneration.

## Tag canonicalization

- `app/_lib/catalog.ts` adds a small per-scope alias table so
  authoring drift like `od.scenario: operation` vs `operations`, or
  `live` vs `live-artifacts`, collapses to a single canonical route
  instead of leaking two near-empty pages. Mode and category alias
  tables are scaffolded but currently empty.

## Validation

- `pnpm --filter @open-design/landing-page typecheck` — 0 errors,
  0 warnings, 0 hints across 25 Astro files
- `pnpm --filter @open-design/landing-page build` — 341 pages built
  (1 home + 8 mode + 18 scenario + 21 category + N detail pages +
  sitemap + RSS), zero external JS, ≥16 Cloudflare-resized hero
  image URLs intact

## Why this matters

After merge, any push to `main` that adds, removes, or edits a skill,
design system, craft principle, or live-artifact template
automatically triggers a fresh build that:

1. picks up the new Markdown via the content-collection glob,
2. regenerates thumbnails for any matching example.html,
3. emits new sitemap entries and JSON-LD,
4. and ships to Cloudflare Pages — no landing-page-side change
   required.

* fix(landing-page): address review feedback on PR #1158

Five fixes from the review pass — none change scope, all close the
"contradictory totals" / "stale data" / "silent CI failure" gaps the
reviewers flagged.

## Hero / catalog claims now read live counts everywhere

`apps/landing-page/app/page.tsx` previously hardcoded `31` skills and
`72` systems in the hero copy and stat rings, while the nav and meta
description had already moved to `getCatalogCounts()`. After this PR
every visible "X skills / Y systems" claim — hero lead, hero stat
rings, capabilities cards body copy, labs section meta + filter pills,
selected-work fractions, the labs CTA, and the footer Library — reads
from a single `counts` prop. `Header` and `Page` now both require
`counts` (no optional fallback) so a future caller can never silently
publish stale numbers.

The labs-section filter pills also stop being decorative buttons:
they now link to the actual `/skills/mode/<slug>/` and `/skills/`
catalog routes the new multi-page architecture exposes.

## Craft README no longer publishes

`apps/landing-page/app/_lib/catalog.ts` filtered out `e.id !== 'README'`,
but Astro normalizes `craft/README.md`'s id to lowercase `readme`, so
the published site shipped `/craft/readme/` as a public craft principle
and the nav badge counted 12 instead of 11. Compare case-insensitively
(`e.id.toLowerCase() !== 'readme'`) so any future README casing is
also filtered out. Verified locally: `apps/landing-page/out/craft/`
now contains exactly 11 entries.

## Preview URL preserves actual file extension

`listPreviews()` was already discovering `.png`, `.webp`, `.jpg`, and
`.jpeg`, but `previewUrlFor()` always emitted `.png`, so a future
sharp/webp post-processor (or a manually committed template asset)
would mark the record as available while the rendered `<img src>`
404'd. Switched the structure from `Set<slug>` to `Map<slug, filename>`
and emit the actual on-disk filename verbatim.

## Preview script: per-artifact soft, systemic hard

Previously any single failed `example.html` capture exited the script
non-zero, which forced both workflows to mark the entire preview step
`continue-on-error: true`. That blanket tolerance also masked
systemic generator failures — a chromium launch that never finds the
browser binary would silently ship a deploy with zero thumbnails.

`scripts/generate-previews.ts` now distinguishes:

- per-artifact failures → logged and skipped, exit 0 (catalog
  degrades gracefully for those skills),
- discoverJobs / chromium.launch / 100%-failure run → exit 1
  (systemic, must fail the build).

Both workflows drop their `continue-on-error: true` flags so a real
problem actually surfaces.

## AGENTS.md reflects the multi-page architecture

`apps/landing-page/AGENTS.md` previously declared the landing page
single-route ("Not multi-page. There is exactly one route ('/')").
That guidance is now wrong — there are six top-level route groups
(`/`, `/skills/`, `/systems/`, `/craft/`, `/templates/`, plus their
facet variants). Updated to describe content-collection sourcing, the
no-mirror rule, the auto-deploy workflow contract, and the
"never hardcode catalog claims" boundary.

## Validation

- `pnpm --filter @open-design/landing-page typecheck` — 0 errors,
  0 warnings, 0 hints across 25 Astro files
- `pnpm --filter @open-design/landing-page build` — 340 pages built
  (was 341 before the README filter; the README route is now
  correctly absent), live counts visible in the built `out/index.html`:
  `driven by 125 composable skills and 149 brand-grade design systems`
- Verified `out/craft/` no longer contains `readme/`
- Verified preview URLs resolve to the actual on-disk filename via
  the regenerated catalog index page

* fix(landing-page): clean up live-artifact template name + summary parsing

Address @mrcfps's follow-up review on `0715d8c`. The
`shapeLiveArtifactTemplate()` parser was passing the README's H1
verbatim (literal backticks intact) and using the first non-empty
post-H1 line as the summary, even when that line was the
`> Category: **Live Artifacts**` editorial blockquote. Result:
`/templates/live-otd-operations-brief/` was shipping a
`<meta name="description" content=">">` and a card title with raw
Markdown noise — a regression for both SEO snippets and the
templates catalog at-a-glance scan.

## Two new shared helpers

- `stripMarkdownInline()` — strip backticks, asterisks, and link
  wrappers so `# \`otd-operations-brief\` · live-artifact template`
  becomes `otd-operations-brief · live-artifact template` before any
  further trimming.
- `extractFirstProseParagraph()` — walk the body after the H1 and
  skip blockquotes (`>`), list markers, table rows, fenced code, and
  HR rules. Stop at the first contiguous prose paragraph and pass it
  through `stripMarkdownInline()` so the result is human-readable.

Both helpers live next to `titleizeSlug()` and are used by
`shapeCraft()` and `shapeLiveArtifactTemplate()` so they share one
implementation.

## Live-artifact title boilerplate trim

Live-artifact READMEs commonly title themselves
`# \`<slug>\` · live-artifact template`. After stripping the inline
backticks the trailing `· live-artifact template` is redundant
("Templates" already groups them) and adds a wide noisy suffix on
catalog cards. Removed it via a narrow regex tail-strip.

## Result on the existing fixture

Verified locally for `templates/live-artifacts/otd-operations-brief/`:

- before: `<title>\`otd-operations-brief\` · live-artifact template …</title>`,
  `<meta name="description" content=">">`
- after:  `<title>otd-operations-brief — Open Design template</title>`,
  `<meta name="description" content="A drop-in html_template_v1
  live-artifact template for an editorial On-Time Delivery brief.
  It ships:">`

Typecheck 0/0/0, build 340 pages.

---------

Co-authored-by: Joey <joey@cursor.so>
Co-authored-by: Joey-nexu <236967869+joeylee12629-star@users.noreply.github.com>
2026-05-12 19:24:50 +08:00
Tom Huang
aefba56a3f
feat(skills): open-design-landing rename, kami skills, landing OG (#428)
* feat(skills): open-design-landing rename, kami skills, landing OG

- Rename editorial-collage skills to open-design-landing and -deck; refresh examples and compose script layout
- Add kami-deck and kami-landing skills with HTML examples
- Landing page: og.astro, index wiring, and style tweaks; package.json bump
- Web i18n: German and Russian copy for renamed and new skills
- Daemon test: update skill-asset-rewrite expectations for new paths
- Design systems: README and atelier-zero doc touch-ups
- Cross-skill SKILL.md reference updates

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(landing-page): document version-slot invariant and deprecation timeline

Address P3 review notes on PR #428:
- Note the `data-github-version` wrapper invariant (version string only)
  near the canonical URL block in `app/page.tsx`.
- Expand the `formatVersion` helper comment in `app/pages/index.astro`
  with concrete `release.name` / `tag_name` example shapes for each
  branch of the regex fallback.
- Tighten the `EditorialCollageDeckInputs` deprecation in
  `skills/open-design-landing-deck/schema.ts` to a specific removal
  version (v0.4.0) and add a "Migrating from editorial-collage-deck"
  section to the skill README.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* docs(landing-page, skills): clarify version slot script and rename migrations

- Describe GitHub version slots as driven by the inline enhancement script,
  not React hydration.
- Add editorial-collage → open-design-landing migration notes; fix README
  link copy (Astro static landing app).
- Extend deck README migration table with shared asset path renames.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(daemon): alias deprecated editorial-collage skill ids

The PR renames the editorial-collage / editorial-collage-deck skills
to open-design-landing / open-design-landing-deck, but the daemon
persists exact skill_id strings on projects and resolves them via
listSkills().find((s) => s.id === storedId). After the rename, any
project saved against an old id silently composes without the intended
skill prompt because the listing no longer exposes that id.

Add a SKILL_ID_ALIASES map in skills.ts plus a findSkillById() helper
that rewrites deprecated ids to their current canonical form, then
route every server-side lookup (skill detail, example HTML, asset
proxy, system-prompt composer) through it. Cover the alias map, the
resolver, and end-to-end resolution against a temp skills directory
with a regression test.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(kami-deck): route host od:slide messages through local go()

The host bridge classifies kami-deck as class-driven because go() toggles
.slide.active, but the visible slide is moved by deck.style.transform
which the bridge cannot drive. Listen for od:slide messages and dispatch
them through the local go() so toolbar next/prev and initialSlideIndex
restore actually shift the deck.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(kami-deck): sync deck transform with host-driven .active changes

The previous fix added a local od:slide listener but the host bridge in
apps/web/src/runtime/srcdoc.ts also listens for the same message and
calls setActive() (toggles .slide.active) without driving the deck
transform. Both listeners fired, the bridge re-read the just-toggled
active class, and overshot by one — and the bridge's restoreInitialSlide
path could move .active without a message at all, leaving the deck on
the original transform.

Stop the bridge from double-handling by calling stopImmediatePropagation
in the local listener (registered first because the bridge script is
appended to </body>), and add a MutationObserver that pulls the deck
transform onto whichever slide currently carries .active so the bridge's
direct setActive calls (notably the initial-slide restore) move the deck
too.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(i18n): align French content with renamed/new skills

PR #434 (French localization) merged into main with French copy for the
old editorial-collage / editorial-collage-deck skill ids; this branch
renamed those to open-design-landing / open-design-landing-deck and
added kami-deck and kami-landing. Update content.fr.ts to track the
rename and add French copy for the new kami skills so the
LOCALIZED_CONTENT_IDS coverage test passes once main is merged.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(open-design-landing-deck): sync deck transform with host-driven .active changes

Apply the same fix that landed in skills/kami-deck/example.html
(commits 96b255b, 8cbca30) to the open-design-landing-deck composer
runtime: the host bridge classifies this deck as class-driven because
go() toggles .slide.active, but the visible slide is moved by
deck.style.transform which the bridge can't drive. Add an od:slide
message listener that calls stopImmediatePropagation() and routes nav
through the local go(), plus a MutationObserver that pulls the deck
transform onto whichever slide carries .active so the bridge's direct
setActive calls (notably restoreInitialSlide) move the deck too.

Regenerates example.html via scripts/compose.ts; the regeneration also
picks up upstream nav-cta and brand-meta CSS additions in the sister
open-design-landing styles.css that the example had drifted from.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* docs(open-design-landing): align deploy story with Astro landing app

- Update SKILL contract: apps/landing-page is Astro static; clarify
  nextjs-app output_format as a historical enum label and <out>/nextjs
  as a legacy folder name.
- Replace optional-deploy section with fork + pnpm --filter landing-page build.
- Fix styles.css header and regenerate landing + deck example.html so
  inlined comments match.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(deck-runtime): bypass interaction lock for host/observer slide sync

The slide deck runtimes for kami-deck and open-design-landing-deck
gate go() behind a 700ms `lock` so wheel/key/touch input bursts can't
overshoot the transform transition. But applying the same gate to the
host bridge's od:slide messages and the MutationObserver watching
`.slide.active` creates a startup race: go(0) at the end of init sets
lock=true, and any host-driven `.active` change inside that window
(notably restoreInitialSlide) fires the observer, which calls go(i),
which exits at the lock guard — leaving the visible deck on slide 1
while the host counter advances to N.

Split the actual state update into an unthrottled `applySlide(n)`
helper that updates transform, `.active`, dot nav, and the progress
bar. Keep `lock` only on the user-input path through `go()`. Route
the message listener, the MutationObserver, and the initial render
through `applySlide` directly so host-driven sync always reaches the
deck transform regardless of the throttle state.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 19:22:46 +08:00
Tom Huang
6c2a8ba09f
feat(editorial-collage): introduce Atelier Zero style landing page as… (#366)
* feat(editorial-collage): introduce Atelier Zero style landing page assets and documentation

- Added new design system for Atelier Zero, including a detailed `DESIGN.md` file.
- Created an `editorial-collage` skill with associated assets for a magazine-grade landing page.
- Included example HTML and image assets for various sections (hero, about, capabilities, etc.).
- Updated README files to guide usage and customization of the new skill and design system.
- Introduced a new image generation prompt pack for consistent visual style across the landing page.

* fix(i18n): cover atelier-zero design system and editorial-collage skill in German content

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(editorial-collage): align manifest with shipped assets and address PR review

- Update image-manifest.json widths/heights/ratios to match the actual PNGs
  on disk: hero/about/cap/testimonial/cta = 1024x1024 (1:1), method-1..4 =
  816x816 (1:1), lab-1..5 and work-1..2 = 768x1024 (3:4). Mirror the new
  dimensions in imagegen-prompts.md headings and in README.md.
- Mark testimonial.png as rekey_on_brand_change so the manifest agrees
  with SKILL.md's "regenerate at minimum testimonial.png" guidance, and
  add work-1/work-2 to the rekey list in SKILL.md and README.md.
- Add a Hero (I.) sec-rule and renumber every following section II..VIII
  in example.html so the eight sections walk sequentially I -> VIII and
  the page-of-008 counter starts at 001.
- Delete editorial-artifact-system/ (16 duplicate PNGs + index.html +
  skills.md draft) — the canonical version is skills/editorial-collage/
  and the duplicate had no consumer references.
- DESIGN.md: spell out which dimensions of each magazine reference
  (Monocle/Apartamento/IDEA), document the rationale for single-accent
  vs multi-accent, and extend the anti-pattern list with AI-image-gen
  artifacts the system explicitly rejects.
- SKILL.md: add italic_words validation guidance (trim, cap at 4,
  verb->noun rewrite, punctuation strip) and replace the broken-image
  fallback with an inline SVG placeholder sized to the slot's
  manifest aspect ratio.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(daemon): serve skill example assets via stable API route

Skill example HTML such as `skills/editorial-collage/example.html`
references shipped images via `./assets/*.png`. The web app loads the
example into a sandboxed iframe via `srcdoc`, where relative URLs
resolve against `about:srcdoc` and the PNGs render as broken images in
the Examples preview.

Add a `GET /api/skills/:id/assets/*` route that serves files under the
skill's `assets/` directory with path-traversal guards, and rewrite
`src='./assets/<file>'` / `href='./assets/<file>'` in the example
response to point at that route. The disk preview keeps working
because the on-disk files are unchanged.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* feat(landing-page): add new static Next.js 16 site for Open Design marketing

- Introduced a new landing page application using Next.js 16, featuring a static export setup.
- Added essential files including `package.json`, `next.config.ts`, and TypeScript configuration.
- Implemented global styles in `globals.css` to match the Atelier Zero design system.
- Created a detailed `AGENTS.md` for module-level boundaries and purpose.
- Included various image assets for the landing page, ensuring a visually cohesive experience.
- Established a root layout and main page structure to support the marketing content.

* style(landing-page): enhance topbar layout and improve responsiveness

- Added nowrap styling to topbar elements to prevent text overflow.
- Introduced media query to hide mid text in the topbar for screen widths between 1200px and 1280px.
- Updated layout.tsx to suppress hydration warnings for better rendering consistency.
- Removed redundant "Compiled by Open Design" text from the page component.

* feat(landing-page): implement scroll-reveal animations for enhanced user experience

- Added a new `RevealRoot` component to manage scroll-triggered reveal animations.
- Updated `globals.css` with styles for elements using the `data-reveal` attribute, including opacity, translation, and scaling effects.
- Modified `layout.tsx` to include the `RevealRoot` component for managing animations.
- Enhanced `page.tsx` by adding `data-reveal` attributes to various elements for staggered reveal effects.
- Implemented reduced motion support to ensure accessibility for users with motion sensitivity.

* fix(landing-page): update import paths and enhance link styles

- Changed the import path in `next-env.d.ts` to reference the correct routes type definition.
- Enhanced `globals.css` with new styles for topbar links, work cards, and partner elements, improving hover effects and transitions.
- Updated `page.tsx` to include canonical project URLs and made various links point to these URLs for better navigation and accessibility.

* feat(landing-page): implement headroom-style sticky header with live GitHub star count

- Introduced a new `Header` component to manage sticky navigation behavior on scroll, enhancing user experience.
- Updated `globals.css` to style the sticky header, including transitions and visibility toggling based on scroll direction.
- Modified `page.tsx` to replace the static header with the new `Header` component, which fetches and displays the live GitHub star count.
- Ensured accessibility by providing a fallback for users who prefer reduced motion.

* feat(landing-page): enhance editorial landing page with global ticker and new styles

- Updated `next-env.d.ts` to reference the correct routes type definition for development.
- Enhanced `globals.css` with new styles for the global ticker, including responsive design and improved overflow handling.
- Introduced a new `WIRE_CITIES` and `WIRE_CONTRIBS` data structure in `page.tsx` to display a counter-scrolling marquee of cities and contributors.
- Added a ghost button style for the navigation call-to-action in the header.
- Updated various sections in `page.tsx` to integrate the new ticker and improve overall layout and accessibility.

* refactor(landing-page): update paper texture overlay and remove multica-ai link

- Enhanced comments in `globals.css` to clarify the purpose and behavior of the paper texture overlay.
- Adjusted z-index of the overlay to ensure proper layering with other elements.
- Removed the `multica-ai` partner link from `page.tsx` to streamline the partner section.

* feat(landing-page): implement dynamic contributor marquee with GitHub integration

- Added a new `Wire` component to display a counter-scrolling marquee of cities and contributors.
- The contributor list is fetched live from the GitHub API, ensuring up-to-date information.
- Updated `page.tsx` to integrate the `Wire` component, replacing the static contributor list with dynamic content.
- Enhanced comments for clarity regarding the functionality and purpose of the global wire.

* fix(i18n): add German display copy for editorial-collage-deck skill

The Validate workspace test asserts that GERMAN_CONTENT_IDS.skills covers
every curated skill on disk; the new editorial-collage-deck skill was
missing from DE_SKILL_COPY, causing src/i18n/content.test.ts to fail.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* feat(landing-page): migrate marketing site to Astro

* perf(landing-page): remove React client runtime

* perf(landing-page): serve images from Cloudflare resizing

* fix(pr): address landing page review feedback

---------

Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-04 13:39:58 +08:00