mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* chore(landing-page): bring PR #2469 content wholesale onto post-revert main Step 1 of replicating @pftom's #2469 work without the deploy-blocking issues that forced #2603. This commit copies the full \`apps/landing-page/\` diff from #2469's HEAD (`9d2a4f1`) onto current main verbatim — every i18n bundle, every page rewrite, every \`[locale]/\` wrapper. Subsequent commits on this branch then surgically restore the SEO fixes that #2469 silently regressed and configure the sitemap to survive the Cloudflare Pages 25 MiB limit, so deploy is healthy when this lands. What's in this commit - Tom's i18n bundle: \`i18n.ts\` (5377 lines), \`home-page-i18n.ts\`, \`info-page-i18n.ts\`, \`landing-ui-i18n.ts\`, \`content-i18n.ts\` (~10K lines total of locale data) - 18 landing-page locales: en, zh, zh-tw, ja, ko, de, fr, ru, es, pt-br, it, vi, pl, id, nl, ar, tr, uk - All existing pages rewritten to consume the new i18n bundle - Full \`[locale]/<route>/\` wrapper tree for every catalog page - \`plugin-registry.ts\` rewrite, \`catalog.ts\` adjustments - \`astro.config.ts\` route + sitemap reconfiguration - \`public/_headers\`, \`public/_redirects\`, \`public/favicon.svg\` adds - \`_components/locale-switcher-script.astro\` add What's intentionally NOT done in this commit (handled in follow-ups on this same branch): - Restore brand mark 44px + rounded corners (was lost from #2588) - Restore HA SoftwareApplication \`alternateName\` array (was lost from #2566) - Restore HA \`url\` canonical pointing at the landing page (was lost from #2586) - Restore Product/Library/Tutorials/Blog nav grouping (was lost from #2588) - Restore catalog-card padding 24px (was lost from #2600) - Configure sitemap to filter \`[locale]/\` routes so the generated XML stays under 25 MiB and Cloudflare Pages accepts the deploy - Add \`/zh-CN/* → /zh/*\` redirects for backwards-compatibility with any externally-linked OD-canonical locale URLs Validation so far - \`pnpm --filter @open-design/landing-page typecheck\` — 0 errors * fix(landing-page): unblock deploy + restore SEO regressions on top of #2469 Step 2 of replicating @pftom's #2469. The previous commit on this branch brings #2469's content wholesale; this commit applies the surgical fixes that make the result actually deploy and preserves the SEO improvements that #2469 silently regressed. Fix 1 — sitemap stays under Cloudflare Pages 25 MiB upload limit - `astro.config.ts` `filter` now drops every `/{locale}/...` route so the sitemap only emits canonical English URLs. - Locale variants are still discoverable via the `<xhtml:link rel="alternate" hreflang="...">` annotations the `namespaces.xhtml: true` option emits inside each canonical entry. This is Google's recommended pattern for a multi-language site. - Verified: post-fix `out/sitemap-0.xml` = 179 KB (was 38.4 MiB on the prior attempt that forced #2603's revert). Fix 2 — header brand block restored to the polished version - Logo `width/height` 36 → 44 (matches PR #2588's brand-mark refresh for visual weight against the new black speech-bubble glyph) - `.brand-meta` block ("Studio Nº 01 · Berlin / Open / Earth") removed from the header bar; the same editorial flourish still lives on the rotated `.side-rail .rail-text` pseudo-elements at page edges. Fix 3 — header nav grouped into Library + standalone Tutorials/Blog - Skills / Systems / Templates / Craft are now children of a Library dropdown (matches PR #2588's grouping). Each row keeps its count badge inline; the trigger highlights when any of the four facet pages is active. - Tutorials and Blog stay as standalone top-row items (PR #2588's original decision after Joey's review on the Learn dropdown). - Contact removed from the header — it was a same-page anchor that the footer already surfaces. - Hardcoded "Library" / "Tutorials" labels match the brand-name pattern: unlocalized across all 18 landing-page locales. Fix 4 — HA SoftwareApplication entity canonicalized on the LP again - `alternateName` is back to an explicit array of real query variants `["html anything", "html-anything", "htmlanything", "HTML Anything Editor", "The agentic HTML editor"]`. #2469 re-routed it through `copy.schemaAlternateName` which dropped the literal alias declarations Google needs for spaced-vs- hyphenated-vs-joined matching. (Restores PR #2566.) - `url` flips back from `HA_URL` (the GitHub repo) to the LP URL itself, matching the `BreadcrumbList` block on the same page. GitHub repo lives in `sameAs` as a peer surface. (Restores PR #2586. Without this, Google credits the GitHub repo as canonical for the entity, which is the opposite of what this surface exists for.) Fix 5 — catalog-card horizontal padding unified at 24 px - featured-card 22 → 24, template-card 20 → 24, system-card 18 → 24, source-card 28 → 24. - For template-card, also moved horizontal padding into the group rule exclusively so future siblings join without re-asserting margin shorthands. (Restores PR #2600.) Fix 6 — `_redirects` for the locale-code rename - This bundle uses `zh` / `zh-tw` / `pt-br` / `es` (the codes Tom's i18n.ts ships). The previous OD landing-page used `zh-CN` / `zh-TW` / `pt-BR` / `es-ES`. Externally-indexed and inbound-linked URLs against the old prefixes now 301 to the new canonical. Validation - `pnpm --filter @open-design/landing-page typecheck` — 0 errors - `pnpm --filter @open-design/landing-page build` — completed successfully; 18,204 pages built; sitemap-0.xml is 179 KB (well under the 25 MiB Cloudflare Pages limit). * docs: promote 'open-source alternative to Claude Design' to README H1 Brings the missing README and .gitignore changes from #2469 that the first wholesale-checkout in this branch missed (the auto-pulled diff scope was filtered to apps/landing-page/ initially). What - Every README.*.md (13 locale variants) now leads with the "open-source alternative to Claude Design" tagline as a subtitle to the project name in the H1 / first paragraph. This was @pftom's brand-positioning commit (`ee851dc`) on the original #2469 branch. - `.gitignore` adds `growth/**` to keep growth-research scratch out of the repo. Why - The README is one of the highest-PageRank surfaces a GitHub project exposes to Google. Promoting the "alternative to Claude Design" framing into the H1/subtitle position makes the project surface for exactly the query the SEO work in this PR is trying to capture. - Without this commit, the replicated #2469 in this branch would still rank against the previous H1 ("Open Design") on GitHub crawls, letting the SEO win at the LP fall short on the GitHub surface. This is a strict subset of #2469's content — pure docs, no code, no behavior change beyond what GitHub renders on the repo overview. --------- Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
202 lines
10 KiB
Text
202 lines
10 KiB
Text
---
|
|
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 {
|
|
PREFIXED_LOCALES,
|
|
getCopy,
|
|
isLocale,
|
|
localizeCategory,
|
|
type Locale,
|
|
} from '../../_lib/i18n';
|
|
import { getPublicPlugins, getRegistryCounts } from '../../plugin-registry';
|
|
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.
|
|
export async function getStaticPaths() {
|
|
const skillModes = await getSkillModeIndex();
|
|
const skillScenarios = await getSkillScenarioIndex();
|
|
const systemCategories = await getSystemCategoryIndex();
|
|
|
|
const paths = [
|
|
'skills',
|
|
'systems',
|
|
'craft',
|
|
'templates',
|
|
'blog',
|
|
'plugins',
|
|
...skillModes.map((item) => `skills/mode/${item.slug}`),
|
|
...skillScenarios.map((item) => `skills/scenario/${item.slug}`),
|
|
...systemCategories.map((item) => `systems/category/${item.slug}`),
|
|
];
|
|
|
|
return PREFIXED_LOCALES.flatMap((locale) =>
|
|
paths.map((path) => ({
|
|
params: { locale, path },
|
|
})),
|
|
);
|
|
}
|
|
|
|
const localeParam = Astro.params.locale;
|
|
const locale: Locale = isLocale(localeParam) ? localeParam : 'en';
|
|
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(),
|
|
getCraftRecords(),
|
|
getTemplateRecords(),
|
|
getCollection('blog'),
|
|
]);
|
|
const plugins = getPublicPlugins();
|
|
|
|
// 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 pluginCounts = getRegistryCounts(plugins);
|
|
|
|
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}`
|
|
: routeRoot === 'plugins'
|
|
? `${copy.pluginsTitle} — ${pluginCounts.all} | ${titleSuffix}`
|
|
: `${copy.blog} — ${titleSuffix}`;
|
|
|
|
const pageDescription = `Open Design ${routeRoot || 'landing'} page.`;
|
|
---
|
|
|
|
<Layout title={pageTitle} description={pageDescription} active={routeRoot as HeaderProps['active']}>
|
|
{routeRoot === 'blog' && (
|
|
<>
|
|
<header class='catalog-head'>
|
|
<span class='label'>{copy.blog}</span>
|
|
<h1 class='display'>{copy.blog}<span class='dot'>.</span></h1>
|
|
<p class='lead'>Notes to help you understand, explore, and build with Open Design.</p>
|
|
</header>
|
|
<section class='catalog-grid'>
|
|
<ol>
|
|
{sortedPosts.map((post, index) => (
|
|
<li class='catalog-row'>
|
|
<a href={href(`/blog/${post.id}/`)}>
|
|
<span class='row-index'>{String(index + 1).padStart(2, '0')}</span>
|
|
<span class='row-body'>
|
|
<span class='row-name'>{post.data.title}</span>
|
|
<span class='row-desc'>{post.data.summary}</span>
|
|
</span>
|
|
<span class='meta-tag'>{localizeCategory(post.data.category, locale)}</span>
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</section>
|
|
</>
|
|
)}
|
|
|
|
{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>
|
|
<section class='catalog-grid'><ol>{craft.map((item, index) => <li class='catalog-row'><a href={href(`/craft/${item.slug}/`)}><span class='row-index'>{String(index + 1).padStart(2, '0')}</span><span class='row-body'><span class='row-name'>{item.name}</span><span class='row-desc'>{item.summary}</span></span></a></li>)}</ol></section>
|
|
</>
|
|
)}
|
|
|
|
{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>
|
|
</>
|
|
)}
|
|
|
|
{routeRoot === 'plugins' && (
|
|
<>
|
|
<header class='catalog-head'><span class='label'>Plugin Registry</span><h1 class='display'><em>{copy.pluginsTitle}</em> — {pluginCounts.all} installable entries<span class='dot'>.</span></h1><p class='lead'>Discover installable workflows, decks, image templates, design systems, and agent-native capabilities.</p></header>
|
|
<section class='catalog-grid'><ol>{plugins.map((plugin, index) => <li class='catalog-row'><a href={href(plugin.detailHref)}><span class='row-index'>{String(index + 1).padStart(2, '0')}</span><span class='row-body'><span class='row-name'>{plugin.title}</span><span class='row-desc'>{plugin.description}</span></span><span class='meta-tag'>{plugin.registryName}</span></a></li>)}</ol></section>
|
|
</>
|
|
)}
|
|
</Layout>
|