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;
----
-
-
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.
- */}
-
-
-
-
- Click for live preview ↗
-
-
-
- {/* Two CTAs matching skills/[slug]: "Use this template" sends
- users to the OD desktop release page (install first, then
- use the template); "Find on GitHub" deep-links to the
- source folder. See skills/[slug].astro for the broader
- rationale on the release-page pivot. */}
-
- Use this template →
-
-
- Find on GitHub →
-
-
-
-
-
- {template.previewUrl && (
-
- {/* Click-to-expand: thumb is the summary; clicking opens the
- live iframe rendering the canonical artifact. Skill-template
- origin → /skills//example.html; live-artifact origin
- → /templates//preview.html. */}
-
-
-
-
- Click for live preview ↗
-
-
-
-
- {ui.catalog.templates.previewCaption}
-
- )}
-
- {/* Share modal — same shape as skills/[slug]; see that file for the
- copy-then-paste rationale and SEO keyword choice. */}
-
-
-
-
-
-