diff --git a/apps/landing-page/app/_components/skill-row.astro b/apps/landing-page/app/_components/skill-row.astro deleted file mode 100644 index 6cd423ec2..000000000 --- a/apps/landing-page/app/_components/skill-row.astro +++ /dev/null @@ -1,62 +0,0 @@ ---- -/* - * Shared skill row used on `/skills/`, `/skills/mode//`, - * `/skills/scenario//`, and any future faceted view. - * - * Renders a `
  • ` with the - * canonical 5-column grid (index, thumb, body, meta, arrow). Centralizes - * the markup so all faceted views stay visually identical to the - * unfiltered index. - */ -import type { SkillRecord } from '../_lib/catalog'; -import { localeFromPath, localizedHref } from '../i18n'; - -export interface Props { - skill: SkillRecord; - index: number; -} - -const { skill, index } = Astro.props; -const locale = localeFromPath(Astro.url.pathname); -const href = (path: string) => localizedHref(path, locale); - -// Catalog row thumbs are tiny (~130×80 rendered, single-format PNGs) -// so we deliberately bypass the precise IntersectionObserver pipeline. -// On long lists like /skills/instructions/ (96 rows) the observer's -// swap latency stranded mid-page rows on the SVG placeholder during -// fast scrolls. Native lazy loading (the browser's own 1250-3000px -// lookahead) keeps the upcoming rows pre-fetched without the -// observer round-trip; only the first three rows go eager so they -// paint immediately on first paint instead of waiting for the -// browser's lazy queue. -const eager = index < 3; ---- - -
  • - - {String(index + 1).padStart(3, '0')} - - {skill.previewUrl ? ( - - ) : ( - - - {skill.name} - {skill.description} - - - {skill.modeLabel && {skill.modeLabel}} - {skill.scenarioLabel && {skill.scenarioLabel}} - {skill.platformLabel && {skill.platformLabel}} - - - -
  • diff --git a/apps/landing-page/app/_components/system-card.astro b/apps/landing-page/app/_components/system-card.astro index 7ff30d98f..a2b77fd24 100644 --- a/apps/landing-page/app/_components/system-card.astro +++ b/apps/landing-page/app/_components/system-card.astro @@ -1,8 +1,14 @@ --- /* - * Shared system card used on `/systems/` and - * `/systems/category//`. Displays palette swatches, name, - * category, and tagline as a clickable card. + * Shared system card used on `/plugins/systems/`. Displays palette + * swatches, name, category, and tagline as a clickable card. + * + * The card links to `/systems//`, which `public/_redirects` + * 301s to the bundled-plugin detail (`/plugins/design-system-/`) + * for the 142 systems that have one, and degrades the 8 without a + * detail page to `/plugins/systems/`. Linking through the redirect + * (rather than hard-coding `design-system-`) keeps those 8 from + * pointing at a non-existent detail page. */ import type { SystemRecord } from '../_lib/catalog'; import { localeFromPath, localizedHref } from '../i18n'; diff --git a/apps/landing-page/app/info-page-i18n.ts b/apps/landing-page/app/info-page-i18n.ts index a13ec9cc8..2dbcc8cc8 100644 --- a/apps/landing-page/app/info-page-i18n.ts +++ b/apps/landing-page/app/info-page-i18n.ts @@ -222,9 +222,9 @@ const INFO_PAGE_COPY: Partial> = { { label: 'Community', name: 'Discord' }, { label: 'Documentation', name: 'GitHub README' }, { label: 'License', name: 'Apache-2.0' }, - { label: 'Skills catalog', name: '/skills/' }, - { label: 'Systems catalog', name: '/systems/' }, - { label: 'Templates catalog', name: '/templates/' }, + { label: 'Skills catalog', name: '/plugins/skills/' }, + { label: 'Systems catalog', name: '/plugins/systems/' }, + { label: 'Templates catalog', name: '/plugins/templates/' }, ], aliasesTitle: 'Naming & aliases', aliasesLead: @@ -538,9 +538,9 @@ INFO_PAGE_COPY.zh = { { label: '社区', name: 'Discord' }, { label: '文档', name: 'GitHub README' }, { label: '许可证', name: 'Apache-2.0' }, - { label: 'Skill 目录', name: '/skills/' }, - { label: '系统目录', name: '/systems/' }, - { label: '模板目录', name: '/templates/' }, + { label: 'Skill 目录', name: '/plugins/skills/' }, + { label: '系统目录', name: '/plugins/systems/' }, + { label: '模板目录', name: '/plugins/templates/' }, ], aliasesTitle: '命名与别名', aliasesLead: '不同工具、受众和语言环境里,这个项目会以几种方式被搜索和书写:', @@ -1027,9 +1027,9 @@ const sourceNames = [ 'Discord', 'GitHub README', 'Apache-2.0', - '/skills/', - '/systems/', - '/templates/', + '/plugins/skills/', + '/plugins/systems/', + '/plugins/templates/', ] as const; const aliasLabels = [ diff --git a/apps/landing-page/app/page.tsx b/apps/landing-page/app/page.tsx index aad960d3a..4d0e55f6c 100644 --- a/apps/landing-page/app/page.tsx +++ b/apps/landing-page/app/page.tsx @@ -730,23 +730,23 @@ export default function Page({ @@ -1325,17 +1325,17 @@ export default function Page({
    {home.footer.columns.library}
    • - + {home.footer.libraryLinks.skills(skills)}
    • - + {home.footer.libraryLinks.systems(systems)}
    • - + {home.footer.libraryLinks.templates}
    • diff --git a/apps/landing-page/app/pages/[locale]/[...path].astro b/apps/landing-page/app/pages/[locale]/[...path].astro index b90582037..995c10a25 100644 --- a/apps/landing-page/app/pages/[locale]/[...path].astro +++ b/apps/landing-page/app/pages/[locale]/[...path].astro @@ -2,17 +2,7 @@ import { getCollection } from 'astro:content'; import Layout from '../../_components/sub-page-layout.astro'; import type { HeaderProps } from '../../_components/header'; -import LazyImg from '../../_components/lazy-img.astro'; -import { - getCraftRecords, - getSkillModeIndex, - getSkillRecords, - getSkillScenarioIndex, - getSystemCategoryIndex, - getSystemRecords, - getTemplateRecords, - tally, -} from '../../_lib/catalog'; +import { getCraftRecords } from '../../_lib/catalog'; import { PREFIXED_LOCALES, getCopy, @@ -23,31 +13,17 @@ import { import '../../globals.css'; import '../../sub-pages.css'; -// Localized routing only generates listing/index pages. Detail pages -// (individual skills, posts, templates, …) stay at canonical English -// URLs to keep the static build bounded; the localized chrome links -// straight to those canonical detail URLs. +// Localized routing only generates the `craft` and `blog` listing pages. +// Detail pages (individual posts, craft items, …) stay at canonical +// English URLs to keep the static build bounded; the localized chrome +// links straight to those canonical detail URLs. export async function getStaticPaths() { - const skillModes = await getSkillModeIndex(); - const skillScenarios = await getSkillScenarioIndex(); - const systemCategories = await getSystemCategoryIndex(); - - const paths = [ - 'skills', - 'systems', - 'craft', - 'templates', - 'blog', - // Plugins library is generated via short-code wrappers under - // `app/pages/[locale]/plugins/` (mirroring the `[locale]/skills/`, - // `[locale]/systems/`, etc. pattern), so it does NOT participate - // in this long-code catch-all. Both surfaces co-exist in `out/` - // because `_redirects` maps `/zh-CN/*` → `/zh/*` for the long-form - // routes; plugins lives under the short-form path only. - ...skillModes.map((item) => `skills/mode/${item.slug}`), - ...skillScenarios.map((item) => `skills/scenario/${item.slug}`), - ...systemCategories.map((item) => `systems/category/${item.slug}`), - ]; + // The skills / systems / templates catalogs moved under `/plugins/*`. + // Their old localized listings are now 301'd by `public/_redirects`, + // so this catch-all only renders the localized `craft` and `blog` + // listings. Plugins itself is generated via short-code wrappers under + // `app/pages/[locale]/plugins/`, so it does NOT participate here. + const paths = ['craft', 'blog']; return PREFIXED_LOCALES.flatMap((locale) => paths.map((path) => ({ @@ -62,36 +38,20 @@ const copy = getCopy(locale); const pathParam = Astro.params.path ?? ''; const segments = pathParam.split('/').filter(Boolean); -const [skills, systems, craft, templates, posts] = await Promise.all([ - getSkillRecords(), - getSystemRecords(), +const [craft, posts] = await Promise.all([ getCraftRecords(), - getTemplateRecords(), getCollection('blog'), ]); // All cross-locale subpage links resolve to canonical (English) URLs. const href = (path: string) => path; const titleSuffix = 'Open Design'; const routeRoot = segments[0] ?? ''; -const routeSecond = segments[1] ?? ''; -const routeThird = segments[2] ?? ''; const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime()); -const modeTags = await getSkillModeIndex(); -const scenarioTags = await getSkillScenarioIndex(); -const systemCategories = await getSystemCategoryIndex(); -const platformTally = tally(skills.map((skill) => skill.platform).filter((item): item is string => Boolean(item))); - -const pageTitle = routeRoot === 'skills' - ? `${copy.skillsTitle} — ${skills.length} | ${titleSuffix}` - : routeRoot === 'systems' - ? `${copy.systemsTitle} — ${systems.length} | ${titleSuffix}` - : routeRoot === 'templates' - ? `${copy.templatesTitle} — ${templates.length} | ${titleSuffix}` - : routeRoot === 'craft' - ? `${copy.craftTitle} — ${craft.length} | ${titleSuffix}` - : `${copy.blog} — ${titleSuffix}`; +const pageTitle = routeRoot === 'craft' + ? `${copy.craftTitle} — ${craft.length} | ${titleSuffix}` + : `${copy.blog} — ${titleSuffix}`; const pageDescription = `Open Design ${routeRoot || 'landing'} page.`; --- @@ -123,61 +83,6 @@ const pageDescription = `Open Design ${routeRoot || 'landing'} page.`; )} - {routeRoot === 'skills' && ( - <> -
      - {copy.catalog} · Nº 01 -

      {copy.skillsTitle} — {skills.length} composable design capabilities.

      -

      Each skill is a folder with one SKILL.md. Drop it in, restart the daemon, and the picker shows it.

      -
      - {routeSecond === '' && ( -
      -
      - {copy.mode} - -
      -
      - {copy.scenario} - -
      -
      - {copy.platform} -
        {platformTally.map(([key, count]) =>
      • {key}{count}
      • )}
      -
      -
      - )} -
      -
        - {skills - .filter((skill) => routeSecond === 'mode' ? skill.mode === routeThird : routeSecond === 'scenario' ? skill.scenario === routeThird : true) - .map((skill, index) => ( -
      1. - - {String(index + 1).padStart(2, '0')} - {skill.name}{skill.description} - {skill.mode && {skill.mode}} - -
      2. - ))} -
      -
      - - )} - - {routeRoot === 'systems' && ( - <> -
      - {copy.catalog} · Nº 02 -

      {copy.systemsTitle} — {systems.length} portable visual systems.

      -

      Each system is a single DESIGN.md token spec that keeps colors, type, spacing, and components consistent.

      -
      - {routeSecond === '' &&
      {copy.category}
      } -
      - -
      - - )} - {routeRoot === 'craft' && ( <>
      {copy.catalog} · Nº 03

      {copy.craftTitle} — {craft.length} rendering principles.

      Quality rules for accessibility, motion, color, type, and state coverage.

      @@ -185,11 +90,4 @@ const pageDescription = `Open Design ${routeRoot || 'landing'} page.`; )} - {routeRoot === 'templates' && ( - <> -
      {copy.catalog} · Nº 04

      {copy.templatesTitle} — {templates.length} ready-to-fork artifacts.

      Pre-wired artifact bundles with examples, visual language, and agent instructions.

      -
      - - )} - diff --git a/apps/landing-page/app/pages/[locale]/skills/[slug].astro b/apps/landing-page/app/pages/[locale]/skills/[slug].astro deleted file mode 100644 index 8db3d1b1f..000000000 --- a/apps/landing-page/app/pages/[locale]/skills/[slug].astro +++ /dev/null @@ -1,19 +0,0 @@ ---- -import SkillPage, { - getStaticPaths as getSkillStaticPaths, -} from '../../skills/[slug]/index.astro'; -import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n'; - -export async function getStaticPaths() { - const basePaths = await getSkillStaticPaths(); - return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).flatMap( - (locale) => - basePaths.map((path) => ({ - params: { ...path.params, locale: locale.code }, - props: path.props, - })), - ); -} ---- - - diff --git a/apps/landing-page/app/pages/[locale]/skills/index.astro b/apps/landing-page/app/pages/[locale]/skills/index.astro deleted file mode 100644 index b11e8e6c1..000000000 --- a/apps/landing-page/app/pages/[locale]/skills/index.astro +++ /dev/null @@ -1,12 +0,0 @@ ---- -import SkillsPage from '../../skills/index.astro'; -import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n'; - -export function getStaticPaths() { - return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).map( - (locale) => ({ params: { locale: locale.code } }), - ); -} ---- - - diff --git a/apps/landing-page/app/pages/[locale]/skills/mode/[mode].astro b/apps/landing-page/app/pages/[locale]/skills/mode/[mode].astro deleted file mode 100644 index 069a7c9e7..000000000 --- a/apps/landing-page/app/pages/[locale]/skills/mode/[mode].astro +++ /dev/null @@ -1,19 +0,0 @@ ---- -import SkillModePage, { - getStaticPaths as getSkillModeStaticPaths, -} from '../../../skills/mode/[mode].astro'; -import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../../i18n'; - -export async function getStaticPaths() { - const basePaths = await getSkillModeStaticPaths(); - return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).flatMap( - (locale) => - basePaths.map((path) => ({ - params: { ...path.params, locale: locale.code }, - props: path.props, - })), - ); -} ---- - - diff --git a/apps/landing-page/app/pages/[locale]/skills/scenario/[scenario].astro b/apps/landing-page/app/pages/[locale]/skills/scenario/[scenario].astro deleted file mode 100644 index 12752c517..000000000 --- a/apps/landing-page/app/pages/[locale]/skills/scenario/[scenario].astro +++ /dev/null @@ -1,19 +0,0 @@ ---- -import SkillScenarioPage, { - getStaticPaths as getSkillScenarioStaticPaths, -} from '../../../skills/scenario/[scenario].astro'; -import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../../i18n'; - -export async function getStaticPaths() { - const basePaths = await getSkillScenarioStaticPaths(); - return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).flatMap( - (locale) => - basePaths.map((path) => ({ - params: { ...path.params, locale: locale.code }, - props: path.props, - })), - ); -} ---- - - diff --git a/apps/landing-page/app/pages/[locale]/systems/[slug].astro b/apps/landing-page/app/pages/[locale]/systems/[slug].astro deleted file mode 100644 index 94b228fc5..000000000 --- a/apps/landing-page/app/pages/[locale]/systems/[slug].astro +++ /dev/null @@ -1,19 +0,0 @@ ---- -import SystemPage, { - getStaticPaths as getSystemStaticPaths, -} from '../../systems/[slug].astro'; -import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n'; - -export async function getStaticPaths() { - const basePaths = await getSystemStaticPaths(); - return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).flatMap( - (locale) => - basePaths.map((path) => ({ - params: { ...path.params, locale: locale.code }, - props: path.props, - })), - ); -} ---- - - diff --git a/apps/landing-page/app/pages/[locale]/systems/category/[category].astro b/apps/landing-page/app/pages/[locale]/systems/category/[category].astro deleted file mode 100644 index 635c14676..000000000 --- a/apps/landing-page/app/pages/[locale]/systems/category/[category].astro +++ /dev/null @@ -1,19 +0,0 @@ ---- -import SystemCategoryPage, { - getStaticPaths as getSystemCategoryStaticPaths, -} from '../../../systems/category/[category].astro'; -import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../../i18n'; - -export async function getStaticPaths() { - const basePaths = await getSystemCategoryStaticPaths(); - return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).flatMap( - (locale) => - basePaths.map((path) => ({ - params: { ...path.params, locale: locale.code }, - props: path.props, - })), - ); -} ---- - - diff --git a/apps/landing-page/app/pages/[locale]/systems/index.astro b/apps/landing-page/app/pages/[locale]/systems/index.astro deleted file mode 100644 index a9c395321..000000000 --- a/apps/landing-page/app/pages/[locale]/systems/index.astro +++ /dev/null @@ -1,12 +0,0 @@ ---- -import SystemsPage from '../../systems/index.astro'; -import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n'; - -export function getStaticPaths() { - return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).map( - (locale) => ({ params: { locale: locale.code } }), - ); -} ---- - - diff --git a/apps/landing-page/app/pages/[locale]/templates/[slug].astro b/apps/landing-page/app/pages/[locale]/templates/[slug].astro deleted file mode 100644 index 40f26bad6..000000000 --- a/apps/landing-page/app/pages/[locale]/templates/[slug].astro +++ /dev/null @@ -1,19 +0,0 @@ ---- -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, - })), - ); -} ---- - - diff --git a/apps/landing-page/app/pages/[locale]/templates/index.astro b/apps/landing-page/app/pages/[locale]/templates/index.astro deleted file mode 100644 index ceb48e23e..000000000 --- a/apps/landing-page/app/pages/[locale]/templates/index.astro +++ /dev/null @@ -1,12 +0,0 @@ ---- -import TemplatesPage from '../../templates/index.astro'; -import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n'; - -export function getStaticPaths() { - return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).map( - (locale) => ({ params: { locale: locale.code } }), - ); -} ---- - - diff --git a/apps/landing-page/app/pages/agents/index.astro b/apps/landing-page/app/pages/agents/index.astro index b237a5d3a..00c032280 100644 --- a/apps/landing-page/app/pages/agents/index.astro +++ b/apps/landing-page/app/pages/agents/index.astro @@ -207,8 +207,8 @@ const jsonLd = [

      {page.nextTitle}

      diff --git a/apps/landing-page/app/pages/blog/[slug].astro b/apps/landing-page/app/pages/blog/[slug].astro index 7d5efefca..5475e3137 100644 --- a/apps/landing-page/app/pages/blog/[slug].astro +++ b/apps/landing-page/app/pages/blog/[slug].astro @@ -81,7 +81,7 @@ const bottomCta = ? { title: ui.blog.cta.skillsTitle, body: ui.blog.cta.skillsBody, - href: '/skills/', + href: '/plugins/skills/', label: ui.blog.cta.skillsLabel, external: false, } diff --git a/apps/landing-page/app/pages/html-anything/index.astro b/apps/landing-page/app/pages/html-anything/index.astro index 9793e9cbb..0421da422 100644 --- a/apps/landing-page/app/pages/html-anything/index.astro +++ b/apps/landing-page/app/pages/html-anything/index.astro @@ -1058,7 +1058,7 @@ pnpm -F @html-anything/next dev

      {copy.visitOpenDesign} - {copy.browseSkills} + {copy.browseSkills} {copy.githubLink}

      diff --git a/apps/landing-page/app/pages/official/index.astro b/apps/landing-page/app/pages/official/index.astro index 2fd6b3d85..c5f39a15f 100644 --- a/apps/landing-page/app/pages/official/index.astro +++ b/apps/landing-page/app/pages/official/index.astro @@ -45,9 +45,9 @@ const sources = [ { ...page.sources[4], href: DISCORD }, { ...page.sources[5], href: DOCS }, { ...page.sources[6], href: REPO_LICENSE }, - { ...page.sources[7], href: href('/skills/') }, - { ...page.sources[8], href: href('/systems/') }, - { ...page.sources[9], href: href('/templates/') }, + { ...page.sources[7], href: href('/plugins/skills/') }, + { ...page.sources[8], href: href('/plugins/systems/') }, + { ...page.sources[9], href: href('/plugins/templates/') }, ]; const jsonLd = [ @@ -140,8 +140,8 @@ const jsonLd = [
    • {page.nextItems[0].label} — {page.nextItems[0].body}
    • {page.nextItems[1].label} — {page.nextItems[1].body}
    • {page.nextItems[2].label} — {page.nextItems[2].body}
    • -
    • {page.nextItems[3].label} — {page.nextItems[3].body}
    • -
    • {page.nextItems[4].label} — {page.nextItems[4].body}
    • +
    • {page.nextItems[3].label} — {page.nextItems[3].body}
    • +
    • {page.nextItems[4].label} — {page.nextItems[4].body}
    diff --git a/apps/landing-page/app/pages/plugins/systems/index.astro b/apps/landing-page/app/pages/plugins/systems/index.astro index 11588b9be..4330508d9 100644 --- a/apps/landing-page/app/pages/plugins/systems/index.astro +++ b/apps/landing-page/app/pages/plugins/systems/index.astro @@ -12,10 +12,7 @@ */ import Layout from '../../../_components/sub-page-layout.astro'; import SystemCard from '../../../_components/system-card.astro'; -import { - getSystemRecords, - getSystemCategoryIndex, -} from '../../../_lib/catalog'; +import { getSystemRecords } from '../../../_lib/catalog'; import { getPluginsCopy } from '../../../_lib/plugins-i18n'; import { getLandingUiCopy, localeFromPath, localizedHref } from '../../../i18n'; @@ -24,7 +21,6 @@ const ui = getLandingUiCopy(locale); const pcopy = getPluginsCopy(locale); const href = (path: string) => localizedHref(path, locale); const systems = await getSystemRecords(locale); -const categoryTags = await getSystemCategoryIndex(locale); const title = `${pcopy.tileSystems} · ${systems.length} · Open Design`; const description = pcopy.systemsLead; @@ -54,21 +50,6 @@ const jsonLd = {

    {pcopy.systemsLead}

    -
    -
    - {ui.catalog.systems.category} - -
    -
    -
      {systems.map((s) => )} diff --git a/apps/landing-page/app/pages/quickstart/index.astro b/apps/landing-page/app/pages/quickstart/index.astro index f11feb98c..93cca40e1 100644 --- a/apps/landing-page/app/pages/quickstart/index.astro +++ b/apps/landing-page/app/pages/quickstart/index.astro @@ -142,8 +142,8 @@ const jsonLd = [

      {page.nextTitle}

      diff --git a/apps/landing-page/app/pages/skills/[slug]/index.astro b/apps/landing-page/app/pages/skills/[slug]/index.astro deleted file mode 100644 index c56690ef6..000000000 --- a/apps/landing-page/app/pages/skills/[slug]/index.astro +++ /dev/null @@ -1,472 +0,0 @@ ---- -/* - * /skills// — a detail page per skill. - * - * Two flavours render slightly differently: - * - `template` skills get a click-to-expand iframe of their - * `example.html` demo and stay deliberately brief — the demo is the - * content, the README is one click away on GitHub. - * - `instruction` skills (no runnable demo) instead render the full - * SKILL.md body inline, so the page reads like a brief: what the - * skill does, when it triggers, how to use it. Otherwise the page - * would be a one-line description and a row of CTAs. - */ -import { getEntry, render } from 'astro:content'; -import Layout from '../../../_components/sub-page-layout.astro'; -import LazyImg from '../../../_components/lazy-img.astro'; -import { getSkillRecords, type SkillRecord } from '../../../_lib/catalog'; -import { - getLandingUiCopy, - localeFromPath, - localizedHref, - type LandingLocaleCode, -} from '../../../i18n'; - -/* - * Localized share-copy template, keyed by landing locale. The brand - * keyword "open-source Claude Design alternative" stays in English - * because that's the canonical search query Google associates with - * the domain — translating it would split the entity claim. The - * surrounding sentence ("I'm using X from @opendesignai") translates - * per locale so the message reads as one coherent voice instead of - * mixing two scripts in a single share post. - * - * `{name}` and `{description}` are interpolated at render time. - * `{url}` is replaced with the canonical detail-page URL. - */ -type ShareTemplate = (vars: { name: string; description: string; url: string }) => string; -const SHARE_COPY: Record = { - en: ({ name, description, url }) => `🎨 Just discovered ${name} on @opendesignai — the open-source Claude Design alternative. -✨ Local-first · BYOK · your agent does the design. - -→ ${url}`, - zh: ({ name, description, url }) => `🎨 安利一个:@opendesignai 上的 ${name} —— Claude Design 的开源替代品。 -✨ 本地优先 · 自带模型 · 让你自己的 agent 做设计。 - -→ ${url}`, - 'zh-tw': ({ name, description, url }) => `🎨 推薦一個:@opendesignai 上的 ${name} —— Claude Design 的開源替代品。 -✨ 本地優先 · 自帶模型 · 讓你自己的 agent 做設計。 - -→ ${url}`, - ja: ({ name, description, url }) => `🎨 @opendesignai で ${name} を発見 —— オープンソースの Claude Design 代替。 -✨ ローカル優先 · BYOK · あなたのエージェントが設計する。 - -→ ${url}`, - ko: ({ name, description, url }) => `🎨 @opendesignai에서 ${name} 발견 —— 오픈 소스 Claude Design 대안. -✨ 로컬 우선 · BYOK · 에이전트가 디자인합니다. - -→ ${url}`, - de: ({ name, description, url }) => `🎨 Gerade entdeckt: ${name} auf @opendesignai — die Open-Source-Alternative zu Claude Design. -✨ Local-first · BYOK · dein Agent designt. - -→ ${url}`, - fr: ({ name, description, url }) => `🎨 Découvert : ${name} sur @opendesignai — l'alternative open-source à Claude Design. -✨ Local-first · BYOK · votre agent fait le design. - -→ ${url}`, - ru: ({ name, description, url }) => `🎨 Нашёл ${name} на @opendesignai — open-source альтернативу Claude Design. -✨ Локально · BYOK · агент сам делает дизайн. - -→ ${url}`, - es: ({ name, description, url }) => `🎨 Acabo de descubrir ${name} en @opendesignai — la alternativa open-source a Claude Design. -✨ Local-first · BYOK · tu agente diseña. - -→ ${url}`, - 'pt-br': ({ name, description, url }) => `🎨 Acabei de descobrir ${name} no @opendesignai — a alternativa open-source ao Claude Design. -✨ Local-first · BYOK · seu agente faz o design. - -→ ${url}`, - it: ({ name, description, url }) => `🎨 Ho appena scoperto ${name} su @opendesignai — l'alternativa open-source a Claude Design. -✨ Local-first · BYOK · il tuo agente progetta. - -→ ${url}`, - vi: ({ name, description, url }) => `🎨 Vừa khám phá ${name} trên @opendesignai — giải pháp mã nguồn mở thay thế Claude Design. -✨ Ưu tiên local · BYOK · agent của bạn thiết kế. - -→ ${url}`, - pl: ({ name, description, url }) => `🎨 Właśnie odkryłem ${name} na @opendesignai — open-source'ową alternatywę dla Claude Design. -✨ Local-first · BYOK · twój agent projektuje. - -→ ${url}`, - id: ({ name, description, url }) => `🎨 Baru nemu ${name} di @opendesignai — alternatif open-source untuk Claude Design. -✨ Local-first · BYOK · agent kamu yang nge-desain. - -→ ${url}`, - nl: ({ name, description, url }) => `🎨 Net ontdekt: ${name} op @opendesignai — het open-source alternatief voor Claude Design. -✨ Local-first · BYOK · jouw agent ontwerpt. - -→ ${url}`, - ar: ({ name, description, url }) => `🎨 اكتشفت للتو ${name} على @opendesignai — البديل مفتوح المصدر لـ Claude Design. -✨ محلي أولًا · BYOK · وكيلك يصمّم. - -→ ${url}`, - tr: ({ name, description, url }) => `🎨 Yeni keşfettim: ${name} (@opendesignai) — Claude Design'a açık kaynaklı alternatif. -✨ Local-first · BYOK · ajanın tasarlıyor. - -→ ${url}`, - uk: ({ name, description, url }) => `🎨 Щойно знайшов ${name} на @opendesignai — open-source альтернативу Claude Design. -✨ Local-first · BYOK · ваш агент робить дизайн. - -→ ${url}`, -}; - -export async function getStaticPaths() { - const skills = await getSkillRecords(); - return skills.map((skill) => ({ - params: { slug: skill.slug }, - props: { skill, all: skills }, - })); -} - -interface Props { - skill: SkillRecord; - all: ReadonlyArray; -} - -const { skill: routeSkill, all: routeAll } = Astro.props as Props; -const locale = localeFromPath(Astro.url.pathname); -const ui = getLandingUiCopy(locale); -const href = (path: string) => localizedHref(path, locale); -const all = locale === 'en' ? routeAll : await getSkillRecords(locale); -const skill = all.find((item) => item.slug === routeSkill.slug) ?? routeSkill; - -const title = ui.catalog.skills.detailTitle(skill.name); -const description = skill.description.length > 0 - ? skill.description - : ui.catalog.skills.detailFallbackDescription(skill.name); - -const skillUrl = `https://open-design.ai/skills/${skill.slug}/`; -const shareCopy = (SHARE_COPY[locale] ?? SHARE_COPY.en)({ - name: skill.name, - description, - url: skillUrl, -}); -// Share-dialog UI strings localized inline. Keeping them next to the -// page that uses them avoids growing the global UI bundle for what's -// effectively four short labels per locale. -const SHARE_UI: Record = { - en: { title: 'Share this skill', lead: 'Copy the message below, then jump to the platform you want to share on and paste.', copyText: 'Copy text', copyLink: 'Copy link only', jumpTo: 'Then jump to:', openLabel: 'Share ↗' }, - zh: { title: '分享这个 skill', lead: '复制下面的文案,然后跳到你想分享的平台粘贴即可。', copyText: '复制文案', copyLink: '只复制链接', jumpTo: '跳转到:', openLabel: '分享 ↗' }, - 'zh-tw': { title: '分享這個 skill', lead: '複製下面的文案,然後跳到你想分享的平台貼上即可。', copyText: '複製文案', copyLink: '只複製連結', jumpTo: '跳轉到:', openLabel: '分享 ↗' }, - ja: { title: 'この skill を共有', lead: '下のメッセージをコピーしてから、共有したいプラットフォームに移動して貼り付けてください。', copyText: 'テキストをコピー', copyLink: 'リンクのみコピー', jumpTo: 'プラットフォームへ:', openLabel: '共有 ↗' }, - ko: { title: '이 skill 공유', lead: '아래 메시지를 복사한 다음 공유할 플랫폼으로 이동해 붙여넣으세요.', copyText: '텍스트 복사', copyLink: '링크만 복사', jumpTo: '플랫폼으로:', openLabel: '공유 ↗' }, - de: { title: 'Diesen Skill teilen', lead: 'Kopiere die Nachricht unten und füge sie auf der gewünschten Plattform ein.', copyText: 'Text kopieren', copyLink: 'Nur Link kopieren', jumpTo: 'Zur Plattform:', openLabel: 'Teilen ↗' }, - fr: { title: 'Partager ce skill', lead: 'Copiez le message ci-dessous, puis ouvrez la plateforme de votre choix et collez.', copyText: 'Copier le texte', copyLink: 'Copier le lien', jumpTo: 'Aller sur :', openLabel: 'Partager ↗' }, - ru: { title: 'Поделиться скиллом', lead: 'Скопируйте сообщение ниже, затем перейдите на нужную платформу и вставьте.', copyText: 'Скопировать текст', copyLink: 'Только ссылка', jumpTo: 'Перейти:', openLabel: 'Поделиться ↗' }, - es: { title: 'Compartir este skill', lead: 'Copia el mensaje y abre la plataforma donde quieras compartirlo.', copyText: 'Copiar texto', copyLink: 'Solo el enlace', jumpTo: 'Ir a:', openLabel: 'Compartir ↗' }, - 'pt-br': { title: 'Compartilhar skill', lead: 'Copie a mensagem e abra a plataforma onde quer compartilhar.', copyText: 'Copiar texto', copyLink: 'Só o link', jumpTo: 'Ir para:', openLabel: 'Compartilhar ↗' }, - it: { title: 'Condividi lo skill', lead: 'Copia il messaggio e apri la piattaforma su cui vuoi condividere.', copyText: 'Copia testo', copyLink: 'Solo il link', jumpTo: 'Vai a:', openLabel: 'Condividi ↗' }, - vi: { title: 'Chia sẻ skill', lead: 'Sao chép nội dung dưới đây, rồi mở nền tảng bạn muốn chia sẻ và dán vào.', copyText: 'Sao chép', copyLink: 'Chỉ sao chép link', jumpTo: 'Mở:', openLabel: 'Chia sẻ ↗' }, - pl: { title: 'Udostępnij ten skill', lead: 'Skopiuj wiadomość poniżej, otwórz wybraną platformę i wklej.', copyText: 'Kopiuj tekst', copyLink: 'Skopiuj link', jumpTo: 'Przejdź do:', openLabel: 'Udostępnij ↗' }, - id: { title: 'Bagikan skill ini', lead: 'Salin pesan di bawah, lalu buka platform yang ingin Anda gunakan dan tempel.', copyText: 'Salin teks', copyLink: 'Salin tautan', jumpTo: 'Buka:', openLabel: 'Bagikan ↗' }, - nl: { title: 'Deel deze skill', lead: 'Kopieer het bericht hieronder en plak het op het platform van jouw keuze.', copyText: 'Tekst kopiëren', copyLink: 'Alleen de link', jumpTo: 'Ga naar:', openLabel: 'Delen ↗' }, - ar: { title: 'شارك هذه المهارة', lead: 'انسخ الرسالة أدناه، ثم انتقل إلى المنصة التي تريد المشاركة عليها والصقها.', copyText: 'انسخ النص', copyLink: 'انسخ الرابط فقط', jumpTo: 'انتقل إلى:', openLabel: 'مشاركة ↗' }, - tr: { title: 'Bu skilli paylaş', lead: 'Aşağıdaki mesajı kopyala, dilediğin platformu açıp yapıştır.', copyText: 'Metni kopyala', copyLink: 'Sadece linki kopyala', jumpTo: 'Şuraya git:', openLabel: 'Paylaş ↗' }, - uk: { title: 'Поділитись скілом', lead: 'Скопіюйте повідомлення нижче, потім перейдіть на платформу й вставте.', copyText: 'Копіювати текст', copyLink: 'Тільки посилання', jumpTo: 'Перейти:', openLabel: 'Поділитись ↗' }, -}; -const shareUi = SHARE_UI[locale] ?? SHARE_UI.en; - -const related = all - .filter((s) => s.slug !== skill.slug) - .filter((s) => s.mode === skill.mode || s.scenario === skill.scenario) - .slice(0, 4); - -/* - * Instruction skills don't have a runnable demo to iframe — to avoid - * a near-empty detail page, render the SKILL.md prose inline so the - * page reads like a brief. Template skills keep the page deliberately - * brief because their demo is the content; their full SKILL.md is one - * "Find on GitHub" click away. - * - * Astro 6 exposes the markdown pipeline through a top-level - * `render(entry)` helper rather than the legacy `entry.render()` - * method. The output (heading anchors, smart-typography, GFM - * tables) styles cleanly with the existing `.detail-md` rules. - */ -const skillEntry = - skill.kind === 'instruction' ? await getEntry('skills', `${skill.slug}/SKILL`) : null; -const SkillBody = skillEntry ? (await render(skillEntry)).Content : null; - -const jsonLd = [ - { - '@context': 'https://schema.org', - '@type': 'BreadcrumbList', - itemListElement: [ - { '@type': 'ListItem', position: 1, name: 'Open Design', item: Astro.site?.toString() }, - { '@type': 'ListItem', position: 2, name: ui.catalog.skills.detailLabel, item: new URL('/skills/', Astro.site).toString() }, - { '@type': 'ListItem', position: 3, name: skill.name, item: new URL(`/skills/${skill.slug}/`, Astro.site).toString() }, - ], - }, - { - '@context': 'https://schema.org', - '@type': 'SoftwareSourceCode', - name: skill.name, - description, - codeRepository: skill.source, - programmingLanguage: 'Markdown', - keywords: skill.triggers.join(', '), - license: 'https://www.apache.org/licenses/LICENSE-2.0', - }, -]; ---- - - - - -
      -
      - - {ui.catalog.skills.detailLabel} - {typeof skill.featured === 'number' && ( - {ui.catalog.skills.featuredNumber(String(skill.featured).padStart(2, '0'))} - )} - -

      {skill.name}.

      -

      {description}

      -
      - {/* - Two primary CTAs. "Use this skill" v1 sends users to the OD - desktop release page — install the app first, then run the - skill. Routing here rather than to /quickstart/ keeps the - flow concrete (download a binary now) instead of asking - users to read an install doc. Once the desktop client - exposes a registered URL scheme, this anchor flips to a - JS-driven `od://skill/` try + fallback without - changing the page surface. - */} - - Use this skill → - - - Find on GitHub → - - {skill.upstream && ( - - {ui.catalog.skills.upstream} - - )} - -
      -
      - - {skill.kind === 'template' && skill.previewUrl && ( -
      - {/* - Click-to-expand interactive preview. Only template-kind skills - ship a runnable example.html, so this block is gated on kind - rather than just `previewUrl` — instruction skills now have a - synthesized cover thumbnail too, but no iframe target. The - thumb is the summary of a `
      ` element: clicking opens - the live iframe, replacing the thumb with the canonical - `/example.html` rendered inside a sandboxed frame. - */} -
      - - - - -
      -