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' && (
- <>
-
- {routeSecond === '' && (
-
-
-
-
-
{copy.platform}
-
{platformTally.map(([key, count]) => {key}{count} )}
-
-
- )}
-
- >
- )}
-
- {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 === '' && }
-
- >
- )}
-
{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' && (
- <>
-
-
- >
- )}
-
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',
- },
-];
----
-
-
-
- Open Design
- /
- {ui.catalog.skills.detailLabel}
- /
- {skill.name}
-
-
-
-
-
- {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 ↗
-
-
-
-
-
- {ui.catalog.skills.previewCaption(skill.slug)}
-
-
- )}
-
- {/*
- Share modal — opens a `` containing the canonical share
- copy (with the brand keyword "open-source Claude Design
- alternative" baked in), a one-click "Copy" button, and a row of
- platform jump buttons. Each platform button just opens the
- vendor's compose URL — the user pastes the already-copied text.
- This works around a real cross-platform pain point: LinkedIn /
- Facebook ignore pre-fill `text` params, X has length limits that
- truncate Chinese content unpredictably, and Reddit's title param
- survives but title-only is a weak signal. Copy-then-paste is
- uniformly reliable.
-
- The trigger sits inside `.detail-actions` instead of as a
- separate row below `.detail-meta` so it has visual weight equal
- to the primary CTAs. Joey called this out specifically.
- */}
-
-
-
-
-
- {skill.mode && (
-
- {ui.catalog.skills.mode}
- {skill.modeLabel ?? skill.mode}
-
- )}
- {skill.scenario && (
-
- {ui.catalog.skills.scenario}
- {skill.scenarioLabel ?? skill.scenario}
-
- )}
- {skill.platform && (
-
- {ui.catalog.skills.platform}
- {skill.platformLabel ?? skill.platform}
-
- )}
- {skill.category && (
-
- {ui.catalog.systems.category}
- {skill.categoryLabel ?? skill.category}
-
- )}
-
-
- {skill.triggers.length > 0 && (
-
- {ui.catalog.skills.triggers}
-
- {ui.catalog.skills.triggersLead}
-
-
- {skill.triggers.map((t) => {t} )}
-
-
- )}
-
- {skill.examplePrompt && (
-
- {ui.catalog.skills.examplePrompt}
- {skill.examplePrompt}
-
- )}
-
- {SkillBody && (
-
- )}
-
- {related.length > 0 && (
-
- {ui.catalog.skills.related}
-
-
- )}
-
-
diff --git a/apps/landing-page/app/pages/skills/index.astro b/apps/landing-page/app/pages/skills/index.astro
deleted file mode 100644
index 86a6cc755..000000000
--- a/apps/landing-page/app/pages/skills/index.astro
+++ /dev/null
@@ -1,133 +0,0 @@
----
-/*
- * /skills/ — index of every shippable skill in the repo.
- *
- * Pulls live data from `skills//SKILL.md` via Astro Content
- * Collections so adding a skill anywhere in the monorepo
- * automatically surfaces here on the next build.
- */
-import Layout from '../../_components/sub-page-layout.astro';
-import LazyImg from '../../_components/lazy-img.astro';
-import SkillRow from '../../_components/skill-row.astro';
-import {
- getSkillRecords,
- getSkillModeIndex,
- getSkillScenarioIndex,
- tally,
-} from '../../_lib/catalog';
-import { getLandingUiCopy, localeFromPath, localizedHref } from '../../i18n';
-
-const locale = localeFromPath(Astro.url.pathname);
-const ui = getLandingUiCopy(locale);
-const href = (path: string) => localizedHref(path, locale);
-const skills = await getSkillRecords(locale);
-
-const modeTags = await getSkillModeIndex(locale);
-const scenarioTags = await getSkillScenarioIndex(locale);
-const platformTally = tally(
- skills.map((s) => s.platformLabel).filter((p): p is string => Boolean(p)),
-);
-
-const featured = skills.filter((s) => typeof s.featured === 'number').slice(0, 6);
-
-const title = ui.catalog.skills.title(skills.length);
-const description = ui.catalog.skills.description;
-
-const jsonLd = {
- '@context': 'https://schema.org',
- '@type': 'CollectionPage',
- name: title,
- description,
- url: new URL('/skills/', Astro.site).toString(),
- isPartOf: {
- '@type': 'WebSite',
- name: 'Open Design',
- url: Astro.site?.toString(),
- },
- numberOfItems: skills.length,
-};
----
-
-
-
-
-
-
-
{ui.catalog.skills.mode}
-
-
-
-
{ui.catalog.skills.scenario}
-
-
- {platformTally.length > 0 && (
-
-
{ui.catalog.skills.platform}
-
- {platformTally.map(([key, count]) => (
-
-
- {key}{count}
-
-
- ))}
-
-
- )}
-
-
- {featured.length > 0 && (
-
- {ui.catalog.skills.featured}
-
-
- )}
-
-
-
- {skills.map((s, idx) => )}
-
-
-
diff --git a/apps/landing-page/app/pages/skills/mode/[mode].astro b/apps/landing-page/app/pages/skills/mode/[mode].astro
deleted file mode 100644
index 572ba7f9c..000000000
--- a/apps/landing-page/app/pages/skills/mode/[mode].astro
+++ /dev/null
@@ -1,78 +0,0 @@
----
-/*
- * /skills/mode// — every skill that emits a given artifact mode
- * (deck, prototype, template, image, video, audio, design-system, utility).
- *
- * One static page per distinct `od.mode` value. Mode is the strongest
- * mental-model facet ("I want a deck-builder") so this is the primary
- * faceted view; scenario/category live alongside.
- */
-import Layout from '../../../_components/sub-page-layout.astro';
-import SkillRow from '../../../_components/skill-row.astro';
-import {
- getSkillModeIndex,
- getSkillsForMode,
- type TagDescriptor,
-} from '../../../_lib/catalog';
-import { getLandingUiCopy, localeFromPath, localizedHref } from '../../../i18n';
-
-export async function getStaticPaths() {
- const tags = await getSkillModeIndex();
- return tags.map((tag) => ({
- params: { mode: tag.slug },
- props: { tag },
- }));
-}
-
-interface Props {
- tag: TagDescriptor;
-}
-
-const { tag } = Astro.props as Props;
-const locale = localeFromPath(Astro.url.pathname);
-const ui = getLandingUiCopy(locale);
-const href = (path: string) => localizedHref(path, locale);
-const { records, label } = await getSkillsForMode(tag.slug, locale);
-const heading = label ?? tag.label;
-
-const title = ui.catalog.skills.filterTitle(heading, records.length);
-const description = ui.catalog.skills.modeDescription(heading, records.length);
-
-const url = new URL(`/skills/mode/${tag.slug}/`, Astro.site).toString();
-const jsonLd = {
- '@context': 'https://schema.org',
- '@type': 'CollectionPage',
- name: title,
- description,
- url,
- numberOfItems: records.length,
-};
----
-
-
-
-
-
-
- {records.map((s, idx) => )}
-
-
-
diff --git a/apps/landing-page/app/pages/skills/scenario/[scenario].astro b/apps/landing-page/app/pages/skills/scenario/[scenario].astro
deleted file mode 100644
index 83d03044c..000000000
--- a/apps/landing-page/app/pages/skills/scenario/[scenario].astro
+++ /dev/null
@@ -1,77 +0,0 @@
----
-/*
- * /skills/scenario// — every skill targeting a given use-case
- * scenario (marketing, engineering, design, research, ...).
- *
- * Mirrors the mode page but facets on `od.scenario`. One page per
- * distinct scenario value found across all SKILL.md files.
- */
-import Layout from '../../../_components/sub-page-layout.astro';
-import SkillRow from '../../../_components/skill-row.astro';
-import {
- getSkillScenarioIndex,
- getSkillsForScenario,
- type TagDescriptor,
-} from '../../../_lib/catalog';
-import { getLandingUiCopy, localeFromPath, localizedHref } from '../../../i18n';
-
-export async function getStaticPaths() {
- const tags = await getSkillScenarioIndex();
- return tags.map((tag) => ({
- params: { scenario: tag.slug },
- props: { tag },
- }));
-}
-
-interface Props {
- tag: TagDescriptor;
-}
-
-const { tag } = Astro.props as Props;
-const locale = localeFromPath(Astro.url.pathname);
-const ui = getLandingUiCopy(locale);
-const href = (path: string) => localizedHref(path, locale);
-const { records, label } = await getSkillsForScenario(tag.slug, locale);
-const heading = label ?? tag.label;
-
-const title = ui.catalog.skills.filterTitle(heading, records.length);
-const description = ui.catalog.skills.scenarioDescription(heading, records.length);
-
-const url = new URL(`/skills/scenario/${tag.slug}/`, Astro.site).toString();
-const jsonLd = {
- '@context': 'https://schema.org',
- '@type': 'CollectionPage',
- name: title,
- description,
- url,
- numberOfItems: records.length,
-};
----
-
-
-
-
-
-
- {records.map((s, idx) => )}
-
-
-
diff --git a/apps/landing-page/app/pages/systems/[slug].astro b/apps/landing-page/app/pages/systems/[slug].astro
deleted file mode 100644
index 79a2eb5d4..000000000
--- a/apps/landing-page/app/pages/systems/[slug].astro
+++ /dev/null
@@ -1,126 +0,0 @@
----
-import Layout from '../../_components/sub-page-layout.astro';
-import { getSystemRecords, type SystemRecord } from '../../_lib/catalog';
-import { getLandingUiCopy, localeFromPath, localizedHref } from '../../i18n';
-
-export async function getStaticPaths() {
- const systems = await getSystemRecords();
- return systems.map((system) => ({
- params: { slug: system.slug },
- props: { system, all: systems },
- }));
-}
-
-interface Props {
- system: SystemRecord;
- all: ReadonlyArray;
-}
-
-const { system: routeSystem, 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 getSystemRecords(locale);
-const system = all.find((item) => item.slug === routeSystem.slug) ?? routeSystem;
-
-const title = ui.catalog.systems.detailTitle(system.name);
-const description = system.tagline
- ? `${system.name} (${system.categoryLabel}) — ${system.tagline}`
- : ui.catalog.systems.detailFallbackDescription(system.name, system.categoryLabel);
-
-const related = all
- .filter((s) => s.slug !== system.slug && s.category === system.category)
- .slice(0, 4);
-
-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.systems.detailLabel, item: new URL('/systems/', Astro.site).toString() },
- { '@type': 'ListItem', position: 3, name: system.name, item: new URL(`/systems/${system.slug}/`, Astro.site).toString() },
- ],
- },
- {
- '@context': 'https://schema.org',
- '@type': 'CreativeWork',
- name: system.name,
- description,
- url: new URL(`/systems/${system.slug}/`, Astro.site).toString(),
- license: 'https://www.apache.org/licenses/LICENSE-2.0',
- genre: system.categoryLabel,
- },
-];
----
-
-
-
- Open Design
- /
- {ui.catalog.systems.detailLabel}
- /
- {system.name}
-
-
-
-
-
- {ui.catalog.systems.detailLabel}
- · {system.categoryLabel}
-
- {system.name}.
- {system.tagline && {system.tagline}
}
-
-
-
- {system.palette.length > 0 && (
-
- {ui.catalog.systems.paletteSample}
-
- {ui.catalog.systems.paletteLead(system.palette.length)}
-
-
- {system.palette.map((hex) => (
-
-
- {hex}
-
- ))}
-
-
- )}
-
- {system.atmosphere && (
-
- {ui.catalog.systems.visualTheme}
- {system.atmosphere}
-
- )}
-
- {related.length > 0 && (
-
- {ui.catalog.systems.related(system.categoryLabel)}
-
-
- )}
-
-
diff --git a/apps/landing-page/app/pages/systems/category/[category].astro b/apps/landing-page/app/pages/systems/category/[category].astro
deleted file mode 100644
index 9686eca07..000000000
--- a/apps/landing-page/app/pages/systems/category/[category].astro
+++ /dev/null
@@ -1,74 +0,0 @@
----
-/*
- * /systems/category// — every design system grouped by category
- * (AI & LLM, Productivity & SaaS, Editorial, Brand, ...).
- */
-import Layout from '../../../_components/sub-page-layout.astro';
-import SystemCard from '../../../_components/system-card.astro';
-import {
- getSystemCategoryIndex,
- getSystemsForCategory,
- type TagDescriptor,
-} from '../../../_lib/catalog';
-import { getLandingUiCopy, localeFromPath, localizedHref } from '../../../i18n';
-
-export async function getStaticPaths() {
- const tags = await getSystemCategoryIndex();
- return tags.map((tag) => ({
- params: { category: tag.slug },
- props: { tag },
- }));
-}
-
-interface Props {
- tag: TagDescriptor;
-}
-
-const { tag } = Astro.props as Props;
-const locale = localeFromPath(Astro.url.pathname);
-const ui = getLandingUiCopy(locale);
-const href = (path: string) => localizedHref(path, locale);
-const { records, label } = await getSystemsForCategory(tag.slug, locale);
-const heading = label ?? tag.label;
-
-const title = ui.catalog.systems.categoryHeading(heading, records.length);
-const description = ui.catalog.systems.categoryDescription(heading, records.length);
-
-const url = new URL(`/systems/category/${tag.slug}/`, Astro.site).toString();
-const jsonLd = {
- '@context': 'https://schema.org',
- '@type': 'CollectionPage',
- name: title,
- description,
- url,
- numberOfItems: records.length,
-};
----
-
-
-
-
-
-
- {records.map((s) => )}
-
-
-
diff --git a/apps/landing-page/app/pages/systems/index.astro b/apps/landing-page/app/pages/systems/index.astro
deleted file mode 100644
index c2c2d93ac..000000000
--- a/apps/landing-page/app/pages/systems/index.astro
+++ /dev/null
@@ -1,61 +0,0 @@
----
-/*
- * /systems/ — index of every portable design system in the repo.
- */
-import Layout from '../../_components/sub-page-layout.astro';
-import SystemCard from '../../_components/system-card.astro';
-import { getSystemRecords, getSystemCategoryIndex } from '../../_lib/catalog';
-import { getLandingUiCopy, localeFromPath, localizedHref } from '../../i18n';
-
-const locale = localeFromPath(Astro.url.pathname);
-const ui = getLandingUiCopy(locale);
-const href = (path: string) => localizedHref(path, locale);
-const systems = await getSystemRecords(locale);
-
-const categoryTags = await getSystemCategoryIndex(locale);
-
-const title = ui.catalog.systems.title(systems.length);
-const description = ui.catalog.systems.description;
-
-const jsonLd = {
- '@context': 'https://schema.org',
- '@type': 'CollectionPage',
- name: title,
- description,
- url: new URL('/systems/', Astro.site).toString(),
- numberOfItems: systems.length,
-};
----
-
-
-
-
-
-
-
{ui.catalog.systems.category}
-
-
-
-
-
-
- {systems.map((s) => )}
-
-
-
diff --git a/apps/landing-page/app/pages/templates/[slug]/index.astro b/apps/landing-page/app/pages/templates/[slug]/index.astro
deleted file mode 100644
index 7f2e2e6d8..000000000
--- a/apps/landing-page/app/pages/templates/[slug]/index.astro
+++ /dev/null
@@ -1,356 +0,0 @@
----
-/*
- * /templates// — detail page for renderable design templates and
- * legacy Live Artifact template bundles.
- */
-import Layout from '../../../_components/sub-page-layout.astro';
-import LazyImg from '../../../_components/lazy-img.astro';
-import { getTemplateRecords, type TemplateRecord } from '../../../_lib/catalog';
-import {
- getLandingUiCopy,
- localeFromPath,
- localizedHref,
- type LandingLocaleCode,
-} from '../../../i18n';
-
-/* See pages/skills/[slug]/index.astro for the rationale on why these
- * tables live inline rather than in the global UI bundle. Same shape,
- * just keyed for the templates surface. */
-type ShareTemplate = (vars: { name: string; description: string; url: string }) => string;
-const SHARE_COPY: Record = {
- en: ({ name, description, url }) => `🎨 Just forked ${name} from @opendesignai — the open-source Claude Design alternative.
-✨ Templates as files, not vendor docs. Fork → swap → ship.
-
-→ ${url}`,
- zh: ({ name, description, url }) => `🎨 fork 了一个:@opendesignai 上的 ${name} —— Claude Design 的开源替代品。
-✨ 模板就是文件,不是 vendor 数据。Fork → 换数据 → 发。
-
-→ ${url}`,
- 'zh-tw': ({ name, description, url }) => `🎨 fork 了一個:@opendesignai 上的 ${name} —— Claude Design 的開源替代品。
-✨ 模板就是檔案,不是 vendor 資料。Fork → 換資料 → 發佈。
-
-→ ${url}`,
- ja: ({ name, description, url }) => `🎨 @opendesignai の ${name} を fork —— オープンソースの Claude Design 代替。
-✨ テンプレートはファイル、ベンダー DB じゃない。Fork → 差し替え → 出荷。
-
-→ ${url}`,
- ko: ({ name, description, url }) => `🎨 @opendesignai의 ${name} fork —— 오픈 소스 Claude Design 대안.
-✨ 템플릿은 파일, 벤더 DB가 아닙니다. Fork → 교체 → 출시.
-
-→ ${url}`,
- de: ({ name, description, url }) => `🎨 Gerade ${name} von @opendesignai geforkt — die Open-Source-Alternative zu Claude Design.
-✨ Vorlagen als Dateien, nicht als Vendor-DB. Fork → swap → ship.
-
-→ ${url}`,
- fr: ({ name, description, url }) => `🎨 Je viens de forker ${name} de @opendesignai — l'alternative open-source à Claude Design.
-✨ Modèles = fichiers, pas une base vendeur. Fork → swap → ship.
-
-→ ${url}`,
- ru: ({ name, description, url }) => `🎨 Форкнул ${name} с @opendesignai — open-source альтернативу Claude Design.
-✨ Шаблоны — это файлы, не vendor-DB. Fork → swap → ship.
-
-→ ${url}`,
- es: ({ name, description, url }) => `🎨 Acabo de hacer fork de ${name} en @opendesignai — la alternativa open-source a Claude Design.
-✨ Plantillas como archivos, no como vendor DB. Fork → swap → ship.
-
-→ ${url}`,
- 'pt-br': ({ name, description, url }) => `🎨 Acabei de dar fork em ${name} do @opendesignai — a alternativa open-source ao Claude Design.
-✨ Templates como arquivos, não como vendor DB. Fork → swap → ship.
-
-→ ${url}`,
- it: ({ name, description, url }) => `🎨 Ho appena forkato ${name} da @opendesignai — l'alternativa open-source a Claude Design.
-✨ Template come file, non come DB vendor. Fork → swap → ship.
-
-→ ${url}`,
- vi: ({ name, description, url }) => `🎨 Vừa fork ${name} từ @opendesignai — giải pháp mã nguồn mở thay thế Claude Design.
-✨ Template là file, không phải DB của vendor. Fork → đổi data → ship.
-
-→ ${url}`,
- pl: ({ name, description, url }) => `🎨 Właśnie sforkowałem ${name} z @opendesignai — open-source'ową alternatywę dla Claude Design.
-✨ Szablony jako pliki, nie vendor DB. Fork → swap → ship.
-
-→ ${url}`,
- id: ({ name, description, url }) => `🎨 Baru fork ${name} dari @opendesignai — alternatif open-source untuk Claude Design.
-✨ Template itu file, bukan vendor DB. Fork → tukar data → ship.
-
-→ ${url}`,
- nl: ({ name, description, url }) => `🎨 Net ${name} geforkt van @opendesignai — het open-source alternatief voor Claude Design.
-✨ Templates als bestanden, niet als vendor-DB. Fork → swap → ship.
-
-→ ${url}`,
- ar: ({ name, description, url }) => `🎨 fork للتو ${name} من @opendesignai — البديل مفتوح المصدر لـ Claude Design.
-✨ القوالب ملفات، ليست قاعدة بيانات للمزوّد. Fork → swap → ship.
-
-→ ${url}`,
- tr: ({ name, description, url }) => `🎨 ${name} fork'ladım (@opendesignai) — Claude Design'a açık kaynaklı alternatif.
-✨ Şablonlar dosya, vendor DB değil. Fork → swap → ship.
-
-→ ${url}`,
- uk: ({ name, description, url }) => `🎨 Форкнув ${name} з @opendesignai — open-source альтернативу Claude Design.
-✨ Шаблони — це файли, а не vendor-DB. Fork → swap → ship.
-
-→ ${url}`,
-};
-const SHARE_UI: Record = {
- en: { title: 'Share this template', 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: '分享这个模板', lead: '复制下面的文案,然后跳到你想分享的平台粘贴即可。', copyText: '复制文案', copyLink: '只复制链接', jumpTo: '跳转到:', openLabel: '分享 ↗' },
- 'zh-tw': { title: '分享這個模板', lead: '複製下面的文案,然後跳到你想分享的平台貼上即可。', copyText: '複製文案', copyLink: '只複製連結', jumpTo: '跳轉到:', openLabel: '分享 ↗' },
- ja: { title: 'このテンプレートを共有', lead: '下のメッセージをコピーしてから、共有したいプラットフォームに移動して貼り付けてください。', copyText: 'テキストをコピー', copyLink: 'リンクのみコピー', jumpTo: 'プラットフォームへ:', openLabel: '共有 ↗' },
- ko: { title: '이 템플릿 공유', lead: '아래 메시지를 복사한 다음 공유할 플랫폼으로 이동해 붙여넣으세요.', copyText: '텍스트 복사', copyLink: '링크만 복사', jumpTo: '플랫폼으로:', openLabel: '공유 ↗' },
- de: { title: 'Diese Vorlage 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 modèle', 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 plantilla', 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 template', 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 il modello', 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ẻ template', 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 szablon', 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 template 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 template', 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 şablonu 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: 'Поділитись ↗' },
-};
-
-export async function getStaticPaths() {
- const records = await getTemplateRecords();
- return records.map((template) => ({
- params: { slug: template.slug },
- props: { template },
- }));
-}
-
-interface Props {
- template: TemplateRecord;
-}
-
-const { template: routeTemplate } = Astro.props as Props;
-const locale = localeFromPath(Astro.url.pathname);
-const ui = getLandingUiCopy(locale);
-const href = (path: string) => localizedHref(path, locale);
-const localizedTemplates = locale === 'en' ? [] : await getTemplateRecords(locale);
-const template =
- localizedTemplates.find((item) => item.slug === routeTemplate.slug) ?? routeTemplate;
-
-const title = ui.catalog.templates.detailTitle(template.name);
-const description = template.summary;
-
-const templateUrl = `https://open-design.ai/templates/${template.slug}/`;
-const shareCopy = (SHARE_COPY[locale] ?? SHARE_COPY.en)({
- name: template.name,
- description: template.summary,
- url: templateUrl,
-});
-const shareUi = SHARE_UI[locale] ?? SHARE_UI.en;
-const originLabel =
- template.origin === 'live-artifact'
- ? ui.catalog.templates.liveArtifact
- : ui.catalog.templates.skillTemplate;
-const files =
- template.origin === 'live-artifact'
- ? [
- ['template.html', ui.catalog.templates.renderer],
- ['data.json', ui.catalog.templates.seedData],
- ['README.md', ui.catalog.templates.readme],
- ]
- : [
- ['SKILL.md', ui.catalog.skills.detailLabel],
- ['example.html', ui.catalog.templates.previewCaption],
- ['assets/', ui.catalog.templates.detailLabel],
- ['references/', ui.catalog.craft.detailLabel],
- ];
-
-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.templates.detailLabel, item: new URL('/templates/', Astro.site).toString() },
- { '@type': 'ListItem', position: 3, name: template.name, item: new URL(template.detailHref, Astro.site).toString() },
- ],
-};
----
-
-
-
- Open Design
- /
- {ui.catalog.templates.detailLabel}
- /
- {template.name}
-
-
-
-
-
- {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. */}
-
-
-
- {shareUi.lead}
- {shareCopy}
-
-
- {shareUi.copyText}
-
-
- {shareUi.copyLink}
-
-
-
-
-
-
-
- {ui.catalog.templates.whatsInside}
-
- {ui.catalog.templates.whatsInsideLead}
-
-
- {files.map(([name, copy]) => (
- {name} — {copy}
- ))}
-
-
-
-
diff --git a/apps/landing-page/app/pages/templates/index.astro b/apps/landing-page/app/pages/templates/index.astro
deleted file mode 100644
index 46fbb9c1d..000000000
--- a/apps/landing-page/app/pages/templates/index.astro
+++ /dev/null
@@ -1,63 +0,0 @@
----
-import Layout from '../../_components/sub-page-layout.astro';
-import LazyImg from '../../_components/lazy-img.astro';
-import { getTemplateRecords } from '../../_lib/catalog';
-import { getLandingUiCopy, localeFromPath, localizedHref } from '../../i18n';
-
-const locale = localeFromPath(Astro.url.pathname);
-const ui = getLandingUiCopy(locale);
-const href = (path: string) => localizedHref(path, locale);
-const templates = await getTemplateRecords(locale);
-
-const title = ui.catalog.templates.title(templates.length);
-const description = ui.catalog.templates.description;
-
-const jsonLd = {
- '@context': 'https://schema.org',
- '@type': 'CollectionPage',
- name: title,
- description,
- url: new URL('/templates/', Astro.site).toString(),
- numberOfItems: templates.length,
-};
----
-
-
-
-
-
-
diff --git a/apps/landing-page/astro.config.ts b/apps/landing-page/astro.config.ts
index c995623c4..4c5a0d43e 100644
--- a/apps/landing-page/astro.config.ts
+++ b/apps/landing-page/astro.config.ts
@@ -257,11 +257,11 @@ export default defineConfig({
item.priority = 0.9;
item.changefreq = changefreq.weekly;
} else if (
- path === '/skills/' ||
- path === '/systems/' ||
- path === '/templates/' ||
path === '/craft/' ||
- path === '/plugins/'
+ path === '/plugins/' ||
+ path === '/plugins/skills/' ||
+ path === '/plugins/systems/' ||
+ path === '/plugins/templates/'
) {
item.priority = 0.7;
item.changefreq = changefreq.weekly;
diff --git a/apps/landing-page/public/_redirects b/apps/landing-page/public/_redirects
index e8bff6f60..f0e39349f 100644
--- a/apps/landing-page/public/_redirects
+++ b/apps/landing-page/public/_redirects
@@ -34,3 +34,85 @@
/fa/plugins/* /plugins/:splat 301
/hu/plugins/* /plugins/:splat 301
/th/plugins/* /plugins/:splat 301
+
+# ─────────────────────────────────────────────────────────────────────
+# Catalog migration: legacy /skills /systems /templates -> /plugins/*
+# The old Astro generators were removed; these 301s preserve inbound
+# links and SEO equity. Cloudflare matches first rule wins, so order is:
+# faceted/specific -> detail prefixes -> bare index -> locale variants.
+# trailingSlash:'always', so every source and target ends in '/'.
+# ─────────────────────────────────────────────────────────────────────
+
+# Faceted pages have no new equivalent -> degrade to the section landing.
+/skills/mode/* /plugins/skills/ 301
+/skills/scenario/* /plugins/skills/ 301
+/systems/category/* /plugins/systems/ 301
+
+# Systems detail: design-system- is the uniform new slug.
+# These 8 folders have no new detail page -> degrade (must precede splat).
+/systems/cisco/ /plugins/systems/ 301
+/systems/hud/ /plugins/systems/ 301
+/systems/loom/ /plugins/systems/ 301
+/systems/perplexity/ /plugins/systems/ 301
+/systems/slack/ /plugins/systems/ 301
+/systems/trading-terminal/ /plugins/systems/ 301
+/systems/webex/ /plugins/systems/ 301
+/systems/wechat/ /plugins/systems/ 301
+/systems/* /plugins/design-system-:splat 301
+
+# Templates detail: example- is the uniform new slug.
+/templates/live-otd-operations-brief/ /plugins/templates/ 301
+/templates/* /plugins/example-:splat 301
+
+# Skills detail: only these 27 have a new artifact-template equivalent.
+# 'replicate' collides with design-system-replicate -> force the section.
+/skills/replicate/ /plugins/skills/ 301
+/skills/article-magazine/ /plugins/example-article-magazine/ 301
+/skills/card-twitter/ /plugins/example-card-twitter/ 301
+/skills/card-xiaohongshu/ /plugins/example-card-xiaohongshu/ 301
+/skills/data-report/ /plugins/example-data-report/ 301
+/skills/deck-guizang-editorial/ /plugins/example-deck-guizang-editorial/ 301
+/skills/deck-open-slide-canvas/ /plugins/example-deck-open-slide-canvas/ 301
+/skills/deck-swiss-international/ /plugins/example-deck-swiss-international/ 301
+/skills/design-brief/ /plugins/example-design-brief/ 301
+/skills/doc-kami-parchment/ /plugins/example-doc-kami-parchment/ 301
+/skills/frame-data-chart-nyt/ /plugins/example-frame-data-chart-nyt/ 301
+/skills/frame-flowchart-sticky/ /plugins/example-frame-flowchart-sticky/ 301
+/skills/frame-glitch-title/ /plugins/example-frame-glitch-title/ 301
+/skills/frame-light-leak-cinema/ /plugins/example-frame-light-leak-cinema/ 301
+/skills/frame-liquid-bg-hero/ /plugins/example-frame-liquid-bg-hero/ 301
+/skills/frame-logo-outro/ /plugins/example-frame-logo-outro/ 301
+/skills/frame-macos-notification/ /plugins/example-frame-macos-notification/ 301
+/skills/hatch-pet/ /plugins/example-hatch-pet/ 301
+/skills/mockup-device-3d/ /plugins/example-mockup-device-3d/ 301
+/skills/poster-hero/ /plugins/example-poster-hero/ 301
+/skills/ppt-keynote/ /plugins/example-ppt-keynote/ 301
+/skills/pptx-html-fidelity-audit/ /plugins/example-pptx-html-fidelity-audit/ 301
+/skills/resume-modern/ /plugins/example-resume-modern/ 301
+/skills/social-reddit-card/ /plugins/example-social-reddit-card/ 301
+/skills/social-spotify-card/ /plugins/example-social-spotify-card/ 301
+/skills/social-x-post-card/ /plugins/example-social-x-post-card/ 301
+/skills/vfx-text-cursor/ /plugins/example-vfx-text-cursor/ 301
+/skills/video-hyperframes/ /plugins/example-video-hyperframes/ 301
+# Remaining ~110 instruction-only skills have no detail page -> section.
+/skills/* /plugins/skills/ 301
+
+# Bare catalog index pages (least specific -> last).
+/skills/ /plugins/skills/ 301
+/systems/ /plugins/systems/ 301
+/templates/ /plugins/templates/ 301
+
+# Locale-prefixed variants (active LANDING_LOCALES minus en: zh zh-tw ja ko).
+# Non-en pages are sitemap-excluded; degrade to the section (no detail precision).
+/zh/skills/* /zh/plugins/skills/ 301
+/zh/systems/* /zh/plugins/systems/ 301
+/zh/templates/* /zh/plugins/templates/ 301
+/zh-tw/skills/* /zh-tw/plugins/skills/ 301
+/zh-tw/systems/* /zh-tw/plugins/systems/ 301
+/zh-tw/templates/* /zh-tw/plugins/templates/ 301
+/ja/skills/* /ja/plugins/skills/ 301
+/ja/systems/* /ja/plugins/systems/ 301
+/ja/templates/* /ja/plugins/templates/ 301
+/ko/skills/* /ko/plugins/skills/ 301
+/ko/systems/* /ko/plugins/systems/ 301
+/ko/templates/* /ko/plugins/templates/ 301