open-design/apps/landing-page/app/pages/[locale]/templates/[slug].astro
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

19 lines
550 B
Text

---
import TemplatePage, {
getStaticPaths as getTemplateStaticPaths,
} from '../../templates/[slug]/index.astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n';
export async function getStaticPaths() {
const basePaths = await getTemplateStaticPaths();
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).flatMap(
(locale) =>
basePaths.map((path) => ({
params: { ...path.params, locale: locale.code },
props: path.props,
})),
);
}
---
<TemplatePage {...Astro.props} />