mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
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/<manifest-slug>/, 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-<x>, example-<x>), 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/<slug>/ 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/<slug>/ hrefs are the system cards that 301 by design). astro check passes.
This commit is contained in:
parent
df1535b7fd
commit
671237708f
31 changed files with 141 additions and 1826 deletions
|
|
@ -1,62 +0,0 @@
|
|||
---
|
||||
/*
|
||||
* Shared skill row used on `/skills/`, `/skills/mode/<slug>/`,
|
||||
* `/skills/scenario/<slug>/`, and any future faceted view.
|
||||
*
|
||||
* Renders a `<li class="catalog-row catalog-row-skill">` 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;
|
||||
---
|
||||
|
||||
<li class="catalog-row catalog-row-skill">
|
||||
<a href={href(`/skills/${skill.slug}/`)}>
|
||||
<span class="row-index">{String(index + 1).padStart(3, '0')}</span>
|
||||
<span class="row-thumb">
|
||||
{skill.previewUrl ? (
|
||||
<img
|
||||
src={skill.previewUrl}
|
||||
alt=""
|
||||
loading={eager ? 'eager' : 'lazy'}
|
||||
decoding="async"
|
||||
fetchpriority={eager ? 'high' : 'auto'}
|
||||
/>
|
||||
) : (
|
||||
<span class="row-thumb-empty" aria-hidden="true" />
|
||||
)}
|
||||
</span>
|
||||
<span class="row-body">
|
||||
<span class="row-name">{skill.name}</span>
|
||||
<span class="row-desc">{skill.description}</span>
|
||||
</span>
|
||||
<span class="row-meta">
|
||||
{skill.modeLabel && <span class="meta-tag">{skill.modeLabel}</span>}
|
||||
{skill.scenarioLabel && <span class="meta-tag muted">{skill.scenarioLabel}</span>}
|
||||
{skill.platformLabel && <span class="meta-tag muted">{skill.platformLabel}</span>}
|
||||
</span>
|
||||
<span class="row-arrow" aria-hidden="true">→</span>
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -1,8 +1,14 @@
|
|||
---
|
||||
/*
|
||||
* Shared system card used on `/systems/` and
|
||||
* `/systems/category/<slug>/`. 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/<slug>/`, which `public/_redirects`
|
||||
* 301s to the bundled-plugin detail (`/plugins/design-system-<slug>/`)
|
||||
* 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-<slug>`) keeps those 8 from
|
||||
* pointing at a non-existent detail page.
|
||||
*/
|
||||
import type { SystemRecord } from '../_lib/catalog';
|
||||
import { localeFromPath, localizedHref } from '../i18n';
|
||||
|
|
|
|||
|
|
@ -222,9 +222,9 @@ const INFO_PAGE_COPY: Partial<Record<LandingLocaleCode, InfoPageCopy>> = {
|
|||
{ 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 = [
|
||||
|
|
|
|||
|
|
@ -730,23 +730,23 @@ export default function Page({
|
|||
</h2>
|
||||
</div>
|
||||
<div className='pills' data-reveal='right'>
|
||||
<a className='pill active' href={href('/skills/')}>
|
||||
<a className='pill active' href={href('/plugins/skills/')}>
|
||||
{home.labs.pills.all}
|
||||
<span className='count'>{skills}</span>
|
||||
</a>
|
||||
<a className='pill' href={href('/skills/mode/prototype/')}>
|
||||
<a className='pill' href={href('/plugins/templates/')}>
|
||||
{home.labs.pills.prototype}
|
||||
<span className='count'>{prototypeCount}</span>
|
||||
</a>
|
||||
<a className='pill' href={href('/skills/mode/deck/')}>
|
||||
<a className='pill' href={href('/plugins/templates/')}>
|
||||
{home.labs.pills.deck}
|
||||
<span className='count'>{deckCount}</span>
|
||||
</a>
|
||||
<a className='pill' href={href('/skills/')}>
|
||||
<a className='pill' href={href('/plugins/templates/')}>
|
||||
{home.labs.pills.mobile}
|
||||
<span className='count'>{mobileCount}</span>
|
||||
</a>
|
||||
<a className='pill' href={href('/skills/')}>
|
||||
<a className='pill' href={href('/plugins/templates/')}>
|
||||
{home.labs.pills.office}
|
||||
<span className='count'>—</span>
|
||||
</a>
|
||||
|
|
@ -839,7 +839,7 @@ export default function Page({
|
|||
{home.labs.foot(skills)}
|
||||
{NBSP}·{NBSP}
|
||||
<a
|
||||
href={href('/skills/')}
|
||||
href={href('/plugins/skills/')}
|
||||
className='library-link'
|
||||
style={{ color: 'var(--coral)' }}
|
||||
>
|
||||
|
|
@ -953,7 +953,7 @@ export default function Page({
|
|||
{home.work.titleSuffix}
|
||||
<span className='dot'>.</span>
|
||||
</h2>
|
||||
<a className='work-link' href={href('/skills/')}>
|
||||
<a className='work-link' href={href('/plugins/skills/')}>
|
||||
{home.work.viewAll(skills)}
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -1325,17 +1325,17 @@ export default function Page({
|
|||
<h5>{home.footer.columns.library}</h5>
|
||||
<ul>
|
||||
<li>
|
||||
<a href={href('/skills/')}>
|
||||
<a href={href('/plugins/skills/')}>
|
||||
{home.footer.libraryLinks.skills(skills)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={href('/systems/')}>
|
||||
<a href={href('/plugins/systems/')}>
|
||||
{home.footer.libraryLinks.systems(systems)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={href('/templates/')}>
|
||||
<a href={href('/plugins/templates/')}>
|
||||
{home.footer.libraryLinks.templates}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -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' && (
|
||||
<>
|
||||
<header class='catalog-head'>
|
||||
<span class='label'>{copy.catalog} · Nº 01</span>
|
||||
<h1 class='display'><em>{copy.skillsTitle}</em> — {skills.length} composable design capabilities<span class='dot'>.</span></h1>
|
||||
<p class='lead'>Each skill is a folder with one <code>SKILL.md</code>. Drop it in, restart the daemon, and the picker shows it.</p>
|
||||
</header>
|
||||
{routeSecond === '' && (
|
||||
<section class='filter-strip' aria-label='Skill filters'>
|
||||
<div class='filter-group'>
|
||||
<span class='filter-label'>{copy.mode}</span>
|
||||
<ul>{modeTags.map((tag) => <li><a class='chip chip-link' href={href(`/skills/mode/${tag.slug}/`)}>{tag.label}<span class='chip-num'>{tag.count}</span></a></li>)}</ul>
|
||||
</div>
|
||||
<div class='filter-group'>
|
||||
<span class='filter-label'>{copy.scenario}</span>
|
||||
<ul>{scenarioTags.slice(0, 12).map((tag) => <li><a class='chip chip-link' href={href(`/skills/scenario/${tag.slug}/`)}>{tag.label}<span class='chip-num'>{tag.count}</span></a></li>)}</ul>
|
||||
</div>
|
||||
<div class='filter-group'>
|
||||
<span class='filter-label'>{copy.platform}</span>
|
||||
<ul>{platformTally.map(([key, count]) => <li><span class='chip'>{key}<span class='chip-num'>{count}</span></span></li>)}</ul>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<section class='catalog-grid catalog-grid-skills'>
|
||||
<ol>
|
||||
{skills
|
||||
.filter((skill) => routeSecond === 'mode' ? skill.mode === routeThird : routeSecond === 'scenario' ? skill.scenario === routeThird : true)
|
||||
.map((skill, index) => (
|
||||
<li class='catalog-row'>
|
||||
<a href={href(`/skills/${skill.slug}/`)}>
|
||||
<span class='row-index'>{String(index + 1).padStart(2, '0')}</span>
|
||||
<span class='row-body'><span class='row-name'>{skill.name}</span><span class='row-desc'>{skill.description}</span></span>
|
||||
{skill.mode && <span class='meta-tag'>{skill.mode}</span>}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
{routeRoot === 'systems' && (
|
||||
<>
|
||||
<header class='catalog-head'>
|
||||
<span class='label'>{copy.catalog} · Nº 02</span>
|
||||
<h1 class='display'><em>{copy.systemsTitle}</em> — {systems.length} portable visual systems<span class='dot'>.</span></h1>
|
||||
<p class='lead'>Each system is a single <code>DESIGN.md</code> token spec that keeps colors, type, spacing, and components consistent.</p>
|
||||
</header>
|
||||
{routeSecond === '' && <section class='filter-strip'><div class='filter-group'><span class='filter-label'>{copy.category}</span><ul>{systemCategories.map((tag) => <li><a class='chip chip-link' href={href(`/systems/category/${tag.slug}/`)}>{tag.label}<span class='chip-num'>{tag.count}</span></a></li>)}</ul></div></section>}
|
||||
<section class='catalog-grid systems-grid'>
|
||||
<ul>{systems.filter((system) => routeSecond === 'category' ? system.category === routeThird : true).map((system) => <li class='system-card'><a href={href(`/systems/${system.slug}/`)}><span class='system-name'>{system.name}</span><p>{system.tagline}</p><span class='meta-tag'>{system.category}</span></a></li>)}</ul>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
{routeRoot === 'craft' && (
|
||||
<>
|
||||
<header class='catalog-head'><span class='label'>{copy.catalog} · Nº 03</span><h1 class='display'><em>{copy.craftTitle}</em> — {craft.length} rendering principles<span class='dot'>.</span></h1><p class='lead'>Quality rules for accessibility, motion, color, type, and state coverage.</p></header>
|
||||
|
|
@ -185,11 +90,4 @@ const pageDescription = `Open Design ${routeRoot || 'landing'} page.`;
|
|||
</>
|
||||
)}
|
||||
|
||||
{routeRoot === 'templates' && (
|
||||
<>
|
||||
<header class='catalog-head'><span class='label'>{copy.catalog} · Nº 04</span><h1 class='display'><em>{copy.templatesTitle}</em> — {templates.length} ready-to-fork artifacts<span class='dot'>.</span></h1><p class='lead'>Pre-wired artifact bundles with examples, visual language, and agent instructions.</p></header>
|
||||
<section class='template-grid'><ul>{templates.map((template, index) => <li class='template-card'><a href={href(template.detailHref)}>{template.previewUrl && <span class='template-thumb'><LazyImg src={template.previewUrl} alt='' loading={index < 4 ? 'eager' : 'precise'} /></span>}<span class='template-name'>{template.name}</span><p class='template-summary'>{template.summary}</p></a></li>)}</ul></section>
|
||||
</>
|
||||
)}
|
||||
|
||||
</Layout>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<SkillPage {...Astro.props} />
|
||||
|
|
@ -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 } }),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<SkillsPage />
|
||||
|
|
@ -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,
|
||||
})),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<SkillModePage {...Astro.props} />
|
||||
|
|
@ -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,
|
||||
})),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<SkillScenarioPage {...Astro.props} />
|
||||
|
|
@ -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,
|
||||
})),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<SystemPage {...Astro.props} />
|
||||
|
|
@ -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,
|
||||
})),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<SystemCategoryPage {...Astro.props} />
|
||||
|
|
@ -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 } }),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<SystemsPage />
|
||||
|
|
@ -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,
|
||||
})),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<TemplatePage {...Astro.props} />
|
||||
|
|
@ -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 } }),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<TemplatesPage />
|
||||
|
|
@ -207,8 +207,8 @@ const jsonLd = [
|
|||
<h2>{page.nextTitle}</h2>
|
||||
<ul>
|
||||
<li><a class="inline-link" href={href('/quickstart/')}>{page.nextItems[0].label}</a> — {page.nextItems[0].body}</li>
|
||||
<li><a class="inline-link" href={href('/skills/')}>{page.nextItems[1].label}</a> — {page.nextItems[1].body}</li>
|
||||
<li><a class="inline-link" href={href('/systems/')}>{page.nextItems[2].label}</a> — {page.nextItems[2].body}</li>
|
||||
<li><a class="inline-link" href={href('/plugins/skills/')}>{page.nextItems[1].label}</a> — {page.nextItems[1].body}</li>
|
||||
<li><a class="inline-link" href={href('/plugins/systems/')}>{page.nextItems[2].label}</a> — {page.nextItems[2].body}</li>
|
||||
<li><a class="inline-link" href={href('/alternatives/claude-design/')}>{page.nextItems[3].label}</a> — {page.nextItems[3].body}</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1058,7 +1058,7 @@ pnpm -F @html-anything/next dev
|
|||
</p>
|
||||
<p>
|
||||
<a class="ha-btn" href={href('/')}>{copy.visitOpenDesign}</a>
|
||||
<a class="ha-btn" href={href('/skills/')} rel="noopener">{copy.browseSkills}</a>
|
||||
<a class="ha-btn" href={href('/plugins/skills/')} rel="noopener">{copy.browseSkills}</a>
|
||||
<a class="ha-btn" href={HA_URL} rel="noopener">{copy.githubLink}</a>
|
||||
</p>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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 = [
|
|||
<li><a class="inline-link" href={href('/quickstart/')}>{page.nextItems[0].label}</a> — {page.nextItems[0].body}</li>
|
||||
<li><a class="inline-link" href={href('/agents/')}>{page.nextItems[1].label}</a> — {page.nextItems[1].body}</li>
|
||||
<li><a class="inline-link" href={href('/alternatives/claude-design/')}>{page.nextItems[2].label}</a> — {page.nextItems[2].body}</li>
|
||||
<li><a class="inline-link" href={href('/skills/')}>{page.nextItems[3].label}</a> — {page.nextItems[3].body}</li>
|
||||
<li><a class="inline-link" href={href('/systems/')}>{page.nextItems[4].label}</a> — {page.nextItems[4].body}</li>
|
||||
<li><a class="inline-link" href={href('/plugins/skills/')}>{page.nextItems[3].label}</a> — {page.nextItems[3].body}</li>
|
||||
<li><a class="inline-link" href={href('/plugins/systems/')}>{page.nextItems[4].label}</a> — {page.nextItems[4].body}</li>
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -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 = {
|
|||
<p class="lead">{pcopy.systemsLead}</p>
|
||||
</header>
|
||||
|
||||
<section class="filter-strip" aria-label={ui.catalog.systems.allAria}>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">{ui.catalog.systems.category}</span>
|
||||
<ul>
|
||||
{categoryTags.map((tag) => (
|
||||
<li>
|
||||
<a class="chip chip-link" href={href(`/systems/category/${tag.slug}/`)}>
|
||||
{tag.label}<span class="chip-num">{tag.count}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="catalog-grid systems-grid" aria-label={ui.catalog.systems.allAria}>
|
||||
<ul>
|
||||
{systems.map((s) => <SystemCard system={s} />)}
|
||||
|
|
|
|||
|
|
@ -142,8 +142,8 @@ const jsonLd = [
|
|||
<section class="info-section" id="next">
|
||||
<h2>{page.nextTitle}</h2>
|
||||
<ul>
|
||||
<li><a class="inline-link" href={href('/skills/')}>{page.nextItems[0].label}</a> — {page.nextItems[0].body}</li>
|
||||
<li><a class="inline-link" href={href('/systems/')}>{page.nextItems[1].label}</a> — {page.nextItems[1].body}</li>
|
||||
<li><a class="inline-link" href={href('/plugins/skills/')}>{page.nextItems[0].label}</a> — {page.nextItems[0].body}</li>
|
||||
<li><a class="inline-link" href={href('/plugins/systems/')}>{page.nextItems[1].label}</a> — {page.nextItems[1].body}</li>
|
||||
<li><a class="inline-link" href={href('/compare/')}>{page.nextItems[2].label}</a> — {page.nextItems[2].body}</li>
|
||||
<li><a class="inline-link" href={REPO_RELEASES} target="_blank" rel="noreferrer noopener">{page.nextItems[3].label}</a> — {page.nextItems[3].body}</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,472 +0,0 @@
|
|||
---
|
||||
/*
|
||||
* /skills/<slug>/ — 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<LandingLocaleCode, ShareTemplate> = {
|
||||
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<SkillRecord>;
|
||||
}
|
||||
|
||||
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<LandingLocaleCode, { title: string; lead: string; copyText: string; copyLink: string; jumpTo: string; openLabel: string }> = {
|
||||
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',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} active="skills" jsonLd={jsonLd}>
|
||||
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
|
||||
<a href={href('/')}>Open Design</a>
|
||||
<span>/</span>
|
||||
<a href={href('/skills/')}>{ui.catalog.skills.detailLabel}</a>
|
||||
<span>/</span>
|
||||
<span aria-current="page">{skill.name}</span>
|
||||
</nav>
|
||||
|
||||
<article class="detail">
|
||||
<header class="detail-head">
|
||||
<span class="label">
|
||||
{ui.catalog.skills.detailLabel}
|
||||
{typeof skill.featured === 'number' && (
|
||||
<span class="ix">{ui.catalog.skills.featuredNumber(String(skill.featured).padStart(2, '0'))}</span>
|
||||
)}
|
||||
</span>
|
||||
<h1 class="display">{skill.name}<span class="dot">.</span></h1>
|
||||
<p class="lead">{description}</p>
|
||||
<div class="detail-actions">
|
||||
{/*
|
||||
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/<slug>` try + fallback without
|
||||
changing the page surface.
|
||||
*/}
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
href="https://github.com/nexu-io/open-design/releases"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Use this skill →
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-ghost"
|
||||
href={skill.source}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Find on GitHub →
|
||||
</a>
|
||||
{skill.upstream && (
|
||||
<a class="btn btn-ghost" href={skill.upstream} target="_blank" rel="noopener">
|
||||
{ui.catalog.skills.upstream}
|
||||
</a>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost detail-share-trigger"
|
||||
data-share-open={`skill:${skill.slug}`}
|
||||
>
|
||||
{shareUi.openLabel}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{skill.kind === 'template' && skill.previewUrl && (
|
||||
<figure class="detail-preview">
|
||||
{/*
|
||||
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 `<details>` element: clicking opens
|
||||
the live iframe, replacing the thumb with the canonical
|
||||
`<slug>/example.html` rendered inside a sandboxed frame.
|
||||
*/}
|
||||
<details class="detail-preview-live">
|
||||
<summary class="detail-preview-thumb-trigger" aria-label={`Open interactive preview for ${skill.name}`}>
|
||||
<LazyImg
|
||||
src={skill.previewUrl}
|
||||
alt={`${skill.name} example output`}
|
||||
loading="priority"
|
||||
/>
|
||||
<span class="detail-preview-thumb-overlay" aria-hidden="true">
|
||||
<span class="detail-preview-thumb-cta">Click for live preview ↗</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="detail-preview-frame-wrap">
|
||||
<iframe
|
||||
src={`/skills/${skill.slug}/example.html`}
|
||||
title={`${skill.name} interactive preview`}
|
||||
loading="lazy"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
class="detail-preview-frame"
|
||||
/>
|
||||
<a
|
||||
class="detail-preview-popout"
|
||||
href={`/skills/${skill.slug}/example.html`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
aria-label="Open preview in new tab"
|
||||
>
|
||||
Open in new tab ↗
|
||||
</a>
|
||||
</div>
|
||||
</details>
|
||||
<figcaption>
|
||||
{ui.catalog.skills.previewCaption(skill.slug)}
|
||||
</figcaption>
|
||||
</figure>
|
||||
)}
|
||||
|
||||
{/*
|
||||
Share modal — opens a `<dialog>` 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.
|
||||
*/}
|
||||
<dialog
|
||||
class="detail-share-dialog"
|
||||
data-share-dialog={`skill:${skill.slug}`}
|
||||
>
|
||||
<form method="dialog" class="detail-share-dialog-form">
|
||||
<header class="detail-share-dialog-head">
|
||||
<h2>{shareUi.title}</h2>
|
||||
<button type="submit" class="detail-share-dialog-close" aria-label="Close" value="cancel">×</button>
|
||||
</header>
|
||||
<p class="detail-share-dialog-lead">{shareUi.lead}</p>
|
||||
<textarea
|
||||
class="detail-share-dialog-text"
|
||||
readonly
|
||||
rows="6"
|
||||
data-share-text
|
||||
>{shareCopy}</textarea>
|
||||
<div class="detail-share-dialog-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary detail-share-dialog-copy"
|
||||
data-share-copy
|
||||
>
|
||||
{shareUi.copyText}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost detail-share-dialog-copy-link"
|
||||
data-copy-link={skillUrl}
|
||||
>
|
||||
{shareUi.copyLink}
|
||||
</button>
|
||||
</div>
|
||||
{/*
|
||||
Platform jump buttons — official brand logos rendered as
|
||||
inline SVG (no third-party icon font, no client JS). Each
|
||||
opens the vendor's compose surface in a new tab; the user
|
||||
pastes the already-copied text. Email channel was dropped
|
||||
per Joey's revision; the four channels here cover the
|
||||
highest-value SEO + virality surfaces.
|
||||
*/}
|
||||
<div class="detail-share-dialog-platforms">
|
||||
<span class="detail-share-dialog-platforms-label">{shareUi.jumpTo}</span>
|
||||
<a class="detail-share-platform-btn" href="https://x.com/compose/post" target="_blank" rel="noopener" aria-label="X">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.65l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25h6.815l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77z"/></svg>
|
||||
<span class="sr-only">X</span>
|
||||
</a>
|
||||
<a class="detail-share-platform-btn" href="https://www.linkedin.com/feed/?shareActive=true" target="_blank" rel="noopener" aria-label="LinkedIn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.063 2.063 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
|
||||
<span class="sr-only">LinkedIn</span>
|
||||
</a>
|
||||
<a class="detail-share-platform-btn" href="https://www.reddit.com/submit" target="_blank" rel="noopener" aria-label="Reddit">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 01-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 01.042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 014.028 12.3c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 01.14-.197.35.35 0 01.238-.042l2.906.617a1.214 1.214 0 011.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 00-.231.094.33.33 0 000 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 00.029-.463.33.33 0 00-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 00-.232-.095z"/></svg>
|
||||
<span class="sr-only">Reddit</span>
|
||||
</a>
|
||||
<a class="detail-share-platform-btn" href="https://www.facebook.com/" target="_blank" rel="noopener" aria-label="Facebook">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
|
||||
<span class="sr-only">Facebook</span>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dl class="detail-meta">
|
||||
{skill.mode && (
|
||||
<Fragment>
|
||||
<dt>{ui.catalog.skills.mode}</dt>
|
||||
<dd>{skill.modeLabel ?? skill.mode}</dd>
|
||||
</Fragment>
|
||||
)}
|
||||
{skill.scenario && (
|
||||
<Fragment>
|
||||
<dt>{ui.catalog.skills.scenario}</dt>
|
||||
<dd>{skill.scenarioLabel ?? skill.scenario}</dd>
|
||||
</Fragment>
|
||||
)}
|
||||
{skill.platform && (
|
||||
<Fragment>
|
||||
<dt>{ui.catalog.skills.platform}</dt>
|
||||
<dd>{skill.platformLabel ?? skill.platform}</dd>
|
||||
</Fragment>
|
||||
)}
|
||||
{skill.category && (
|
||||
<Fragment>
|
||||
<dt>{ui.catalog.systems.category}</dt>
|
||||
<dd>{skill.categoryLabel ?? skill.category}</dd>
|
||||
</Fragment>
|
||||
)}
|
||||
</dl>
|
||||
|
||||
{skill.triggers.length > 0 && (
|
||||
<section class="detail-block">
|
||||
<h2>{ui.catalog.skills.triggers}</h2>
|
||||
<p class="block-lead">
|
||||
{ui.catalog.skills.triggersLead}
|
||||
</p>
|
||||
<ul class="trigger-list">
|
||||
{skill.triggers.map((t) => <li><code>{t}</code></li>)}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{skill.examplePrompt && (
|
||||
<section class="detail-block">
|
||||
<h2>{ui.catalog.skills.examplePrompt}</h2>
|
||||
<pre class="example-prompt">{skill.examplePrompt}</pre>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{SkillBody && (
|
||||
<section class="detail-block detail-md">
|
||||
<h2>About this skill</h2>
|
||||
<SkillBody />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{related.length > 0 && (
|
||||
<section class="detail-block">
|
||||
<h2>{ui.catalog.skills.related}</h2>
|
||||
<ul class="related-grid">
|
||||
{related.map((r) => (
|
||||
<li>
|
||||
<a href={href(`/skills/${r.slug}/`)}>
|
||||
<span class="related-name">{r.name}</span>
|
||||
<span class="related-desc">{r.description}</span>
|
||||
<span class="related-meta">
|
||||
{r.modeLabel && <span class="meta-tag">{r.modeLabel}</span>}
|
||||
{r.scenarioLabel && <span class="meta-tag muted">{r.scenarioLabel}</span>}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</article>
|
||||
</Layout>
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
---
|
||||
/*
|
||||
* /skills/ — index of every shippable skill in the repo.
|
||||
*
|
||||
* Pulls live data from `skills/<slug>/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,
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} active="skills" jsonLd={jsonLd}>
|
||||
<header class="catalog-head">
|
||||
<span class="label">{ui.catalog.skills.label}</span>
|
||||
<h1 class="display">
|
||||
{ui.catalog.skills.heading(skills.length)}
|
||||
</h1>
|
||||
<p class="lead">
|
||||
{ui.catalog.skills.lead}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="filter-strip" aria-label={ui.catalog.skills.allAria}>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">{ui.catalog.skills.mode}</span>
|
||||
<ul>
|
||||
{modeTags.map((tag) => (
|
||||
<li>
|
||||
<a class="chip chip-link" href={href(`/skills/mode/${tag.slug}/`)}>
|
||||
{tag.label}<span class="chip-num">{tag.count}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">{ui.catalog.skills.scenario}</span>
|
||||
<ul>
|
||||
{scenarioTags.slice(0, 12).map((tag) => (
|
||||
<li>
|
||||
<a class="chip chip-link" href={href(`/skills/scenario/${tag.slug}/`)}>
|
||||
{tag.label}<span class="chip-num">{tag.count}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{platformTally.length > 0 && (
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">{ui.catalog.skills.platform}</span>
|
||||
<ul>
|
||||
{platformTally.map(([key, count]) => (
|
||||
<li>
|
||||
<span class="chip">
|
||||
{key}<span class="chip-num">{count}</span>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{featured.length > 0 && (
|
||||
<section class="featured-strip" aria-labelledby="featured-skills">
|
||||
<h2 id="featured-skills" class="strip-title">{ui.catalog.skills.featured}</h2>
|
||||
<ul class="featured-grid">
|
||||
{featured.map((s, i) => (
|
||||
<li class="featured-card">
|
||||
<a href={href(`/skills/${s.slug}/`)}>
|
||||
{s.previewUrl ? (
|
||||
<span class="featured-thumb">
|
||||
<LazyImg src={s.previewUrl} alt="" loading={i < 4 ? 'eager' : 'precise'} />
|
||||
</span>
|
||||
) : (
|
||||
<span class="featured-thumb featured-thumb-empty" aria-hidden="true" />
|
||||
)}
|
||||
<span class="featured-num">Nº {String(s.featured).padStart(2, '0')}</span>
|
||||
<span class="featured-name">{s.name}</span>
|
||||
<p>{s.description}</p>
|
||||
{s.modeLabel && <span class="meta-tag">{s.modeLabel}</span>}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section class="catalog-grid catalog-grid-skills" aria-label={ui.catalog.skills.allAria}>
|
||||
<ol>
|
||||
{skills.map((s, idx) => <SkillRow skill={s} index={idx} />)}
|
||||
</ol>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
---
|
||||
/*
|
||||
* /skills/mode/<slug>/ — 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,
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} active="skills" jsonLd={jsonLd}>
|
||||
<header class="catalog-head">
|
||||
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
|
||||
<a href={href('/skills/')}>{ui.catalog.skills.detailLabel}</a>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span>{ui.catalog.skills.mode}</span>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span class="crumb-active">{heading}</span>
|
||||
</nav>
|
||||
<span class="label">{ui.catalog.skills.label}</span>
|
||||
<h1 class="display">
|
||||
{ui.catalog.skills.modeHeading(heading, records.length)}
|
||||
</h1>
|
||||
<p class="lead">
|
||||
{ui.catalog.skills.modeLead(label ?? tag.label)}
|
||||
</p>
|
||||
<p class="filter-clear">
|
||||
<a href={href('/skills/')}>{ui.catalog.skills.allSkills(tag.count)}</a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="catalog-grid catalog-grid-skills" aria-label={ui.catalog.skills.allAria}>
|
||||
<ol>
|
||||
{records.map((s, idx) => <SkillRow skill={s} index={idx} />)}
|
||||
</ol>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
---
|
||||
/*
|
||||
* /skills/scenario/<slug>/ — 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,
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} active="skills" jsonLd={jsonLd}>
|
||||
<header class="catalog-head">
|
||||
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
|
||||
<a href={href('/skills/')}>{ui.catalog.skills.detailLabel}</a>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span>{ui.catalog.skills.scenario}</span>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span class="crumb-active">{heading}</span>
|
||||
</nav>
|
||||
<span class="label">{ui.catalog.skills.label}</span>
|
||||
<h1 class="display">
|
||||
{ui.catalog.skills.scenarioHeading(heading, records.length)}
|
||||
</h1>
|
||||
<p class="lead">
|
||||
{ui.catalog.skills.scenarioLead(label ?? tag.label)}
|
||||
</p>
|
||||
<p class="filter-clear">
|
||||
<a href={href('/skills/')}>{ui.catalog.skills.allSkills()}</a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="catalog-grid catalog-grid-skills" aria-label={ui.catalog.skills.allAria}>
|
||||
<ol>
|
||||
{records.map((s, idx) => <SkillRow skill={s} index={idx} />)}
|
||||
</ol>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -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<SystemRecord>;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} active="systems" jsonLd={jsonLd}>
|
||||
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
|
||||
<a href={href('/')}>Open Design</a>
|
||||
<span>/</span>
|
||||
<a href={href('/systems/')}>{ui.catalog.systems.detailLabel}</a>
|
||||
<span>/</span>
|
||||
<span aria-current="page">{system.name}</span>
|
||||
</nav>
|
||||
|
||||
<article class="detail">
|
||||
<header class="detail-head">
|
||||
<span class="label">
|
||||
{ui.catalog.systems.detailLabel}
|
||||
<span class="ix">· {system.categoryLabel}</span>
|
||||
</span>
|
||||
<h1 class="display">{system.name}<span class="dot">.</span></h1>
|
||||
{system.tagline && <p class="lead">{system.tagline}</p>}
|
||||
<div class="detail-actions">
|
||||
<a class="btn btn-primary" href={system.source} target="_blank" rel="noopener">
|
||||
{ui.catalog.systems.viewOnGithub}
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{system.palette.length > 0 && (
|
||||
<section class="detail-block">
|
||||
<h2>{ui.catalog.systems.paletteSample}</h2>
|
||||
<p class="block-lead">
|
||||
{ui.catalog.systems.paletteLead(system.palette.length)}
|
||||
</p>
|
||||
<div class="palette-row">
|
||||
{system.palette.map((hex) => (
|
||||
<div class="palette-cell">
|
||||
<span class="swatch" style={`background:${hex}`} />
|
||||
<code>{hex}</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{system.atmosphere && (
|
||||
<section class="detail-block">
|
||||
<h2>{ui.catalog.systems.visualTheme}</h2>
|
||||
<p class="atmosphere">{system.atmosphere}</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{related.length > 0 && (
|
||||
<section class="detail-block">
|
||||
<h2>{ui.catalog.systems.related(system.categoryLabel)}</h2>
|
||||
<ul class="related-grid">
|
||||
{related.map((r) => (
|
||||
<li>
|
||||
<a href={href(`/systems/${r.slug}/`)}>
|
||||
<span class="related-name">{r.name}</span>
|
||||
<span class="related-desc">{r.tagline}</span>
|
||||
<div class="system-swatches" aria-hidden="true">
|
||||
{r.palette.slice(0, 4).map((hex) => (
|
||||
<span class="swatch" style={`background:${hex}`} />
|
||||
))}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</article>
|
||||
</Layout>
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
/*
|
||||
* /systems/category/<slug>/ — 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,
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} active="systems" jsonLd={jsonLd}>
|
||||
<header class="catalog-head">
|
||||
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
|
||||
<a href={href('/systems/')}>{ui.catalog.systems.detailLabel}</a>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span>{ui.catalog.systems.category}</span>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span class="crumb-active">{heading}</span>
|
||||
</nav>
|
||||
<span class="label">{ui.catalog.systems.label}</span>
|
||||
<h1 class="display">
|
||||
{ui.catalog.systems.categoryHeading(heading, records.length)}
|
||||
</h1>
|
||||
<p class="lead">
|
||||
{ui.catalog.systems.categoryLead(label ?? tag.label)}
|
||||
</p>
|
||||
<p class="filter-clear">
|
||||
<a href={href('/systems/')}>{ui.catalog.systems.allSystems}</a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="catalog-grid systems-grid" aria-label={ui.catalog.systems.allAria}>
|
||||
<ul>
|
||||
{records.map((s) => <SystemCard system={s} />)}
|
||||
</ul>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -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,
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} active="systems" jsonLd={jsonLd}>
|
||||
<header class="catalog-head">
|
||||
<span class="label">{ui.catalog.systems.label}</span>
|
||||
<h1 class="display">
|
||||
{ui.catalog.systems.heading(systems.length)}
|
||||
</h1>
|
||||
<p class="lead">
|
||||
{ui.catalog.systems.lead}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="filter-strip" aria-label={ui.catalog.systems.allAria}>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">{ui.catalog.systems.category}</span>
|
||||
<ul>
|
||||
{categoryTags.map((tag) => (
|
||||
<li>
|
||||
<a class="chip chip-link" href={href(`/systems/category/${tag.slug}/`)}>
|
||||
{tag.label}<span class="chip-num">{tag.count}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="catalog-grid systems-grid" aria-label={ui.catalog.systems.allAria}>
|
||||
<ul>
|
||||
{systems.map((s) => <SystemCard system={s} />)}
|
||||
</ul>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -1,356 +0,0 @@
|
|||
---
|
||||
/*
|
||||
* /templates/<slug>/ — 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<LandingLocaleCode, ShareTemplate> = {
|
||||
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<LandingLocaleCode, { title: string; lead: string; copyText: string; copyLink: string; jumpTo: string; openLabel: string }> = {
|
||||
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() },
|
||||
],
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} active="templates" jsonLd={jsonLd}>
|
||||
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
|
||||
<a href={href('/')}>Open Design</a>
|
||||
<span>/</span>
|
||||
<a href={href('/templates/')}>{ui.catalog.templates.detailLabel}</a>
|
||||
<span>/</span>
|
||||
<span aria-current="page">{template.name}</span>
|
||||
</nav>
|
||||
|
||||
<article class="detail">
|
||||
<header class="detail-head">
|
||||
<span class="label">
|
||||
{ui.catalog.templates.detailLabel}
|
||||
<span class="ix">· {originLabel}</span>
|
||||
</span>
|
||||
<h1 class="display">{template.name}<span class="dot">.</span></h1>
|
||||
<p class="lead">{template.summary}</p>
|
||||
{(template.mode || template.platform || template.scenario) && (
|
||||
<dl class="detail-meta">
|
||||
{template.mode && (
|
||||
<>
|
||||
<dt>{ui.catalog.skills.mode}</dt>
|
||||
<dd>{template.modeLabel ?? template.mode}</dd>
|
||||
</>
|
||||
)}
|
||||
{template.platform && (
|
||||
<>
|
||||
<dt>{ui.catalog.skills.platform}</dt>
|
||||
<dd>{template.platformLabel ?? template.platform}</dd>
|
||||
</>
|
||||
)}
|
||||
{template.scenario && (
|
||||
<>
|
||||
<dt>{ui.catalog.skills.scenario}</dt>
|
||||
<dd>{template.scenarioLabel ?? template.scenario}</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
)}
|
||||
<div class="detail-actions">
|
||||
{/* 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. */}
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
href="https://github.com/nexu-io/open-design/releases"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Use this template →
|
||||
</a>
|
||||
<a class="btn btn-ghost" href={template.source} target="_blank" rel="noopener">
|
||||
Find on GitHub →
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost detail-share-trigger"
|
||||
data-share-open={`template:${template.slug}`}
|
||||
>
|
||||
{shareUi.openLabel}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{template.previewUrl && (
|
||||
<figure class="detail-preview">
|
||||
{/* Click-to-expand: thumb is the summary; clicking opens the
|
||||
live iframe rendering the canonical artifact. Skill-template
|
||||
origin → /skills/<slug>/example.html; live-artifact origin
|
||||
→ /templates/<slug>/preview.html. */}
|
||||
<details class="detail-preview-live">
|
||||
<summary class="detail-preview-thumb-trigger" aria-label={`Open interactive preview for ${template.name}`}>
|
||||
<LazyImg
|
||||
src={template.previewUrl}
|
||||
alt={`${template.name} preview`}
|
||||
loading="priority"
|
||||
/>
|
||||
<span class="detail-preview-thumb-overlay" aria-hidden="true">
|
||||
<span class="detail-preview-thumb-cta">Click for live preview ↗</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="detail-preview-frame-wrap">
|
||||
<iframe
|
||||
src={
|
||||
template.origin === 'live-artifact'
|
||||
? `/templates/${template.slug}/preview.html`
|
||||
: `/skills/${template.slug}/example.html`
|
||||
}
|
||||
title={`${template.name} interactive preview`}
|
||||
loading="lazy"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
class="detail-preview-frame"
|
||||
/>
|
||||
<a
|
||||
class="detail-preview-popout"
|
||||
href={
|
||||
template.origin === 'live-artifact'
|
||||
? `/templates/${template.slug}/preview.html`
|
||||
: `/skills/${template.slug}/example.html`
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
aria-label="Open preview in new tab"
|
||||
>
|
||||
Open in new tab ↗
|
||||
</a>
|
||||
</div>
|
||||
</details>
|
||||
<figcaption>{ui.catalog.templates.previewCaption}</figcaption>
|
||||
</figure>
|
||||
)}
|
||||
|
||||
{/* Share modal — same shape as skills/[slug]; see that file for the
|
||||
copy-then-paste rationale and SEO keyword choice. */}
|
||||
<dialog
|
||||
class="detail-share-dialog"
|
||||
data-share-dialog={`template:${template.slug}`}
|
||||
>
|
||||
<form method="dialog" class="detail-share-dialog-form">
|
||||
<header class="detail-share-dialog-head">
|
||||
<h2>{shareUi.title}</h2>
|
||||
<button type="submit" class="detail-share-dialog-close" aria-label="Close" value="cancel">×</button>
|
||||
</header>
|
||||
<p class="detail-share-dialog-lead">{shareUi.lead}</p>
|
||||
<textarea
|
||||
class="detail-share-dialog-text"
|
||||
readonly
|
||||
rows="6"
|
||||
data-share-text
|
||||
>{shareCopy}</textarea>
|
||||
<div class="detail-share-dialog-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary detail-share-dialog-copy"
|
||||
data-share-copy
|
||||
>
|
||||
{shareUi.copyText}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost detail-share-dialog-copy-link"
|
||||
data-copy-link={templateUrl}
|
||||
>
|
||||
{shareUi.copyLink}
|
||||
</button>
|
||||
</div>
|
||||
<div class="detail-share-dialog-platforms">
|
||||
<span class="detail-share-dialog-platforms-label">{shareUi.jumpTo}</span>
|
||||
<a class="detail-share-platform-btn" href="https://x.com/compose/post" target="_blank" rel="noopener" aria-label="X">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.65l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25h6.815l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77z"/></svg>
|
||||
<span class="sr-only">X</span>
|
||||
</a>
|
||||
<a class="detail-share-platform-btn" href="https://www.linkedin.com/feed/?shareActive=true" target="_blank" rel="noopener" aria-label="LinkedIn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.063 2.063 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
|
||||
<span class="sr-only">LinkedIn</span>
|
||||
</a>
|
||||
<a class="detail-share-platform-btn" href="https://www.reddit.com/submit" target="_blank" rel="noopener" aria-label="Reddit">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 01-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 01.042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 014.028 12.3c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 01.14-.197.35.35 0 01.238-.042l2.906.617a1.214 1.214 0 011.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 00-.231.094.33.33 0 000 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 00.029-.463.33.33 0 00-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 00-.232-.095z"/></svg>
|
||||
<span class="sr-only">Reddit</span>
|
||||
</a>
|
||||
<a class="detail-share-platform-btn" href="https://www.facebook.com/" target="_blank" rel="noopener" aria-label="Facebook">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
|
||||
<span class="sr-only">Facebook</span>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<section class="detail-block">
|
||||
<h2>{ui.catalog.templates.whatsInside}</h2>
|
||||
<p class="block-lead">
|
||||
{ui.catalog.templates.whatsInsideLead}
|
||||
</p>
|
||||
<ul class="trigger-list">
|
||||
{files.map(([name, copy]) => (
|
||||
<li><code>{name}</code> — {copy}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
</Layout>
|
||||
|
|
@ -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,
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} active="templates" jsonLd={jsonLd}>
|
||||
<header class="catalog-head">
|
||||
<span class="label">{ui.catalog.templates.label}</span>
|
||||
<h1 class="display">
|
||||
{ui.catalog.templates.heading(templates.length)}
|
||||
</h1>
|
||||
<p class="lead">
|
||||
{ui.catalog.templates.lead}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="template-grid" aria-label={ui.catalog.templates.allAria}>
|
||||
<ul>
|
||||
{templates.map((t, i) => (
|
||||
<li class="template-card">
|
||||
<a href={href(t.detailHref)}>
|
||||
{t.previewUrl ? (
|
||||
<span class="template-thumb">
|
||||
<LazyImg src={t.previewUrl} alt="" loading={i < 4 ? 'eager' : 'precise'} />
|
||||
</span>
|
||||
) : (
|
||||
<span class="template-thumb template-thumb-empty" aria-hidden="true" />
|
||||
)}
|
||||
<span class={`meta-tag ${t.origin === 'live-artifact' ? 'coral' : ''}`}>
|
||||
{t.origin === 'live-artifact' ? ui.catalog.templates.liveArtifact : (t.modeLabel ?? ui.catalog.templates.skillTemplate)}
|
||||
</span>
|
||||
<span class="template-name">{t.name}</span>
|
||||
<p class="template-summary">{t.summary}</p>
|
||||
{(t.platform || t.scenario) && (
|
||||
<span class="template-meta-line">
|
||||
{[t.platformLabel ?? t.platform, t.scenarioLabel ?? t.scenario].filter(Boolean).join(' · ')}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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-<folder> 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-<folder> 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue