From 671237708f6962531701e65e7ef1440b930af791 Mon Sep 17 00:00:00 2001 From: Joey-nexu Date: Sat, 30 May 2026 22:38:40 +0800 Subject: [PATCH] feat(landing-page): 301 legacy /skills /systems /templates to /plugins The 2026-05 plugins library rebuild introduced /plugins/skills/, /plugins/systems/, /plugins/templates/ and a unified detail route /plugins//, but the old /skills/, /systems/, /templates/ catalogs were left live in parallel. Two equivalent page trees split SEO equity, and the homepage, footer, quickstart, agents, official and blog pages all still linked to the old routes. Retire the legacy generators and 301 every old URL to its new plugins equivalent so inbound links and search equity are preserved: - Remove the /skills, /systems, /templates page generators (English + [locale] wrappers) and the now-orphaned skill-row component, and prune the skills/systems/templates branches from the [locale]/[...path] catch-all (it now renders only craft + blog). - Add the migration block to public/_redirects. Detail slugs differ from the old folder names (new slugs are manifest-name based, e.g. design-system-, example-), so systems/templates use a prefixed splat plus a short degrade list, and skills map the 27 with a template equivalent explicitly while the ~110 instruction-only skills and all mode/scenario/category facet pages degrade to the section landing. 'replicate' is forced to the section to avoid colliding with the design-system of the same name. Locale variants (zh, zh-tw, ja, ko) strip to the section. - Repoint in-site links to /plugins/* across page.tsx (footer, work, labs pills), info-page-i18n.ts (en + zh + sourceNames), official, quickstart, agents, blog and html-anything, and update the sitemap serialize priority list. The system-card keeps linking through /systems// so the 8 systems without a detail page ride the redirect's degrade rather than pointing at a missing page. Verified with a full astro build: old routes no longer emit any HTML, the new section pages exist, _redirects is copied verbatim, and no in-site link targets a removed route (the remaining /systems// hrefs are the system cards that 301 by design). astro check passes. --- .../app/_components/skill-row.astro | 62 --- .../app/_components/system-card.astro | 12 +- apps/landing-page/app/info-page-i18n.ts | 18 +- apps/landing-page/app/page.tsx | 20 +- .../app/pages/[locale]/[...path].astro | 132 +---- .../app/pages/[locale]/skills/[slug].astro | 19 - .../app/pages/[locale]/skills/index.astro | 12 - .../pages/[locale]/skills/mode/[mode].astro | 19 - .../[locale]/skills/scenario/[scenario].astro | 19 - .../app/pages/[locale]/systems/[slug].astro | 19 - .../systems/category/[category].astro | 19 - .../app/pages/[locale]/systems/index.astro | 12 - .../app/pages/[locale]/templates/[slug].astro | 19 - .../app/pages/[locale]/templates/index.astro | 12 - .../landing-page/app/pages/agents/index.astro | 4 +- apps/landing-page/app/pages/blog/[slug].astro | 2 +- .../app/pages/html-anything/index.astro | 2 +- .../app/pages/official/index.astro | 10 +- .../app/pages/plugins/systems/index.astro | 21 +- .../app/pages/quickstart/index.astro | 4 +- .../app/pages/skills/[slug]/index.astro | 472 ------------------ .../landing-page/app/pages/skills/index.astro | 133 ----- .../app/pages/skills/mode/[mode].astro | 78 --- .../pages/skills/scenario/[scenario].astro | 77 --- .../app/pages/systems/[slug].astro | 126 ----- .../pages/systems/category/[category].astro | 74 --- .../app/pages/systems/index.astro | 61 --- .../app/pages/templates/[slug]/index.astro | 356 ------------- .../app/pages/templates/index.astro | 63 --- apps/landing-page/astro.config.ts | 8 +- apps/landing-page/public/_redirects | 82 +++ 31 files changed, 141 insertions(+), 1826 deletions(-) delete mode 100644 apps/landing-page/app/_components/skill-row.astro delete mode 100644 apps/landing-page/app/pages/[locale]/skills/[slug].astro delete mode 100644 apps/landing-page/app/pages/[locale]/skills/index.astro delete mode 100644 apps/landing-page/app/pages/[locale]/skills/mode/[mode].astro delete mode 100644 apps/landing-page/app/pages/[locale]/skills/scenario/[scenario].astro delete mode 100644 apps/landing-page/app/pages/[locale]/systems/[slug].astro delete mode 100644 apps/landing-page/app/pages/[locale]/systems/category/[category].astro delete mode 100644 apps/landing-page/app/pages/[locale]/systems/index.astro delete mode 100644 apps/landing-page/app/pages/[locale]/templates/[slug].astro delete mode 100644 apps/landing-page/app/pages/[locale]/templates/index.astro delete mode 100644 apps/landing-page/app/pages/skills/[slug]/index.astro delete mode 100644 apps/landing-page/app/pages/skills/index.astro delete mode 100644 apps/landing-page/app/pages/skills/mode/[mode].astro delete mode 100644 apps/landing-page/app/pages/skills/scenario/[scenario].astro delete mode 100644 apps/landing-page/app/pages/systems/[slug].astro delete mode 100644 apps/landing-page/app/pages/systems/category/[category].astro delete mode 100644 apps/landing-page/app/pages/systems/index.astro delete mode 100644 apps/landing-page/app/pages/templates/[slug]/index.astro delete mode 100644 apps/landing-page/app/pages/templates/index.astro 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. - */} -
      - - - - -
      -