Enhance landing page with SEO-focused content and FAQ section (#2469)

* Enhance landing page with SEO-focused content and FAQ section

- Updated `.gitignore` to include growth directory.
- Modified `astro.config.ts` to prioritize high-intent landing pages for SEO.
- Added new FAQ styles and layout in `globals.css` for better user experience.
- Implemented FAQ section in `page.tsx`, ensuring it aligns with structured data requirements.
- Created dedicated pages for agents and alternatives to Claude Design, enhancing SEO and user navigation.
- Introduced comparison page for evaluating Open Design against competitors.
- Added favicon links component for consistent branding across all pages.

* Add SVG favicon and update favicon links for improved branding

* Enhance landing page with official source pillars for improved branding and navigation

- Added five canonical "official source" pillars to the homepage, reinforcing key links: official site, GitHub repository, releases, documentation, and Discord community.
- Updated URLs for releases, issues, documentation, and license to streamline access and improve user experience.

* Add locale support and enhance landing page with language switcher

- Introduced locale management with a new i18n module, defining multiple languages for the landing page.
- Implemented a locale switcher in the topbar and header, allowing users to select their preferred language.
- Updated global styles for the locale selector and adjusted layout for better responsiveness.
- Enhanced SEO by ensuring localized content is served based on user selection.
- Added a script for automatic locale detection and persistence in local storage.

* Implement localized routing and enhance navigation with href utility

- Added `stripLocaleFromPath` and `localizedHref` functions to manage locale-based URL paths.
- Updated `localePath` to normalize paths based on the detected locale.
- Refactored links in the header, footer, and main page components to utilize the new `localizedHref` function for improved navigation.
- Introduced locale-aware routing for new pages, ensuring consistent user experience across different languages.
- Enhanced SEO with alternate links for localized content in the sub-page layout.

* Update header component to use new logo format

- Replaced favicon image with a new logo in WebP format for improved performance and quality.
- Ensured consistent branding across the landing page with the updated logo.

* Enhance landing page with localization and new UI components

- Introduced a comprehensive localization schema for content management, allowing for multilingual support across various sections.
- Updated the blog and collection schemas to include internationalization (i18n) fields for better content localization.
- Implemented a new official source strip for improved navigation and branding, linking to key resources like the official site and documentation.
- Enhanced the locale switcher functionality, allowing users to select their preferred language with improved UX.
- Updated styles for the locale switcher and added new components for better responsiveness and accessibility.
- Refactored existing components to utilize localized URLs, ensuring a consistent user experience across different languages.

* Implement comprehensive localization and enhance landing page UI

- Introduced new localization features, including `EXTRA_LOCALIZED_HOME_BODY_COPY` and `EXTRA_LOCALIZED_LANDING_UI_COPY`, to support multilingual content across the landing page.
- Updated `astro.config.ts` to integrate internationalization (i18n) settings for the sitemap, improving SEO for localized content.
- Created new files for home page and info page internationalization, defining structured content for various languages.
- Enhanced the locale switcher functionality with improved UX, allowing users to easily select their preferred language.
- Refactored existing components to utilize localized content and URLs, ensuring a consistent experience across different languages.
- Made CSS adjustments for better responsiveness and accessibility in the UI components.

* Enhance landing page with new design templates and localization improvements

- Added support for design templates in the content management system, allowing for better organization and access to design resources.
- Implemented comprehensive localization for blog topics, enhancing multilingual support across various sections of the landing page.
- Updated the header component to include new product menu items, improving navigation and user experience.
- Refactored CSS for improved responsiveness and accessibility, including a new sticky chrome bar for better navigation.
- Enhanced the locale switcher functionality, ensuring a seamless experience for users selecting their preferred language.

* docs(readme): promote 'open-source alternative to Claude Design' tagline to subtitle across locales
This commit is contained in:
Tom Huang 2026-05-21 23:40:58 +08:00 committed by GitHub
parent e5bea2c134
commit 26ee030b4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 16425 additions and 2324 deletions

2
.gitignore vendored
View file

@ -66,3 +66,5 @@ docs/superpowers/
# `skills/<slug>/example.html` and `templates/live-artifacts/<slug>/`
# on every deploy. Should not be committed (~70MB of PNGs).
apps/landing-page/public/previews/
growth/**

View file

BIN
.pnpm-store/v11/index.db Normal file

Binary file not shown.

View file

@ -1,18 +1,18 @@
<div dir="rtl">
# Open Design
# Open Design — البديل الرسمي مفتوح المصدر لـ [Claude Design][cd]
> **Open Design هو البديل مفتوح المصدر والمحلي أولاً لـ [Claude Design][cd].** قابل للنشر على Vercel، ويدعم BYOK في كل طبقة — **16 أداة CLI لوكلاء البرمجة** يكتشفها تلقائياً من `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) لتصبح هي محرّك التصميم، مدفوعةً بـ **31 Skill قابلة للتركيب** و**72 نظام تصميم بمستوى الهوية البصرية**. لا توجد لديك CLI؟ بروكسي BYOK متوافق مع OpenAI يقدّم نفس الحلقة بدون عملية الـ spawn.
> [!IMPORTANT]
> ### 🔥 وصلت النسخة `0.8.0-preview`. عالم التصميم القديم ينتهي هنا.
>
> بديل مفتوح المصدر و agent-native لـ Claude Design / Figma — 40k نجمة في أسبوعين أوصلتنا إلى هنا. **نحتاجك لدفعنا بقية الطريق.**
> بديل مفتوح المصدر لـ Claude Design / Figma — 40k نجمة في أسبوعين أوصلتنا إلى هنا. **نحتاجك لدفعنا بقية الطريق.**
>
> **تكرار سريع على `main`** — 0.8.0 هي المرحلة التالية من Open Design. أرسل PR، اطرح فكرة جامحة، أبلغ عن عُلّة — ما تجلبه أنت هو ما تصير إليه هذه الحركة.
>
> → [**اقرأ الإعلان · حمّل المثبّت · انضم إلى الحركة**](https://github.com/nexu-io/open-design/discussions/1727) · يعمل جنبًا إلى جنب مع نسخة 0.7 الحالية لديك.
> **البديل مفتوح المصدر لـ [Claude Design][cd].** يعمل محلياً أولاً، قابل للنشر على Vercel، ويدعم BYOK في كل طبقة — **16 أداة CLI لوكلاء البرمجة** يكتشفها تلقائياً من `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) لتصبح هي محرّك التصميم، مدفوعةً بـ **31 Skill قابلة للتركيب** و**72 نظام تصميم بمستوى الهوية البصرية**. لا توجد لديك CLI؟ بروكسي BYOK متوافق مع OpenAI يقدّم نفس الحلقة بدون عملية الـ spawn.
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design — غلاف افتتاحي: صمّم مع الوكيل على حاسوبك المحمول" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design — die offizielle Open-Source-Alternative zu [Claude Design][cd]
> **Open Design ist die Open-Source- und Local-first-Alternative zu [Claude Design][cd].** Web-deploybar, BYOK auf jeder Ebene: **16 coding-agent CLIs** werden automatisch in Ihrem `PATH` erkannt (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) und werden zur Design-Engine, gesteuert von **31 kombinierbaren Skills** und **72 brandreifen Design Systems**. Keine CLI? Ein OpenAI-kompatibler BYOK-Proxy ist dieselbe Schleife ohne Spawn.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` ist da. Hier endet die alte Welt des Designs.
>
> Eine Open-Source-, agent-native Alternative zu Claude Design / Figma — 40k Sterne in zwei Wochen haben uns hierher gebracht. **Wir brauchen dich für den Rest des Weges.**
> Eine Open-Source-Alternative zu Claude Design / Figma — 40k Sterne in zwei Wochen haben uns hierher gebracht. **Wir brauchen dich für den Rest des Weges.**
>
> **Schnelle Iteration auf `main`** — 0.8.0 ist die nächste Phase von Open Design. Schick einen PR, wirf eine wilde Idee rein, melde einen Bug — was du mitbringst, dazu wird diese Bewegung.
>
> → [**Ankündigung lesen · Installer herunterladen · der Bewegung beitreten**](https://github.com/nexu-io/open-design/discussions/1727) · läuft parallel zu deinem aktuellen 0.7.
> **Die Open-Source-Alternative zu [Claude Design][cd].** Local-first, web-deploybar, BYOK auf jeder Ebene: **16 coding-agent CLIs** werden automatisch in Ihrem `PATH` erkannt (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) und werden zur Design-Engine, gesteuert von **31 kombinierbaren Skills** und **72 brandreifen Design Systems**. Keine CLI? Ein OpenAI-kompatibler BYOK-Proxy ist dieselbe Schleife ohne Spawn.
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design — editorial cover: design with the agent on your laptop" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design — la alternativa open source oficial a [Claude Design][cd]
> **Open Design es la alternativa open source y local-first a [Claude Design][cd].** Desplegable en web, BYOK en cada capa: **16 CLI de coding agents** detectadas automáticamente en tu `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) se convierten en el motor de diseño, impulsadas por **31 Skills componibles** y **72 Design Systems de nivel marca**. ¿No tienes una CLI? Un proxy BYOK compatible con OpenAI ejecuta el mismo bucle sin el spawn local.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` ya está aquí. El viejo mundo del diseño termina aquí.
>
> Una alternativa open source y agent-native a Claude Design / Figma — 40k estrellas en dos semanas nos trajeron hasta aquí. **Te necesitamos para empujar el resto del camino.**
> Una alternativa open source a Claude Design / Figma — 40k estrellas en dos semanas nos trajeron hasta aquí. **Te necesitamos para empujar el resto del camino.**
>
> **Iterando rápido sobre `main`** — 0.8.0 es la próxima fase de Open Design. Envía un PR, lanza una idea loca, reporta un bug — lo que traes tú es en lo que este movimiento se convierte.
>
> → [**Lee el anuncio · descarga el instalador · únete al movimiento**](https://github.com/nexu-io/open-design/discussions/1727) · funciona en paralelo con tu 0.7 actual.
> **La alternativa open source a [Claude Design][cd].** Local-first, desplegable en web, BYOK en cada capa: **16 CLI de coding agents** detectadas automáticamente en tu `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) se convierten en el motor de diseño, impulsadas por **31 Skills componibles** y **72 Design Systems de nivel marca**. ¿No tienes una CLI? Un proxy BYOK compatible con OpenAI ejecuta el mismo bucle sin el spawn local.
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design — editorial cover: design with the agent on your laptop" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design — lalternative open source officielle à [Claude Design][cd]
> **Open Design est lalternative open source et local-first à [Claude Design][cd].** Déployable sur le web, BYOK à chaque couche : vos CLI de coding agents détectées automatiquement dans le `PATH` deviennent le design engine, piloté par les catalogues de **Skills** et de **Design Systems** du repo. Aucune CLI ? Le proxy BYOK multi-provider exécute la même boucle, sans spawn local.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` est là. L'ancien monde du design s'arrête ici.
>
> Une alternative open source et agent-native à Claude Design / Figma — 40k étoiles en deux semaines nous ont menés jusqu'ici. **Nous avons besoin de toi pour faire le reste du chemin.**
> Une alternative open source à Claude Design / Figma — 40k étoiles en deux semaines nous ont menés jusqu'ici. **Nous avons besoin de toi pour faire le reste du chemin.**
>
> **Itération rapide sur `main`** — 0.8.0 est la prochaine phase d'Open Design. Envoie une PR, balance une idée folle, signale un bug — ce que tu apportes, c'est ce que ce mouvement devient.
>
> → [**Lire l'annonce · télécharger l'installateur · rejoindre le mouvement**](https://github.com/nexu-io/open-design/discussions/1727) · s'installe à côté de votre 0.7 actuel.
> **Lalternative open source à [Claude Design][cd].** Local-first, déployable sur le web, BYOK à chaque couche : vos CLI de coding agents détectées automatiquement dans le `PATH` deviennent le design engine, piloté par les catalogues de **Skills** et de **Design Systems** du repo. Aucune CLI ? Le proxy BYOK multi-provider exécute la même boucle, sans spawn local.
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design : couverture éditoriale, design avec lagent sur votre laptop" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design — [Claude Design][cd] の公式オープンソース代替
> **Open Design は [Claude Design][cd] のオープンソース、ローカルファーストな代替です。** Vercel デプロイ可能、あらゆるレイヤーで BYOKBring Your Own Key`PATH` 上で自動検出される **16 種類の coding-agent CLI**Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUIがデザインエンジンとなり、**31 個の組み合わせ可能な Skill** と **72 種のブランドグレード Design System** で駆動されます。CLI が未インストールでも、OpenAI 互換の BYOK プロキシ `/api/proxy/stream` で同じループを spawn なしで実行できます。
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` が公開されました。デザインの旧時代は、ここで終わります。
>
> オープンソースで agent-native な Claude Design / Figma の代替 —— 2 週間で 40k stars、ここまで来ました。**残りの道のりは、あなたと一緒に進みたい。**
> Claude Design / Figma のオープンソース代替 —— 2 週間で 40k stars、ここまで来ました。**残りの道のりは、あなたと一緒に進みたい。**
>
> **`main` で高速イテレーション中** — 0.8.0 は Open Design の次のフェーズです。PR を投げ、突飛なアイデアを放り込み、バグを報告してください —— あなたが持ち込んだものが、このムーブメントの次の姿になります。
>
> → [**告知を読む · インストーラーを入手 · このムーブメントに参加**](https://github.com/nexu-io/open-design/discussions/1727) · 現在の 0.7 と並行してインストールできます。
> **[Claude Design][cd] のオープンソース代替。** ローカルファースト、Vercel デプロイ可能、あらゆるレイヤーで BYOKBring Your Own Key`PATH` 上で自動検出される **16 種類の coding-agent CLI**Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUIがデザインエンジンとなり、**31 個の組み合わせ可能な Skill** と **72 種のブランドグレード Design System** で駆動されます。CLI が未インストールでも、OpenAI 互換の BYOK プロキシ `/api/proxy/stream` で同じループを spawn なしで実行できます。
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design — ノートパソコン上のエージェントとデザインする" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design — [Claude Design][cd]의 공식 오픈소스 대안
> **Open Design은 [Claude Design][cd]의 오픈소스, 로컬 우선 대안입니다.** 웹 배포 가능, 모든 레이어에서 BYOK — `PATH`에서 자동 감지되는 **16개의 코딩 에이전트 CLI**(Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI)가 **31가지 조합 가능한 Skill**과 **72가지 브랜드급 디자인 시스템**으로 구동되는 디자인 엔진이 됩니다. CLI가 하나도 없다? OpenAI 호환 BYOK 프록시가 spawn만 빠진 동일한 루프를 돌립니다.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview`가 도착했습니다. 디자인의 옛 시대는 여기서 끝납니다.
>
> 오픈소스이자 agent-native한 Claude Design / Figma 대안 — 2주 만에 40k stars로 여기까지 왔습니다. **남은 길은 당신과 함께 가야 합니다.**
> Claude Design / Figma의 오픈소스 대안 — 2주 만에 40k stars로 여기까지 왔습니다. **남은 길은 당신과 함께 가야 합니다.**
>
> **`main`에서 빠르게 이터레이션 중** — 0.8.0은 Open Design의 다음 단계입니다. PR을 보내고, 거친 아이디어를 던지고, 버그를 신고하세요 — 당신이 가져오는 것이 곧 이 무브먼트가 됩니다.
>
> → [**공지 읽기 · 인스톨러 다운로드 · 무브먼트에 합류**](https://github.com/nexu-io/open-design/discussions/1727) · 현재 사용 중인 0.7과 나란히 설치됩니다.
> **[Claude Design][cd]의 오픈소스 대안.** 로컬 우선, 웹 배포 가능, 모든 레이어에서 BYOK — `PATH`에서 자동 감지되는 **16개의 코딩 에이전트 CLI**(Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI)가 **31가지 조합 가능한 Skill**과 **72가지 브랜드급 디자인 시스템**으로 구동되는 디자인 엔진이 됩니다. CLI가 하나도 없다? OpenAI 호환 BYOK 프록시가 spawn만 빠진 동일한 루프를 돌립니다.
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design — 노트북 위의 에이전트와 함께 설계하는 표지" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design — the open-source alternative to Claude Design
> **Open Design is the open-source, local-first alternative to [Claude Design][cd].** Web-deployable, BYOK at every layer — **16 coding-agent CLIs** auto-detected on your `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) become the design engine, driven by **31 composable Skills** and **72 brand-grade Design Systems**. No CLI? An OpenAI-compatible BYOK proxy is the same loop minus the spawn.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` is here. Design's old world ends here.
>
> An open, agent-native alternative to Claude Design / Figma — 40k stars in two weeks got us this far. **We need you to push the rest of the way.**
> The open-source alternative to Claude Design / Figma — 40k stars in two weeks got us this far. **We need you to push the rest of the way.**
>
> **Iterating fast on `main`** — 0.8.0 is the next phase of Open Design. Ship a PR, drop a wild idea, file a bug — what you bring is what this movement becomes.
>
> → [**Read the announcement, grab the installer, join the movement**](https://github.com/nexu-io/open-design/discussions/1727) · runs side-by-side with your current 0.7.
> **The open-source alternative to [Claude Design][cd].** Local-first, web-deployable, BYOK at every layer — **16 coding-agent CLIs** auto-detected on your `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) become the design engine, driven by **31 composable Skills** and **72 brand-grade Design Systems**. No CLI? An OpenAI-compatible BYOK proxy is the same loop minus the spawn.
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design — editorial cover: design with the agent on your laptop" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design — a alternativa open source oficial ao [Claude Design][cd]
> **Open Design é a alternativa open source e local-first ao [Claude Design][cd].** Deployável via web, BYOK em toda camada — **16 CLIs de agentes de código** detectados automaticamente no seu `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) viram a engine de design, dirigidos por **31 Skills compositáveis** e **72 Design Systems de qualidade de marca**. Sem CLI? Um proxy BYOK compatível com OpenAI é o mesmo loop, só sem o spawn.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` chegou. O velho mundo do design acaba aqui.
>
> Uma alternativa open source e agent-native ao Claude Design / Figma — 40k estrelas em duas semanas nos trouxeram até aqui. **Precisamos de você para nos levar o resto do caminho.**
> Uma alternativa open source ao Claude Design / Figma — 40k estrelas em duas semanas nos trouxeram até aqui. **Precisamos de você para nos levar o resto do caminho.**
>
> **Iterando rápido na `main`** — 0.8.0 é a próxima fase do Open Design. Mande um PR, jogue uma ideia maluca, abra um bug — o que você traz é no que este movimento se transforma.
>
> → [**Leia o anúncio · baixe o instalador · junte-se ao movimento**](https://github.com/nexu-io/open-design/discussions/1727) · roda em paralelo com seu 0.7 atual.
> **A alternativa open-source ao [Claude Design][cd].** Local-first, deployável via web, BYOK em toda camada — **16 CLIs de agentes de código** detectados automaticamente no seu `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) viram a engine de design, dirigidos por **31 Skills compositáveis** e **72 Design Systems de qualidade de marca**. Sem CLI? Um proxy BYOK compatível com OpenAI é o mesmo loop, só sem o spawn.
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design — capa editorial: design com o agente no seu laptop" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design — официальная open-source альтернатива [Claude Design][cd]
> **Open Design — открытая, локально-ориентированная альтернатива [Claude Design][cd].** Пригодна для web-деплоя, с BYOK на каждом уровне: **16 CLI coding-агентов** автоматически обнаруживаются в вашем `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) и превращаются в движок генерации дизайна, управляемый **31 комбинируемым навыком** и **72 дизайн-системами уровня бренда**. Нет CLI? OpenAI-совместимый BYOK-прокси даёт тот же цикл без локального запуска агента.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` уже здесь. Старый мир дизайна заканчивается здесь.
>
> Open-source, agent-native альтернатива Claude Design / Figma — 40k звёзд за две недели довели нас до этой точки. **Дальше — только с тобой.**
> Open-source альтернатива Claude Design / Figma — 40k звёзд за две недели довели нас до этой точки. **Дальше — только с тобой.**
>
> **Быстрая итерация на `main`** — 0.8.0 — следующая фаза Open Design. Кидай PR, бросай безумную идею, заводи баг — что приносишь ты, таким и становится это движение.
>
> → [**Прочитать анонс · скачать установщик · присоединиться к движению**](https://github.com/nexu-io/open-design/discussions/1727) · устанавливается параллельно с твоей текущей 0.7.
> **Открытая альтернатива [Claude Design][cd].** Локально-ориентированная, пригодная для web-деплоя, с BYOK на каждом уровне: **16 CLI coding-агентов** автоматически обнаруживаются в вашем `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) и превращаются в движок генерации дизайна, управляемый **31 комбинируемым навыком** и **72 дизайн-системами уровня бренда**. Нет CLI? OpenAI-совместимый BYOK-прокси даёт тот же цикл без локального запуска агента.
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design — редакционная обложка: дизайн вместе с агентом на вашем ноутбуке" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design — [Claude Design][cd]'in resmi açık kaynak alternatifi
> **Open Design, [Claude Design][cd]'in açık kaynak ve yerel öncelikli alternatifidir.** Web'e dağıtılabilir, her katmanda BYOK; `PATH` üzerinde otomatik algılanan **16 coding-agent CLI** (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) tasarım motoruna dönüşür. Hepsi **31 birleştirilebilir Skill** ve **72 marka kalitesinde Design System** tarafından yönlendirilir. CLI yok mu? OpenAI uyumlu BYOK proxy aynı döngünün agent spawn olmadan çalışan halidir.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` burada. Tasarımın eski dünyası burada bitiyor.
>
> Claude Design / Figma'ya açık kaynaklı, agent-native bir alternatif — iki haftada 40k stars bizi buraya getirdi. **Yolun geri kalanını birlikte yürüyelim.**
> Claude Design / Figma'ya açık kaynaklı bir alternatif — iki haftada 40k stars bizi buraya getirdi. **Yolun geri kalanını birlikte yürüyelim.**
>
> **`main` üzerinde hızlı iterasyon** — 0.8.0 Open Design'in bir sonraki aşaması. Bir PR gönder, çılgın bir fikir at, bir bug bildir — sen ne getirirsen bu hareket o olur.
>
> → [**Duyuruyu oku · kurulum dosyasını indir · harekete katıl**](https://github.com/nexu-io/open-design/discussions/1727) · mevcut 0.7'nin yanına paralel kurulur.
> **[Claude Design][cd] için açık kaynak alternatif.** Yerel öncelikli, web'e dağıtılabilir, her katmanda BYOK; `PATH` üzerinde otomatik algılanan **16 coding-agent CLI** (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) tasarım motoruna dönüşür. Hepsi **31 birleştirilebilir Skill** ve **72 marka kalitesinde Design System** tarafından yönlendirilir. CLI yok mu? OpenAI uyumlu BYOK proxy aynı döngünün agent spawn olmadan çalışan halidir.
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design — dizüstü bilgisayarındaki agent ile tasarım yapma editoryal kapağı" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design — офіційна open-source альтернатива [Claude Design][cd]
> **Open Design — це альтернатива з відкритим кодом і локально-перший варіант [Claude Design][cd].** Розгортується в web, BYOK на кожному рівні — **16 CLI агентів для кодування** автоматично виявляються у вашому `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) стають механізмом дизайну, керуються **31 компонуваною навичкою** та **72 системами дизайну комерційного класу**. Немає CLI? OpenAI-сумісний BYOK проксі — це той же цикл без spawn.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` уже тут. Старий світ дизайну закінчується тут.
>
> Open-source, agent-native альтернатива Claude Design / Figma — 40k зірок за два тижні привели нас сюди. **Далі — тільки з тобою.**
> Open-source альтернатива Claude Design / Figma — 40k зірок за два тижні привели нас сюди. **Далі — тільки з тобою.**
>
> **Швидка ітерація на `main`** — 0.8.0 — наступна фаза Open Design. Кидай PR, кидай шалену ідею, заводь баг — те, що приносиш ти, тим стає цей рух.
>
> → [**Прочитати анонс · завантажити інсталятор · приєднатися до руху**](https://github.com/nexu-io/open-design/discussions/1727) · встановлюється паралельно з твоєю поточною 0.7.
> **Альтернатива з відкритим кодом до [Claude Design][cd].** Локально-перший, розгортується в web, BYOK на кожному рівні — **16 CLI агентів для кодування** автоматично виявляються у вашому `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) стають механізмом дизайну, керуються **31 компонуваною навичкою** та **72 системами дизайну комерційного класу**. Немає CLI? OpenAI-сумісний BYOK проксі — це той же цикл без spawn.
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design — editorial cover: design with the agent on your laptop" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design —— [Claude Design][cd] 的官方开源替代品
> **Open Design 是 [Claude Design][cd] 的开源、本地优先替代品。** 可部署到 Vercel、每一层都 BYOK —— **16 套 coding-agent CLI**`PATH` 上自动检测Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI就是设计引擎**31 个可组合 Skills****72 套品牌级 Design System** 驱动。
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` 已发布。设计的旧世界,到此为止。
>
> 开源、agent-native 的 Claude Design / Figma 替代品 —— 上线两周40k stars 在身,且仍在加速。**剩下的路,需要你和我们一起推完。**
> Claude Design / Figma 的开源替代品 —— 上线两周40k stars 在身,且仍在加速。**剩下的路,需要你和我们一起推完。**
>
> **正在 `main` 分支飞速迭代中** —— 0.8.0 是 Open Design 的下一阶段。提一个 PR、扔一个想法、报一个 bug —— 你带来的,就是这场运动接下来的样子。
>
> → [**读公告 · 下载安装包 · 加入这场运动**](https://github.com/nexu-io/open-design/discussions/1727) · 可与你现有的 0.7 并行安装。
> **[Claude Design][cd] 的开源替代品。** 本地优先、可部署到 Vercel、每一层都 BYOK —— **16 套 coding-agent CLI**`PATH` 上自动检测Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI就是设计引擎**31 个可组合 Skills****72 套品牌级 Design System** 驱动。
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design 封面:与本地 AI 智能体共同设计" width="100%" />
</p>

View file

@ -1,16 +1,16 @@
# Open Design
# Open Design —— [Claude Design][cd] 的官方開源替代品
> **Open Design 是 [Claude Design][cd] 的開源、本地優先替代品。** 可部署到 Vercel、每一層都 BYOK —— **16 套 coding-agent CLI**`PATH` 上自動檢測Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI就是設計引擎**31 個可組合 Skills****72 套品牌級 Design System** 驅動。一個都沒裝?還有 OpenAI 相容的 BYOK 代理 `/api/proxy/stream` 備援,同一條 loop少一次 spawn 而已。
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` 已發佈。設計的舊世界,到此為止。
>
> 開源、agent-native 的 Claude Design / Figma 替代品 —— 上線兩週40k stars 在身,且仍在加速。**剩下的路,需要你和我們一起推完。**
> Claude Design / Figma 的開源替代品 —— 上線兩週40k stars 在身,且仍在加速。**剩下的路,需要你和我們一起推完。**
>
> **正在 `main` 分支飛速迭代中** —— 0.8.0 是 Open Design 的下一階段。提一個 PR、扔一個想法、報一個 bug —— 你帶來的,就是這場運動接下來的樣子。
>
> → [**讀公告 · 下載安裝包 · 加入這場運動**](https://github.com/nexu-io/open-design/discussions/1727) · 可與你現有的 0.7 並行安裝。
> **[Claude Design][cd] 的開源替代品。** 本地優先、可部署到 Vercel、每一層都 BYOK —— **16 套 coding-agent CLI**`PATH` 上自動檢測Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI就是設計引擎**31 個可組合 Skills****72 套品牌級 Design System** 驅動。一個都沒裝?還有 OpenAI 相容的 BYOK 代理 `/api/proxy/stream` 備援,同一條 loop少一次 spawn 而已。
<p align="center">
<img src="docs/assets/banner.png" alt="Open Design 封面:與本地 AI 智慧體共同設計" width="100%" />
</p>

View file

@ -22,8 +22,8 @@
return fromName(release?.name) ?? fromTag(release?.tag_name) ?? null;
};
const nav = document.querySelector('[data-nav-headroom]');
if (nav) {
const chrome = document.querySelector('[data-chrome-headroom]');
if (chrome) {
let lastY = window.scrollY;
const showTopThreshold = 100;
const scrollDelta = 6;
@ -32,9 +32,9 @@
() => {
const y = window.scrollY;
const delta = y - lastY;
if (y <= showTopThreshold) nav.classList.remove('is-hidden');
else if (delta > scrollDelta) nav.classList.add('is-hidden');
else if (delta < -scrollDelta) nav.classList.remove('is-hidden');
if (y <= showTopThreshold) chrome.classList.remove('is-hidden');
else if (delta > scrollDelta) chrome.classList.add('is-hidden');
else if (delta < -scrollDelta) chrome.classList.remove('is-hidden');
lastY = y;
},
{ passive: true },
@ -47,14 +47,15 @@
// menu close it again. Keeps `aria-expanded` in sync.
const toggle = document.querySelector('[data-nav-toggle]');
const primaryNav = document.querySelector('[data-nav-primary]');
if (toggle && primaryNav && nav) {
const navEl = toggle ? toggle.closest('header.nav') : null;
if (toggle && primaryNav && navEl) {
const setOpen = (open) => {
nav.classList.toggle('is-open', open);
navEl.classList.toggle('is-open', open);
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
};
toggle.addEventListener('click', (ev) => {
ev.stopPropagation();
setOpen(!nav.classList.contains('is-open'));
setOpen(!navEl.classList.contains('is-open'));
});
// Close when clicking any actual nav link (but not the Product
// parent trigger — its href='/' is a real page link, so we let it
@ -63,7 +64,7 @@
link.addEventListener('click', () => setOpen(false));
});
document.addEventListener('click', (ev) => {
if (!nav.contains(ev.target)) setOpen(false);
if (!navEl.contains(ev.target)) setOpen(false);
});
document.addEventListener('keydown', (ev) => {
if (ev.key === 'Escape') setOpen(false);

View file

@ -12,12 +12,12 @@
import {
DEFAULT_LOCALE,
LOCALES,
LOCALE_LABEL,
getCopy,
localePath,
type Locale,
} from '../_lib/i18n';
getCommonCopy,
getHeaderProductMenuCopy,
localizedHref,
type HeaderCopy,
type LandingLocaleCode,
} from '../i18n';
const REPO = 'https://github.com/nexu-io/open-design';
const REPO_RELEASES = `${REPO}/releases`;
@ -37,8 +37,8 @@ export interface HeaderProps {
| 'systems'
| 'templates'
| 'craft'
| 'tutorials'
| 'blog';
| 'blog'
| 'tutorials';
/**
* Live counts from the Markdown catalogs. Required so we can never
* silently render stale fallback numbers when a caller forgets to
@ -55,97 +55,42 @@ export interface HeaderProps {
github?: {
starsLabel: string;
};
/** UI locale for nav labels and accessibility text. */
locale?: LandingLocaleCode;
/** Optional override for callers that already resolved localized chrome. */
copy?: HeaderCopy;
/** Brand link target — `#top` on the homepage, `/` on sub-pages. */
brandHref?: string;
/** Active page locale. Default routes remain unprefixed English. */
locale?: Locale;
/** Keep `/en/...` links when rendering the explicit English locale route. */
prefixDefaultLocale?: boolean;
/**
* Active pathname (e.g. `/skills/`, `/zh-CN/blog/`). Used by the locale
* switcher to compute the equivalent URL in each language so a click on
* "日本語" from `/zh-CN/blog/` goes straight to `/ja/blog/`, not `/ja/`.
*/
pathname?: string;
}
export function Header({
active = 'home',
counts,
github,
brandHref = '#top',
locale = DEFAULT_LOCALE,
prefixDefaultLocale = false,
pathname = '/',
copy,
brandHref = '#top',
}: HeaderProps) {
const linkClass = (key: NonNullable<HeaderProps['active']>) =>
active === key ? 'is-active' : undefined;
const copy = getCopy(locale);
const href = (path: string) =>
localePath(path, locale, { prefixDefault: prefixDefaultLocale });
const localizedBrandHref =
brandHref === '#top' ? brandHref : href(brandHref);
const contactHref = brandHref === '#top' ? '#contact' : `${href('/')}#contact`;
/**
* Minimal line-art globe icon, sized to sit next to the locale label
* without dominating the pill. `currentColor` so it inherits the ghost
* CTA color treatment (ink at rest, coral on hover).
*/
const globeIcon = (
<svg
className='nav-locale-glyph'
viewBox='0 0 24 24'
width='14'
height='14'
fill='none'
stroke='currentColor'
strokeWidth='1.5'
aria-hidden='true'
>
<circle cx='12' cy='12' r='9' />
<path d='M3 12h18' />
<path d='M12 3a14 14 0 0 1 0 18' />
<path d='M12 3a14 14 0 0 0 0 18' />
</svg>
);
const chevronIcon = (
<svg
className='nav-locale-chevron'
viewBox='0 0 24 24'
width='10'
height='10'
fill='none'
stroke='currentColor'
strokeWidth='2'
aria-hidden='true'
>
<path d='M6 9l6 6 6-6' />
</svg>
);
const checkIcon = (
<svg
className='nav-locale-check'
viewBox='0 0 24 24'
width='12'
height='12'
fill='none'
stroke='currentColor'
strokeWidth='2'
aria-hidden='true'
>
<path d='M5 12l5 5L20 7' />
</svg>
);
const headerCopy = copy ?? getCommonCopy(locale).header;
const href = (path: string) => localizedHref(path, locale);
const homeBrandHref = brandHref === '/' ? href('/') : brandHref;
const contactHref = brandHref === '#top' ? '#contact' : href('/#contact');
const productMenuCopy = getHeaderProductMenuCopy(locale);
return (
<header className='nav' data-od-id='nav' data-nav-headroom>
<header className='nav' data-od-id='nav'>
<div className='container nav-inner'>
<a href={localizedBrandHref} className='brand'>
<a href={homeBrandHref} className='brand'>
<span className='brand-mark'>
<img src='/logo.webp' alt='' width={44} height={44} />
<img src='/logo.webp' alt='' width={36} height={36} />
</span>
<span className='brand-name'>Open Design</span>
<span className='brand-meta'>
<b>{headerCopy.brandMetaTitle}</b>
{headerCopy.brandMetaBody}
</span>
</a>
{/*
Mobile / tablet hamburger. Hidden by CSS at 1100px (the desktop
@ -157,7 +102,7 @@ export function Header({
<button
type='button'
className='nav-toggle'
aria-label='Toggle navigation menu'
aria-label={productMenuCopy.toggleNavigationMenu}
aria-controls='primary-nav'
aria-expanded='false'
data-nav-toggle
@ -176,232 +121,119 @@ export function Header({
aria-haspopup signaling the submenu to assistive tech.
*/}
<a
href='/'
href={href('/')}
className={
active === 'product' ||
active === 'home' ||
active === 'html-anything'
active === 'html-anything' ||
active === 'tutorials'
? 'is-active'
: undefined
}
aria-haspopup='true'
aria-expanded='false'
>
Product
{productMenuCopy.product}
<span className='dropdown-caret' aria-hidden='true'></span>
</a>
<ul className='nav-dropdown' role='menu'>
<li role='none'>
<a
role='menuitem'
href='/'
href={href('/')}
className={
active === 'home' || active === 'product'
? 'is-active'
: undefined
}
>
<span className='dropdown-name'>Open Design</span>
<span className='dropdown-name'>{productMenuCopy.openDesignName}</span>
<span className='dropdown-blurb'>
The agentic design surface skills, systems, templates.
{productMenuCopy.openDesignBlurb}
</span>
</a>
</li>
<li role='none'>
<a
role='menuitem'
href='/html-anything/'
href={href('/html-anything/')}
className={linkClass('html-anything')}
>
<span className='dropdown-name'>HTML Anything</span>
<span className='dropdown-name'>{productMenuCopy.htmlAnythingName}</span>
<span className='dropdown-blurb'>
Markdown / data ship-ready HTML, by your local agent.
{productMenuCopy.htmlAnythingBlurb}
</span>
</a>
</li>
<li role='none'>
<a
role='menuitem'
href={href('/tutorials/')}
className={linkClass('tutorials')}
>
<span className='dropdown-name'>{productMenuCopy.tutorialsName}</span>
<span className='dropdown-blurb'>
{productMenuCopy.tutorialsBlurb}
</span>
</a>
</li>
</ul>
</li>
{/*
Library catalog facets (Skills / Systems / Templates / Craft).
Each is a top-level entry-point in its own right and keeps its
own count badge inside the panel, but they share the same
shape (catalog list detail page), so the surface treats them
as facets of one library group instead of competing for nav
real estate one row at a time.
The trigger highlights when any one of the four facet pages
is active. The same CSS-only :hover / :focus-within mechanic
from Product applies no JS, no React runtime in the browser.
*/}
<li className='has-dropdown'>
<a
href={href('/skills/')}
className={
active === 'skills' ||
active === 'systems' ||
active === 'templates' ||
active === 'craft'
? 'is-active'
: undefined
}
aria-haspopup='true'
aria-expanded='false'
>
Library
<span className='dropdown-caret' aria-hidden='true'></span>
</a>
<ul className='nav-dropdown' role='menu'>
<li role='none'>
<a
role='menuitem'
href={href('/skills/')}
className={linkClass('skills')}
>
<span className='dropdown-name'>
{copy.navSkills}
<span className='dropdown-num'>{counts.skills}</span>
</span>
<span className='dropdown-blurb'>
Composable skill templates the agent invokes mid-task.
</span>
</a>
</li>
<li role='none'>
<a
role='menuitem'
href={href('/systems/')}
className={linkClass('systems')}
>
<span className='dropdown-name'>
{copy.navSystems}
<span className='dropdown-num'>{counts.systems}</span>
</span>
<span className='dropdown-blurb'>
Brand-grade design systems tokens, type, voice.
</span>
</a>
</li>
<li role='none'>
<a
role='menuitem'
href={href('/templates/')}
className={linkClass('templates')}
>
<span className='dropdown-name'>
{copy.navTemplates}
<span className='dropdown-num'>{counts.templates}</span>
</span>
<span className='dropdown-blurb'>
Ready-to-fork artifact bundles with sample data.
</span>
</a>
</li>
<li role='none'>
<a
role='menuitem'
href={href('/craft/')}
className={linkClass('craft')}
>
<span className='dropdown-name'>
{copy.navCraft}
<span className='dropdown-num'>{counts.craft}</span>
</span>
<span className='dropdown-blurb'>
Universal craft principles a skill can opt into.
</span>
</a>
</li>
</ul>
</li>
{/*
Tutorials and Blog stay as standalone top-row links rather
than nesting under a Learn group: they're the only two
editorial reading surfaces, and rolling two items into a
dropdown adds a click without earning back any horizontal
space. The Library grouping above is what reclaimed the
row Tutorials/Blog can live side by side at the cost of
one extra slot.
*/}
<li>
<a href={href('/tutorials/')} className={linkClass('tutorials')}>
Tutorials
<a href={href('/skills/')} className={linkClass('skills')}>
{headerCopy.nav.skills}
<span className='num'>{counts.skills}</span>
</a>
</li>
<li>
<a href={href('/systems/')} className={linkClass('systems')}>
{headerCopy.nav.systems}
<span className='num'>{counts.systems}</span>
</a>
</li>
<li>
<a href={href('/templates/')} className={linkClass('templates')}>
{headerCopy.nav.templates}
<span className='num'>{counts.templates}</span>
</a>
</li>
<li>
<a href={href('/craft/')} className={linkClass('craft')}>
{headerCopy.nav.craft}
<span className='num'>{counts.craft}</span>
</a>
</li>
<li>
<a href={href('/blog/')} className={linkClass('blog')}>
{copy.navBlog}
{headerCopy.nav.blog}
</a>
</li>
<li>
<a href={contactHref}>
{headerCopy.nav.contact}
</a>
</li>
{/*
Contact intentionally NOT exposed in the top nav: it's a
page-internal anchor (`#contact` on the homepage CTA section)
that the footer already surfaces. Keeping it out of the bar
frees a slot at narrow widths where the row was overflowing.
*/}
</ul>
</nav>
<div className='nav-side'>
{/*
* Site-level locale switcher.
*
* Lives in nav-side (not the metadata topbar) so it carries the
* same visual weight as Download/Star CTAs. Uses `<details>` so
* the dropdown works without JavaScript and is recognised as
* a disclosure widget by screen readers. The trigger always
* shows the active locale in its native script, matching
* opendesigner.io's pattern.
*/}
<details className='nav-locale' data-od-id='nav-locale'>
<summary
className='nav-locale-trigger'
aria-label='Switch language'
title='Switch language'
>
{globeIcon}
<span className='nav-locale-current' lang={locale}>
{LOCALE_LABEL[locale]}
</span>
{chevronIcon}
</summary>
<div className='nav-locale-panel' role='menu'>
{LOCALES.map((item) => {
const isCurrent = item === locale;
return (
<a
key={item}
className={`nav-locale-item${isCurrent ? ' is-current' : ''}`}
href={localePath(pathname, item)}
hrefLang={item}
lang={item}
role='menuitem'
aria-current={isCurrent ? 'true' : undefined}
>
<span className='nav-locale-name'>
{LOCALE_LABEL[item]}
</span>
{isCurrent ? checkIcon : null}
</a>
);
})}
</div>
</details>
<a
className='nav-cta ghost'
href={REPO_RELEASES}
aria-label='Download Open Design desktop'
title='Download the desktop app'
aria-label={headerCopy.downloadAria}
title={headerCopy.downloadTitle}
{...ext}
>
{copy.download}
{headerCopy.download}
</a>
<a
className='nav-cta'
href={REPO}
aria-label='Star Open Design on GitHub'
title='Click to star us on GitHub'
aria-label={headerCopy.starAria}
title={headerCopy.starTitle}
{...ext}
>
{copy.star} · <span data-github-stars>{github?.starsLabel ?? '40K+'}</span>
{headerCopy.starPrefix} ·{' '}
<span data-github-stars>{github?.starsLabel ?? '40K+'}</span>
</a>
<span className='status-dot' aria-hidden='true' />
</div>

View file

@ -0,0 +1,126 @@
---
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../i18n';
const localeRoutes = LANDING_LOCALES.map((locale) => ({
code: locale.code,
htmlLang: locale.htmlLang,
}));
const script = `
(() => {
const STORAGE_KEY = 'od.preferredLocale';
const DEFAULT_LOCALE = ${JSON.stringify(DEFAULT_LOCALE)};
const LOCALES = ${JSON.stringify(localeRoutes)};
const codes = new Set(LOCALES.map((locale) => locale.code));
const normalizeLocale = (value) => {
if (typeof value !== 'string' || value.trim() === '') return null;
const raw = value.toLowerCase().replace(/_/g, '-');
if (raw === 'zh-cn' || raw === 'zh-hans' || raw.startsWith('zh-hans-')) return 'zh';
if (
raw === 'zh-hk' ||
raw === 'zh-mo' ||
raw === 'zh-tw' ||
raw === 'zh-hant' ||
raw.startsWith('zh-hant-')
) {
return 'zh-tw';
}
if (raw === 'pt' || raw.startsWith('pt-br')) return 'pt-br';
if (codes.has(raw)) return raw;
const base = raw.split('-')[0];
return codes.has(base) ? base : null;
};
const currentFromPath = () => {
const first = window.location.pathname.split('/').filter(Boolean)[0]?.toLowerCase();
return first && codes.has(first) ? first : DEFAULT_LOCALE;
};
const basePathFromCurrent = () => {
const parts = window.location.pathname.split('/').filter(Boolean);
if (parts[0] && codes.has(parts[0].toLowerCase())) parts.shift();
return parts.length > 0 ? \`/\${parts.join('/')}/\` : '/';
};
const targetFor = (locale) => {
const basePath = basePathFromCurrent();
const nextPath = locale === DEFAULT_LOCALE ? basePath : \`/\${locale}\${basePath}\`;
return \`\${nextPath}\${window.location.search}\${window.location.hash}\`;
};
const persistLocale = (locale) => {
try {
window.localStorage.setItem(STORAGE_KEY, locale);
} catch {}
};
const selectLocale = (locale, persist) => {
const next = normalizeLocale(locale) ?? DEFAULT_LOCALE;
if (persist) persistLocale(next);
const target = targetFor(next);
const current = \`\${window.location.pathname}\${window.location.search}\${window.location.hash}\`;
if (current !== target) window.location.assign(target);
};
const bindSwitchers = () => {
// Each locale entry is a real <a> link with a server-computed href, so
// navigation happens for free (and survives right-click → "open in new
// tab"). The click handler's only job is to persist the user's choice
// so future visits to '/' auto-route to it.
for (const link of document.querySelectorAll('[data-locale-link]')) {
if (link.dataset.localeBound === 'true') continue;
link.dataset.localeBound = 'true';
link.addEventListener('click', (event) => {
if (event.defaultPrevented) return;
if (event.button !== 0) return;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const code = link.dataset.localeCode;
if (!code) return;
persistLocale(code);
const details = link.closest('[data-locale-switch]');
if (details && details.open) details.open = false;
});
}
// Native <details> stays open until the summary is clicked again. Add
// outside-click + Escape close so it behaves like a real menu.
document.addEventListener('click', (event) => {
const target = event.target;
for (const details of document.querySelectorAll('[data-locale-switch][open]')) {
if (target instanceof Node && details.contains(target)) continue;
details.open = false;
}
});
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') return;
for (const details of document.querySelectorAll('[data-locale-switch][open]')) {
details.open = false;
const summary = details.querySelector('summary');
if (summary instanceof HTMLElement) summary.focus();
}
});
};
const autoAdapt = () => {
if (currentFromPath() !== DEFAULT_LOCALE) return;
let saved = null;
try {
saved = normalizeLocale(window.localStorage.getItem(STORAGE_KEY) ?? '');
} catch {}
const detected =
saved ??
(Array.isArray(navigator.languages)
? navigator.languages.map(normalizeLocale).find(Boolean)
: normalizeLocale(navigator.language));
if (detected && detected !== DEFAULT_LOCALE) selectLocale(detected, false);
};
bindSwitchers();
autoAdapt();
})();
`;
---
<script is:inline set:html={script} />

View file

@ -17,14 +17,12 @@
import { ogDefaultImage } from '../image-assets';
import {
DEFAULT_LOCALE,
LOCALES,
LOCALE_OG,
alternateLinks,
localePath,
stripLocale,
type Locale,
} from '../_lib/i18n';
LANDING_LOCALES,
alternateLinksForPath,
getLocaleDefinition,
localeFromPath,
stripLocaleFromPath,
} from '../i18n';
import FaviconLinks from './favicon-links.astro';
export interface SeoHeadProps {
@ -52,10 +50,6 @@ export interface SeoHeadProps {
* Domain property type uses DNS TXT instead and doesn't need this.
*/
googleSiteVerification?: string;
/** Active locale for canonical, OpenGraph and hreflang output. */
locale?: Locale;
/** Whether this render is under `/en/...` instead of the default English route. */
prefixDefaultLocale?: boolean;
}
const props = Astro.props as SeoHeadProps;
@ -63,12 +57,16 @@ const props = Astro.props as SeoHeadProps;
const SITE_NAME = 'Open Design';
const TAGLINE = 'Design with the agent already on your laptop.';
const isArticle = props.kind === 'article';
const locale = props.locale ?? DEFAULT_LOCALE;
const locale = localeFromPath(props.pathname);
const localeDef = getLocaleDefinition(locale);
const basePath = stripLocaleFromPath(props.pathname).pathname;
const alternateLinks = alternateLinksForPath(props.pathname).map((entry) => ({
...entry,
href: new URL(entry.hrefPath, Astro.site).toString(),
}));
const xDefaultHref = new URL(alternateLinks[0]!.hrefPath, Astro.site).toString();
const canonicalPath = localePath(props.pathname, locale, {
prefixDefault: props.prefixDefaultLocale,
});
const canonical = new URL(canonicalPath, Astro.site).toString();
const canonical = new URL(props.pathname, Astro.site).toString();
const image = props.image ?? new URL(ogDefaultImage, Astro.site).toString();
const rssUrl = new URL('/blog/rss.xml', Astro.site).toString();
@ -90,7 +88,7 @@ const articleJsonLd =
'@type': 'Article',
headline: props.title,
description: props.description,
inLanguage: locale,
inLanguage: localeDef.htmlLang,
image: [image],
datePublished: isoPublished,
dateModified: isoModified,
@ -117,27 +115,27 @@ const articleJsonLd =
: null;
const websiteJsonLd =
props.kind === 'website' && stripLocale(props.pathname).path === '/'
props.kind === 'website' && basePath === '/'
? {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: SITE_NAME,
alternateName: TAGLINE,
url: Astro.site?.toString() ?? 'https://open-design.ai/',
inLanguage: locale,
availableLanguage: LOCALES,
inLanguage: localeDef.htmlLang,
availableLanguage: LANDING_LOCALES.map((entry) => entry.htmlLang),
}
: null;
const blogJsonLd =
props.kind === 'website' && stripLocale(props.pathname).path === '/blog/'
props.kind === 'website' && basePath === '/blog/'
? {
'@context': 'https://schema.org',
'@type': 'Blog',
name: 'Open Design — Blog',
description: props.description,
url: canonical,
inLanguage: locale,
inLanguage: localeDef.htmlLang,
publisher: {
'@type': 'Organization',
name: SITE_NAME,
@ -145,28 +143,17 @@ const blogJsonLd =
},
}
: null;
/*
* x-default canonical hreflang. Per Google's guidance the x-default link
* should point at the URL shown to users whose preferred language is
* unknown. We use the unprefixed English path — alternateLinks() emits
* the per-locale variants, this fills the implicit "default" slot.
*/
const xDefaultPath = localePath(stripLocale(props.pathname).path, DEFAULT_LOCALE, {
prefixDefault: false,
});
const xDefaultHref = new URL(xDefaultPath, Astro.site).toString();
---
<title>{fullTitle}</title>
<meta name='description' content={props.description} />
<meta name='theme-color' content='#efe7d2' />
<link rel='canonical' href={canonical} />
<link rel='alternate' type='application/rss+xml' title='Open Design Blog' href={rssUrl} />
{alternateLinks(props.pathname).map((item) => (
<link rel='alternate' hreflang={item.hreflang} href={new URL(item.href, Astro.site).toString()} />
{alternateLinks.map((entry) => (
<link rel='alternate' hreflang={entry.hreflang} href={entry.href} />
))}
<link rel='alternate' hreflang='x-default' href={xDefaultHref} />
<link rel='alternate' type='application/rss+xml' title='Open Design Blog' href={rssUrl} />
<FaviconLinks />
{props.googleSiteVerification && (
@ -179,9 +166,9 @@ const xDefaultHref = new URL(xDefaultPath, Astro.site).toString();
<meta property='og:description' content={props.description} />
<meta property='og:url' content={canonical} />
<meta property='og:image' content={image} />
<meta property='og:locale' content={LOCALE_OG[locale]} />
{LOCALES.filter((l) => l !== locale).map((l) => (
<meta property='og:locale:alternate' content={LOCALE_OG[l]} />
<meta property='og:locale' content={localeDef.ogLocale} />
{LANDING_LOCALES.filter((entry) => entry.code !== locale).map((entry) => (
<meta property='og:locale:alternate' content={entry.ogLocale} />
))}
{props.kind === 'article' && (

View file

@ -1,16 +1,23 @@
---
import type { HeaderProps } from './header';
import { DEFAULT_LOCALE, getCopy, localePath, type Locale } from '../_lib/i18n';
import {
DEFAULT_LOCALE,
getCommonCopy,
getLandingUiCopy,
isLandingLocale,
localizedHref,
} from '../i18n';
interface Props {
counts: HeaderProps['counts'];
locale?: Locale;
prefixDefaultLocale?: boolean;
locale?: string;
}
const { counts, locale = DEFAULT_LOCALE, prefixDefaultLocale = false } = Astro.props;
const copy = getCopy(locale);
const href = (path: string) => localePath(path, locale, { prefixDefault: prefixDefaultLocale });
const { counts, locale: rawLocale = DEFAULT_LOCALE } = Astro.props as Props;
const locale = isLandingLocale(rawLocale) ? rawLocale : DEFAULT_LOCALE;
const copy = getCommonCopy(locale).header;
const ui = getLandingUiCopy(locale);
const href = (path: string) => localizedHref(path, locale);
const REPO = 'https://github.com/nexu-io/open-design';
const DISCORD = 'https://discord.gg/9ptkbbqRu';
---
@ -26,33 +33,43 @@ const DISCORD = 'https://discord.gg/9ptkbbqRu';
<span>Open Design</span>
</a>
<p>
{copy.footerPitch}
{ui.footer.summary}
</p>
</div>
<div class='sub-footer-col'>
<h5>{copy.catalog}</h5>
<h5>{ui.footer.catalog}</h5>
<ul>
<li><a href={href('/skills/')}>{counts.skills} {copy.navSkills}</a></li>
<li><a href={href('/systems/')}>{counts.systems} {copy.navSystems}</a></li>
<li><a href={href('/templates/')}>{counts.templates} {copy.navTemplates}</a></li>
<li><a href={href('/craft/')}>{counts.craft} {copy.navCraft}</a></li>
<li><a href={href('/skills/')}>{counts.skills} {copy.nav.skills}</a></li>
<li><a href={href('/systems/')}>{counts.systems} {copy.nav.systems}</a></li>
<li><a href={href('/templates/')}>{counts.templates} {copy.nav.templates}</a></li>
<li><a href={href('/craft/')}>{counts.craft} {copy.nav.craft}</a></li>
</ul>
</div>
<div class='sub-footer-col'>
<h5>{copy.connect}</h5>
<h5>{ui.footer.openDesign}</h5>
<ul>
<li><a href={REPO} target='_blank' rel='noopener'>GitHub</a></li>
<li><a href={`${REPO}/issues`} target='_blank' rel='noopener'>{copy.issues}</a></li>
<li><a href={`${REPO}/releases`} target='_blank' rel='noopener'>{copy.releases}</a></li>
<li><a href={DISCORD} target='_blank' rel='noopener'>Discord</a></li>
<li><a href='/blog/rss.xml'>RSS</a></li>
<li><a href={`${href('/')}#contact`}>{copy.contact}</a></li>
<li><a href={href('/official/')}>{ui.footer.official}</a></li>
<li><a href={href('/quickstart/')}>{ui.footer.quickstart}</a></li>
<li><a href={href('/agents/')}>{ui.footer.agents}</a></li>
<li><a href={href('/compare/')}>{ui.footer.compare}</a></li>
<li><a href={href('/alternatives/claude-design/')}>{ui.footer.claudeAlternative}</a></li>
</ul>
</div>
<div class='sub-footer-col'>
<h5>{ui.footer.connect}</h5>
<ul>
<li><a href={REPO} target='_blank' rel='noopener'>{ui.footer.github}</a></li>
<li><a href={`${REPO}/issues`} target='_blank' rel='noopener'>{ui.footer.issues}</a></li>
<li><a href={`${REPO}/releases`} target='_blank' rel='noopener'>{ui.footer.releases}</a></li>
<li><a href={DISCORD} target='_blank' rel='noopener'>{ui.footer.discord}</a></li>
<li><a href='/blog/rss.xml'>{ui.footer.rss}</a></li>
<li><a href={href('/#contact')}>{copy.nav.contact}</a></li>
</ul>
</div>
</div>
<div class='sub-footer-bottom'>
<span>● Open Design · Apache-2.0 · 2026 / Volume 01 / Issue Nº 26</span>
<span>Berlin / Open / Earth · 52.5200° N · 13.4050° E</span>
<span>{ui.footer.bottomLeft}</span>
<span>{ui.footer.bottomRight}</span>
</div>
</div>
</footer>

View file

@ -9,6 +9,7 @@
* unfiltered index.
*/
import type { SkillRecord } from '../_lib/catalog';
import { localeFromPath, localizedHref } from '../i18n';
import LazyImg from './lazy-img.astro';
export interface Props {
@ -17,6 +18,8 @@ export interface Props {
}
const { skill, index } = Astro.props;
const locale = localeFromPath(Astro.url.pathname);
const href = (path: string) => localizedHref(path, locale);
// The first ~4 rows are visible above the fold on a typical laptop and
// always render at the top of the first catalog list a visitor sees.
@ -26,7 +29,7 @@ const aboveFold = index < 4;
---
<li class="catalog-row catalog-row-skill">
<a href={`/skills/${skill.slug}/`}>
<a href={href(`/skills/${skill.slug}/`)}>
<span class="row-index">{String(index + 1).padStart(3, '0')}</span>
<span class="row-thumb">
{skill.previewUrl ? (
@ -44,9 +47,9 @@ const aboveFold = index < 4;
<span class="row-desc">{skill.description}</span>
</span>
<span class="row-meta">
{skill.mode && <span class="meta-tag">{skill.mode}</span>}
{skill.scenario && <span class="meta-tag muted">{skill.scenario}</span>}
{skill.platform && <span class="meta-tag muted">{skill.platform}</span>}
{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>

View file

@ -19,17 +19,22 @@ import '../sub-pages.css';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import FontStylesheet from './font-stylesheet.astro';
import GoogleAnalytics from './google-analytics.astro';
import FaviconLinks from './favicon-links.astro';
import HeaderEnhancer from './header-enhancer.astro';
import LocaleSwitcherEnhancer from './locale-switcher-enhancer.astro';
import { Header, type HeaderProps } from './header';
import LocaleSwitcherScript from './locale-switcher-script.astro';
import PreciseLazyload from './precise-lazyload.astro';
import SiteFooter from './site-footer.astro';
import Topbar from './topbar.astro';
import { heroImage } from '../image-assets';
import {
LANDING_LOCALES,
alternateLinksForPath,
getLocaleDefinition,
localeFromPath,
} from '../i18n';
import { getCatalogCounts } from '../_lib/catalog';
import { getGithubRepoMeta } from '../_lib/github';
import { DEFAULT_LOCALE, LOCALE_OG, alternateLinks, localeDir, type Locale } from '../_lib/i18n';
export interface Props {
title: string;
@ -37,39 +42,28 @@ export interface Props {
active?: HeaderProps['active'];
ogImage?: string;
jsonLd?: Record<string, unknown> | Array<Record<string, unknown>>;
locale?: Locale;
prefixDefaultLocale?: boolean;
}
const {
title,
description,
active = 'home',
ogImage,
jsonLd,
locale = DEFAULT_LOCALE,
prefixDefaultLocale = false,
} = Astro.props;
const { title, description, active = 'home', ogImage, jsonLd } = Astro.props;
const locale = localeFromPath(Astro.url.pathname);
const localeDef = getLocaleDefinition(locale);
const canonical = new URL(Astro.url.pathname, Astro.site).toString();
const alternateLinks = alternateLinksForPath(Astro.url.pathname).map((entry) => ({
...entry,
href: new URL(entry.hrefPath, Astro.site).toString(),
}));
const xDefaultHref = new URL(alternateLinks[0]!.hrefPath, Astro.site).toString();
const og = ogImage ?? heroImage;
const counts = await getCatalogCounts();
const github = await getGithubRepoMeta();
const headerHtml = renderToStaticMarkup(
Header({
active,
counts,
github,
brandHref: '/',
locale,
prefixDefaultLocale,
pathname: Astro.url.pathname,
}) as ReturnType<typeof createElement>,
Header({ active, counts, github, brandHref: '/', locale }) as ReturnType<typeof createElement>,
);
const ldArray = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
---
<!doctype html>
<html lang={locale} dir={localeDir(locale)}>
<html lang={localeDef.htmlLang} dir={localeDef.dir}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@ -77,16 +71,12 @@ const ldArray = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
{alternateLinks(Astro.url.pathname).map((item) => (
<link rel="alternate" hreflang={item.hreflang} href={new URL(item.href, Astro.site).toString()} />
{alternateLinks.map((entry) => (
<link rel="alternate" hreflang={entry.hreflang} href={entry.href} />
))}
<link rel="alternate" hreflang="x-default" href={xDefaultHref} />
{/* See `index.astro` for why we publish + link `/favicon.ico` in
* addition to the PNG: SEO crawlers and link-preview services
* hard-probe that exact path. */}
<link rel="icon" type="image/x-icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<FaviconLinks />
<FontStylesheet />
@ -96,7 +86,10 @@ const ldArray = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={og} />
<meta property="og:locale" content={LOCALE_OG[locale]} />
<meta property="og:locale" content={localeDef.ogLocale} />
{LANDING_LOCALES.filter((entry) => entry.code !== locale).map((entry) => (
<meta property="og:locale:alternate" content={entry.ogLocale} />
))}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
@ -106,24 +99,25 @@ const ldArray = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
{ldArray.map((data) => (
<script is:inline type="application/ld+json" set:html={JSON.stringify(data)} />
))}
<GoogleAnalytics />
</head>
<body class="sub-page">
<div class="shell">
<Topbar github={github} locale={locale} />
{/* Same React-rendered Header used by the homepage. SSR'd here
* so we have one nav implementation that handles active state. */}
<Fragment set:html={headerHtml} />
<div class="site-chrome" data-chrome-headroom>
<Topbar github={github} locale={locale} />
{/* Same React-rendered Header used by the homepage. SSR'd here
* so we have one nav implementation that handles active state. */}
<Fragment set:html={headerHtml} />
</div>
<main class="sub-main container">
<slot />
</main>
<SiteFooter counts={counts} locale={locale} prefixDefaultLocale={prefixDefaultLocale} />
<SiteFooter counts={counts} locale={locale} />
</div>
<HeaderEnhancer />
<LocaleSwitcherEnhancer />
<LocaleSwitcherScript />
<PreciseLazyload />
</body>
</html>

View file

@ -5,16 +5,19 @@
* category, and tagline as a clickable card.
*/
import type { SystemRecord } from '../_lib/catalog';
import { localeFromPath, localizedHref } from '../i18n';
export interface Props {
system: SystemRecord;
}
const { system } = Astro.props;
const locale = localeFromPath(Astro.url.pathname);
const href = (path: string) => localizedHref(path, locale);
---
<li class="system-card">
<a href={`/systems/${system.slug}/`}>
<a href={href(`/systems/${system.slug}/`)}>
<div class="system-swatches" aria-hidden="true">
{system.palette.length > 0 ? (
system.palette.slice(0, 4).map((hex) => (
@ -25,7 +28,7 @@ const { system } = Astro.props;
)}
</div>
<span class="system-name">{system.name}</span>
<span class="system-cat">{system.category}</span>
<span class="system-cat">{system.categoryLabel}</span>
{system.tagline && <p class="system-tagline">{system.tagline}</p>}
</a>
</li>

View file

@ -1,38 +1,90 @@
---
import { DEFAULT_LOCALE, getCopy, type Locale } from '../_lib/i18n';
import {
DEFAULT_LOCALE,
LANDING_LOCALES,
getCommonCopy,
isLandingLocale,
localePath,
stripLocaleFromPath,
} from '../i18n';
const REPO_RELEASES = 'https://github.com/nexu-io/open-design/releases';
const REPO = 'https://github.com/nexu-io/open-design';
const REPO_RELEASES = `${REPO}/releases`;
const ext = {
target: '_blank',
rel: 'noreferrer noopener',
} as const;
const NBSP = '\u00A0';
const {
github = { versionLabel: 'v0.3.0' },
locale = DEFAULT_LOCALE,
} = Astro.props as {
github?: { versionLabel: string };
locale?: Locale;
};
const copy = getCopy(locale);
const NBSP = ' ';
const { github = { versionLabel: 'v0.3.0' }, locale: rawLocale = DEFAULT_LOCALE } =
Astro.props;
const locale = isLandingLocale(rawLocale) ? rawLocale : DEFAULT_LOCALE;
const copy = getCommonCopy(locale).topbar;
const { pathname: basePath } = stripLocaleFromPath(Astro.url.pathname);
const localeOptions = LANDING_LOCALES.map((entry) => ({
...entry,
href: localePath(entry.code, basePath),
}));
const currentCode = locale.toUpperCase();
---
<div class='topbar' data-od-id='topbar'>
<div class='container topbar-inner'>
<span>
<b>OD / 2026</b>{NBSP}·{NBSP}Vol. 01 / Issue Nº 26
<b>OD / 2026</b>{NBSP}·{NBSP}{copy.issue ?? 'Vol. 01 / Issue Nº 26'}
</span>
<span class='mid'>
<span>
Filed under <b class='coral'>Design · Intelligence</b>
{copy.filedUnder} <b class='coral'>{copy.category}</b>
</span>
<span>Apache-2.0 · Made on Earth</span>
<span>{copy.madeOnEarth}</span>
</span>
<span class='right'>
<a class='topbar-link' href={REPO_RELEASES} {...ext}>
<span class='pulse'></span>
{copy.live} · <span data-github-version>{github.versionLabel}</span>
</a>
<details class='locale-switch' data-locale-switch>
<summary class='locale-trigger' aria-label={copy.languageSwitcherLabel}>
<span class='locale-trigger-prefix' aria-hidden='true'>
{copy.languageSwitcherPrefix ?? 'Lang'}
</span>
<span class='locale-trigger-sep' aria-hidden='true'>·</span>
<span class='locale-trigger-code'>{currentCode}</span>
<svg
class='locale-trigger-caret'
viewBox='0 0 8 5'
aria-hidden='true'
focusable='false'
>
<path
d='M0.5 0.75 L4 4 L7.5 0.75'
fill='none'
stroke='currentColor'
stroke-width='1'
stroke-linecap='square'
/>
</svg>
</summary>
<div class='locale-menu' role='menu'>
{localeOptions.map((entry) => (
<a
class:list={[
'locale-menu-item',
{ 'is-active': entry.code === locale },
]}
role='menuitem'
data-locale-link
data-locale-code={entry.code}
href={entry.href}
lang={entry.htmlLang}
aria-current={entry.code === locale ? 'true' : undefined}
>
<span class='locale-menu-code'>{entry.code.toUpperCase()}</span>
<span class='locale-menu-label'>{entry.label}</span>
</a>
))}
</div>
</details>
</span>
</div>
</div>

View file

@ -11,6 +11,19 @@ import { getCollection, type CollectionEntry } from 'astro:content';
import { existsSync, readdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import {
DEFAULT_LOCALE,
type LandingLocaleCode,
type LocalizedStringValue,
} from '../i18n';
import {
explicitLocalizedString,
localizeCraftText,
localizeSkillDescription,
localizeSystemText,
localizeTaxonomyValue,
localizeTemplateText,
} from '../content-i18n';
// ---------------------------------------------------------------------------
// Preview imagery lookup
@ -86,9 +99,13 @@ export interface SkillRecord {
description: string;
triggers: ReadonlyArray<string>;
mode?: string;
modeLabel?: string;
platform?: string;
platformLabel?: string;
scenario?: string;
scenarioLabel?: string;
category?: string;
categoryLabel?: string;
featured?: number;
upstream?: string;
examplePrompt?: string;
@ -112,12 +129,20 @@ function firstParagraph(text: string | undefined, fallback = ''): string {
export function shapeSkill(
entry: SkillEntry,
previews: Map<string, string>,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): SkillRecord {
const slug = deriveSkillSlug(entry.id);
const data = entry.data as {
name?: string;
description?: string;
name?: LocalizedStringValue;
description?: LocalizedStringValue;
triggers?: string[];
i18n?: Record<string, {
name?: string;
description?: string;
triggers?: string[];
examplePrompt?: string;
example_prompt?: string;
}>;
od?: {
mode?: string;
platform?: string;
@ -125,32 +150,54 @@ export function shapeSkill(
category?: string;
featured?: number;
upstream?: string;
example_prompt?: string;
example_prompt?: LocalizedStringValue;
};
};
const description = (data.description ?? '').trim();
const localized = data.i18n?.[locale];
const name = explicitLocalizedString(localized?.name ?? data.name, locale) ?? slug;
const rawDescription = explicitLocalizedString(data.description, DEFAULT_LOCALE) ?? '';
const description =
explicitLocalizedString(localized?.description ?? data.description, locale) ??
localizeSkillDescription({
name,
mode: data.od?.mode,
scenario: data.od?.scenario,
category: data.od?.category,
locale,
fallback: rawDescription,
});
const examplePrompt = explicitLocalizedString(
localized?.examplePrompt ?? localized?.example_prompt ?? data.od?.example_prompt,
locale,
) ?? '';
return {
slug,
name: data.name ?? slug,
name,
description,
triggers: data.triggers ?? [],
triggers: localized?.triggers ?? (locale === DEFAULT_LOCALE ? data.triggers ?? [] : []),
mode: data.od?.mode,
modeLabel: localizeTaxonomyValue(data.od?.mode, locale),
platform: data.od?.platform,
platformLabel: localizeTaxonomyValue(data.od?.platform, locale),
scenario: data.od?.scenario,
scenarioLabel: localizeTaxonomyValue(data.od?.scenario, locale),
category: data.od?.category,
categoryLabel: localizeTaxonomyValue(data.od?.category, locale),
featured: data.od?.featured,
upstream: data.od?.upstream,
examplePrompt: data.od?.example_prompt,
examplePrompt,
source: `${REPO_TREE}/skills/${slug}`,
body: entry.body ?? '',
previewUrl: previewUrlFor('skills', slug, previews),
};
}
export async function getSkillRecords(): Promise<ReadonlyArray<SkillRecord>> {
export async function getSkillRecords(
locale: LandingLocaleCode = DEFAULT_LOCALE,
): Promise<ReadonlyArray<SkillRecord>> {
const previews = listPreviews('skills');
const entries = await getCollection('skills');
const shaped = entries.map((entry) => shapeSkill(entry, previews));
const shaped = entries.map((entry) => shapeSkill(entry, previews, locale));
return shaped.sort((a, b) => {
// Featured (lower number = higher priority) first, then alphabetical.
const af = a.featured ?? Number.POSITIVE_INFINITY;
@ -170,6 +217,7 @@ export interface SystemRecord {
slug: string;
name: string;
category: string;
categoryLabel: string;
tagline: string;
atmosphere: string;
palette: ReadonlyArray<string>;
@ -249,29 +297,56 @@ function extractPalette(body: string, limit = 5): ReadonlyArray<string> {
return Array.from(seen);
}
export function shapeSystem(entry: SystemEntry): SystemRecord {
export function shapeSystem(
entry: SystemEntry,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): SystemRecord {
const slug = entry.id.split('/')[0] ?? entry.id;
const body = entry.body ?? '';
const data = entry.data as {
i18n?: Record<string, {
name?: string;
category?: string;
tagline?: string;
atmosphere?: string;
}>;
};
const localized = data.i18n?.[locale];
const h1 = extractH1(body) ?? slug;
const { category, tagline } = extractCategoryBlock(body);
const atmosphere = extractAtmosphere(body);
const palette = extractPalette(body);
const name =
localized?.name ??
(h1.replace(/^Design System Inspired by\s+/i, '').trim() || slug);
const rawCategory = localized?.category ?? (category || 'Uncategorized');
const localizedText = localizeSystemText({
name,
category: rawCategory,
paletteCount: palette.length,
locale,
fallbackTagline: localized?.tagline ?? tagline,
fallbackAtmosphere: localized?.atmosphere ?? atmosphere,
});
return {
slug,
name: h1.replace(/^Design System Inspired by\s+/i, '').trim() || slug,
category: category || 'Uncategorized',
tagline,
atmosphere,
name,
category: rawCategory,
categoryLabel: localizedText.category,
tagline: localizedText.tagline,
atmosphere: localizedText.atmosphere,
palette,
source: `${REPO_TREE}/design-systems/${slug}`,
body,
};
}
export async function getSystemRecords(): Promise<ReadonlyArray<SystemRecord>> {
export async function getSystemRecords(
locale: LandingLocaleCode = DEFAULT_LOCALE,
): Promise<ReadonlyArray<SystemRecord>> {
const entries = await getCollection('systems');
return entries
.map(shapeSystem)
.map((entry) => shapeSystem(entry, locale))
.sort((a, b) => a.name.localeCompare(b.name));
}
@ -378,21 +453,41 @@ function extractFirstProseParagraph(body: string): string {
return stripMarkdownInline(buf.join(' '));
}
export function shapeCraft(entry: CraftEntry): CraftRecord {
export function shapeCraft(
entry: CraftEntry,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): CraftRecord {
const slug = entry.id;
const body = entry.body ?? '';
const data = entry.data as {
i18n?: Record<string, {
name?: string;
summary?: string;
}>;
};
const localized = data.i18n?.[locale];
const h1 = extractH1(body);
const cleanH1 = h1 ? stripMarkdownInline(h1).replace(/\s+craft rules?$/i, '').trim() : '';
const fallbackName = localized?.name ?? (cleanH1 || titleizeSlug(slug));
const fallbackSummary = localized?.summary ?? extractFirstProseParagraph(body);
const localizedText = localizeCraftText({
slug,
name: fallbackName,
summary: fallbackSummary,
locale,
});
return {
slug,
name: cleanH1 || titleizeSlug(slug),
summary: extractFirstProseParagraph(body),
name: localizedText.name,
summary: localizedText.summary,
source: `${REPO_BLOB}/craft/${slug}.md`,
body,
};
}
export async function getCraftRecords(): Promise<ReadonlyArray<CraftRecord>> {
export async function getCraftRecords(
locale: LandingLocaleCode = DEFAULT_LOCALE,
): Promise<ReadonlyArray<CraftRecord>> {
const entries = await getCollection('craft');
// Astro normalizes the entry id from `craft/README.md` to `readme`
// (lowercase, extension stripped). Comparing the raw `'README'` string
@ -402,7 +497,7 @@ export async function getCraftRecords(): Promise<ReadonlyArray<CraftRecord>> {
// also filtered out.
return entries
.filter((e) => e.id.toLowerCase() !== 'readme')
.map(shapeCraft)
.map((entry) => shapeCraft(entry, locale))
.sort((a, b) => a.name.localeCompare(b.name));
}
@ -416,8 +511,11 @@ export interface TemplateRecord {
summary: string;
origin: 'design-template' | 'live-artifact';
mode?: string;
modeLabel?: string;
platform?: string;
platformLabel?: string;
scenario?: string;
scenarioLabel?: string;
featured?: number;
source: string;
detailHref: string;
@ -432,11 +530,17 @@ export type DesignTemplateEntry = CollectionEntry<'designTemplates'>;
export function shapeDesignTemplate(
entry: DesignTemplateEntry,
previews: Map<string, string>,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): TemplateRecord {
const slug = deriveSkillSlug(entry.id);
const data = entry.data as {
name?: string;
description?: string;
name?: LocalizedStringValue;
description?: LocalizedStringValue;
i18n?: Record<string, {
name?: string;
description?: string;
summary?: string;
}>;
od?: {
mode?: string;
platform?: string;
@ -445,19 +549,30 @@ export function shapeDesignTemplate(
};
};
const body = entry.body ?? '';
const localized = data.i18n?.[locale];
const name =
explicitLocalizedString(localized?.name ?? data.name, locale) ?? titleizeSlug(slug);
const summary =
firstParagraph(data.description) ||
explicitLocalizedString(
localized?.summary ?? localized?.description ?? data.description,
locale,
) ||
firstParagraph(explicitLocalizedString(data.description, DEFAULT_LOCALE)) ||
extractFirstProseParagraph(body) ||
'Open Design renderable design template.';
const localizedText = localizeTemplateText({ name, summary, locale });
return {
slug,
name: data.name ?? titleizeSlug(slug),
summary,
name: localizedText.name,
summary: localizedText.summary,
origin: 'design-template',
mode: data.od?.mode,
modeLabel: localizeTaxonomyValue(data.od?.mode, locale),
platform: data.od?.platform,
platformLabel: localizeTaxonomyValue(data.od?.platform, locale),
scenario: data.od?.scenario,
scenarioLabel: localizeTaxonomyValue(data.od?.scenario, locale),
featured: data.od?.featured,
source: `${REPO_TREE}/design-templates/${slug}`,
detailHref: `/templates/${slug}/`,
@ -469,9 +584,17 @@ export function shapeDesignTemplate(
export function shapeLiveArtifactTemplate(
entry: TemplateEntry,
previews: Map<string, string>,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): TemplateRecord {
const slug = entry.id.split('/')[0] ?? entry.id;
const body = entry.body ?? '';
const data = entry.data as {
i18n?: Record<string, {
name?: string;
summary?: string;
}>;
};
const localized = data.i18n?.[locale];
const h1 = extractH1(body);
// Some authors write `# \`otd-operations-brief\` · live-artifact template`
@ -484,15 +607,22 @@ export function shapeLiveArtifactTemplate(
.trim();
const summary = extractFirstProseParagraph(body) || 'Open Design Live Artifact template.';
const localizedText = localizeTemplateText({
name: localized?.name ?? (cleanH1 || titleizeSlug(slug)),
summary: localized?.summary ?? summary,
locale,
});
const liveSlug = `live-${slug}`;
return {
slug: liveSlug,
name: cleanH1 || titleizeSlug(slug),
summary,
name: localizedText.name,
summary: localizedText.summary,
origin: 'live-artifact',
mode: 'template',
modeLabel: localizeTaxonomyValue('template', locale),
scenario: 'live-artifacts',
scenarioLabel: localizeTaxonomyValue('live-artifacts', locale),
source: `${REPO_TREE}/templates/live-artifacts/${slug}`,
detailHref: `/templates/${liveSlug}/`,
body,
@ -500,13 +630,19 @@ export function shapeLiveArtifactTemplate(
};
}
export async function getTemplateRecords(): Promise<ReadonlyArray<TemplateRecord>> {
export async function getTemplateRecords(
locale: LandingLocaleCode = DEFAULT_LOCALE,
): Promise<ReadonlyArray<TemplateRecord>> {
const previews = listPreviews('templates');
const designEntries = await getCollection('designTemplates');
const designRecords = designEntries.map((entry) => shapeDesignTemplate(entry, previews));
const designRecords = designEntries.map((entry) =>
shapeDesignTemplate(entry, previews, locale),
);
const liveEntries = await getCollection('templates');
const liveRecords = liveEntries.map((entry) => shapeLiveArtifactTemplate(entry, previews));
const liveRecords = liveEntries.map((entry) =>
shapeLiveArtifactTemplate(entry, previews, locale),
);
return [...designRecords, ...liveRecords].sort((a, b) => {
// Keep explicitly featured templates first, then group the canonical
@ -680,62 +816,95 @@ export function tagIndex(values: ReadonlyArray<string | undefined>): ReadonlyArr
// human label (preserving the original `od.mode` casing for the heading).
// ---------------------------------------------------------------------------
export async function getSkillsForMode(slug: string): Promise<{
export async function getSkillsForMode(
slug: string,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): Promise<{
label: string | null;
records: ReadonlyArray<SkillRecord>;
}> {
const all = await getSkillRecords();
const all = await getSkillRecords(locale);
const matches = all.filter((s) => {
const canonical = canonicalMode(s.mode);
return canonical && slugifyTag(canonical) === slug;
});
return {
label: canonicalMode(matches[0]?.mode) ?? null,
label:
localizeTaxonomyValue(canonicalMode(matches[0]?.mode), locale) ??
canonicalMode(matches[0]?.mode) ??
null,
records: matches,
};
}
export async function getSkillsForScenario(slug: string): Promise<{
export async function getSkillsForScenario(
slug: string,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): Promise<{
label: string | null;
records: ReadonlyArray<SkillRecord>;
}> {
const all = await getSkillRecords();
const all = await getSkillRecords(locale);
const matches = all.filter((s) => {
const canonical = canonicalScenario(s.scenario);
return canonical && slugifyTag(canonical) === slug;
});
return {
label: canonicalScenario(matches[0]?.scenario) ?? null,
label:
localizeTaxonomyValue(canonicalScenario(matches[0]?.scenario), locale) ??
canonicalScenario(matches[0]?.scenario) ??
null,
records: matches,
};
}
export async function getSystemsForCategory(slug: string): Promise<{
export async function getSystemsForCategory(
slug: string,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): Promise<{
label: string | null;
records: ReadonlyArray<SystemRecord>;
}> {
const all = await getSystemRecords();
const all = await getSystemRecords(locale);
const matches = all.filter((s) => {
const canonical = canonicalCategory(s.category);
return canonical !== undefined && slugifyTag(canonical) === slug;
});
return {
label: canonicalCategory(matches[0]?.category) ?? null,
label:
localizeTaxonomyValue(canonicalCategory(matches[0]?.category), locale) ??
canonicalCategory(matches[0]?.category) ??
null,
records: matches,
};
}
export async function getSkillModeIndex(): Promise<ReadonlyArray<TagDescriptor>> {
export async function getSkillModeIndex(
locale: LandingLocaleCode = DEFAULT_LOCALE,
): Promise<ReadonlyArray<TagDescriptor>> {
const all = await getSkillRecords();
return tagIndex(all.map((s) => canonicalMode(s.mode)));
return tagIndex(all.map((s) => canonicalMode(s.mode))).map((tag) => ({
...tag,
label: localizeTaxonomyValue(tag.label, locale) ?? tag.label,
}));
}
export async function getSkillScenarioIndex(): Promise<ReadonlyArray<TagDescriptor>> {
export async function getSkillScenarioIndex(
locale: LandingLocaleCode = DEFAULT_LOCALE,
): Promise<ReadonlyArray<TagDescriptor>> {
const all = await getSkillRecords();
return tagIndex(all.map((s) => canonicalScenario(s.scenario)));
return tagIndex(all.map((s) => canonicalScenario(s.scenario))).map((tag) => ({
...tag,
label: localizeTaxonomyValue(tag.label, locale) ?? tag.label,
}));
}
export async function getSystemCategoryIndex(): Promise<ReadonlyArray<TagDescriptor>> {
export async function getSystemCategoryIndex(
locale: LandingLocaleCode = DEFAULT_LOCALE,
): Promise<ReadonlyArray<TagDescriptor>> {
const all = await getSystemRecords();
return tagIndex(all.map((s) => canonicalCategory(s.category)));
return tagIndex(all.map((s) => canonicalCategory(s.category))).map((tag) => ({
...tag,
label: localizeTaxonomyValue(tag.label, locale) ?? tag.label,
}));
}

View file

@ -0,0 +1,772 @@
import {
DEFAULT_LOCALE,
getLocaleDefinition,
type LandingLocaleCode,
type LocalizedStringValue,
} from './i18n';
type ContentCopy = {
skillNoun: string;
systemNoun: string;
templateNoun: string;
craftNoun: string;
pluginNoun: string;
blogNoun: string;
unknownTag: string;
skillDescription: (name: string, labels: string[]) => string;
systemTagline: (name: string, category: string) => string;
systemAtmosphere: (name: string, category: string, paletteCount: number) => string;
craftName: (name: string) => string;
craftSummary: (name: string) => string;
templateName: (name: string) => string;
templateSummary: (name: string) => string;
pluginTitle: (kind: string, id: string) => string;
pluginDescription: (kind: string, labels: string[]) => string;
pluginExample: (kind: string) => string;
blogTitle: (topic: string) => string;
blogSummary: (topic: string) => string;
blogBody: (topic: string, summary: string) => string;
};
const CONTENT_COPY: Record<Exclude<LandingLocaleCode, 'en'>, ContentCopy> = {
zh: {
skillNoun: 'Skill',
systemNoun: '设计系统',
templateNoun: '模板',
craftNoun: '工艺规则',
pluginNoun: '插件',
blogNoun: '文章',
unknownTag: '分类',
skillDescription: (name, labels) => `${name} 是一个可组合的 Open Design Skill用于${labels.join('、') || '设计产出'}工作流;可由本地代理调用,并和仓库中的设计系统一起复用。`,
systemTagline: (name, category) => `${name} 设计系统将${category}风格整理成可移植的 DESIGN.md 规则,供每个 Skill 复用。`,
systemAtmosphere: (name, category, paletteCount) => `${name}${category}为视觉方向,包含 ${paletteCount} 个核心色板、排版节奏、组件边界和反模式约束。`,
craftName: (name) => `${name}工艺规则`,
craftSummary: (name) => `这条 Open Design 工艺规则定义 ${name} 的执行标准,帮助代理在生成 artifact 时保持一致、可读和可交付。`,
templateName: (name) => `${name}模板`,
templateSummary: (name) => `${name} 是可复用的 Open Design Live Artifact 模板,包含渲染入口、示例数据和可 fork 的文件结构。`,
pluginTitle: (kind, id) => `${kind}插件 · ${id}`,
pluginDescription: (kind, labels) => `用于${kind}工作流的 Open Design 插件。安装后可在本地 daemon 和 od CLI 中复用${labels.length ? `,覆盖${labels.join('、')}` : ''}`,
pluginExample: (kind) => `使用该插件创建一个${kind}任务,并在本地 Open Design 工作区中查看生成结果。`,
blogTitle: (topic) => `Open Design 指南:${topic}`,
blogSummary: (topic) => `这篇本地化摘要说明 ${topic} 与 Open Design 的本地优先、BYOK 和可组合 Skill 工作流之间的关系。`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>本地化摘要</h2><p>这篇文章围绕 ${topic} 展开,说明 Open Design 如何把设计 artifact、Skill、设计系统和本地代理工作流连接起来。</p><p>当前页面使用站内 i18n fallback 渲染本地化正文;完整人工翻译可继续通过 frontmatter 的 <code>i18n.bodyHtml</code> 覆盖。</p>`,
},
'zh-tw': {
skillNoun: 'Skill',
systemNoun: '設計系統',
templateNoun: '模板',
craftNoun: '工藝規則',
pluginNoun: '外掛',
blogNoun: '文章',
unknownTag: '分類',
skillDescription: (name, labels) => `${name} 是一個可組合的 Open Design Skill用於${labels.join('、') || '設計產出'}工作流;可由本地代理呼叫,並和 repo 中的設計系統一起複用。`,
systemTagline: (name, category) => `${name} 設計系統將${category}風格整理成可攜式 DESIGN.md 規則,供每個 Skill 複用。`,
systemAtmosphere: (name, category, paletteCount) => `${name}${category}為視覺方向,包含 ${paletteCount} 個核心色板、排版節奏、元件邊界和反模式約束。`,
craftName: (name) => `${name}工藝規則`,
craftSummary: (name) => `這條 Open Design 工藝規則定義 ${name} 的執行標準,幫助代理在生成 artifact 時保持一致、可讀和可交付。`,
templateName: (name) => `${name}模板`,
templateSummary: (name) => `${name} 是可複用的 Open Design Live Artifact 模板,包含渲染入口、示例資料和可 fork 的檔案結構。`,
pluginTitle: (kind, id) => `${kind}外掛 · ${id}`,
pluginDescription: (kind, labels) => `用於${kind}工作流的 Open Design 外掛。安裝後可在本地 daemon 和 od CLI 中複用${labels.length ? `,覆蓋${labels.join('、')}` : ''}`,
pluginExample: (kind) => `使用該外掛建立一個${kind}任務,並在本地 Open Design 工作區中查看生成結果。`,
blogTitle: (topic) => `Open Design 指南:${topic}`,
blogSummary: (topic) => `這篇本地化摘要說明 ${topic} 與 Open Design 的本地優先、BYOK 和可組合 Skill 工作流之間的關係。`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>本地化摘要</h2><p>這篇文章圍繞 ${topic} 展開,說明 Open Design 如何把設計 artifact、Skill、設計系統和本地代理工作流連接起來。</p><p>目前頁面使用站內 i18n fallback 渲染本地化正文;完整人工翻譯可繼續透過 frontmatter 的 <code>i18n.bodyHtml</code> 覆蓋。</p>`,
},
ja: {
skillNoun: 'スキル',
systemNoun: 'デザインシステム',
templateNoun: 'テンプレート',
craftNoun: 'クラフトルール',
pluginNoun: 'プラグイン',
blogNoun: '記事',
unknownTag: '分類',
skillDescription: (name, labels) => `${name} は、${labels.join('、') || 'デザイン制作'}のための Open Design スキルです。ローカルエージェントから呼び出せ、リポジトリ内のデザインシステムと一緒に再利用できます。`,
systemTagline: (name, category) => `${name}${category} の方向性を DESIGN.md として整理した、移植可能なデザインシステムです。`,
systemAtmosphere: (name, category, paletteCount) => `${name}${category} を基調に、${paletteCount} 個のパレット、タイポグラフィ、コンポーネント境界、避けるべきパターンを定義します。`,
craftName: (name) => `${name} のクラフトルール`,
craftSummary: (name) => `${name} の実行基準を定義し、エージェントが一貫して読みやすく納品可能な artifact を生成できるようにします。`,
templateName: (name) => `${name} テンプレート`,
templateSummary: (name) => `${name} は再利用可能な Open Design Live Artifact テンプレートで、レンダー入口、サンプルデータ、fork 可能な構成を含みます。`,
pluginTitle: (kind, id) => `${kind} プラグイン · ${id}`,
pluginDescription: (kind, labels) => `${kind} ワークフロー向けの Open Design プラグインです。インストール後はローカル daemon と od CLI から再利用できます${labels.length ? `。対象: ${labels.join('、')}` : ''}`,
pluginExample: (kind) => `このプラグインで ${kind} タスクを作成し、ローカルの Open Design ワークスペースで結果を確認します。`,
blogTitle: (topic) => `Open Design ガイド: ${topic}`,
blogSummary: (topic) => `${topic} と、Open Design のローカルファースト、BYOK、構成可能なスキルワークフローの関係をまとめます。`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>ローカライズ概要</h2><p>この記事は ${topic} を起点に、Open Design が artifact、スキル、デザインシステム、ローカルエージェントをどう接続するかを説明します。</p><p>このページは i18n fallback で本文を表示しています。完全な人手翻訳は frontmatter の <code>i18n.bodyHtml</code> で上書きできます。</p>`,
},
ko: {
skillNoun: '스킬',
systemNoun: '디자인 시스템',
templateNoun: '템플릿',
craftNoun: '크래프트 규칙',
pluginNoun: '플러그인',
blogNoun: '글',
unknownTag: '분류',
skillDescription: (name, labels) => `${name}${labels.join(', ') || '디자인 산출물'} 워크플로를 위한 조합 가능한 Open Design 스킬입니다. 로컬 에이전트가 호출하고 저장소의 디자인 시스템과 함께 재사용할 수 있습니다.`,
systemTagline: (name, category) => `${name} 디자인 시스템은 ${category} 방향을 이식 가능한 DESIGN.md 규칙으로 정리합니다.`,
systemAtmosphere: (name, category, paletteCount) => `${name}${category} 분위기를 바탕으로 ${paletteCount}개의 팔레트, 타이포그래피 리듬, 컴포넌트 경계, 안티패턴을 정의합니다.`,
craftName: (name) => `${name} 크래프트 규칙`,
craftSummary: (name) => `${name}의 실행 기준을 정의해 에이전트가 일관되고 읽기 쉬운 artifact를 만들도록 돕습니다.`,
templateName: (name) => `${name} 템플릿`,
templateSummary: (name) => `${name}은 재사용 가능한 Open Design Live Artifact 템플릿이며 렌더링入口, 샘플 데이터, fork 가능한 구조를 포함합니다.`,
pluginTitle: (kind, id) => `${kind} 플러그인 · ${id}`,
pluginDescription: (kind, labels) => `${kind} 워크플로용 Open Design 플러그인입니다. 설치 후 로컬 daemon과 od CLI에서 재사용할 수 있습니다${labels.length ? `: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `이 플러그인으로 ${kind} 작업을 만들고 로컬 Open Design 워크스페이스에서 결과를 확인합니다.`,
blogTitle: (topic) => `Open Design 가이드: ${topic}`,
blogSummary: (topic) => `${topic}이 Open Design의 로컬 우선, BYOK, 조합 가능한 스킬 워크플로와 어떻게 연결되는지 요약합니다.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>현지화 요약</h2><p>이 글은 ${topic}을 중심으로 Open Design이 artifact, 스킬, 디자인 시스템, 로컬 에이전트를 어떻게 연결하는지 설명합니다.</p><p>현재 본문은 i18n fallback으로 렌더링됩니다. 완전한 번역은 frontmatter의 <code>i18n.bodyHtml</code>로 덮어쓸 수 있습니다.</p>`,
},
de: {
skillNoun: 'Skill',
systemNoun: 'Designsystem',
templateNoun: 'Vorlage',
craftNoun: 'Gestaltungsregel',
pluginNoun: 'Plugin',
blogNoun: 'Artikel',
unknownTag: 'Kategorie',
skillDescription: (name, labels) => `${name} ist ein kombinierbarer Open-Design-Skill fuer ${labels.join(', ') || 'Design-Artefakte'}. Er laesst sich lokal vom Agenten ausfuehren und mit DESIGN.md-Systemen wiederverwenden.`,
systemTagline: (name, category) => `${name} buendelt die Richtung ${category} als portables DESIGN.md-System fuer alle Skills.`,
systemAtmosphere: (name, category, paletteCount) => `${name} uebersetzt ${category} in ${paletteCount} Kernfarben, Typografie, Komponentenregeln und Anti-Patterns.`,
craftName: (name) => `${name}-Gestaltungsregel`,
craftSummary: (name) => `Diese Open-Design-Regel definiert Standards fuer ${name}, damit Agenten konsistente und lieferbare Artefakte erzeugen.`,
templateName: (name) => `${name}-Vorlage`,
templateSummary: (name) => `${name} ist eine wiederverwendbare Live-Artifact-Vorlage mit Render-Einstieg, Beispieldaten und forkbarer Struktur.`,
pluginTitle: (kind, id) => `${kind}-Plugin · ${id}`,
pluginDescription: (kind, labels) => `Open-Design-Plugin fuer ${kind}-Workflows. Nach der Installation ist es lokal ueber daemon und od CLI nutzbar${labels.length ? `; Schwerpunkte: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Erstelle mit diesem Plugin eine ${kind}-Aufgabe und pruefe das Ergebnis im lokalen Open-Design-Workspace.`,
blogTitle: (topic) => `Open-Design-Leitfaden: ${topic}`,
blogSummary: (topic) => `Lokalisierte Zusammenfassung zu ${topic} und dem lokalen BYOK-Skill-Workflow von Open Design.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Lokalisierte Zusammenfassung</h2><p>Dieser Beitrag erklaert, wie Open Design ${topic} mit Artefakten, Skills, Designsystemen und lokalen Agenten verbindet.</p><p>Der Text nutzt aktuell einen i18n-Fallback. Eine vollstaendige Uebersetzung kann ueber <code>i18n.bodyHtml</code> im Frontmatter hinterlegt werden.</p>`,
},
fr: {
skillNoun: 'skill',
systemNoun: 'systeme de design',
templateNoun: 'modele',
craftNoun: 'regle de conception',
pluginNoun: 'plugin',
blogNoun: 'article',
unknownTag: 'categorie',
skillDescription: (name, labels) => `${name} est un skill Open Design composable pour les flux ${labels.join(', ') || 'de production design'}. Il s'execute avec l'agent local et se reutilise avec les systemes DESIGN.md.`,
systemTagline: (name, category) => `${name} transforme la direction ${category} en systeme DESIGN.md portable pour tous les skills.`,
systemAtmosphere: (name, category, paletteCount) => `${name} formalise ${category} avec ${paletteCount} couleurs, une hierarchie typographique, des composants et des anti-patterns.`,
craftName: (name) => `Regle de conception ${name}`,
craftSummary: (name) => `Cette regle Open Design definit les standards ${name} pour produire des artefacts coherents, lisibles et livrables.`,
templateName: (name) => `Modele ${name}`,
templateSummary: (name) => `${name} est un modele Live Artifact reutilisable avec point de rendu, donnees d'exemple et structure forkable.`,
pluginTitle: (kind, id) => `Plugin ${kind} · ${id}`,
pluginDescription: (kind, labels) => `Plugin Open Design pour les flux ${kind}. Une fois installe, il fonctionne avec le daemon local et la CLI od${labels.length ? `; portee: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Utilisez ce plugin pour lancer une tache ${kind} et verifier le resultat dans l'espace de travail Open Design local.`,
blogTitle: (topic) => `Guide Open Design : ${topic}`,
blogSummary: (topic) => `Resume localise de ${topic} dans le contexte local-first, BYOK et skills composables d'Open Design.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Resume localise</h2><p>Cet article explique comment Open Design relie ${topic}, les artefacts, les skills, les systemes de design et les agents locaux.</p><p>Cette page utilise un fallback i18n. Une traduction complete peut etre fournie via <code>i18n.bodyHtml</code> dans le frontmatter.</p>`,
},
ru: {
skillNoun: 'навык',
systemNoun: 'дизайн-система',
templateNoun: 'шаблон',
craftNoun: 'правило качества',
pluginNoun: 'плагин',
blogNoun: 'статья',
unknownTag: 'категория',
skillDescription: (name, labels) => `${name} — составной навык Open Design для сценариев ${labels.join(', ') || 'дизайн-артефактов'}. Его запускает локальный агент, а правила DESIGN.md переиспользуются между задачами.`,
systemTagline: (name, category) => `${name} превращает направление ${category} в переносимую DESIGN.md дизайн-систему для всех навыков.`,
systemAtmosphere: (name, category, paletteCount) => `${name} описывает ${category}: ${paletteCount} основных цветов, типографику, компоненты и анти-паттерны.`,
craftName: (name) => `Правило качества: ${name}`,
craftSummary: (name) => `Это правило Open Design задает стандарт ${name}, чтобы агент создавал согласованные и пригодные к передаче артефакты.`,
templateName: (name) => `Шаблон ${name}`,
templateSummary: (name) => `${name} — переиспользуемый Live Artifact шаблон с точкой рендера, примером данных и структурой для fork.`,
pluginTitle: (kind, id) => `Плагин ${kind} · ${id}`,
pluginDescription: (kind, labels) => `Плагин Open Design для сценариев ${kind}. После установки доступен локально через daemon и CLI od${labels.length ? `; охват: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Создайте задачу ${kind} с этим плагином и проверьте результат в локальном рабочем пространстве Open Design.`,
blogTitle: (topic) => `Гид Open Design: ${topic}`,
blogSummary: (topic) => `Локализованное резюме о ${topic} и о том, как это связано с local-first, BYOK и составными навыками Open Design.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Локализованное резюме</h2><p>Статья объясняет, как Open Design связывает ${topic}, артефакты, навыки, дизайн-системы и локальных агентов.</p><p>Сейчас страница использует i18n fallback. Полный перевод можно задать через <code>i18n.bodyHtml</code> во frontmatter.</p>`,
},
es: {
skillNoun: 'skill',
systemNoun: 'sistema de diseño',
templateNoun: 'plantilla',
craftNoun: 'regla de oficio',
pluginNoun: 'plugin',
blogNoun: 'articulo',
unknownTag: 'categoria',
skillDescription: (name, labels) => `${name} es un skill componible de Open Design para flujos de ${labels.join(', ') || 'artefactos de diseño'}. Lo ejecuta el agente local y reutiliza sistemas DESIGN.md.`,
systemTagline: (name, category) => `${name} convierte la direccion ${category} en un sistema DESIGN.md portable para todos los skills.`,
systemAtmosphere: (name, category, paletteCount) => `${name} expresa ${category} con ${paletteCount} colores base, ritmo tipografico, componentes y anti-patrones.`,
craftName: (name) => `Regla de oficio ${name}`,
craftSummary: (name) => `Esta regla de Open Design define el estandar ${name} para producir artefactos coherentes, legibles y entregables.`,
templateName: (name) => `Plantilla ${name}`,
templateSummary: (name) => `${name} es una plantilla Live Artifact reutilizable con entrada de render, datos de ejemplo y estructura lista para fork.`,
pluginTitle: (kind, id) => `Plugin de ${kind} · ${id}`,
pluginDescription: (kind, labels) => `Plugin de Open Design para flujos de ${kind}. Tras instalarlo, funciona con el daemon local y la CLI od${labels.length ? `; cubre: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Usa este plugin para crear una tarea de ${kind} y revisar el resultado en el workspace local de Open Design.`,
blogTitle: (topic) => `Guia Open Design: ${topic}`,
blogSummary: (topic) => `Resumen localizado sobre ${topic} dentro del flujo local-first, BYOK y de skills componibles de Open Design.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Resumen localizado</h2><p>Este articulo explica como Open Design conecta ${topic}, artefactos, skills, sistemas de diseño y agentes locales.</p><p>La pagina usa un fallback i18n; una traduccion completa puede sobrescribirse con <code>i18n.bodyHtml</code> en el frontmatter.</p>`,
},
'pt-br': {
skillNoun: 'skill',
systemNoun: 'sistema de design',
templateNoun: 'modelo',
craftNoun: 'regra de craft',
pluginNoun: 'plugin',
blogNoun: 'artigo',
unknownTag: 'categoria',
skillDescription: (name, labels) => `${name} e um skill componivel do Open Design para fluxos de ${labels.join(', ') || 'artefatos de design'}. Ele roda com o agente local e reutiliza sistemas DESIGN.md.`,
systemTagline: (name, category) => `${name} transforma a direcao ${category} em um sistema DESIGN.md portavel para todos os skills.`,
systemAtmosphere: (name, category, paletteCount) => `${name} traduz ${category} em ${paletteCount} cores principais, tipografia, componentes e anti-padroes.`,
craftName: (name) => `Regra de craft ${name}`,
craftSummary: (name) => `Esta regra do Open Design define o padrao ${name} para gerar artefatos consistentes, legiveis e entregaveis.`,
templateName: (name) => `Modelo ${name}`,
templateSummary: (name) => `${name} e um modelo Live Artifact reutilizavel com entrada de renderizacao, dados de exemplo e estrutura pronta para fork.`,
pluginTitle: (kind, id) => `Plugin de ${kind} · ${id}`,
pluginDescription: (kind, labels) => `Plugin do Open Design para fluxos de ${kind}. Depois de instalado, funciona no daemon local e na CLI od${labels.length ? `; cobre: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Use este plugin para criar uma tarefa de ${kind} e revisar o resultado no workspace local do Open Design.`,
blogTitle: (topic) => `Guia Open Design: ${topic}`,
blogSummary: (topic) => `Resumo localizado sobre ${topic} no fluxo local-first, BYOK e de skills componiveis do Open Design.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Resumo localizado</h2><p>Este artigo explica como o Open Design conecta ${topic}, artefatos, skills, sistemas de design e agentes locais.</p><p>A pagina usa um fallback i18n; uma traducao completa pode ser sobrescrita por <code>i18n.bodyHtml</code> no frontmatter.</p>`,
},
it: {
skillNoun: 'skill',
systemNoun: 'sistema di design',
templateNoun: 'modello',
craftNoun: 'regola di craft',
pluginNoun: 'plugin',
blogNoun: 'articolo',
unknownTag: 'categoria',
skillDescription: (name, labels) => `${name} e uno skill componibile di Open Design per flussi ${labels.join(', ') || 'di artefatti design'}. Viene eseguito dall'agente locale e riusa sistemi DESIGN.md.`,
systemTagline: (name, category) => `${name} traduce la direzione ${category} in un sistema DESIGN.md portabile per tutti gli skill.`,
systemAtmosphere: (name, category, paletteCount) => `${name} definisce ${category} con ${paletteCount} colori base, tipografia, componenti e anti-pattern.`,
craftName: (name) => `Regola di craft ${name}`,
craftSummary: (name) => `Questa regola Open Design definisce lo standard ${name} per produrre artefatti coerenti, leggibili e consegnabili.`,
templateName: (name) => `Modello ${name}`,
templateSummary: (name) => `${name} e un modello Live Artifact riutilizzabile con ingresso di rendering, dati di esempio e struttura forkabile.`,
pluginTitle: (kind, id) => `Plugin ${kind} · ${id}`,
pluginDescription: (kind, labels) => `Plugin Open Design per flussi ${kind}. Dopo l'installazione funziona con il daemon locale e la CLI od${labels.length ? `; copre: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Usa questo plugin per creare un task ${kind} e controllare il risultato nel workspace locale Open Design.`,
blogTitle: (topic) => `Guida Open Design: ${topic}`,
blogSummary: (topic) => `Sintesi localizzata di ${topic} nel flusso local-first, BYOK e skill componibili di Open Design.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Sintesi localizzata</h2><p>Questo articolo spiega come Open Design collega ${topic}, artefatti, skill, sistemi di design e agenti locali.</p><p>La pagina usa un fallback i18n; una traduzione completa puo essere fornita con <code>i18n.bodyHtml</code> nel frontmatter.</p>`,
},
vi: {
skillNoun: 'skill',
systemNoun: 'he thong thiet ke',
templateNoun: 'mau',
craftNoun: 'quy tac craft',
pluginNoun: 'plugin',
blogNoun: 'bai viet',
unknownTag: 'phan loai',
skillDescription: (name, labels) => `${name} la skill Open Design co the ghep noi cho luong ${labels.join(', ') || 'artifact thiet ke'}. Skill chay voi agent cuc bo va tai su dung cac he DESIGN.md.`,
systemTagline: (name, category) => `${name} bien huong ${category} thanh he DESIGN.md di dong cho moi skill.`,
systemAtmosphere: (name, category, paletteCount) => `${name} mo ta ${category} voi ${paletteCount} mau cot loi, nhip chu, thanh phan va cac mau can tranh.`,
craftName: (name) => `Quy tac craft ${name}`,
craftSummary: (name) => `Quy tac Open Design nay dat chuan ${name} de agent tao artifact nhat quan, de doc va co the ban giao.`,
templateName: (name) => `Mau ${name}`,
templateSummary: (name) => `${name} la mau Live Artifact co the tai su dung, gom diem render, du lieu mau va cau truc co the fork.`,
pluginTitle: (kind, id) => `Plugin ${kind} · ${id}`,
pluginDescription: (kind, labels) => `Plugin Open Design cho luong ${kind}. Sau khi cai dat, plugin chay voi daemon cuc bo va CLI od${labels.length ? `; pham vi: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Dung plugin nay de tao tac vu ${kind} va xem ket qua trong workspace Open Design cuc bo.`,
blogTitle: (topic) => `Huong dan Open Design: ${topic}`,
blogSummary: (topic) => `Tom tat ban dia hoa ve ${topic} trong luong local-first, BYOK va skill co the ghep noi cua Open Design.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Tom tat ban dia hoa</h2><p>Bai viet nay giai thich cach Open Design ket noi ${topic}, artifact, skill, he thiet ke va agent cuc bo.</p><p>Trang dang dung fallback i18n; ban dich day du co the ghi de bang <code>i18n.bodyHtml</code> trong frontmatter.</p>`,
},
pl: {
skillNoun: 'skill',
systemNoun: 'system projektowy',
templateNoun: 'szablon',
craftNoun: 'regula craft',
pluginNoun: 'plugin',
blogNoun: 'artykul',
unknownTag: 'kategoria',
skillDescription: (name, labels) => `${name} to komponowalny skill Open Design dla przeplywow ${labels.join(', ') || 'artefaktow designu'}. Dziala z lokalnym agentem i wykorzystuje systemy DESIGN.md.`,
systemTagline: (name, category) => `${name} zamienia kierunek ${category} w przenosny system DESIGN.md dla wszystkich skillow.`,
systemAtmosphere: (name, category, paletteCount) => `${name} opisuje ${category}: ${paletteCount} kolorow, typografie, komponenty i antywzorce.`,
craftName: (name) => `Regula craft ${name}`,
craftSummary: (name) => `Ta regula Open Design definiuje standard ${name}, aby agent tworzyl spojne i gotowe do przekazania artefakty.`,
templateName: (name) => `Szablon ${name}`,
templateSummary: (name) => `${name} to wielorazowy szablon Live Artifact z punktem renderowania, danymi przykladowymi i struktura do forkowania.`,
pluginTitle: (kind, id) => `Plugin ${kind} · ${id}`,
pluginDescription: (kind, labels) => `Plugin Open Design dla przeplywow ${kind}. Po instalacji dziala lokalnie przez daemon i CLI od${labels.length ? `; zakres: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Utworz zadanie ${kind} tym pluginem i sprawdz wynik w lokalnym workspace Open Design.`,
blogTitle: (topic) => `Przewodnik Open Design: ${topic}`,
blogSummary: (topic) => `Zlokalizowane podsumowanie ${topic} w przeplywie local-first, BYOK i komponowalnych skillow Open Design.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Zlokalizowane podsumowanie</h2><p>Ten artykul pokazuje, jak Open Design laczy ${topic}, artefakty, skille, systemy projektowe i lokalnych agentow.</p><p>Strona uzywa fallbacku i18n; pelne tlumaczenie mozna podac przez <code>i18n.bodyHtml</code> we frontmatter.</p>`,
},
id: {
skillNoun: 'skill',
systemNoun: 'sistem desain',
templateNoun: 'templat',
craftNoun: 'aturan craft',
pluginNoun: 'plugin',
blogNoun: 'artikel',
unknownTag: 'kategori',
skillDescription: (name, labels) => `${name} adalah skill Open Design yang dapat dikomposisi untuk alur ${labels.join(', ') || 'artifact desain'}. Skill ini berjalan lewat agen lokal dan memakai ulang sistem DESIGN.md.`,
systemTagline: (name, category) => `${name} mengubah arah ${category} menjadi sistem DESIGN.md portabel untuk semua skill.`,
systemAtmosphere: (name, category, paletteCount) => `${name} merumuskan ${category} dengan ${paletteCount} warna inti, tipografi, komponen, dan anti-pola.`,
craftName: (name) => `Aturan craft ${name}`,
craftSummary: (name) => `Aturan Open Design ini menetapkan standar ${name} agar agen menghasilkan artifact yang konsisten, terbaca, dan siap diserahkan.`,
templateName: (name) => `Templat ${name}`,
templateSummary: (name) => `${name} adalah templat Live Artifact yang dapat dipakai ulang, berisi entry render, data contoh, dan struktur yang bisa di-fork.`,
pluginTitle: (kind, id) => `Plugin ${kind} · ${id}`,
pluginDescription: (kind, labels) => `Plugin Open Design untuk alur ${kind}. Setelah dipasang, plugin berjalan di daemon lokal dan CLI od${labels.length ? `; cakupan: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Gunakan plugin ini untuk membuat tugas ${kind} dan memeriksa hasilnya di workspace Open Design lokal.`,
blogTitle: (topic) => `Panduan Open Design: ${topic}`,
blogSummary: (topic) => `Ringkasan lokal tentang ${topic} dalam alur local-first, BYOK, dan skill yang dapat dikomposisi di Open Design.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Ringkasan lokal</h2><p>Artikel ini menjelaskan cara Open Design menghubungkan ${topic}, artifact, skill, sistem desain, dan agen lokal.</p><p>Halaman ini memakai fallback i18n; terjemahan lengkap dapat ditimpa lewat <code>i18n.bodyHtml</code> di frontmatter.</p>`,
},
nl: {
skillNoun: 'skill',
systemNoun: 'designsysteem',
templateNoun: 'sjabloon',
craftNoun: 'craftregel',
pluginNoun: 'plugin',
blogNoun: 'artikel',
unknownTag: 'categorie',
skillDescription: (name, labels) => `${name} is een composeerbare Open Design-skill voor ${labels.join(', ') || 'designartefacten'}. De lokale agent voert hem uit en hergebruikt DESIGN.md-systemen.`,
systemTagline: (name, category) => `${name} vertaalt ${category} naar een draagbaar DESIGN.md-designsysteem voor elke skill.`,
systemAtmosphere: (name, category, paletteCount) => `${name} beschrijft ${category} met ${paletteCount} kernkleuren, typografie, componentregels en anti-patronen.`,
craftName: (name) => `Craftregel ${name}`,
craftSummary: (name) => `Deze Open Design-regel definieert ${name}, zodat agenten consistente, leesbare en overdraagbare artefacten maken.`,
templateName: (name) => `Sjabloon ${name}`,
templateSummary: (name) => `${name} is een herbruikbaar Live Artifact-sjabloon met render-ingang, voorbeelddata en een forkbare structuur.`,
pluginTitle: (kind, id) => `${kind}-plugin · ${id}`,
pluginDescription: (kind, labels) => `Open Design-plugin voor ${kind}-workflows. Na installatie werkt hij lokaal via de daemon en od CLI${labels.length ? `; bereik: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Gebruik deze plugin om een ${kind}-taak te maken en het resultaat in de lokale Open Design-workspace te bekijken.`,
blogTitle: (topic) => `Open Design-gids: ${topic}`,
blogSummary: (topic) => `Gelokaliseerde samenvatting van ${topic} binnen de local-first, BYOK en composeerbare skill-workflow van Open Design.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Gelokaliseerde samenvatting</h2><p>Dit artikel legt uit hoe Open Design ${topic}, artefacten, skills, designsystemen en lokale agenten verbindt.</p><p>Deze pagina gebruikt een i18n-fallback. Een volledige vertaling kan via <code>i18n.bodyHtml</code> in de frontmatter worden geplaatst.</p>`,
},
ar: {
skillNoun: 'مهارة',
systemNoun: 'نظام تصميم',
templateNoun: 'قالب',
craftNoun: 'قاعدة جودة',
pluginNoun: 'إضافة',
blogNoun: 'مقال',
unknownTag: 'تصنيف',
skillDescription: (name, labels) => `${name} مهارة قابلة للتركيب في Open Design لسير عمل ${labels.join('، ') || 'إنتاج التصميم'}. تعمل مع الوكيل المحلي وتعيد استخدام أنظمة DESIGN.md.`,
systemTagline: (name, category) => `${name} يحول اتجاه ${category} إلى نظام DESIGN.md قابل للنقل لكل المهارات.`,
systemAtmosphere: (name, category, paletteCount) => `${name} يصف ${category} عبر ${paletteCount} ألوان أساسية وإيقاع طباعي وقواعد مكونات وأنماط يجب تجنبها.`,
craftName: (name) => `قاعدة جودة ${name}`,
craftSummary: (name) => `تحدد هذه القاعدة معيار ${name} حتى ينتج الوكيل ملفات متسقة وقابلة للتسليم.`,
templateName: (name) => `قالب ${name}`,
templateSummary: (name) => `${name} قالب Live Artifact قابل لإعادة الاستخدام، مع مدخل عرض وبيانات مثال وبنية قابلة للتفرع.`,
pluginTitle: (kind, id) => `إضافة ${kind} · ${id}`,
pluginDescription: (kind, labels) => `إضافة Open Design لسير عمل ${kind}. بعد التثبيت تعمل محليا عبر daemon و od CLI${labels.length ? `؛ النطاق: ${labels.join('، ')}` : ''}.`,
pluginExample: (kind) => `استخدم هذه الإضافة لإنشاء مهمة ${kind} ومراجعة النتيجة في مساحة عمل Open Design المحلية.`,
blogTitle: (topic) => `دليل Open Design: ${topic}`,
blogSummary: (topic) => `ملخص محلي حول ${topic} ضمن سير Open Design المحلي و BYOK والمهارات القابلة للتركيب.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>ملخص محلي</h2><p>تشرح هذه المقالة كيف يصل Open Design بين ${topic} والملفات والمهارات وأنظمة التصميم والوكلاء المحليين.</p><p>تعرض الصفحة حاليا نصا عبر i18n fallback؛ يمكن توفير ترجمة كاملة عبر <code>i18n.bodyHtml</code> في frontmatter.</p>`,
},
tr: {
skillNoun: 'skill',
systemNoun: 'tasarim sistemi',
templateNoun: 'sablon',
craftNoun: 'craft kurali',
pluginNoun: 'eklenti',
blogNoun: 'yazi',
unknownTag: 'kategori',
skillDescription: (name, labels) => `${name}, ${labels.join(', ') || 'tasarim artifact'} akislarinda kullanilan birlesebilir bir Open Design skillidir. Yerel ajanla calisir ve DESIGN.md sistemlerini yeniden kullanir.`,
systemTagline: (name, category) => `${name}, ${category} yonunu tum skilllerin kullanabilecegi tasinabilir bir DESIGN.md sistemine donusturur.`,
systemAtmosphere: (name, category, paletteCount) => `${name}, ${category} icin ${paletteCount} ana renk, tipografi, bilesen sinirlari ve anti-pattern kurallari tanimlar.`,
craftName: (name) => `${name} craft kurali`,
craftSummary: (name) => `Bu Open Design kurali ${name} standardini belirler; ajanlarin tutarli, okunabilir ve teslim edilebilir artifact uretmesine yardim eder.`,
templateName: (name) => `${name} sablonu`,
templateSummary: (name) => `${name}, render girisi, ornek veri ve fork edilebilir dosya yapisi iceren yeniden kullanilabilir bir Live Artifact sablonudur.`,
pluginTitle: (kind, id) => `${kind} eklentisi · ${id}`,
pluginDescription: (kind, labels) => `${kind} akislari icin Open Design eklentisi. Kurulumdan sonra yerel daemon ve od CLI ile calisir${labels.length ? `; kapsam: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Bu eklentiyle bir ${kind} gorevi olusturun ve sonucu yerel Open Design workspace'inde inceleyin.`,
blogTitle: (topic) => `Open Design rehberi: ${topic}`,
blogSummary: (topic) => `${topic} konusunu Open Design'in local-first, BYOK ve birlesebilir skill akisi baglaminda ozetler.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Yerellestirilmis ozet</h2><p>Bu yazi Open Design'in ${topic}, artifact, skill, tasarim sistemi ve yerel ajanlari nasil bagladigini aciklar.</p><p>Sayfa su anda i18n fallback kullanir. Tam ceviri frontmatter icindeki <code>i18n.bodyHtml</code> ile verilebilir.</p>`,
},
uk: {
skillNoun: 'навичка',
systemNoun: 'дизайн-система',
templateNoun: 'шаблон',
craftNoun: 'правило якості',
pluginNoun: 'плагін',
blogNoun: 'стаття',
unknownTag: 'категорія',
skillDescription: (name, labels) => `${name} — компонована навичка Open Design для сценаріїв ${labels.join(', ') || 'дизайн-артефактів'}. Її запускає локальний агент, а системи DESIGN.md можна перевикористовувати.`,
systemTagline: (name, category) => `${name} перетворює напрям ${category} на переносну DESIGN.md дизайн-систему для всіх навичок.`,
systemAtmosphere: (name, category, paletteCount) => `${name} описує ${category}: ${paletteCount} основних кольорів, типографіку, компоненти й анти-патерни.`,
craftName: (name) => `Правило якості: ${name}`,
craftSummary: (name) => `Це правило Open Design задає стандарт ${name}, щоб агент створював послідовні й готові до передачі артефакти.`,
templateName: (name) => `Шаблон ${name}`,
templateSummary: (name) => `${name} — багаторазовий Live Artifact шаблон із точкою рендеру, прикладом даних і структурою для fork.`,
pluginTitle: (kind, id) => `Плагін ${kind} · ${id}`,
pluginDescription: (kind, labels) => `Плагін Open Design для сценаріїв ${kind}. Після встановлення працює локально через daemon і CLI od${labels.length ? `; охоплення: ${labels.join(', ')}` : ''}.`,
pluginExample: (kind) => `Створіть завдання ${kind} цим плагіном і перевірте результат у локальному workspace Open Design.`,
blogTitle: (topic) => `Гід Open Design: ${topic}`,
blogSummary: (topic) => `Локалізоване резюме про ${topic} у local-first, BYOK і компонованому skill-процесі Open Design.`,
blogBody: (topic, summary) => `<p>${summary}</p><h2>Локалізоване резюме</h2><p>Стаття пояснює, як Open Design поєднує ${topic}, артефакти, навички, дизайн-системи й локальних агентів.</p><p>Зараз сторінка використовує i18n fallback. Повний переклад можна задати через <code>i18n.bodyHtml</code> у frontmatter.</p>`,
},
};
const TAXONOMY_TERMS: Record<string, Partial<Record<LandingLocaleCode, string>>> = {
prototype: { zh: '原型', 'zh-tw': '原型', ja: 'プロトタイプ', ko: '프로토타입', de: 'Prototyp', fr: 'prototype', ru: 'прототип', es: 'prototipo', 'pt-br': 'prototipo', it: 'prototipo', vi: 'nguyen mau', pl: 'prototyp', id: 'prototipe', nl: 'prototype', ar: 'نموذج أولي', tr: 'prototip', uk: 'прототип' },
template: { zh: '模板', 'zh-tw': '模板', ja: 'テンプレート', ko: '템플릿', de: 'Vorlage', fr: 'modele', ru: 'шаблон', es: 'plantilla', 'pt-br': 'modelo', it: 'modello', vi: 'mau', pl: 'szablon', id: 'templat', nl: 'sjabloon', ar: 'قالب', tr: 'sablon', uk: 'шаблон' },
deck: { zh: '演示文稿', 'zh-tw': '簡報', ja: 'スライド', ko: '슬라이드', de: 'Deck', fr: 'presentation', ru: 'презентация', es: 'presentacion', 'pt-br': 'apresentacao', it: 'presentazione', vi: 'slide', pl: 'prezentacja', id: 'presentasi', nl: 'presentatie', ar: 'عرض تقديمي', tr: 'sunum', uk: 'презентація' },
image: { zh: '图像', 'zh-tw': '影像', ja: '画像', ko: '이미지', de: 'Bild', fr: 'image', ru: 'изображение', es: 'imagen', 'pt-br': 'imagem', it: 'immagine', vi: 'hinh anh', pl: 'obraz', id: 'gambar', nl: 'afbeelding', ar: 'صورة', tr: 'gorsel', uk: 'зображення' },
video: { zh: '视频', 'zh-tw': '影片', ja: '動画', ko: '비디오', de: 'Video', fr: 'video', ru: 'видео', es: 'video', 'pt-br': 'video', it: 'video', vi: 'video', pl: 'wideo', id: 'video', nl: 'video', ar: 'فيديو', tr: 'video', uk: 'відео' },
audio: { zh: '音频', 'zh-tw': '音訊', ja: '音声', ko: '오디오', de: 'Audio', fr: 'audio', ru: 'аудио', es: 'audio', 'pt-br': 'audio', it: 'audio', vi: 'am thanh', pl: 'audio', id: 'audio', nl: 'audio', ar: 'صوت', tr: 'ses', uk: 'аудіо' },
utility: { zh: '工具', 'zh-tw': '工具', ja: 'ユーティリティ', ko: '유틸리티', de: 'Werkzeug', fr: 'outil', ru: 'утилита', es: 'utilidad', 'pt-br': 'utilitario', it: 'utility', vi: 'tien ich', pl: 'narzedzie', id: 'utilitas', nl: 'hulpmiddel', ar: 'أداة', tr: 'arac', uk: 'утиліта' },
design: { zh: '设计', 'zh-tw': '設計', ja: 'デザイン', ko: '디자인', de: 'Design', fr: 'design', ru: 'дизайн', es: 'diseño', 'pt-br': 'design', it: 'design', vi: 'thiet ke', pl: 'design', id: 'desain', nl: 'design', ar: 'تصميم', tr: 'tasarim', uk: 'дизайн' },
marketing: { zh: '营销', 'zh-tw': '行銷', ja: 'マーケティング', ko: '마케팅', de: 'Marketing', fr: 'marketing', ru: 'маркетинг', es: 'marketing', 'pt-br': 'marketing', it: 'marketing', vi: 'marketing', pl: 'marketing', id: 'pemasaran', nl: 'marketing', ar: 'تسويق', tr: 'pazarlama', uk: 'маркетинг' },
operations: { zh: '运营', 'zh-tw': '營運', ja: '運用', ko: '운영', de: 'Betrieb', fr: 'operations', ru: 'операции', es: 'operaciones', 'pt-br': 'operacoes', it: 'operazioni', vi: 'van hanh', pl: 'operacje', id: 'operasi', nl: 'operaties', ar: 'عمليات', tr: 'operasyon', uk: 'операції' },
product: { zh: '产品', 'zh-tw': '產品', ja: 'プロダクト', ko: '제품', de: 'Produkt', fr: 'produit', ru: 'продукт', es: 'producto', 'pt-br': 'produto', it: 'prodotto', vi: 'san pham', pl: 'produkt', id: 'produk', nl: 'product', ar: 'منتج', tr: 'urun', uk: 'продукт' },
personal: { zh: '个人', 'zh-tw': '個人', ja: '個人', ko: '개인', de: 'Persoenlich', fr: 'personnel', ru: 'личное', es: 'personal', 'pt-br': 'pessoal', it: 'personale', vi: 'ca nhan', pl: 'osobiste', id: 'personal', nl: 'persoonlijk', ar: 'شخصي', tr: 'kisisel', uk: 'особисте' },
finance: { zh: '金融', 'zh-tw': '金融', ja: '金融', ko: '금융', de: 'Finanzen', fr: 'finance', ru: 'финансы', es: 'finanzas', 'pt-br': 'financas', it: 'finanza', vi: 'tai chinh', pl: 'finanse', id: 'keuangan', nl: 'financien', ar: 'مالية', tr: 'finans', uk: 'фінанси' },
docs: { zh: '文档', 'zh-tw': '文件', ja: 'ドキュメント', ko: '문서', de: 'Dokumente', fr: 'documents', ru: 'документы', es: 'documentos', 'pt-br': 'documentos', it: 'documenti', vi: 'tai lieu', pl: 'dokumenty', id: 'dokumen', nl: 'documenten', ar: 'مستندات', tr: 'belgeler', uk: 'документи' },
};
const CRAFT_LABELS: Record<string, Partial<Record<LandingLocaleCode, string>>> = {
color: { zh: '色彩', 'zh-tw': '色彩', ja: 'カラー', ko: '색상', de: 'Farbe', fr: 'couleur', ru: 'цвет', es: 'color', 'pt-br': 'cor', it: 'colore', vi: 'mau sac', pl: 'kolor', id: 'warna', nl: 'kleur', ar: 'اللون', tr: 'renk', uk: 'колір' },
typography: { zh: '排版', 'zh-tw': '排版', ja: 'タイポグラフィ', ko: '타이포그래피', de: 'Typografie', fr: 'typographie', ru: 'типографика', es: 'tipografia', 'pt-br': 'tipografia', it: 'tipografia', vi: 'kieu chu', pl: 'typografia', id: 'tipografi', nl: 'typografie', ar: 'الطباعة', tr: 'tipografi', uk: 'типографіка' },
'rtl-and-bidi': { zh: 'RTL 与双向文本', 'zh-tw': 'RTL 與雙向文字', ja: 'RTL と双方向テキスト', ko: 'RTL 및 양방향 텍스트', de: 'RTL und bidirektionaler Text', fr: 'RTL et texte bidirectionnel', ru: 'RTL и двунаправленный текст', es: 'RTL y texto bidireccional', 'pt-br': 'RTL e texto bidirecional', it: 'RTL e testo bidirezionale', vi: 'RTL va van ban hai chieu', pl: 'RTL i tekst dwukierunkowy', id: 'RTL dan teks dua arah', nl: 'RTL en bidirectionele tekst', ar: 'النص من اليمين والاتجاه المزدوج', tr: 'RTL ve cift yonlu metin', uk: 'RTL і двонапрямний текст' },
};
const CATEGORY_LABELS: Record<string, Partial<Record<LandingLocaleCode, string>>> = {
'ai & llm': { zh: 'AI 与大模型', 'zh-tw': 'AI 與大模型', ja: 'AI と LLM', ko: 'AI 및 LLM', de: 'KI und LLM', fr: 'IA et LLM', ru: 'AI и LLM', es: 'IA y LLM', 'pt-br': 'IA e LLM', it: 'IA e LLM', vi: 'AI va LLM', pl: 'AI i LLM', id: 'AI dan LLM', nl: 'AI en LLM', ar: 'الذكاء الاصطناعي والنماذج اللغوية', tr: 'AI ve LLM', uk: 'AI та LLM' },
'developer tools': { zh: '开发者工具', 'zh-tw': '開發者工具', ja: '開発者ツール', ko: '개발자 도구', de: 'Entwicklerwerkzeuge', fr: 'outils developpeur', ru: 'инструменты разработчика', es: 'herramientas de desarrollo', 'pt-br': 'ferramentas de desenvolvimento', it: 'strumenti per sviluppatori', vi: 'cong cu lap trinh', pl: 'narzedzia developerskie', id: 'alat developer', nl: 'ontwikkelaarstools', ar: 'أدوات المطورين', tr: 'gelistirici araclari', uk: 'інструменти розробника' },
'productivity & saas': { zh: '效率与 SaaS', 'zh-tw': '效率與 SaaS', ja: '生産性と SaaS', ko: '생산성 및 SaaS', de: 'Produktivitaet und SaaS', fr: 'productivite et SaaS', ru: 'продуктивность и SaaS', es: 'productividad y SaaS', 'pt-br': 'produtividade e SaaS', it: 'produttivita e SaaS', vi: 'nang suat va SaaS', pl: 'produktywnosc i SaaS', id: 'produktivitas dan SaaS', nl: 'productiviteit en SaaS', ar: 'الإنتاجية وSaaS', tr: 'uretkenlik ve SaaS', uk: 'продуктивність і SaaS' },
'design & creative': { zh: '设计与创意', 'zh-tw': '設計與創意', ja: 'デザインとクリエイティブ', ko: '디자인 및 크리에이티브', de: 'Design und Kreativitaet', fr: 'design et creation', ru: 'дизайн и креатив', es: 'diseño y creatividad', 'pt-br': 'design e criatividade', it: 'design e creativita', vi: 'thiet ke va sang tao', pl: 'design i kreatywnosc', id: 'desain dan kreatif', nl: 'design en creativiteit', ar: 'التصميم والإبداع', tr: 'tasarim ve yaraticilik', uk: 'дизайн і креатив' },
};
const normalizeTerm = (value: string) => value.trim().toLowerCase();
const copyFor = (locale: LandingLocaleCode): ContentCopy | undefined =>
locale === DEFAULT_LOCALE ? undefined : CONTENT_COPY[locale];
const compactId = (value: string) =>
value
.split('/')
.at(-1)!
.replace(/^example-/, '')
.replace(/^design-system-/, '')
.replace(/^video-template-/, '')
.replace(/^image-template-/, '')
.replace(/^od-/, 'od-');
const BLOG_TOPIC_TITLES: Record<string, Partial<Record<Exclude<LandingLocaleCode, 'en'>, string>>> = {
'31-skills-72-systems-how-the-library-works': {
zh: '31 个 Skill 与 72 个系统的资料库运作方式',
'zh-tw': '31 個 Skill 與 72 個系統的資料庫運作方式',
ja: '31個のSkillと72個のシステムのライブラリ構造',
ko: '31개 Skill과 72개 시스템 라이브러리의 작동 방식',
de: 'wie die Bibliothek mit 31 Skills und 72 Systemen funktioniert',
fr: 'le fonctionnement de la bibliothèque de 31 skills et 72 systèmes',
ru: 'как работает библиотека из 31 навыка и 72 систем',
es: 'cómo funciona la biblioteca de 31 skills y 72 sistemas',
'pt-br': 'como funciona a biblioteca de 31 skills e 72 sistemas',
it: 'come funziona la libreria con 31 skill e 72 sistemi',
vi: 'cách vận hành thư viện 31 skill và 72 hệ thống',
pl: 'jak działa biblioteka 31 skill i 72 systemów',
id: 'cara kerja pustaka 31 skill dan 72 sistem',
nl: 'hoe de bibliotheek met 31 skills en 72 systemen werkt',
ar: 'طريقة عمل مكتبة تضم 31 مهارة و72 نظاما',
tr: '31 skill ve 72 sistemden oluşan kitaplığın çalışma biçimi',
uk: 'як працює бібліотека з 31 навички та 72 систем',
},
'byok-design-workflow-claude-codex-qwen': {
zh: '面向 Claude、Codex 与 Qwen 的 BYOK 设计工作流',
'zh-tw': '面向 Claude、Codex 與 Qwen 的 BYOK 設計工作流',
ja: 'Claude、Codex、Qwen向けBYOKデザインワークフロー',
ko: 'Claude, Codex, Qwen을 위한 BYOK 디자인 워크플로',
de: 'BYOK-Designworkflow für Claude, Codex und Qwen',
fr: 'workflow de design BYOK pour Claude, Codex et Qwen',
ru: 'BYOK-дизайн-процесс для Claude, Codex и Qwen',
es: 'flujo de diseño BYOK para Claude, Codex y Qwen',
'pt-br': 'fluxo de design BYOK para Claude, Codex e Qwen',
it: 'workflow di design BYOK per Claude, Codex e Qwen',
vi: 'quy trình thiết kế BYOK cho Claude, Codex và Qwen',
pl: 'workflow projektowy BYOK dla Claude, Codex i Qwen',
id: 'alur desain BYOK untuk Claude, Codex, dan Qwen',
nl: 'BYOK-designworkflow voor Claude, Codex en Qwen',
ar: 'سير عمل تصميم BYOK مع Claude وCodex وQwen',
tr: 'Claude, Codex ve Qwen için BYOK tasarım akışı',
uk: 'BYOK дизайн-процес для Claude, Codex і Qwen',
},
'byok-reality-check-5-things-that-break': {
zh: 'BYOK 现实检查5 个容易断裂的环节',
'zh-tw': 'BYOK 現實檢查5 個容易斷裂的環節',
ja: 'BYOKの現実チェック: 壊れやすい5つの点',
ko: 'BYOK 현실 점검: 깨지기 쉬운 5가지 지점',
de: 'BYOK-Realitätscheck: fünf Dinge, die brechen',
fr: 'réalité BYOK : cinq points qui cassent',
ru: 'проверка BYOK на практике: пять слабых мест',
es: 'revisión realista de BYOK: cinco puntos que fallan',
'pt-br': 'checagem realista do BYOK: cinco pontos que quebram',
it: 'reality check BYOK: cinque punti che si rompono',
vi: 'kiểm tra thực tế BYOK: 5 điểm dễ hỏng',
pl: 'sprawdzenie BYOK w praktyce: pięć miejsc awarii',
id: 'cek realitas BYOK: lima hal yang mudah rusak',
nl: 'BYOK-realiteitscheck: vijf dingen die breken',
ar: 'اختبار واقعي ل BYOK: خمسة مواضع تتعطل',
tr: 'BYOK gerçeklik kontrolü: bozulan beş nokta',
uk: 'реалістична перевірка BYOK: пʼять місць, які ламаються',
},
'layout-layer-canvas-used-to-hide': {
zh: '过去被画布隐藏的布局层',
'zh-tw': '過去被畫布隱藏的版面層',
ja: 'キャンバスが隠していたレイアウト層',
ko: '캔버스가 숨겨 왔던 레이아웃 계층',
de: 'die Layoutschicht, die Canvas früher verborgen hat',
fr: 'la couche de mise en page que le canvas cachait',
ru: 'слой макета, который раньше скрывал canvas',
es: 'la capa de layout que antes ocultaba el canvas',
'pt-br': 'a camada de layout que o canvas escondia',
it: 'il livello di layout che il canvas nascondeva',
vi: 'lớp bố cục từng bị canvas che khuất',
pl: 'warstwa layoutu, którą dawniej ukrywał canvas',
id: 'lapisan layout yang dulu disembunyikan kanvas',
nl: 'de layoutlaag die canvas vroeger verborg',
ar: 'طبقة التخطيط التي كان canvas يخفيها',
tr: 'canvasın eskiden sakladığı yerleşim katmanı',
uk: 'шар макета, який раніше приховував canvas',
},
'open-source-alternative-to-claude-design': {
zh: 'Claude Design 的开源替代方案',
'zh-tw': 'Claude Design 的開源替代方案',
ja: 'Claude Designのオープンソース代替',
ko: 'Claude Design의 오픈소스 대안',
de: 'Open-Source-Alternative zu Claude Design',
fr: 'alternative open source à Claude Design',
ru: 'open-source альтернатива Claude Design',
es: 'alternativa open source a Claude Design',
'pt-br': 'alternativa open source ao Claude Design',
it: 'alternativa open source a Claude Design',
vi: 'giải pháp mã nguồn mở thay cho Claude Design',
pl: 'open source alternatywa dla Claude Design',
id: 'alternatif open source untuk Claude Design',
nl: 'open-source alternatief voor Claude Design',
ar: 'بديل مفتوح المصدر ل Claude Design',
tr: 'Claude Design için açık kaynak alternatif',
uk: 'open-source альтернатива Claude Design',
},
'port-figma-workflow-open-design-plugin': {
zh: '把 Figma 工作流迁移成 Open Design 插件',
'zh-tw': '把 Figma 工作流遷移成 Open Design 外掛',
ja: 'FigmaワークフローをOpen Designプラグインへ移植する',
ko: 'Figma 워크플로를 Open Design 플러그인으로 옮기기',
de: 'Figma-Workflows als Open-Design-Plugin portieren',
fr: 'porter un workflow Figma en plugin Open Design',
ru: 'перенос Figma-процесса в плагин Open Design',
es: 'llevar un flujo de Figma a un plugin de Open Design',
'pt-br': 'migrar um fluxo do Figma para um plugin Open Design',
it: 'portare un workflow Figma in un plugin Open Design',
vi: 'chuyển quy trình Figma thành plugin Open Design',
pl: 'przenoszenie workflow Figma do pluginu Open Design',
id: 'memindahkan alur Figma menjadi plugin Open Design',
nl: 'een Figma-workflow omzetten naar een Open Design-plugin',
ar: 'نقل سير عمل Figma إلى إضافة Open Design',
tr: 'Figma akışını Open Design eklentisine taşıma',
uk: 'перенесення Figma-процесу в плагін Open Design',
},
'why-we-built-open-design-as-a-skill-layer': {
zh: '为什么把 Open Design 做成 Skill 层',
'zh-tw': '為什麼把 Open Design 做成 Skill 層',
ja: 'Open DesignをSkillレイヤーとして作った理由',
ko: 'Open Design을 Skill 레이어로 만든 이유',
de: 'warum Open Design als Skill-Schicht gebaut wurde',
fr: 'pourquoi Open Design est une couche de skills',
ru: 'почему Open Design построен как слой навыков',
es: 'por qué Open Design se construyó como capa de skills',
'pt-br': 'por que o Open Design foi criado como camada de skills',
it: 'perché Open Design è stato costruito come livello di skill',
vi: 'vì sao Open Design được xây như một lớp skill',
pl: 'dlaczego Open Design powstał jako warstwa skill',
id: 'mengapa Open Design dibangun sebagai lapisan skill',
nl: 'waarom Open Design als skill-laag is gebouwd',
ar: 'لماذا بنينا Open Design كطبقة مهارات',
tr: 'Open Design neden bir skill katmanı olarak kuruldu',
uk: 'чому Open Design створено як шар навичок',
},
};
const localizedBlogTopic = (id: string, locale: LandingLocaleCode) => {
const compact = compactId(id);
if (locale === DEFAULT_LOCALE) return compact.replace(/-/g, ' ');
return BLOG_TOPIC_TITLES[compact]?.[locale] ?? compact.replace(/-/g, ' ');
};
export function explicitLocalizedString(
value: LocalizedStringValue,
locale: LandingLocaleCode,
): string | undefined {
if (typeof value === 'string') {
return locale === DEFAULT_LOCALE && value.trim() ? value.trim() : undefined;
}
if (!value || typeof value !== 'object') return undefined;
const localeDef = getLocaleDefinition(locale);
const candidates = [
locale,
localeDef.htmlLang,
localeDef.htmlLang.toLowerCase(),
localeDef.htmlLang.replace('-', '_'),
locale === 'zh' ? 'zh-CN' : undefined,
locale === 'zh-tw' ? 'zh-TW' : undefined,
locale === 'pt-br' ? 'pt-BR' : undefined,
].filter((item): item is string => Boolean(item));
for (const key of candidates) {
const text = value[key];
if (typeof text === 'string' && text.trim()) {
return text.trim();
}
}
return undefined;
}
export function localizeTaxonomyValue(
value: string | undefined,
locale: LandingLocaleCode,
): string | undefined {
if (!value) return undefined;
if (locale === DEFAULT_LOCALE) return value;
const key = normalizeTerm(value);
return (
TAXONOMY_TERMS[key]?.[locale] ??
CATEGORY_LABELS[key]?.[locale] ??
copyFor(locale)?.unknownTag
);
}
export function localizeContentTag(
value: string | undefined,
locale: LandingLocaleCode,
): string | undefined {
if (!value) return undefined;
if (locale === DEFAULT_LOCALE) return value;
return localizeTaxonomyValue(value, locale) ?? copyFor(locale)?.unknownTag;
}
export function localizeSkillDescription(args: {
name: string;
mode?: string;
scenario?: string;
category?: string;
locale: LandingLocaleCode;
fallback: string;
}): string {
const copy = copyFor(args.locale);
if (!copy) return args.fallback;
const labels = [args.mode, args.scenario, args.category]
.map((value) => localizeTaxonomyValue(value, args.locale))
.filter((value): value is string => Boolean(value));
return copy.skillDescription(args.name, Array.from(new Set(labels)));
}
export function localizeSystemText(args: {
name: string;
category: string;
paletteCount: number;
locale: LandingLocaleCode;
fallbackTagline: string;
fallbackAtmosphere: string;
}): { category: string; tagline: string; atmosphere: string } {
const copy = copyFor(args.locale);
if (!copy) {
return {
category: args.category,
tagline: args.fallbackTagline,
atmosphere: args.fallbackAtmosphere,
};
}
const category = localizeTaxonomyValue(args.category, args.locale) ?? copy.systemNoun;
return {
category,
tagline: copy.systemTagline(args.name, category),
atmosphere: copy.systemAtmosphere(args.name, category, args.paletteCount),
};
}
export function localizeCraftText(args: {
slug: string;
name: string;
summary: string;
locale: LandingLocaleCode;
}): { name: string; summary: string } {
const copy = copyFor(args.locale);
if (!copy) return { name: args.name, summary: args.summary };
const baseName = CRAFT_LABELS[args.slug]?.[args.locale] ?? args.name;
return {
name: copy.craftName(baseName),
summary: copy.craftSummary(baseName),
};
}
export function localizeTemplateText(args: {
name: string;
summary: string;
locale: LandingLocaleCode;
}): { name: string; summary: string } {
const copy = copyFor(args.locale);
if (!copy) return { name: args.name, summary: args.summary };
return {
name: copy.templateName(args.name),
summary: copy.templateSummary(args.name),
};
}
export function localizePluginText(args: {
id: string;
title: string;
description: string;
locale: LandingLocaleCode;
mode?: string;
taskKind?: string;
surface?: string;
visualKind?: string;
labels?: string[];
}): { title: string; description: string; exampleQuery: string | undefined } {
const copy = copyFor(args.locale);
if (!copy) {
return {
title: args.title,
description: args.description,
exampleQuery: undefined,
};
}
const kind =
localizeTaxonomyValue(args.mode ?? args.surface ?? args.visualKind, args.locale) ??
copy.pluginNoun;
const labels = (args.labels ?? [])
.map((value) => localizeTaxonomyValue(value, args.locale))
.filter((value): value is string => Boolean(value));
return {
title: copy.pluginTitle(kind, compactId(args.id)),
description: copy.pluginDescription(kind, Array.from(new Set(labels)).slice(0, 4)),
exampleQuery: copy.pluginExample(kind),
};
}
export function localizeBlogPostText(args: {
id: string;
title: string;
summary: string;
category: string;
locale: LandingLocaleCode;
}): { title: string; summary: string; category: string; bodyHtml: string | undefined } {
const copy = copyFor(args.locale);
if (!copy) {
return {
title: args.title,
summary: args.summary,
category: args.category,
bodyHtml: undefined,
};
}
const topic = localizedBlogTopic(args.id, args.locale);
const title = copy.blogTitle(topic);
const summary = copy.blogSummary(topic);
return {
title,
summary,
category: localizeTaxonomyValue(args.category, args.locale) ?? copy.blogNoun,
bodyHtml: copy.blogBody(topic, summary),
};
}

View file

@ -7,11 +7,34 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const localizedContentSchema = z
.record(
z.string(),
z
.object({
name: z.string().optional(),
title: z.string().optional(),
description: z.string().optional(),
summary: z.string().optional(),
category: z.string().optional(),
tagline: z.string().optional(),
atmosphere: z.string().optional(),
body: z.string().optional(),
bodyHtml: z.string().optional(),
triggers: z.array(z.string()).optional(),
examplePrompt: z.string().optional(),
example_prompt: z.string().optional(),
})
.passthrough(),
)
.optional();
const skillSchema = z
.object({
name: z.string().optional(),
description: z.string().optional(),
triggers: z.array(z.string()).optional(),
i18n: localizedContentSchema,
od: z
.object({
mode: z.string().optional(),
@ -53,7 +76,7 @@ const systems = defineCollection({
base: '../../design-systems',
pattern: '*/DESIGN.md',
}),
schema: z.object({}).passthrough(),
schema: z.object({ i18n: localizedContentSchema }).passthrough(),
});
const craft = defineCollection({
@ -61,7 +84,7 @@ const craft = defineCollection({
base: '../../craft',
pattern: '*.md',
}),
schema: z.object({}).passthrough(),
schema: z.object({ i18n: localizedContentSchema }).passthrough(),
});
// `templates/live-artifacts/<slug>/README.md` — legacy Live Artifact bundles.
@ -73,7 +96,7 @@ const templates = defineCollection({
base: '../../templates/live-artifacts',
pattern: '*/README.md',
}),
schema: z.object({}).passthrough(),
schema: z.object({ i18n: localizedContentSchema }).passthrough(),
});
// Blog posts live in `app/content/blog/*.md`. Each post must declare a typed
@ -87,13 +110,29 @@ const blog = defineCollection({
pattern: ['*.md', '!_*.md'],
base: './app/content/blog',
}),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
category: z.enum(['Product', 'Guides', 'Use cases', 'Community']),
readingTime: z.number().int().positive(),
summary: z.string(),
}),
schema: z
.object({
title: z.string(),
date: z.coerce.date(),
category: z.enum(['Product', 'Guides', 'Use cases', 'Community']),
readingTime: z.number().int().positive(),
summary: z.string(),
i18n: z
.record(
z.string(),
z
.object({
title: z.string().optional(),
summary: z.string().optional(),
category: z.string().optional(),
body: z.string().optional(),
bodyHtml: z.string().optional(),
})
.passthrough(),
)
.optional(),
})
.passthrough(),
});
// Tutorials live in `app/content/tutorials/*.md`. Each entry maps to a

View file

@ -133,13 +133,50 @@ body::before {
.side-rail.right .rail-text { transform: rotate(180deg); }
.side-rail.left .rail-text { writing-mode: vertical-rl; transform: none; }
/*
* Sticky chrome bar wraps the editorial metadata strip + main nav as a
* single Headroom-style unit. Keeping them welded means the language
* switcher stays reachable while reading the page (you can flip locales
* mid-scroll without scrolling back to the top), and the masthead +
* navigation always read as a single nameplate rather than two unrelated
* rows.
*
* Sticky/transform/transition lived on `.nav` historically they belong
* here now so both rows slide together. The `data-chrome-headroom` hook
* is what `header-enhancer.astro` (and the homepage inline script)
* toggle `is-hidden` on.
*/
.site-chrome {
position: sticky;
top: 0;
z-index: 50;
background: var(--paper);
transform: translateY(0);
transition: transform 360ms cubic-bezier(0.22, 0.61, 0.36, 1),
box-shadow 220ms ease,
border-color 220ms ease;
will-change: transform;
}
.site-chrome.is-hidden {
transform: translateY(-100%);
pointer-events: none;
box-shadow: none;
}
/* top metadata strip */
.topbar {
border-bottom: 1px solid var(--line);
padding: 10px 0;
background: var(--paper);
/*
* Stays a positioned context so the locale menu has an absolute
* anchor. The z-index is small but non-zero so the topbar and the
* menu that drops out of it paint above the sibling nav inside
* `.site-chrome`. Without this the menu would be obscured by the
* nav's download/star buttons.
*/
position: relative;
z-index: 4;
z-index: 2;
}
.topbar-inner {
display: flex;
@ -168,118 +205,154 @@ body::before {
}
.topbar-link:hover { color: var(--coral); border-bottom-color: var(--coral); }
/*
* Site-level locale switcher in the main nav.
*
* Sits in `.nav-side` next to Download/Star so it carries the same visual
* weight as the primary CTAs (the previous attempt buried it in the small
* topbar metadata strip where users missed it). Built on `<details>` so
* the dropdown works without JavaScript the close-on-outside-click
* behaviour is a tiny inline script attached by `header-enhancer.astro`.
* Editorial language switcher. The trigger reads as part of the
* masthead line ("● LIVE · V0.7.0 / LANG · ZH-TW") uppercase
* tracked text, no pill, no border. The dropdown is a small
* floating panel that mirrors the topbar's paper/ink palette but
* drops the tracking so native scripts (中文 / 한국어 / العربية)
* read at their intended rhythm.
*/
.nav-locale {
.locale-switch {
display: inline-block;
position: relative;
}
.nav-locale > summary {
list-style: none;
cursor: pointer;
.locale-trigger {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
border-radius: 999px;
border: 1px solid rgba(21, 20, 15, 0.18);
background: transparent;
color: var(--ink);
font-family: var(--sans);
font-size: 13px;
font-weight: 500;
white-space: nowrap;
transition: background 160ms ease, border-color 160ms ease, color 160ms ease;
gap: 6px;
cursor: pointer;
color: inherit;
list-style: none;
border-bottom: 1px solid transparent;
transition: color 160ms ease, border-color 160ms ease;
user-select: none;
outline: none;
padding: 0;
}
.nav-locale > summary::-webkit-details-marker { display: none; }
.nav-locale > summary::marker { display: none; content: ''; }
.nav-locale > summary:hover,
.nav-locale[open] > summary {
background: rgba(21, 20, 15, 0.04);
border-color: rgba(21, 20, 15, 0.36);
.locale-trigger::-webkit-details-marker { display: none; }
.locale-trigger:hover,
.locale-switch[open] > .locale-trigger,
.locale-trigger:focus-visible {
color: var(--coral);
border-bottom-color: var(--coral);
}
.nav-locale-current {
/* Cap unusually long native labels (e.g. "Português (Brasil)") so the
* pill stays close to the width of Download / Star and the nav row
* doesn't reflow when the active locale changes. */
max-width: 11ch;
overflow: hidden;
text-overflow: ellipsis;
.locale-trigger-prefix {
color: var(--ink-faint);
}
.nav-locale-glyph { flex: 0 0 auto; opacity: 0.7; }
.nav-locale-chevron {
flex: 0 0 auto;
.locale-trigger:hover .locale-trigger-prefix,
.locale-switch[open] > .locale-trigger .locale-trigger-prefix,
.locale-trigger:focus-visible .locale-trigger-prefix {
color: var(--coral);
}
.locale-trigger-sep {
opacity: 0.55;
transition: transform 180ms ease;
}
.nav-locale[open] .nav-locale-chevron { transform: rotate(180deg); opacity: 0.9; }
.nav-locale-panel {
.locale-trigger-code {
font: inherit;
letter-spacing: inherit;
color: var(--ink);
font-weight: 600;
}
.locale-trigger:hover .locale-trigger-code,
.locale-switch[open] > .locale-trigger .locale-trigger-code,
.locale-trigger:focus-visible .locale-trigger-code {
color: var(--coral);
}
.locale-trigger-caret {
width: 8px;
height: 5px;
flex-shrink: 0;
opacity: 0.7;
transition: transform 160ms ease, opacity 160ms ease;
}
.locale-switch[open] > .locale-trigger .locale-trigger-caret {
transform: rotate(180deg);
opacity: 1;
}
/*
* Two-column editorial catalogue. 17 locales × single-column would either
* scroll (ugly) or run most of the hero's height (worse). A 2-col grid
* keeps the panel under 9 rows so every language is visible at once and
* reads like a small index of nameplates rather than a dropdown.
*
* The 20px top offset gives clear breathing room from the topbar baseline
* without it the active row (which mirrors the trigger label) feels
* fused to the chrome above it.
*/
.locale-menu {
position: absolute;
top: calc(100% + 10px);
top: calc(100% + 20px);
right: 0;
z-index: 30;
width: 280px;
max-height: min(560px, calc(100vh - 140px));
overflow-y: auto;
z-index: 60;
display: grid;
gap: 2px;
grid-template-columns: repeat(2, minmax(168px, 1fr));
gap: 2px 4px;
padding: 10px;
border: 1px solid rgba(21, 20, 15, 0.16);
border-radius: 14px;
background: var(--paper);
border: 1px solid rgba(21, 20, 15, 0.22);
box-shadow:
0 1px 0 rgba(21, 20, 15, 0.02),
0 20px 50px -12px rgba(21, 20, 15, 0.22);
letter-spacing: 0;
0 1px 0 rgba(21, 20, 15, 0.04),
0 28px 60px -32px rgba(21, 20, 15, 0.42);
font-family: var(--sans);
letter-spacing: normal;
text-transform: none;
animation: nav-locale-pop 160ms ease;
/* Subtle entrance — matches the rest of the site's 200ms ease-out */
animation: locale-menu-in 180ms cubic-bezier(0.23, 1, 0.32, 1);
}
@keyframes nav-locale-pop {
@keyframes locale-menu-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.nav-locale-item {
.locale-menu-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 9px 12px;
border-radius: 10px;
align-items: baseline;
gap: 12px;
padding: 8px 12px;
color: var(--ink);
font-family: var(--sans);
font-size: 14px;
text-decoration: none;
font-size: 12.5px;
line-height: 1.2;
transition: background 140ms ease, color 140ms ease;
}
.nav-locale-item:hover {
background: rgba(21, 20, 15, 0.06);
color: var(--ink);
.locale-menu-item:hover {
background: color-mix(in oklab, var(--paper), var(--ink) 6%);
color: var(--coral);
}
.nav-locale-item.is-current {
background: var(--ink);
color: var(--paper);
font-weight: 600;
cursor: default;
pointer-events: none;
.locale-menu-item:hover .locale-menu-code { color: var(--coral); }
.locale-menu-item.is-active {
background: color-mix(in oklab, var(--paper), var(--coral) 8%);
color: var(--coral);
}
.locale-menu-item.is-active .locale-menu-code {
color: var(--coral);
}
.locale-menu-code {
flex-shrink: 0;
min-width: 46px;
font-size: 9.5px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-faint);
transition: color 140ms ease;
}
.locale-menu-label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 520px) {
.locale-menu {
grid-template-columns: minmax(0, 1fr);
min-width: min(260px, calc(100vw - 32px));
}
.locale-menu-item { padding: 9px 12px; }
}
@media (prefers-reduced-motion: reduce) {
.locale-menu { animation: none; }
.locale-trigger-caret { transition: none; }
}
.nav-locale-item .nav-locale-check { opacity: 0.95; }
.nav-locale-name { line-height: 1.2; }
/* RTL locales: keep the panel anchored right but flip item alignment so
* the script reads naturally. The Header runs LTR but each item carries
* `lang="ar"` etc., so individual rows still render in their own script. */
[dir='rtl'] .nav-locale-panel { left: 0; right: auto; }
/* On narrow screens the responsive cleanup at the bottom of this file
* hides `.nav-cta` (Download / Star) but keeps `.nav-locale` visible,
* so a mobile user always has a one-tap way to change language without
* scrolling to the footer. */
.topbar .pulse {
width: 6px; height: 6px;
border-radius: 50%;
@ -295,41 +368,14 @@ body::before {
/* nav */
/*
* Headroom-style sticky header.
*
* The element is always `position: sticky`, so the browser docks it to the
* top of the viewport once the topbar has scrolled away. The
* `<Header />` client island then toggles the `is-hidden` modifier based
* on scroll direction, which animates the bar in and out via `transform`.
*
* When the user is at the very top of the page, the topbar is still
* visible above the nav and `position: sticky` simply leaves the nav in
* its natural flow position exactly the brief.
* The sticky + headroom behavior lives on the outer `.site-chrome`
* wrapper now (so the topbar + nav slide as one unit). This rule keeps
* only the spacing and surface treatment.
*/
.nav {
padding: 22px 0;
position: sticky;
top: 0;
z-index: 50;
background: var(--paper);
transform: translateY(0);
transition: transform 360ms cubic-bezier(0.22, 0.61, 0.36, 1),
box-shadow 220ms ease,
border-color 220ms ease;
border-bottom: 1px solid transparent;
will-change: transform;
}
/*
* Subtle visual cue once we leave the top of the page. We can't tell from
* CSS alone whether the bar is "stuck"; the deadband + class toggle in
* <Header /> approximates it well enough for our purpose. We rely on the
* `is-hidden` toggle to flicker the border while moving and a steady
* border once docked.
*/
.nav.is-hidden {
transform: translateY(-100%);
pointer-events: none;
box-shadow: none;
}
.nav-inner {
display: flex;
@ -340,53 +386,43 @@ body::before {
.brand {
display: inline-flex;
align-items: center;
gap: 14px;
gap: 8px;
font-family: var(--sans);
font-weight: 700;
letter-spacing: -0.01em;
color: var(--ink);
text-decoration: none;
font-size: 18px;
/*
* Keep the brand block on a single line. At narrow widths the
* "Open Design" wordmark was wrapping to two lines once enough
* top-row links accumulated to push the brand against the next
* column. The hamburger fallback already hides everything past
* the brand at 1080px, so the brand has plenty of horizontal
* room wrapping was a layout glitch, not a constraint.
*/
white-space: nowrap;
}
.brand-mark {
/*
* Bumped from 36px 44px (22% larger). The previous size felt
* undersized against the wordmark's optical weight, especially on
* the new black brand mark where the negative space inside the
* speech bubble visually shrinks the glyph further.
*/
width: 44px; height: 44px;
width: 36px; height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
flex: 0 0 auto;
}
.brand-name {
white-space: nowrap;
}
.brand-mark img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
/*
* Round the corners of the brand glyph. The source PNG is a
* solid-fill square; clipping the corners gives the mark the
* modern app-icon silhouette (~22% of side length, matching the
* iOS / macOS visual convention) so it sits cleanly next to the
* "Open Design" wordmark instead of looking like a raw screenshot.
*/
border-radius: 10px;
}
.brand-name {
white-space: nowrap;
.brand-meta {
font-family: var(--sans);
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-faint);
line-height: 1.3;
margin-left: 4px;
border-left: 1px solid var(--line);
padding-left: 14px;
}
.brand-meta b { display: block; color: var(--ink); font-weight: 600; }
.nav-links {
display: flex;
gap: 38px;
@ -487,20 +523,6 @@ body::before {
font-weight: 600;
margin-bottom: 2px;
}
/*
* Inline count badge inside a dropdown row (e.g. "Skills 132"). Uses
* a distinct class from the absolutely-positioned `.num` on top-row
* nav links there the count floats above the link as a superscript;
* here we want it inline beside the label, more like a tag.
*/
.nav-dropdown .dropdown-num {
display: inline-block;
margin-left: 6px;
font-size: 11px;
font-weight: 500;
color: var(--ink-faint);
letter-spacing: 0.02em;
}
.nav-dropdown .dropdown-blurb {
display: block;
font-size: 12px;
@ -1969,6 +1991,203 @@ section.tight { padding: 90px 0; }
transform: rotate(180deg);
}
/* ---------- OFFICIAL SOURCE STRIP ----------
*
* Slim attestation band that sits between the hero/wire and About.
* Reinforces the canonical surfaces official site, GitHub repo,
* releases, download, docs, Discord for both Google entity merge
* and human verification. Minimal chrome on purpose.
*/
.official-strip {
padding: 38px 0 18px;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
background: var(--paper);
}
.official-strip-inner {
display: grid;
grid-template-columns: minmax(180px, 220px) 1fr;
gap: 28px;
align-items: center;
}
.official-strip-label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-mute);
}
.official-strip-label .ix {
color: var(--coral);
margin-left: 6px;
}
.official-strip-list {
list-style: none;
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 18px 24px;
padding: 0;
margin: 0;
}
.official-strip-list li {
display: flex;
min-width: 0;
}
.official-strip-list a {
display: grid;
gap: 4px;
padding: 6px 0;
border-bottom: 1px solid transparent;
text-decoration: none;
color: var(--ink);
font-family: var(--sans);
min-width: 0;
transition: border-color 0.16s ease, color 0.16s ease;
}
.official-strip-list a:hover {
border-bottom-color: var(--coral);
color: var(--coral);
}
.official-strip-list .label {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ink-mute);
}
.official-strip-list .value {
font-size: 13.5px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 980px) {
.official-strip-inner {
grid-template-columns: 1fr;
gap: 18px;
}
.official-strip-list {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 560px) {
.official-strip-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px 18px;
}
}
/* ---------- FAQ ---------- */
.faq-head {
max-width: 880px;
margin: 36px 0 48px;
}
.faq-head .label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-mute);
display: inline-block;
margin-bottom: 18px;
}
.faq-head .display {
font-family: var(--serif);
font-weight: 600;
font-size: clamp(36px, 5.4vw, 64px);
line-height: 1.06;
letter-spacing: -0.022em;
color: var(--ink);
}
.faq-head .display em {
font-style: italic;
font-weight: 600;
color: var(--coral);
}
.faq-head .display .dot { color: var(--coral); }
.faq-list {
list-style: none;
padding: 0;
margin: 0;
border-top: 1px solid var(--line);
}
.faq-item {
border-bottom: 1px solid var(--line);
}
.faq-item details {
padding: 0;
}
.faq-item summary {
display: grid;
grid-template-columns: 56px 1fr 32px;
gap: 24px;
align-items: center;
padding: 26px 0;
cursor: pointer;
list-style: none;
user-select: none;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-index {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.16em;
color: var(--ink-faint);
}
.faq-q {
font-family: var(--serif);
font-size: clamp(20px, 2.2vw, 26px);
line-height: 1.25;
color: var(--ink);
}
.faq-toggle {
justify-self: end;
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid var(--line);
color: var(--ink);
display: inline-flex;
align-items: center;
justify-content: center;
font-family: var(--sans);
font-size: 16px;
transition: transform 0.22s cubic-bezier(0.23, 1, 0.32, 1),
background-color 0.22s cubic-bezier(0.23, 1, 0.32, 1),
color 0.22s cubic-bezier(0.23, 1, 0.32, 1);
}
.faq-item details[open] .faq-toggle {
transform: rotate(45deg);
background: var(--ink);
color: var(--paper);
border-color: var(--ink);
}
.faq-a {
grid-column: 2 / 3;
margin: 0 0 26px;
padding-left: 80px;
max-width: 64ch;
font-family: var(--sans);
font-size: 15.5px;
line-height: 1.7;
color: var(--ink-mute);
}
@media (max-width: 720px) {
.faq-item summary {
grid-template-columns: 36px 1fr 28px;
gap: 14px;
padding: 20px 0;
}
.faq-a {
padding-left: 50px;
font-size: 14.5px;
}
}
/* ---------- FOOTER ---------- */
footer {
border-top: 1px solid var(--line);
@ -2184,9 +2403,9 @@ footer {
scale: 1 !important;
transition: none !important;
}
/* Skip the slide-in on the sticky header for users who prefer no motion;
/* Skip the slide-in on the sticky chrome for users who prefer no motion;
* the show/hide still toggles, just instantly. */
.nav { transition: none !important; }
.site-chrome { transition: none !important; }
}
/* ---------- responsive ----------
@ -2211,10 +2430,12 @@ footer {
@media (max-width: 1200px) {
.topbar-inner .mid { display: none; }
}
/* nav: between 1080 and 1180 the brand + nav links + 2 CTAs + dot
* crowd the row. Tighten link spacing so the Star CTA never compresses. */
/* nav: between 1080 and 1180 the brand tail + nav links + 2 CTAs + dot
* crowd the row. Drop the brand sub-meta first, then tighten link spacing,
* so the Star CTA never has to compress. */
@media (max-width: 1180px) {
.nav-inner { gap: 18px; }
.brand-meta { display: none; }
.nav-links { gap: 28px; }
}
@ -2378,9 +2599,10 @@ footer {
.foot-bottom { flex-direction: column; align-items: flex-start; gap: 12px; }
.foot-bottom .right { flex-wrap: wrap; gap: 12px 20px; }
/* nav at 880px tighten padding; nav-links stay reachable through
* the hamburger panel introduced at 1080px. Download stays hidden;
* Star CTA still pings in the bar. */
* the hamburger panel introduced at 1080px. Brand meta and Download
* stay hidden; Star CTA still pings in the bar. */
.nav { padding: 16px 0; }
.brand-meta { display: none; }
.nav-side .nav-cta.ghost { display: none; }
}
@media (max-width: 640px) {
@ -2409,6 +2631,10 @@ footer {
section { padding: 80px 0; }
.topbar-inner { font-size: 9px; gap: 10px; }
.topbar-inner .right { gap: 10px; }
.locale-trigger { gap: 4px; }
.locale-trigger-prefix { display: none; }
.locale-trigger-sep { display: none; }
.locale-menu { min-width: 208px; }
.partners { grid-template-columns: repeat(2, 1fr); gap: 14px; }
.foot-grid { grid-template-columns: 1fr; gap: 24px; }
.work { margin: 0 8px; padding: 48px 20px; border-radius: 20px; }
@ -2418,15 +2644,20 @@ footer {
.cta-actions .btn,
.cta-actions .email-pill { flex: 1 1 auto; justify-content: center; }
.nav-inner { gap: 12px; }
.brand { font-size: 16px; gap: 10px; }
.brand { font-size: 16px; gap: 8px; }
.brand-mark { width: 32px; height: 32px; font-size: 16px; }
.read-more { margin-top: 36px; }
.foot-mega .word { font-size: clamp(48px, 16vw, 88px); }
}
@media (max-width: 420px) {
.container { padding: 0 14px; }
/* topbar — drop the locale switcher tail; keep version pill + brand */
.topbar-inner .right > a:nth-of-type(n + 3) { display: none; }
.topbar-inner { align-items: flex-start; }
.topbar-inner .right {
flex-direction: column;
align-items: flex-end;
gap: 6px;
}
.locale-menu { min-width: 192px; }
.hero-stats { gap: 10px 16px; }
.hero-stats .stat { font-size: 11px; }
.hero-foot { font-size: 9.5px; }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -97,7 +97,7 @@ const pageTitle = routeRoot === 'skills'
const pageDescription = `Open Design ${routeRoot || 'landing'} page.`;
---
<Layout title={pageTitle} description={pageDescription} active={routeRoot as HeaderProps['active']} locale={locale} prefixDefaultLocale={true}>
<Layout title={pageTitle} description={pageDescription} active={routeRoot as HeaderProps['active']}>
{routeRoot === 'blog' && (
<>
<header class='catalog-head'>

View file

@ -1,17 +1,11 @@
---
/*
* Locale-prefixed wrapper for `/agents/`. Re-renders the canonical
* English page under each non-default locale URL so hreflang
* alternates generated by sub-page-layout point at real routes.
*
* Translations are a follow-up — for now every locale serves the
* same English content with the canonical pointing back to /agents/.
*/
import { PREFIXED_LOCALES } from '../../../_lib/i18n';
import AgentsPage from '../../agents/index.astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n';
export function getStaticPaths() {
return PREFIXED_LOCALES.map((locale) => ({ params: { locale } }));
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).map(
(locale) => ({ params: { locale: locale.code } }),
);
}
---

View file

@ -1,11 +1,11 @@
---
/* Locale-prefixed wrapper for `/alternatives/claude-design/`. See
* `[locale]/agents/index.astro` for the rationale. */
import { PREFIXED_LOCALES } from '../../../../_lib/i18n';
import ClaudeDesignAlternativePage from '../../../alternatives/claude-design/index.astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../../i18n';
export function getStaticPaths() {
return PREFIXED_LOCALES.map((locale) => ({ params: { locale } }));
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).map(
(locale) => ({ params: { locale: locale.code } }),
);
}
---

View file

@ -0,0 +1,19 @@
---
import BlogPostPage, {
getStaticPaths as getBlogPostStaticPaths,
} from '../../blog/[slug].astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n';
export async function getStaticPaths() {
const basePaths = await getBlogPostStaticPaths();
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).flatMap(
(locale) =>
basePaths.map((path) => ({
params: { ...path.params, locale: locale.code },
props: path.props,
})),
);
}
---
<BlogPostPage {...Astro.props} />

View file

@ -0,0 +1,12 @@
---
import BlogPage from '../../blog/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 } }),
);
}
---
<BlogPage />

View file

@ -1,11 +1,11 @@
---
/* Locale-prefixed wrapper for `/compare/`. See `[locale]/agents/index.astro`
* for the rationale. */
import { PREFIXED_LOCALES } from '../../../_lib/i18n';
import ComparePage from '../../compare/index.astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n';
export function getStaticPaths() {
return PREFIXED_LOCALES.map((locale) => ({ params: { locale } }));
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).map(
(locale) => ({ params: { locale: locale.code } }),
);
}
---

View file

@ -0,0 +1,19 @@
---
import CraftDetailPage, {
getStaticPaths as getCraftStaticPaths,
} from '../../craft/[slug].astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n';
export async function getStaticPaths() {
const basePaths = await getCraftStaticPaths();
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).flatMap(
(locale) =>
basePaths.map((path) => ({
params: { ...path.params, locale: locale.code },
props: path.props,
})),
);
}
---
<CraftDetailPage {...Astro.props} />

View file

@ -0,0 +1,12 @@
---
import CraftPage from '../../craft/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 } }),
);
}
---
<CraftPage />

View file

@ -0,0 +1,12 @@
---
import HtmlAnythingPage from '../../html-anything/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 } }),
);
}
---
<HtmlAnythingPage />

View file

@ -1,82 +1,12 @@
---
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import FontStylesheet from '../../_components/font-stylesheet.astro';
import GoogleAnalytics from '../../_components/google-analytics.astro';
import HomeEnhancer from '../../_components/home-enhancer.astro';
import LocaleSwitcherEnhancer from '../../_components/locale-switcher-enhancer.astro';
import PreciseLazyload from '../../_components/precise-lazyload.astro';
import Page from '../../page';
import { getCatalogCounts } from '../../_lib/catalog';
import { getGithubRepoMeta } from '../../_lib/github';
import {
LOCALE_OG,
PREFIXED_LOCALES,
alternateLinks,
isLocale,
localeDir,
localePath,
type Locale,
} from '../../_lib/i18n';
import { heroImage, heroImageSrcset } from '../../image-assets';
import '../../globals.css';
import HomePage from '../index.astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../i18n';
export function getStaticPaths() {
return PREFIXED_LOCALES.map((locale) => ({
params: { locale },
}));
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).map(
(locale) => ({ params: { locale: locale.code } }),
);
}
const localeParam = Astro.params.locale;
const locale: Locale = isLocale(localeParam) ? localeParam : 'en';
const counts = await getCatalogCounts();
const github = await getGithubRepoMeta();
const pathname = localePath('/', locale, { prefixDefault: true });
const title = 'Open Design — Design with the agent already on your laptop.';
const description = `The open-source alternative to Claude Design. Your existing coding agent becomes the design engine, driven by ${counts.skills} composable skills and ${counts.systems} brand-grade design systems.`;
const canonical = new URL(pathname, Astro.site).toString();
const pageHtml = renderToStaticMarkup(
Page({ counts, github, locale, pathname, prefixDefaultLocale: true }) as ReturnType<typeof createElement>,
);
---
<!doctype html>
<html lang={locale} dir={localeDir(locale)}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#efe7d2" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
{alternateLinks(pathname).map((item) => (
<link rel="alternate" hreflang={item.hreflang} href={new URL(item.href, Astro.site).toString()} />
))}
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<FontStylesheet />
<link
rel="preload"
as="image"
href={heroImage}
imagesrcset={heroImageSrcset}
imagesizes="(max-width: 768px) 100vw, 60vw"
fetchpriority="high"
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Open Design" />
<meta property="og:locale" content={LOCALE_OG[locale]} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={heroImage} />
<meta name="twitter:card" content="summary_large_image" />
<GoogleAnalytics />
</head>
<body>
<Fragment set:html={pageHtml} />
<LocaleSwitcherEnhancer />
<PreciseLazyload />
<HomeEnhancer />
</body>
</html>
<HomePage />

View file

@ -1,11 +1,11 @@
---
/* Locale-prefixed wrapper for `/official/`. See `[locale]/agents/index.astro`
* for the rationale. */
import { PREFIXED_LOCALES } from '../../../_lib/i18n';
import OfficialPage from '../../official/index.astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n';
export function getStaticPaths() {
return PREFIXED_LOCALES.map((locale) => ({ params: { locale } }));
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).map(
(locale) => ({ params: { locale: locale.code } }),
);
}
---

View file

@ -0,0 +1,19 @@
---
import PluginPage, {
getStaticPaths as getPluginStaticPaths,
} from '../../plugins/[...slug].astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n';
export function getStaticPaths() {
const basePaths = getPluginStaticPaths();
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).flatMap(
(locale) =>
basePaths.map((path) => ({
params: { ...path.params, locale: locale.code },
props: path.props,
})),
);
}
---
<PluginPage {...Astro.props} />

View file

@ -0,0 +1,12 @@
---
import PluginsPage from '../../plugins/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 } }),
);
}
---
<PluginsPage />

View file

@ -0,0 +1,19 @@
---
import PluginPreviewPage, {
getStaticPaths as getPluginPreviewStaticPaths,
} from '../../../plugins/previews/[...slug].astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../../i18n';
export function getStaticPaths() {
const basePaths = getPluginPreviewStaticPaths();
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).flatMap(
(locale) =>
basePaths.map((path) => ({
params: { ...path.params, locale: locale.code },
props: path.props,
})),
);
}
---
<PluginPreviewPage {...Astro.props} />

View file

@ -1,12 +1,11 @@
---
/* Locale-prefixed wrapper for `/quickstart/`. See `[locale]/agents/index.astro`
* for the rationale; same shape, English content per locale until
* translations land. */
import { PREFIXED_LOCALES } from '../../../_lib/i18n';
import QuickstartPage from '../../quickstart/index.astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n';
export function getStaticPaths() {
return PREFIXED_LOCALES.map((locale) => ({ params: { locale } }));
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).map(
(locale) => ({ params: { locale: locale.code } }),
);
}
---

View file

@ -0,0 +1,19 @@
---
import SkillPage, {
getStaticPaths as getSkillStaticPaths,
} from '../../skills/[slug].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} />

View file

@ -0,0 +1,12 @@
---
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 />

View file

@ -0,0 +1,19 @@
---
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} />

View file

@ -0,0 +1,19 @@
---
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} />

View file

@ -0,0 +1,19 @@
---
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} />

View file

@ -0,0 +1,19 @@
---
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} />

View file

@ -0,0 +1,12 @@
---
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 />

View file

@ -0,0 +1,19 @@
---
import TemplatePage, {
getStaticPaths as getTemplateStaticPaths,
} from '../../templates/[slug].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} />

View file

@ -0,0 +1,12 @@
---
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 />

View file

@ -0,0 +1,19 @@
---
import TutorialPage, {
getStaticPaths as getTutorialStaticPaths,
} from '../../tutorials/[slug].astro';
import { DEFAULT_LOCALE, LANDING_LOCALES } from '../../../i18n';
export async function getStaticPaths() {
const basePaths = await getTutorialStaticPaths();
return LANDING_LOCALES.filter((locale) => locale.code !== DEFAULT_LOCALE).flatMap(
(locale) =>
basePaths.map((path) => ({
params: { ...path.params, locale: locale.code },
props: path.props,
})),
);
}
---
<TutorialPage {...Astro.props} />

View file

@ -0,0 +1,12 @@
---
import TutorialsPage from '../../tutorials/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 } }),
);
}
---
<TutorialsPage />

View file

@ -9,71 +9,130 @@
* P2 work — this hub is the SEO entry point.
*/
import Layout from '../../_components/sub-page-layout.astro';
import { getInfoPageCopy } from '../../info-page-i18n';
import { DEFAULT_LOCALE, localeFromPath, localizedHref, type LandingLocaleCode } from '../../i18n';
// Derive SITE from Astro.site so preview/staging JSON-LD matches the
// layout's canonical link. Falls back to the production origin.
const SITE = Astro.site?.toString() ?? 'https://open-design.ai/';
const REPO = 'https://github.com/nexu-io/open-design';
const REPO_RELEASES = `${REPO}/releases`;
const DISCORD = 'https://discord.gg/9ptkbbqRu';
const locale = localeFromPath(Astro.url.pathname);
const href = (path: string) => localizedHref(path, locale);
const copy = getInfoPageCopy(locale);
const page = copy.agents;
const common = copy.common;
const title = 'Open Design agents — 17 BYOK adapters';
const description =
'Open Design ships 17 BYOK adapters out of the box. Drive design from the same agent you use for code — no separate vendor login.';
const title = page.title;
const description = page.description;
// 17 first-party adapters live in `apps/daemon/src/runtimes/defs/`. Keep
// this list in lockstep with that directory.
// this list in lockstep with that directory; opendesigner.io advertises
// "16 out of the box" — Open Design ships one more, and the names below
// match the canonical `name:` field on each `RuntimeAgentDef`.
const tiers = [
{
label: 'Tier 1 — first-party tested',
blurb: 'Battle-tested daily by the Open Design maintainers. Stream-JSON IPC where supported, full AskUserQuestion mid-turn, skill-aware system prompts.',
label: page.tiers[0].label,
blurb: page.tiers[0].blurb,
agents: [
{ slug: 'claude-code', name: 'Claude Code', vendor: 'Anthropic', key: 'Anthropic API key (BYOK) or Claude subscription' },
{ slug: 'codex', name: 'Codex', vendor: 'OpenAI', key: 'OpenAI API key (BYOK) or ChatGPT subscription' },
{ slug: 'cursor', name: 'Cursor Agent', vendor: 'Cursor', key: 'Cursor account (uses your provider keys)' },
{ slug: 'gemini', name: 'Gemini CLI', vendor: 'Google', key: 'Google AI Studio key (BYOK)' },
{ slug: 'copilot', name: 'GitHub Copilot CLI', vendor: 'GitHub', key: 'GitHub Copilot subscription' },
{ slug: 'opencode', name: 'OpenCode', vendor: 'community', key: 'Provider keys via OpenCode config (BYOK)' },
{ slug: 'qwen', name: 'Qwen', vendor: 'Alibaba', key: 'DashScope / Qwen API key (BYOK)' },
{ slug: 'claude-code', name: 'Claude Code', vendor: 'Anthropic', key: 'Anthropic API key (BYOK) or Claude subscription' },
{ slug: 'codex', name: 'Codex', vendor: 'OpenAI', key: 'OpenAI API key (BYOK) or ChatGPT subscription' },
{ slug: 'cursor', name: 'Cursor Agent', vendor: 'Cursor', key: 'Cursor account (uses your provider keys)' },
{ slug: 'gemini', name: 'Gemini CLI', vendor: 'Google', key: 'Google AI Studio key (BYOK)' },
{ slug: 'copilot', name: 'GitHub Copilot CLI', vendor: 'GitHub', key: 'GitHub Copilot subscription' },
{ slug: 'opencode', name: 'OpenCode', vendor: 'community', key: 'Provider keys via OpenCode config (BYOK)' },
{ slug: 'qwen', name: 'Qwen', vendor: 'Alibaba', key: 'DashScope / Qwen API key (BYOK)' },
],
},
{
label: 'Tier 2 — supported adapters',
blurb: 'Wired through the same skill protocol. Slightly less daily exposure than Tier 1 but still maintained in-tree.',
label: page.tiers[1].label,
blurb: page.tiers[1].blurb,
agents: [
{ slug: 'grok', name: 'Grok', vendor: 'xAI', key: 'xAI SuperGrok OAuth (`grok login --oauth`)' },
{ slug: 'hermes', name: 'Hermes', vendor: 'community', key: 'xAI / OpenAI / Anthropic keys via `hermes auth add`' },
{ slug: 'kimi', name: 'Kimi CLI', vendor: 'Moonshot', key: 'Moonshot API key (BYOK)' },
{ slug: 'devin', name: 'Devin for Terminal', vendor: 'Cognition', key: 'Devin account' },
{ slug: 'deepseek', name: 'DeepSeek TUI', vendor: 'DeepSeek', key: 'DeepSeek API key (BYOK)' },
{ slug: 'pi', name: 'Pi', vendor: 'Inflection', key: 'Pi account (interactive auth)' },
{ slug: 'grok', name: 'Grok', vendor: 'xAI', key: 'xAI SuperGrok OAuth (`grok login --oauth`)' },
{ slug: 'hermes', name: 'Hermes', vendor: 'community', key: 'xAI / OpenAI / Anthropic keys via `hermes auth add`' },
{ slug: 'kimi', name: 'Kimi CLI', vendor: 'Moonshot', key: 'Moonshot API key (BYOK)' },
{ slug: 'devin', name: 'Devin for Terminal', vendor: 'Cognition', key: 'Devin account' },
{ slug: 'deepseek', name: 'DeepSeek TUI', vendor: 'DeepSeek', key: 'DeepSeek API key (BYOK)' },
{ slug: 'pi', name: 'Pi', vendor: 'Inflection', key: 'Pi account (interactive auth)' },
],
},
{
label: 'Tier 3 — community / experimental',
blurb: 'Newer adapters with narrower coverage. Useful where the vendor offers a workflow Tier 1 does not.',
label: page.tiers[2].label,
blurb: page.tiers[2].blurb,
agents: [
{ slug: 'vibe', name: 'Mistral Vibe CLI', vendor: 'Mistral', key: 'Mistral API key (BYOK)' },
{ slug: 'kiro', name: 'Kiro CLI', vendor: 'Amazon (preview)', key: 'AWS credentials (BYOK)' },
{ slug: 'kilo', name: 'Kilo', vendor: 'community', key: 'Provider keys via Kilo config (BYOK)' },
{ slug: 'qoder', name: 'Qoder CLI', vendor: 'community', key: 'Provider keys via Qoder config (BYOK)' },
{ slug: 'vibe', name: 'Mistral Vibe CLI', vendor: 'Mistral', key: 'Mistral API key (BYOK)' },
{ slug: 'kiro', name: 'Kiro CLI', vendor: 'Amazon (preview)', key: 'AWS credentials (BYOK)' },
{ slug: 'kilo', name: 'Kilo', vendor: 'community', key: 'Provider keys via Kilo config (BYOK)' },
{ slug: 'qoder', name: 'Qoder CLI', vendor: 'community', key: 'Provider keys via Qoder config (BYOK)' },
],
},
];
const allAgents = tiers.flatMap((t) => t.agents);
const byokItems = [
'Credentials live in .od/media-config.json or your shell env.',
'API calls go from your machine straight to your provider.',
'Switching providers is a key swap, not a re-onboard.',
'API spend bills to your account on each provider.',
];
const localizedCredentialLabels: Partial<Record<LandingLocaleCode, Record<string, string>>> = {
zh: {
'claude-code': 'Anthropic API keyBYOK或 Claude 订阅',
codex: 'OpenAI API keyBYOK或 ChatGPT 订阅',
cursor: 'Cursor 账号,使用你自己的模型凭据',
gemini: 'Google AI Studio keyBYOK',
copilot: 'GitHub Copilot 订阅',
opencode: '通过 OpenCode 配置接入模型凭据BYOK',
qwen: 'DashScope / Qwen API keyBYOK',
grok: 'xAI SuperGrok OAuth',
hermes: '通过 Hermes 添加 xAI / OpenAI / Anthropic 凭据',
kimi: 'Moonshot API keyBYOK',
devin: 'Devin 账号',
deepseek: 'DeepSeek API keyBYOK',
pi: 'Pi 账号(交互式登录)',
vibe: 'Mistral API keyBYOK',
kiro: 'AWS 凭据BYOK',
kilo: '通过 Kilo 配置接入模型凭据BYOK',
qoder: '通过 Qoder 配置接入模型凭据BYOK',
},
'zh-tw': {
'claude-code': 'Anthropic API keyBYOK或 Claude 訂閱',
codex: 'OpenAI API keyBYOK或 ChatGPT 訂閱',
cursor: 'Cursor 帳號,使用你自己的模型憑據',
gemini: 'Google AI Studio keyBYOK',
copilot: 'GitHub Copilot 訂閱',
opencode: '透過 OpenCode 設定接入模型憑據BYOK',
qwen: 'DashScope / Qwen API keyBYOK',
grok: 'xAI SuperGrok OAuth',
hermes: '透過 Hermes 加入 xAI / OpenAI / Anthropic 憑據',
kimi: 'Moonshot API keyBYOK',
devin: 'Devin 帳號',
deepseek: 'DeepSeek API keyBYOK',
pi: 'Pi 帳號(互動式登入)',
vibe: 'Mistral API keyBYOK',
kiro: 'AWS 憑據BYOK',
kilo: '透過 Kilo 設定接入模型憑據BYOK',
qoder: '透過 Qoder 設定接入模型憑據BYOK',
},
uk: {
'claude-code': 'Anthropic API key (BYOK) або передплата Claude',
codex: 'OpenAI API key (BYOK) або передплата ChatGPT',
cursor: 'Обліковий запис Cursor із вашими ключами моделей',
gemini: 'Google AI Studio key (BYOK)',
copilot: 'Передплата GitHub Copilot',
opencode: 'Ключі моделей через конфігурацію OpenCode (BYOK)',
qwen: 'DashScope / Qwen API key (BYOK)',
grok: 'xAI SuperGrok OAuth',
hermes: 'Ключі xAI / OpenAI / Anthropic через Hermes',
kimi: 'Moonshot API key (BYOK)',
devin: 'Обліковий запис Devin',
deepseek: 'DeepSeek API key (BYOK)',
pi: 'Обліковий запис Pi з інтерактивною авторизацією',
vibe: 'Mistral API key (BYOK)',
kiro: 'AWS credentials (BYOK)',
kilo: 'Ключі моделей через конфігурацію Kilo (BYOK)',
qoder: 'Ключі моделей через конфігурацію Qoder (BYOK)',
},
};
const nextItems = [
{ label: 'Quickstart', href: '/quickstart/', body: 'install in three commands.' },
{ label: 'Browse the skill catalog', href: '/skills/', body: 'choose the workflow you want to run.' },
{ label: 'Browse design systems', href: '/systems/', body: 'pick the brand contract.' },
{ label: 'Claude Design alternative', href: '/alternatives/claude-design/', body: 'full comparison.' },
];
const credentialLabel = (agent: (typeof allAgents)[number]) => {
if (locale === DEFAULT_LOCALE) return agent.key;
return localizedCredentialLabels[locale]?.[agent.slug] ?? `${common.byok} · ${agent.vendor}`;
};
const jsonLd = [
{
@ -81,7 +140,7 @@ const jsonLd = [
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: SITE },
{ '@type': 'ListItem', position: 2, name: 'Agents', item: `${SITE}agents/` },
{ '@type': 'ListItem', position: 2, name: page.breadcrumb, item: `${SITE}agents/` },
],
},
{
@ -100,24 +159,22 @@ const jsonLd = [
---
<Layout title={title} description={description} active="home" jsonLd={jsonLd}>
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/">Open Design</a>
<nav class="breadcrumb" aria-label={common.breadcrumbAria}>
<a href={href('/')}>Open Design</a>
<span>/</span>
<span aria-current="page">Agents</span>
<span aria-current="page">{page.breadcrumb}</span>
</nav>
<article class="info-page">
<header class="catalog-head">
<span class="label">Adapters · Nº 04</span>
<h1 class="display">{allAgents.length} BYOK agents, one skill protocol<span class="dot">.</span></h1>
<p class="lead">
Open Design ships {allAgents.length} first-party adapters out of the box. The same composable skills and portable DESIGN.md systems work with every one. BYOK throughout — your keys, your spend, your data.
</p>
<span class="label">{page.label}</span>
<h1 class="display">{page.heading(allAgents.length)}</h1>
<p class="lead">{page.lead(allAgents.length)}</p>
</header>
<section class="info-section">
<h2>How adapters plug in</h2>
<p>Every adapter is a thin shim between the agent native message format and Open Design skill protocol. Adding a new adapter is a single file — no fork required.</p>
<h2>{page.adaptersTitle}</h2>
<p>{page.adaptersBody}</p>
</section>
{tiers.map((tier) => (
@ -128,8 +185,8 @@ const jsonLd = [
{tier.agents.map((a) => (
<li class="agent-card" id={a.slug}>
<h3>{a.name}</h3>
<p><strong>Vendor:</strong> {a.vendor}</p>
<p><strong>Credential:</strong> {a.key}</p>
<p><strong>{page.vendor}:</strong> {a.vendor}</p>
<p><strong>{page.credential}:</strong> {credentialLabel(a)}</p>
</li>
))}
</ul>
@ -137,38 +194,39 @@ const jsonLd = [
))}
<section class="info-section" id="byok">
<h2>What BYOK means here</h2>
<p>BYOK ("bring your own key") in Open Design keeps credentials and spend on your side:</p>
<h2>{page.byokTitle}</h2>
<p>{page.byokLead}</p>
<ul>
{byokItems.map((item) => (
{page.byokItems.map((item) => (
<li>{item}</li>
))}
</ul>
</section>
<section class="info-section" id="next">
<h2>Next steps</h2>
<h2>{page.nextTitle}</h2>
<ul>
{nextItems.map((n) => (
<li><a class="inline-link" href={n.href}>{n.label}</a> — {n.body}</li>
))}
<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('/alternatives/claude-design/')}>{page.nextItems[3].label}</a> — {page.nextItems[3].body}</li>
</ul>
</section>
<section class="info-cta" aria-label="Open Design call to action">
<div>
<h2>{allAgents.length} adapters. <em>Your agent.</em></h2>
<p>Pick the agent already on your laptop, point Open Design at it, and start rendering.</p>
<h2>{page.ctaTitle(allAgents.length)}</h2>
<p>{page.ctaBody}</p>
</div>
<div class="info-cta-actions">
<a class="btn btn-primary" href={REPO} target="_blank" rel="noreferrer noopener">Star on GitHub</a>
<a class="btn btn-ghost" href={REPO_RELEASES} target="_blank" rel="noreferrer noopener">Download desktop</a>
<a class="btn btn-ghost" href={DISCORD} target="_blank" rel="noreferrer noopener">Request an adapter</a>
<a class="btn btn-primary" href={REPO} target="_blank" rel="noreferrer noopener">{common.starOnGithub}</a>
<a class="btn btn-ghost" href={`${REPO}/releases`} target="_blank" rel="noreferrer noopener">{common.downloadDesktop}</a>
<a class="btn btn-ghost" href="https://discord.gg/9ptkbbqRu" target="_blank" rel="noreferrer noopener">{common.requestAdapter}</a>
</div>
<div class="info-cta-meta">
<span class="stamp">● BYOK</span>
<span>{allAgents.length} adapters · Apache-2.0</span>
<span>macOS · Windows · Linux</span>
<span class="stamp">● {common.byok}</span>
<span>{allAgents.length} adapters · {common.apache}</span>
<span>{common.macWinLinux}</span>
</div>
</section>
</article>

View file

@ -2,8 +2,9 @@
/*
* /alternatives/claude-design/ — primary commercial-intent page.
*
* Highest-leverage SEO surface per growth/seo-opendesigner-analysis.md §8.
* Captures "claude design alternative", "open source claude design", and
* This is the highest-leverage SEO surface per
* `growth/seo-opendesigner-analysis.md` §8. Captures "claude design
* alternative", "open source claude design", and
* "claude design vs open design" queries.
*
* Structure follows the spec: TL;DR, why-people-search, local-first +
@ -11,138 +12,109 @@
* migration / first run, and a visible FAQ that matches FAQPage JSON-LD.
*/
import Layout from '../../../_components/sub-page-layout.astro';
import { getInfoPageCopy } from '../../../info-page-i18n';
import { localeFromPath, localizedHref } from '../../../i18n';
// Derive SITE from Astro.site so preview/staging JSON-LD matches the
// layout's canonical link. Falls back to the production origin.
const SITE = Astro.site?.toString() ?? 'https://open-design.ai/';
const REPO = 'https://github.com/nexu-io/open-design';
const REPO_RELEASES = `${REPO}/releases`;
const locale = localeFromPath(Astro.url.pathname);
const href = (path: string) => localizedHref(path, locale);
const copy = getInfoPageCopy(locale);
const page = copy.claudeAlternative;
const common = copy.common;
const title = 'Open-source Claude Design alternative — Open Design (BYOK, local-first)';
const description =
'Open Design is the open-source, local-first alternative to Claude Design. BYOK with Claude Code, Codex, Cursor, Gemini, OpenCode, or Qwen.';
const title = page.title;
const description = page.description;
const toc = ['Why people search', 'Local-first + BYOK', 'Feature comparison', 'Who should pick which', 'Migration / first run', 'FAQ'];
const features = page.features;
const reasons = [
{ label: 'Data ownership.', body: 'Designs should live as files in a repo, not documents in a vendor DB.' },
{ label: 'BYOK economics.', body: 'Bring your own provider key; API spend bills to your account.' },
{ label: 'Agent choice.', body: 'Drive design from the agent you already use for code.' },
{ label: 'Brand portability.', body: 'One DESIGN.md file encodes a brand for every skill.' },
{ label: 'Self-host / fork.', body: 'Apache-2.0, full source, rebrandable for your studio or company.' },
];
const localByokBody = [
'Open Design runs a desktop app, a local daemon, and Markdown skill/system catalogs on your machine.',
'No design output is forced through a vendor cloud. Credentials stay in local config or environment variables.',
];
const features = [
{ name: 'License', od: 'Apache-2.0, full source on GitHub', cd: 'Closed-source, hosted product' },
{ name: 'Runtime', od: 'Local daemon on your machine', cd: 'Vendor cloud' },
{ name: 'Agent', od: 'BYOK: Claude Code, Codex, Cursor, Gemini, OpenCode, Qwen', cd: 'Vendor-managed agent' },
{ name: 'API spend', od: 'Bills to your account', cd: 'Bundled into vendor subscription' },
{ name: 'Design system', od: 'Portable DESIGN.md in your repo', cd: 'Stored in vendor DB' },
{ name: 'Skills', od: 'Composable SKILL.md you can fork', cd: 'Built-in templates' },
{ name: 'Self-host', od: 'Yes, run anywhere Node 24 runs', cd: 'No' },
{ name: 'Pricing', od: 'Free product; you pay agent API costs', cd: 'Vendor subscription' },
{ name: 'CLI / CI', od: 'Yes via od CLI + HTTP daemon', cd: 'Web UI only' },
{ name: 'Artifact ownership', od: 'Files in your project directory', cd: 'Vendor-hosted documents' },
];
const pickClaude = [
'You want zero local setup and one vendor bill.',
'You are already deep in a Claude-first workflow.',
'Your team prefers a hosted UI over Markdown files.',
];
const pickOpen = [
'You want design artifacts as version-controlled files.',
'You want BYOK with your existing coding agent.',
'You want to fork, rebrand, embed in CLI, or self-host.',
'You want one DESIGN.md per brand that every skill respects.',
];
const migrateSteps = [
'Install Open Design from the quickstart.',
'Open the web UI and point your agent at a Claude Design artifact you like.',
'Ask the agent to extract the brand into a DESIGN.md file.',
'Pick a skill and render it against your new brand.',
];
const faq = [
{ name: 'Is Open Design really a drop-in alternative to Claude Design?', text: 'Not literally, but they overlap on prompt-to-design-artifact use cases.' },
{ name: 'Can I use Claude as my agent in Open Design?', text: 'Yes. Open Design supports Claude Code and Anthropic API BYOK flows.' },
{ name: 'What happens to my Claude Design designs?', text: 'You can keep using Claude Design alongside Open Design; migration is manual today.' },
{ name: 'Does Open Design generate the same artifact types?', text: 'Yes for common types: landing pages, decks, dashboards, social posts, brand systems, and prototypes.' },
{ name: 'Why "open-source Claude Design" vs "open-source AI design tool"?', text: 'That is how many users describe the product shape they are searching for.' },
{ name: 'Who builds and maintains Open Design?', text: 'The project lives at github.com/nexu-io/open-design and is Apache-2.0.' },
];
const faq = page.faq.map(({ name, text }) => ({ q: name, a: text }));
const jsonLd = [
{
// Two-level breadcrumb only. There is no real `/alternatives/`
// index page today, so we don't fabricate a middle node — both
// the JSON-LD and the visible nav point straight from the home
// surface to this leaf. Mirror this if/when `/alternatives/`
// becomes a real index page.
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: SITE },
{ '@type': 'ListItem', position: 2, name: 'Open-source Claude Design alternative', item: `${SITE}alternatives/claude-design/` },
{ '@type': 'ListItem', position: 2, name: page.breadcrumb, item: `${SITE}alternatives/claude-design/` },
],
},
{
'@context': 'https://schema.org',
'@type': 'WebPage',
name: title,
description,
url: `${SITE}alternatives/claude-design/`,
isPartOf: { '@id': `${SITE}#website` },
},
{
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faq.map((q) => ({
mainEntity: faq.map(({ q, a }) => ({
'@type': 'Question',
name: q.name,
acceptedAnswer: { '@type': 'Answer', text: q.text },
name: q,
acceptedAnswer: { '@type': 'Answer', text: a },
})),
},
];
---
<Layout title={title} description={description} active="home" jsonLd={jsonLd}>
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/">Open Design</a>
<nav class="breadcrumb" aria-label={common.breadcrumbAria}>
<a href={href('/')}>Open Design</a>
<span>/</span>
<span aria-current="page">Open-source Claude Design alternative</span>
<span aria-current="page">{page.breadcrumb}</span>
</nav>
<article class="info-page">
<header class="catalog-head">
<span class="label">Alternative · Nº 03</span>
<h1 class="display">Open-source <em>Claude Design</em> alternative<span class="dot">.</span></h1>
<p class="lead">
Open Design is the official open-source, local-first alternative to Claude Design. BYOK with the agent you already use, keep your brand as a portable DESIGN.md file, and ship artifacts as files in your project.
</p>
<span class="label">{page.label}</span>
<h1 class="display">{page.heading}</h1>
<p class="lead">{page.lead}</p>
</header>
<aside class="tldr-card">
<h3>TL;DR</h3>
<p>Same use case, different posture: local-first, BYOK, open source (Apache-2.0), with portable DESIGN.md systems and composable SKILL.md skills.</p>
</aside>
<div class="tldr-card">
<h3>{page.tldrTitle}</h3>
<p>{page.tldrBody}</p>
</div>
<nav class="info-toc" aria-label="On this page">
<span>On this page:</span>
{toc.map((label, idx) => (
<a href={`#section-${idx}`}>{label}</a>
))}
<nav class="info-toc" aria-label={common.onThisPage}>
<span>{common.onThisPage}</span>
<a href="#why-search">{page.toc[0]}</a>
<a href="#local-byok">{page.toc[1]}</a>
<a href="#compare">{page.toc[2]}</a>
<a href="#who">{page.toc[3]}</a>
<a href="#migrate">{page.toc[4]}</a>
<a href="#faq">{page.toc[5]}</a>
</nav>
<section class="info-section" id="section-0">
<h2>Why people search for a Claude Design alternative</h2>
<p>Five reasons keep showing up in support threads, GitHub discussions, and Discord:</p>
<ul>
{reasons.map((r) => (
<li><strong>{r.label}</strong> {r.body}</li>
<section class="info-section" id="why-search">
<h2>{page.whyTitle}</h2>
<p>{page.whyLead}</p>
<ol>
{page.reasons.map((item) => (
<li><strong>{item.label}</strong> {item.body}</li>
))}
</ul>
</ol>
</section>
<section class="info-section" id="section-1">
<h2>Local-first + BYOK, explained</h2>
{localByokBody.map((para) => <p>{para}</p>)}
<section class="info-section" id="local-byok">
<h2>{page.localByokTitle}</h2>
{page.localByokBody.map((paragraph) => (
<p>{paragraph}</p>
))}
</section>
<section class="info-section" id="section-2">
<h2>Feature comparison</h2>
<section class="info-section" id="compare">
<h2>{page.featureTitle}</h2>
<div class="compare-table-wrap">
<table class="compare-table">
<thead>
@ -156,8 +128,8 @@ const jsonLd = [
{features.map((f) => (
<tr>
<th scope="row">{f.name}</th>
<td class="yes">{f.od}</td>
<td class="no">{f.cd}</td>
<td class="yes"><span class="check">●</span>{f.od}</td>
<td>{f.cd}</td>
</tr>
))}
</tbody>
@ -165,45 +137,65 @@ const jsonLd = [
</div>
</section>
<section class="info-section" id="section-3">
<h2>Who should pick which</h2>
<h3>Pick Claude Design if</h3>
<ul>{pickClaude.map((it) => <li>{it}</li>)}</ul>
<h3>Pick Open Design if</h3>
<ul>{pickOpen.map((it) => <li>{it}</li>)}</ul>
<section class="info-section" id="who">
<h2>{page.whoTitle}</h2>
<h3>{page.pickClaudeTitle}</h3>
<ul>
{page.pickClaude.map((item) => (
<li>{item}</li>
))}
</ul>
<h3>{page.pickOpenTitle}</h3>
<ul>
{page.pickOpen.map((item) => (
<li>{item}</li>
))}
</ul>
</section>
<section class="info-section" id="section-4">
<h2>Migration / first run</h2>
<p>There is no automatic import from Claude Design today; use a one-time brand-extraction run:</p>
<ol>{migrateSteps.map((it) => <li>{it}</li>)}</ol>
<p>From then on, every skill renders in your brand without re-prompting.</p>
<section class="info-section" id="migrate">
<h2>{page.migrateTitle}</h2>
<p>{page.migrateLead}</p>
<ol>
{page.migrateSteps.map((item) => (
<li>{item}</li>
))}
</ol>
<p>{page.migrateClosing}</p>
</section>
<section class="info-section" id="section-5">
<h2>FAQ</h2>
{faq.map((q) => (
<>
<h3>{q.name}</h3>
<p>{q.text}</p>
</>
))}
<section class="info-section" id="faq">
<h2>{page.faqTitle}</h2>
<ol class="faq-list">
{faq.map(({ q, a }, idx) => (
<li class="faq-item">
<details>
<summary>
<span class="faq-index">{String(idx + 1).padStart(2, '0')}</span>
<span class="faq-q">{q}</span>
<span class="faq-toggle" aria-hidden="true">+</span>
</summary>
<p class="faq-a">{a}</p>
</details>
</li>
))}
</ol>
</section>
<section class="info-cta" aria-label="Open Design call to action">
<div>
<h2>Switch in <em>three commands</em>.</h2>
<p>Star the repo, grab the desktop build, or run the install in your terminal. Your DESIGN.md system stays in your repo from the first render onward.</p>
<h2>{page.ctaTitle}</h2>
<p>{page.ctaBody}</p>
</div>
<div class="info-cta-actions">
<a class="btn btn-primary" href={REPO} target="_blank" rel="noreferrer noopener">Star on GitHub</a>
<a class="btn btn-ghost" href={REPO_RELEASES} target="_blank" rel="noreferrer noopener">Download desktop</a>
<a class="btn btn-ghost" href="/quickstart/">Quickstart</a>
<a class="btn btn-primary" href={REPO} target="_blank" rel="noreferrer noopener">{common.starOnGithub}</a>
<a class="btn btn-ghost" href={href('/quickstart/')}>{common.quickstart}</a>
<a class="btn btn-ghost" href={REPO_RELEASES} target="_blank" rel="noreferrer noopener">{common.downloadDesktop}</a>
</div>
<div class="info-cta-meta">
<span class="stamp">● Local-first</span>
<span>BYOK · Apache-2.0</span>
<span>macOS · Windows · Linux</span>
<span class="stamp">● {common.apache}</span>
<span>{common.localFirst} · {common.byok}</span>
<span>{common.macWinLinux}</span>
</div>
</section>
</article>

View file

@ -13,11 +13,22 @@ import FontStylesheet from '../../_components/font-stylesheet.astro';
import GoogleAnalytics from '../../_components/google-analytics.astro';
import HeaderEnhancer from '../../_components/header-enhancer.astro';
import { Header, type HeaderProps } from '../../_components/header';
import LocaleSwitcherScript from '../../_components/locale-switcher-script.astro';
import SeoHead from '../../_components/seo-head.astro';
import SiteFooter from '../../_components/site-footer.astro';
import Topbar from '../../_components/topbar.astro';
import { getCatalogCounts } from '../../_lib/catalog';
import { getGithubRepoMeta } from '../../_lib/github';
import {
explicitLocalizedString,
localizeBlogPostText,
} from '../../content-i18n';
import {
getLandingUiCopy,
getLocaleDefinition,
localeFromPath,
localizedHref,
} from '../../i18n';
import '../../globals.css';
import '../../sub-pages.css';
@ -31,47 +42,89 @@ export async function getStaticPaths() {
const { post } = Astro.props;
const { Content } = await render(post);
const locale = localeFromPath(Astro.url.pathname);
const localeDef = getLocaleDefinition(locale);
const ui = getLandingUiCopy(locale);
const href = (path: string) => localizedHref(path, locale);
const localizedPostI18n = (post.data as typeof post.data & {
i18n?: Record<string, Partial<Record<'title' | 'summary' | 'category' | 'bodyHtml', string>>>;
}).i18n?.[locale];
const fallbackPost = localizeBlogPostText({
id: post.id,
title: post.data.title,
summary: post.data.summary,
category: post.data.category,
locale,
});
const localizedPostField = (field: 'title' | 'summary' | 'category') => {
return explicitLocalizedString(
localizedPostI18n?.[field] as Parameters<typeof explicitLocalizedString>[0],
locale,
) ?? fallbackPost[field];
};
const localizedBodyHtml = explicitLocalizedString(
localizedPostI18n?.bodyHtml as Parameters<typeof explicitLocalizedString>[0],
locale,
) ?? fallbackPost.bodyHtml;
const DISCORD = 'https://discord.gg/9ptkbbqRu';
const sourceUrl = `https://github.com/nexu-io/open-design/blob/main/apps/landing-page/app/content/blog/${post.id}.md`;
const category = post.data.category.toLowerCase();
const bottomCta =
category.includes('announcement') || category.includes('product')
? {
title: 'Download the desktop build',
body: 'Take the open-source design workspace for a spin, inspect the release notes on GitHub, or join our Discord for live community feedback.',
title: ui.blog.cta.downloadTitle,
body: ui.blog.cta.downloadBody,
href: 'https://github.com/nexu-io/open-design/releases',
label: 'Download desktop ↗',
label: ui.blog.cta.downloadLabel,
external: true,
}
: category.includes('guide') || category.includes('use') || category.includes('case')
? {
title: 'Run the skill workflow locally',
body: 'Browse the workflow library, pick a starting point, and wire it into the agent you already use. Join our Discord if you want help adapting it.',
title: ui.blog.cta.skillsTitle,
body: ui.blog.cta.skillsBody,
href: '/skills/',
label: 'Browse workflows ↗',
label: ui.blog.cta.skillsLabel,
external: false,
}
: {
title: 'See the implementation on GitHub',
body: 'Open the repository to inspect the source, star the project, fork the workflow, or join our Discord to discuss what should land next.',
title: ui.blog.cta.repoTitle,
body: ui.blog.cta.repoBody,
href: 'https://github.com/nexu-io/open-design',
label: 'Open repository ↗',
label: ui.blog.cta.repoLabel,
external: true,
};
const counts = await getCatalogCounts();
const github = await getGithubRepoMeta();
const origin = Astro.site?.toString() ?? 'https://open-design.ai/';
const postUrl = new URL(Astro.url.pathname, Astro.site ?? 'https://open-design.ai/').toString();
// BreadcrumbList for the blog post. SeoHead already emits the Article
// JSON-LD via `kind='article'`; the breadcrumb here adds the trail
// (Open Design → Blog → <post title>) so Google can render the post
// with breadcrumb-style URLs in the SERP.
const breadcrumbJsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: origin },
{ '@type': 'ListItem', position: 2, name: 'Blog', item: new URL('/blog/', origin).toString() },
{ '@type': 'ListItem', position: 3, name: localizedPostField('title'), item: postUrl },
],
};
const headerHtml = renderToStaticMarkup(
createElement<HeaderProps>(Header, {
counts,
github,
brandHref: '/',
active: 'blog',
locale,
}),
);
const fmtDate = (d: Date) =>
d.toLocaleDateString('en-US', {
d.toLocaleDateString(localeDef.htmlLang, {
year: 'numeric',
month: 'long',
day: 'numeric',
@ -79,64 +132,71 @@ const fmtDate = (d: Date) =>
---
<!doctype html>
<html lang='en'>
<html lang={localeDef.htmlLang} dir={localeDef.dir}>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<SeoHead
kind='article'
title={post.data.title}
description={post.data.summary}
title={localizedPostField('title')}
description={localizedPostField('summary')}
pathname={Astro.url.pathname}
datePublished={post.data.date}
category={post.data.category}
/>
<script is:inline type='application/ld+json' set:html={JSON.stringify(breadcrumbJsonLd)} />
<FontStylesheet />
<GoogleAnalytics />
</head>
<body>
<div class='shell'>
<Topbar github={github} />
<Fragment set:html={headerHtml} />
<div class='site-chrome' data-chrome-headroom>
<Topbar github={github} locale={locale} />
<Fragment set:html={headerHtml} />
</div>
<main class='post-shell'>
<article class='post'>
<div class='container'>
<a class='post-back' href='/blog/'>← Back to Blog</a>
<a class='post-back' href={href('/blog/')}>{ui.blog.backToBlog}</a>
<h1 class='post-title'>{post.data.title}</h1>
<h1 class='post-title'>{localizedPostField('title')}</h1>
<p class='post-summary'>{post.data.summary}</p>
<p class='post-summary'>{localizedPostField('summary')}</p>
<div class='post-meta'>
<span>{fmtDate(post.data.date)}</span>
<span>{post.data.readingTime} min read</span>
<span>{post.data.category}</span>
<span>{post.data.readingTime} {ui.blog.minRead}</span>
<span>{localizedPostField('category')}</span>
</div>
<hr class='post-rule' />
<div class='post-body'>
<Content />
{localizedBodyHtml ? (
<Fragment set:html={localizedBodyHtml} />
) : (
<Content />
)}
</div>
<aside class='post-conversion' aria-labelledby='post-conversion-title'>
<div>
<span class='label'>Next step</span>
<span class='label'>{ui.blog.nextStep}</span>
<h2 id='post-conversion-title'>{bottomCta.title}</h2>
<p>{bottomCta.body}</p>
</div>
<div class='post-conversion-actions'>
<a
class='btn btn-primary'
href={bottomCta.href}
href={bottomCta.external === false ? href(bottomCta.href) : bottomCta.href}
target={bottomCta.external === false ? undefined : '_blank'}
rel={bottomCta.external === false ? undefined : 'noreferrer noopener'}
>
{bottomCta.label}
</a>
<a class='btn btn-ghost' href={DISCORD} target='_blank' rel='noreferrer noopener'>
Join Discord
{ui.blog.joinDiscord}
</a>
</div>
</aside>
@ -144,23 +204,24 @@ const fmtDate = (d: Date) =>
<hr class='post-rule' />
<div class='post-foot'>
<a class='post-back' href='/blog/'>← Back to Blog</a>
<a class='post-back' href={href('/blog/')}>{ui.blog.backToBlog}</a>
<a
class='post-github'
href={sourceUrl}
target='_blank'
rel='noreferrer noopener'
>
View source on GitHub ↗
{ui.blog.viewSource}
</a>
</div>
</div>
</article>
</main>
<SiteFooter counts={counts} />
<SiteFooter counts={counts} locale={locale} />
</div>
<HeaderEnhancer />
<LocaleSwitcherScript />
<style>
.post-shell {

View file

@ -18,15 +18,31 @@ import GoogleAnalytics from '../../_components/google-analytics.astro';
import HeaderEnhancer from '../../_components/header-enhancer.astro';
import { Header, type HeaderProps } from '../../_components/header';
import LazyImg from '../../_components/lazy-img.astro';
import LocaleSwitcherScript from '../../_components/locale-switcher-script.astro';
import PreciseLazyload from '../../_components/precise-lazyload.astro';
import SeoHead from '../../_components/seo-head.astro';
import SiteFooter from '../../_components/site-footer.astro';
import Topbar from '../../_components/topbar.astro';
import { getCatalogCounts } from '../../_lib/catalog';
import { getGithubRepoMeta } from '../../_lib/github';
import {
explicitLocalizedString,
localizeBlogPostText,
} from '../../content-i18n';
import {
getLandingUiCopy,
getLocaleDefinition,
localeFromPath,
localizedHref,
} from '../../i18n';
import '../../globals.css';
import '../../sub-pages.css';
const locale = localeFromPath(Astro.url.pathname);
const localeDef = getLocaleDefinition(locale);
const ui = getLandingUiCopy(locale);
const href = (path: string) => localizedHref(path, locale);
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
);
@ -42,21 +58,21 @@ const headerHtml = renderToStaticMarkup(
github,
brandHref: '/',
active: 'blog',
locale,
}),
);
const seoTitle = 'Blog — Open Design';
const seoDescription =
'Notes to help you understand, explore, and build with Open Design — across product, guides, use cases, and community.';
const seoTitle = ui.blog.seoTitle;
const seoDescription = ui.blog.description;
const categoryChips = [
{ label: 'All', value: 'all' },
{ label: 'Product', value: 'product' },
{ label: 'Guides', value: 'guides' },
{ label: 'Use cases', value: 'use-cases' },
{ label: 'Community', value: 'community' },
{ label: ui.blog.categories.all, value: 'all' },
{ label: ui.blog.categories.product, value: 'product' },
{ label: ui.blog.categories.guides, value: 'guides' },
{ label: ui.blog.categories.useCases, value: 'use-cases' },
{ label: ui.blog.categories.community, value: 'community' },
];
const fmtDate = (d: Date) =>
d.toLocaleDateString('en-US', {
d.toLocaleDateString(localeDef.htmlLang, {
year: 'numeric',
month: 'long',
day: 'numeric',
@ -93,11 +109,30 @@ const postImages: Record<string, { src: string; alt: string }> = {
},
};
const getPostImage = (id: string) => postImages[id];
const localizedPostField = (
post: (typeof posts)[number],
field: 'title' | 'summary' | 'category',
) => {
const i18n = (post.data as typeof post.data & {
i18n?: Record<string, Partial<Record<typeof field, string>>>;
}).i18n;
const fallbackPost = localizeBlogPostText({
id: post.id,
title: post.data.title,
summary: post.data.summary,
category: post.data.category,
locale,
});
return explicitLocalizedString(
i18n?.[locale]?.[field] as Parameters<typeof explicitLocalizedString>[0],
locale,
) ?? fallbackPost[field];
};
---
<!doctype html>
<html lang='en'>
<html lang={localeDef.htmlLang} dir={localeDef.dir}>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
@ -112,16 +147,17 @@ const getPostImage = (id: string) => postImages[id];
</head>
<body>
<div class='shell'>
<Topbar github={github} />
<Fragment set:html={headerHtml} />
<div class='site-chrome' data-chrome-headroom>
<Topbar github={github} locale={locale} />
<Fragment set:html={headerHtml} />
</div>
<main class='blog-shell'>
<section class='blog-masthead'>
<div class='container'>
<h1 class='blog-title'>Blog</h1>
<h1 class='blog-title'>{ui.blog.title}</h1>
<p class='blog-sub'>
Notes to help you understand, explore, and build with Open Design
— across product, guides, use cases, and community.
{ui.blog.description}
</p>
</div>
</section>
@ -136,7 +172,7 @@ const getPostImage = (id: string) => postImages[id];
<div class='container'>
<a
class='blog-feature-card'
href={`/blog/${featuredPost.id}/`}
href={href(`/blog/${featuredPost.id}/`)}
style={`--accent-index: ${posts.findIndex((post) => post.id === featuredPost.id) + 1};`}
>
<span
@ -150,22 +186,22 @@ const getPostImage = (id: string) => postImages[id];
/>
) : (
<>
<span class='media-kicker'>{featuredPost.data.category}</span>
<span class='media-title'>{featuredPost.data.title}</span>
<span class='media-kicker'>{localizedPostField(featuredPost, 'category')}</span>
<span class='media-title'>{localizedPostField(featuredPost, 'title')}</span>
<span class='media-grid' />
<span class='media-orbit' />
</>
)}
</span>
<span class='blog-feature-copy'>
<span class='blog-card-cat'>{featuredPost.data.category}</span>
<h2 class='blog-feature-title'>{featuredPost.data.title}</h2>
<p class='blog-feature-summary'>{featuredPost.data.summary}</p>
<span class='blog-card-cat'>{localizedPostField(featuredPost, 'category')}</span>
<h2 class='blog-feature-title'>{localizedPostField(featuredPost, 'title')}</h2>
<p class='blog-feature-summary'>{localizedPostField(featuredPost, 'summary')}</p>
<span class='blog-feature-meta'>
<span>{fmtDate(featuredPost.data.date)}</span>
<span>·</span>
<span>{featuredPost.data.readingTime} min read</span>
<span class='blog-feature-cta'>Read more →</span>
<span>{featuredPost.data.readingTime} {ui.blog.minRead}</span>
<span class='blog-feature-cta'>{ui.blog.readMore}</span>
</span>
</span>
</a>
@ -176,11 +212,11 @@ const getPostImage = (id: string) => postImages[id];
<section class='blog-categories'>
<div class='container'>
<div class='blog-category-strip' aria-label='Blog categories'>
<div class='blog-category-strip' aria-label={ui.blog.categoriesLabel}>
{categoryChips.map((category, index) => (
<a
class:list={['blog-chip', index === 0 && 'is-active']}
href={category.value === 'all' ? '/blog/' : `/blog/?category=${category.value}`}
href={category.value === 'all' ? href('/blog/') : href(`/blog/?category=${category.value}`)}
data-category-filter={category.value}
>
{category.label}
@ -194,7 +230,7 @@ const getPostImage = (id: string) => postImages[id];
<div class='container'>
{
posts.length === 0 ? (
<p class='blog-empty'>No entries yet. Check back soon.</p>
<p class='blog-empty'>{ui.blog.noEntries}</p>
) : (
<ol class='blog-grid'>
{cardPosts.map((post, idx) => (
@ -205,7 +241,7 @@ const getPostImage = (id: string) => postImages[id];
>
<a
class='blog-card-link'
href={`/blog/${post.id}/`}
href={href(`/blog/${post.id}/`)}
style={`--accent-index: ${idx + 2};`}
>
<span class={`media-panel ${getPostImage(post.id) ? 'has-image' : ''}`}>
@ -217,21 +253,21 @@ const getPostImage = (id: string) => postImages[id];
/>
) : (
<>
<span class='media-kicker'>{post.data.category}</span>
<span class='media-title'>{post.data.title}</span>
<span class='media-kicker'>{localizedPostField(post, 'category')}</span>
<span class='media-title'>{localizedPostField(post, 'title')}</span>
<span class='media-grid' />
<span class='media-orbit' />
</>
)}
</span>
<span class='blog-card-body'>
<span class='blog-card-cat'>{post.data.category}</span>
<h2 class='blog-card-title'>{post.data.title}</h2>
<p class='blog-card-summary'>{post.data.summary}</p>
<span class='blog-card-cat'>{localizedPostField(post, 'category')}</span>
<h2 class='blog-card-title'>{localizedPostField(post, 'title')}</h2>
<p class='blog-card-summary'>{localizedPostField(post, 'summary')}</p>
<span class='blog-card-foot'>
<span class='blog-card-date'>{fmtDate(post.data.date)}</span>
<span class='blog-card-rt'>{post.data.readingTime} min read</span>
<span class='blog-card-cta'>Read →</span>
<span class='blog-card-rt'>{post.data.readingTime} {ui.blog.minRead}</span>
<span class='blog-card-cta'>{ui.blog.read}</span>
</span>
</span>
</a>
@ -241,13 +277,13 @@ const getPostImage = (id: string) => postImages[id];
)
}
<p class='blog-empty is-filter-empty' data-blog-empty hidden>
No posts in this category yet. More field notes are coming.
{ui.blog.noPostsInCategory}
</p>
</div>
</section>
</main>
<SiteFooter counts={counts} />
<SiteFooter counts={counts} locale={locale} />
</div>
<script is:inline>
@ -278,6 +314,7 @@ const getPostImage = (id: string) => postImages[id];
})();
</script>
<HeaderEnhancer />
<LocaleSwitcherScript />
<PreciseLazyload />
<style>

View file

@ -7,70 +7,38 @@
* primary Claude Design alternative page; others will follow as
* `/alternatives/<slug>/`).
*
* Honest-limitations block at the bottom is required by §8 of the SEO
* plan — comparison pages without a limitation section read as
* Honest-limitations block at the bottom is required by §8 of the
* SEO plan — comparison pages without a limitation section read as
* adversarial and tank trust.
*/
import Layout from '../../_components/sub-page-layout.astro';
import { getInfoPageCopy } from '../../info-page-i18n';
import { localeFromPath, localizedHref } from '../../i18n';
// Derive SITE from Astro.site so preview/staging deploys produce JSON-LD
// that matches the layout's `canonical` link. Falls back to the
// production origin so a missing `site` config never emits a broken URL.
const SITE = Astro.site?.toString() ?? 'https://open-design.ai/';
const locale = localeFromPath(Astro.url.pathname);
const href = (path: string) => localizedHref(path, locale);
const copy = getInfoPageCopy(locale);
const page = copy.compare;
const common = copy.common;
const title = 'Open Design vs Claude Design, Figma Make, v0, Lovable — honest comparison';
const description =
'Compare Open Design to the major AI design tools. Hosted vs local-first, BYOK vs vendor-locked, single-shot generation vs portable DESIGN.md systems.';
const title = page.title;
const description = page.description;
const toc = ['vs Claude Design', 'vs Figma Make', 'vs v0', 'vs Lovable / Bolt', 'vs Open CoDesign', 'Honest limits'];
// Honest-limits FAQ — visible at the bottom of the page AND emitted as
// FAQPage JSON-LD. Google requires schema entries to mirror visible
// content; converting our "what we don't do" prose into question form
// gives us a shot at the People Also Ask rich result on comparison
// queries.
const limitsFaq = page.limitsFaq.map(({ name, text }) => ({ q: name, a: text }));
const comparisons = [
{
competitor: 'Claude Design',
summary:
'Hosted product tied to a single vendor. Open Design is local-first, BYOK, and Apache-2.0 — your skills and DESIGN.md live in your repo.',
href: '/alternatives/claude-design/',
cta: 'Read the full comparison →',
external: false,
},
{
competitor: 'Figma Make',
summary:
'Figma Make focuses on prompt-to-mockup inside Figma. Open Design ships portable artifacts directly into your project.',
href: 'https://github.com/nexu-io/open-design',
cta: 'See the repo for migration notes →',
external: true,
},
{
competitor: 'v0 by Vercel',
summary:
'v0 generates React components on a hosted runtime. Open Design generates decks, dashboards, landing pages, and brand systems locally.',
href: 'https://github.com/nexu-io/open-design',
cta: 'See the repo for migration notes →',
external: true,
},
{
competitor: 'Lovable / Bolt',
summary:
'Lovable and Bolt focus on hosted prompt-to-app. Open Design is the design-skill layer for an agent you already use.',
href: 'https://github.com/nexu-io/open-design',
cta: 'See the repo for migration notes →',
external: true,
},
{
competitor: 'Open CoDesign',
summary:
'Open CoDesign is a sibling open-source project. Open Design can wrap codesign-style workflows through its skill protocol.',
href: 'https://github.com/nexu-io/open-design',
cta: 'See the repo for migration notes →',
external: true,
},
];
const limitsFaq = [
{ name: 'Does Open Design offer a hosted web sandbox?', text: 'No. Open Design is local-first by design.' },
{ name: 'Can I use Open Design without installing anything?', text: 'Not today. The minimum is a local daemon plus a coding agent.' },
{ name: 'Is Open Design a v0 / Lovable / Bolt replacement?', text: 'It depends. Open Design focuses on prompt-to-design-artifact via a skill protocol you can fork.' },
{ name: 'Does Open Design send my data to Anthropic, OpenAI, or Google?', text: 'Only your prompt and skill context goes to the provider whose key you brought.' },
{ name: 'Can I self-host Open Design on my own infrastructure?', text: 'Yes. Apache-2.0 license, Node 24 daemon, no required SaaS.' },
];
const comparisons = page.comparisons.map((comparison, idx) => ({
...comparison,
href: idx === 0 ? href('/alternatives/claude-design/') : 'https://github.com/nexu-io/open-design',
}));
const jsonLd = [
{
@ -78,67 +46,102 @@ const jsonLd = [
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: SITE },
{ '@type': 'ListItem', position: 2, name: 'Compare', item: `${SITE}compare/` },
{ '@type': 'ListItem', position: 2, name: page.breadcrumb, item: `${SITE}compare/` },
],
},
{
'@context': 'https://schema.org',
'@type': 'WebPage',
name: title,
description,
url: `${SITE}compare/`,
isPartOf: { '@id': `${SITE}#website` },
},
{
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: limitsFaq.map((q) => ({
'@id': `${SITE}compare/#limits-faq`,
mainEntity: limitsFaq.map(({ q, a }) => ({
'@type': 'Question',
name: q.name,
acceptedAnswer: { '@type': 'Answer', text: q.text },
name: q,
acceptedAnswer: { '@type': 'Answer', text: a },
})),
},
];
---
<Layout title={title} description={description} active="home" jsonLd={jsonLd}>
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/">Open Design</a>
<nav class="breadcrumb" aria-label={common.breadcrumbAria}>
<a href={href('/')}>Open Design</a>
<span>/</span>
<span aria-current="page">Compare</span>
<span aria-current="page">{page.breadcrumb}</span>
</nav>
<article class="info-page">
<header class="catalog-head">
<span class="label">Evaluation · Nº 02</span>
<h1 class="display">Open Design vs <em>everything else</em><span class="dot">.</span></h1>
<p class="lead">
Short, honest summaries of how Open Design relates to the other AI design tools you might be evaluating.
</p>
<span class="label">{page.label}</span>
<h1 class="display">{page.heading}</h1>
<p class="lead">{page.lead}</p>
</header>
<nav class="info-toc" aria-label="On this page">
<span>On this page:</span>
{toc.map((label, idx) => (
<a href={`#section-${idx}`}>{label}</a>
))}
<nav class="info-toc" aria-label={common.onThisPage}>
<span>{common.onThisPage}</span>
<a href="#claude-design">{page.toc[0]}</a>
<a href="#figma-make">{page.toc[1]}</a>
<a href="#v0">{page.toc[2]}</a>
<a href="#lovable-bolt">{page.toc[3]}</a>
<a href="#open-codesign">{page.toc[4]}</a>
<a href="#limits">{page.toc[5]}</a>
</nav>
<section class="info-section">
<ul class="compare-grid">
{comparisons.map((c, idx) => (
<li class="compare-card" id={`section-${idx}`}>
<h3>vs {c.competitor}</h3>
<p>{c.summary}</p>
<a href={c.href} target={c.external ? '_blank' : undefined} rel={c.external ? 'noreferrer noopener' : undefined}>
{c.cta}
</a>
</li>
))}
{comparisons.map((c) => {
const id =
c.competitor === 'Claude Design'
? 'claude-design'
: c.competitor === 'Figma Make'
? 'figma-make'
: c.competitor === 'v0 by Vercel'
? 'v0'
: c.competitor === 'Lovable / Bolt'
? 'lovable-bolt'
: 'open-codesign';
const isInternal = c.href.startsWith('/');
return (
<li class="compare-card" id={id}>
<h3>Open Design vs <em>{c.competitor}</em></h3>
<p>{c.summary}</p>
<a
href={c.href}
target={isInternal ? undefined : '_blank'}
rel={isInternal ? undefined : 'noreferrer noopener'}
>
{c.cta}
</a>
</li>
);
})}
</ul>
</section>
<section class="info-section" id={`section-${toc.length - 1}`}>
<h2>Honest limits — what Open Design isn't</h2>
<p>Open Design is not trying to be every hosted AI design tool. These questions describe the trade-offs instead of glossing them.</p>
{limitsFaq.map((q) => (
<>
<h3>{q.name}</h3>
<p>{q.text}</p>
</>
))}
<section class="info-section" id="limits">
<h2>{page.limitsTitle}</h2>
<p>{page.limitsBody}</p>
<ol class="faq-list">
{limitsFaq.map(({ q, a }, idx) => (
<li class="faq-item">
<details>
<summary>
<span class="faq-index">{String(idx + 1).padStart(2, '0')}</span>
<span class="faq-q">{q}</span>
<span class="faq-toggle" aria-hidden="true">+</span>
</summary>
<p class="faq-a">{a}</p>
</details>
</li>
))}
</ol>
</section>
</article>
</Layout>

View file

@ -1,6 +1,7 @@
---
import Layout from '../../_components/sub-page-layout.astro';
import { getCraftRecords, type CraftRecord } from '../../_lib/catalog';
import { getLandingUiCopy, localeFromPath, localizedHref } from '../../i18n';
export async function getStaticPaths() {
const records = await getCraftRecords();
@ -15,52 +16,90 @@ interface Props {
all: ReadonlyArray<CraftRecord>;
}
const { craft, all } = Astro.props as Props;
const { craft: routeCraft, 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 getCraftRecords(locale);
const craft = all.find((item) => item.slug === routeCraft.slug) ?? routeCraft;
const title = `${craft.name} — Open Design craft principle`;
const description = craft.summary || `Open Design craft rule: ${craft.name}.`;
const title = ui.catalog.craft.detailTitle(craft.name);
const description = craft.summary || ui.catalog.craft.detailFallbackDescription(craft.name);
const related = all.filter((c) => c.slug !== craft.slug).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: 'Craft', item: new URL('/craft/', Astro.site).toString() },
{ '@type': 'ListItem', position: 3, name: craft.name, item: new URL(`/craft/${craft.slug}/`, Astro.site).toString() },
],
};
const origin = Astro.site?.toString() ?? 'https://open-design.ai/';
const craftUrl = new URL(`/craft/${craft.slug}/`, origin).toString();
const jsonLd = [
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: origin },
{ '@type': 'ListItem', position: 2, name: ui.catalog.craft.detailLabel, item: new URL('/craft/', origin).toString() },
{ '@type': 'ListItem', position: 3, name: craft.name, item: craftUrl },
],
},
{
// Article schema for the craft principle. Craft entries are
// editorial guidance (a11y, motion, typography, etc.) — treating
// them as Article gives them the same rich-result eligibility as
// blog posts. The summary is the description; the body lives
// upstream as `craft.source` on GitHub.
'@context': 'https://schema.org',
'@type': 'Article',
headline: craft.name,
description,
mainEntityOfPage: { '@type': 'WebPage', '@id': craftUrl },
articleSection: 'Craft',
inLanguage: locale,
author: {
'@type': 'Organization',
name: 'Open Design',
url: origin,
},
publisher: {
'@type': 'Organization',
name: 'Open Design',
url: origin,
logo: {
'@type': 'ImageObject',
url: new URL('/android-chrome-512x512.png', origin).toString(),
},
},
},
];
---
<Layout title={title} description={description} active="craft" jsonLd={jsonLd}>
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/">Open Design</a>
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
<a href={href('/')}>Open Design</a>
<span>/</span>
<a href="/craft/">Craft</a>
<a href={href('/craft/')}>{ui.catalog.craft.detailLabel}</a>
<span>/</span>
<span aria-current="page">{craft.name}</span>
</nav>
<article class="detail">
<header class="detail-head">
<span class="label">Craft principle</span>
<span class="label">{ui.catalog.craft.detailLabel}</span>
<h1 class="display">{craft.name}<span class="dot">.</span></h1>
<p class="lead">{craft.summary}</p>
<div class="detail-actions">
<a class="btn btn-primary" href={craft.source} target="_blank" rel="noopener">
Read the full rule on GitHub
{ui.catalog.craft.readFullRule}
</a>
</div>
</header>
{related.length > 0 && (
<section class="detail-block">
<h2>Other craft principles</h2>
<h2>{ui.catalog.craft.related}</h2>
<ul class="related-grid">
{related.map((r) => (
<li>
<a href={`/craft/${r.slug}/`}>
<a href={href(`/craft/${r.slug}/`)}>
<span class="related-name">{r.name}</span>
<span class="related-desc">{r.summary}</span>
</a>

View file

@ -1,17 +1,20 @@
---
import Layout from '../../_components/sub-page-layout.astro';
import { getCraftRecords } from '../../_lib/catalog';
import { getLandingUiCopy, localeFromPath, localizedHref } from '../../i18n';
const craft = await getCraftRecords();
const locale = localeFromPath(Astro.url.pathname);
const ui = getLandingUiCopy(locale);
const href = (path: string) => localizedHref(path, locale);
const craft = await getCraftRecords(locale);
const title = `Craft — ${craft.length} brand-agnostic rendering principles | Open Design`;
const description =
'Universal craft rules every Open Design skill can opt into: accessibility, animation discipline, color, form validation, laws of UX, RTL/Bidi, state coverage, and typography hierarchy.';
const title = ui.catalog.craft.title(craft.length);
const description = ui.catalog.craft.description;
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Open Design Craft principles',
name: title,
description,
url: new URL('/craft/', Astro.site).toString(),
numberOfItems: craft.length,
@ -20,22 +23,20 @@ const jsonLd = {
<Layout title={title} description={description} active="craft" jsonLd={jsonLd}>
<header class="catalog-head">
<span class="label">Catalog · Nº 03</span>
<span class="label">{ui.catalog.craft.label}</span>
<h1 class="display">
<em>Craft</em> — {craft.length} brand-agnostic rendering principles<span class="dot">.</span>
{ui.catalog.craft.heading(craft.length)}
</h1>
<p class="lead">
Skills declare which craft rules they require. The agent loads the
matching rules into its system prompt so quality concerns
(a11y, motion, color, type) stay invariant across visual systems.
{ui.catalog.craft.lead}
</p>
</header>
<section class="catalog-grid">
<section class="catalog-grid" aria-label={ui.catalog.craft.allAria}>
<ol>
{craft.map((c, idx) => (
<li class="catalog-row">
<a href={`/craft/${c.slug}/`}>
<a href={href(`/craft/${c.slug}/`)}>
<span class="row-index">{String(idx + 1).padStart(2, '0')}</span>
<span class="row-body">
<span class="row-name">{c.name}</span>

File diff suppressed because it is too large Load diff

View file

@ -3,28 +3,134 @@ import Page from '../page';
import '../globals.css';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import FaviconLinks from '../_components/favicon-links.astro';
import FontStylesheet from '../_components/font-stylesheet.astro';
import GoogleAnalytics from '../_components/google-analytics.astro';
import HomeEnhancer from '../_components/home-enhancer.astro';
import LocaleSwitcherEnhancer from '../_components/locale-switcher-enhancer.astro';
import LocaleSwitcherScript from '../_components/locale-switcher-script.astro';
import PreciseLazyload from '../_components/precise-lazyload.astro';
import { heroImage, heroImageSrcset } from '../image-assets';
import {
LANDING_LOCALES,
alternateLinksForPath,
getHomeFaq,
getHomeSeo,
getLocaleDefinition,
localeFromPath,
localePath,
type LandingLocaleCode,
} from '../i18n';
import { getCatalogCounts } from '../_lib/catalog';
import { getGithubRepoMeta } from '../_lib/github';
import { alternateLinks, localeDir } from '../_lib/i18n';
const locale: LandingLocaleCode = localeFromPath(Astro.url.pathname);
const localeDef = getLocaleDefinition(locale);
const counts = await getCatalogCounts();
const github = await getGithubRepoMeta();
const title = 'Open Design — Design with the agent already on your laptop.';
const description = `The open-source alternative to Claude Design. Your existing coding agent — Claude · Codex · Cursor · Gemini · OpenCode · Qwen — becomes the design engine, driven by ${counts.skills} composable skills and ${counts.systems} brand-grade design systems.`;
const canonical = new URL(Astro.url.pathname, Astro.site).toString();
const { title, description } = getHomeSeo(locale, counts);
const canonical = new URL(localePath(locale), Astro.site).toString();
const origin = Astro.site?.toString() ?? 'https://open-design.ai/';
const logoUrl = new URL('/android-chrome-512x512.png', Astro.site).toString();
const alternateLinks = alternateLinksForPath('/').map((entry) => ({
...entry,
href: new URL(entry.hrefPath, Astro.site).toString(),
}));
const xDefaultHref = new URL('/', Astro.site).toString();
const REPO_URL = 'https://github.com/nexu-io/open-design';
const RELEASES_URL = `${REPO_URL}/releases`;
const ISSUES_URL = `${REPO_URL}/issues`;
const DOCS_URL = `${REPO_URL}#readme`;
const LICENSE_URL = `${REPO_URL}/blob/main/LICENSE`;
const DISCORD_URL = 'https://discord.gg/9ptkbbqRu';
const OFFICIAL_URL = `${origin}official/`;
// Pass bare hosts (not full URLs) so localized Q2 reads
// "lives at open-design.ai" instead of the awkward
// "lives at https://open-design.ai/" with a stray protocol/slash.
const faq = getHomeFaq(locale, {
origin: 'open-design.ai',
repo: 'github.com/nexu-io/open-design',
});
const websiteSchema = {
'@type': 'WebSite',
'@id': `${origin}#website`,
name: 'Open Design',
alternateName: ['OpenDesign', 'open-design', 'opendesign', 'Open Design AI', 'OD'],
url: origin,
inLanguage: localeDef.htmlLang,
availableLanguage: LANDING_LOCALES.map((entry) => entry.htmlLang),
publisher: { '@id': `${origin}#organization` },
};
const organizationSchema = {
'@type': 'Organization',
'@id': `${origin}#organization`,
name: 'Open Design',
alternateName: ['OpenDesign', 'open-design', 'opendesign', 'Open Design AI', 'OD', 'nexu-io/open-design'],
url: origin,
logo: {
'@type': 'ImageObject',
url: logoUrl,
width: 512,
height: 512,
},
// Five canonical pillars — Google uses sameAs to merge entity claims
// across sources. Listing the official site, GitHub repo, release
// feed, README docs, and Discord here prevents capture sites from
// splitting the brand entity.
sameAs: [REPO_URL, RELEASES_URL, DOCS_URL, DISCORD_URL, OFFICIAL_URL],
};
const softwareSchema = {
'@type': 'SoftwareApplication',
'@id': `${origin}#software`,
name: 'Open Design',
alternateName: ['OpenDesign', 'open-design', 'opendesign', 'Open Design AI', 'OD'],
description,
url: origin,
inLanguage: localeDef.htmlLang,
applicationCategory: 'DesignApplication',
operatingSystem: 'macOS, Windows, Linux',
license: 'https://www.apache.org/licenses/LICENSE-2.0',
softwareVersion: github.versionLabel,
downloadUrl: RELEASES_URL,
installUrl: `${origin}quickstart/`,
softwareHelp: { '@type': 'CreativeWork', url: DOCS_URL },
releaseNotes: RELEASES_URL,
codeRepository: REPO_URL,
discussionUrl: DISCORD_URL,
issueTracker: ISSUES_URL,
sameAs: [REPO_URL, RELEASES_URL, DOCS_URL, DISCORD_URL, OFFICIAL_URL, LICENSE_URL],
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
publisher: { '@id': `${origin}#organization` },
};
const faqSchema = {
'@type': 'FAQPage',
'@id': `${canonical}#faq`,
inLanguage: localeDef.htmlLang,
mainEntity: faq.map(({ q, a }) => ({
'@type': 'Question',
name: q,
acceptedAnswer: { '@type': 'Answer', text: a },
})),
};
const homepageGraph = {
'@context': 'https://schema.org',
'@graph': [websiteSchema, organizationSchema, softwareSchema, faqSchema],
};
const pageHtml = renderToStaticMarkup(
Page({ counts, github, locale: 'en', pathname: Astro.url.pathname }) as ReturnType<typeof createElement>,
Page({ counts, github, faq, locale }) as ReturnType<typeof createElement>,
);
---
<!doctype html>
<html lang="en" dir={localeDir('en')}>
<html lang={localeDef.htmlLang} dir={localeDef.dir}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@ -32,20 +138,12 @@ const pageHtml = renderToStaticMarkup(
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
{alternateLinks(Astro.url.pathname).map((item) => (
<link rel="alternate" hreflang={item.hreflang} href={new URL(item.href, Astro.site).toString()} />
{alternateLinks.map((entry) => (
<link rel="alternate" hreflang={entry.hreflang} href={entry.href} />
))}
<link rel="alternate" hreflang="x-default" href={xDefaultHref} />
{/*
* Favicon links. Modern browsers prefer the explicit PNG. We also
* publish `/favicon.ico` and link it because many SEO crawlers,
* link-preview services, and older clients hard-probe that exact
* path — without a real ICO at that URL they get the SPA fallback
* HTML and render an empty/broken icon in third-party UIs.
*/}
<link rel="icon" type="image/x-icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<FaviconLinks />
<FontStylesheet />
@ -72,17 +170,209 @@ const pageHtml = renderToStaticMarkup(
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={heroImage} />
<meta property="og:locale" content={localeDef.ogLocale} />
{LANDING_LOCALES.filter((entry) => entry.code !== locale).map((entry) => (
<meta property="og:locale:alternate" content={entry.ogLocale} />
))}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={heroImage} />
<GoogleAnalytics />
{/*
* Single @graph JSON-LD block — WebSite + Organization +
* SoftwareApplication + FAQPage. The FAQPage entries must mirror
* the visible FAQ rendered by `<HomeFaq />` further down the page.
*/}
<script is:inline type="application/ld+json" set:html={JSON.stringify(homepageGraph)} />
</head>
<body>
<Fragment set:html={pageHtml} />
<LocaleSwitcherEnhancer />
<PreciseLazyload />
<HomeEnhancer />
<LocaleSwitcherScript />
<script is:inline>
(() => {
const formatStars = (count) => {
if (!Number.isFinite(count) || count <= 0) return '0';
if (count < 1000) return String(count);
return `${(count / 1000).toFixed(1).replace(/\.0$/, '')}K`;
};
// Pull a clean 'v0.3.0'-style label from a GitHub release record.
// We prefer release.name (e.g. 'Open Design 0.3.0') because that's
// what we hand-author; fall back to tag_name (e.g.
// 'open-design-v0.3.0') with the project prefix stripped.
//
// Expected input shapes (release.name / release.tag_name):
// { name: 'Open Design 0.3.0', tag_name: 'v0.3.0' } → 'v0.3.0'
// { name: 'Open Design v0.3.0', tag_name: 'open-design-v0.3.0' } → 'v0.3.0'
// { name: '0.3.0-beta.1', tag_name: 'open-design_0.3.0' } → 'v0.3.0-beta.1' (name wins)
// { name: null, tag_name: 'open-design-v0.3.0' } → 'v0.3.0' (tag fallback)
// { name: null, tag_name: null } → null (caller skips)
const formatVersion = (release) => {
const fromTag = (tag) => {
if (typeof tag !== 'string') return null;
const cleaned = tag.replace(/^open-design[-_]?v?/i, '').trim();
return cleaned ? `v${cleaned.replace(/^v/, '')}` : null;
};
const fromName = (name) => {
if (typeof name !== 'string') return null;
const m = name.match(/(\d+\.\d+\.\d+(?:[-+][\w.]+)?)/);
return m ? `v${m[1]}` : null;
};
return fromName(release?.name) ?? fromTag(release?.tag_name) ?? null;
};
const enhanceHeader = () => {
const chrome = document.querySelector('[data-chrome-headroom]');
if (chrome) {
let lastY = window.scrollY;
const showTopThreshold = 100;
const scrollDelta = 6;
window.addEventListener(
'scroll',
() => {
const y = window.scrollY;
const delta = y - lastY;
if (y <= showTopThreshold) chrome.classList.remove('is-hidden');
else if (delta > scrollDelta) chrome.classList.add('is-hidden');
else if (delta < -scrollDelta) chrome.classList.remove('is-hidden');
lastY = y;
},
{ passive: true },
);
}
const stars = document.querySelector('[data-github-stars]');
if (stars) {
fetch('https://api.github.com/repos/nexu-io/open-design', {
headers: { Accept: 'application/vnd.github+json' },
})
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('http error'))))
.then((data) => {
if (typeof data?.stargazers_count === 'number') {
stars.textContent = formatStars(data.stargazers_count);
}
})
.catch(() => {});
}
// Latest stable release powers every "v0.x.y" badge on the page
// (topbar pulse, hero CTA-foot, footer download). Hits one
// unauthenticated API call per page view; the static fallback in
// each slot keeps the layout sane if the request fails or 403s.
const versionSlots = document.querySelectorAll('[data-github-version]');
if (versionSlots.length === 0) return;
fetch('https://api.github.com/repos/nexu-io/open-design/releases/latest', {
headers: { Accept: 'application/vnd.github+json' },
})
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('http error'))))
.then((data) => {
const label = formatVersion(data);
if (!label) return;
for (const slot of versionSlots) slot.textContent = label;
})
.catch(() => {});
};
const enhanceWire = () => {
const track = document.querySelector('[data-wire-contributors-track]');
const count = document.querySelector('[data-wire-contributors-count]');
if (!track) return;
const roleOverrides = {
tw93: 'kami',
op7418: 'guizang',
alchaincyf: 'huashu',
OpenCoworkAI: 'codesign',
'nexu-io': 'studio',
lewislulu: 'html-ppt',
};
const roleFor = (login, contributions) =>
roleOverrides[login] ?? `${contributions} ${contributions === 1 ? 'commit' : 'commits'}`;
const isContributor = (value) =>
value &&
typeof value.login === 'string' &&
typeof value.html_url === 'string' &&
typeof value.type === 'string' &&
typeof value.contributions === 'number';
const renderContributor = (contributor, index) => {
const link = document.createElement('a');
link.className = 'wire-item is-link';
link.href = contributor.href;
link.target = '_blank';
link.rel = 'noreferrer noopener';
link.setAttribute('aria-label', `Open ${contributor.handle} on GitHub`);
link.dataset.liveWireItem = String(index);
const dot = document.createElement('span');
dot.className = 'wire-dot';
dot.textContent = '·';
const handle = document.createElement('span');
handle.className = 'wire-handle';
handle.textContent = `@${contributor.handle}`;
const role = document.createElement('span');
role.className = 'wire-role';
role.textContent = contributor.role;
link.append(dot, handle, role);
return link;
};
fetch('https://api.github.com/repos/nexu-io/open-design/contributors?per_page=12', {
headers: { Accept: 'application/vnd.github+json' },
})
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('http error'))))
.then((data) => {
if (!Array.isArray(data)) return;
const live = data
.filter(isContributor)
.filter((c) => c.type !== 'Bot' && !c.login.endsWith('[bot]'))
.slice(0, 12)
.map((c) => ({
handle: c.login,
role: roleFor(c.login, c.contributions),
href: c.html_url,
}));
if (live.length === 0) return;
live.push({
handle: 'you',
role: 'be next',
href: 'https://github.com/nexu-io/open-design/graphs/contributors',
});
if (count) count.textContent = String(Math.max(0, live.length - 1));
track.replaceChildren(
...[...live, ...live].map((contributor, index) => renderContributor(contributor, index)),
);
})
.catch(() => {});
};
const elements = document.querySelectorAll('[data-reveal]:not([data-revealed])');
enhanceHeader();
enhanceWire();
if (elements.length === 0) return;
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceMotion || !('IntersectionObserver' in window)) {
for (const el of elements) el.dataset.revealed = 'true';
return;
}
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
entry.target.dataset.revealed = 'true';
observer.unobserve(entry.target);
}
},
{ threshold: 0.12, rootMargin: '0px 0px -8% 0px' },
);
for (const el of elements) observer.observe(el);
})();
</script>
</body>
</html>

View file

@ -2,12 +2,11 @@
/*
* /official/ — brand-authority surface.
*
* Exists to satisfy "Open Design vs OpenDesigner / opendesign.ai"
* brand-confusion queries. Lists every canonical link, names every
* alias the project is searched under, and points at the GitHub
* source of truth.
* This page exists to satisfy "Open Design vs OpenDesigner / opendesign.ai"
* brand-confusion queries: it lists every canonical link, names every alias
* the project is searched under, and points at the GitHub source of truth.
*
* Per growth/seo-opendesigner-analysis.md §2 + §8, the page must:
* Per `growth/seo-opendesigner-analysis.md` §2 + §8, the page must:
* - lead with canonical links (site, GitHub, releases, docs, Discord)
* - explain the OpenDesign / open-design / opendesign alias relationship
* - never call third-party capture sites "fake" in visible copy
@ -15,53 +14,40 @@
*/
import Layout from '../../_components/sub-page-layout.astro';
import { getGithubRepoMeta } from '../../_lib/github';
import { getInfoPageCopy } from '../../info-page-i18n';
import { localeFromPath, localizedHref } from '../../i18n';
const github = await getGithubRepoMeta();
const locale = localeFromPath(Astro.url.pathname);
const href = (path: string) => localizedHref(path, locale);
const copy = getInfoPageCopy(locale);
const page = copy.official;
const common = copy.common;
// Derive SITE from Astro.site so preview/staging JSON-LD matches the
// layout's canonical link. Falls back to the production origin.
const SITE = Astro.site?.toString() ?? 'https://open-design.ai/';
const REPO = 'https://github.com/nexu-io/open-design';
const REPO_RELEASES = `${REPO}/releases`;
const REPO_ISSUES = `${REPO}/issues`;
const REPO_LICENSE = `${REPO}/blob/main/LICENSE`;
const DOCS = `${REPO}#readme`;
const DISCORD = 'https://discord.gg/9ptkbbqRu';
const github = await getGithubRepoMeta();
const title = 'Official Open Design — Source page, GitHub, releases, and aliases';
const description =
'Official source page for Open Design (also searched as OpenDesign, open-design, opendesign, Open Design AI, OD). Canonical site, GitHub repository, releases, Discord, license, and maintainer identity in one place.';
const title = page.title;
const description = page.description;
const sources = [
{ label: 'Official website', name: 'open-design.ai', href: 'https://open-design.ai/' },
{ label: 'GitHub repository', name: 'nexu-io/open-design', href: REPO },
{ label: 'Latest release', name: github.versionLabel, href: REPO_RELEASES },
{ label: 'Issues / discussion', name: 'GitHub issues', href: REPO_ISSUES },
{ label: 'Community', name: 'Discord', href: DISCORD },
{ label: 'Documentation', name: 'GitHub README', href: REPO },
{ label: 'License', name: 'Apache-2.0', href: `${REPO}/blob/main/LICENSE` },
{ label: 'Skills catalog', name: '/skills/', href: '/skills/' },
{ label: 'Systems catalog', name: '/systems/', href: '/systems/' },
{ label: 'Templates catalog', name: '/templates/', href: '/templates/' },
];
const aliases = [
{ label: 'Open Design', body: 'display name in the product UI, blog, and READMEs.' },
{ label: 'OpenDesign', body: 'common one-word search variant; same project.' },
{ label: 'open-design', body: 'repository / package slug.' },
{ label: 'opendesign', body: 'lowercase alias used in URLs and CLI invocations.' },
{ label: 'Open Design AI', body: 'long-form search variant for AI-design queries.' },
{ label: 'OD', body: 'internal abbreviation for the runtime and CLI bin.' },
];
const runtimeItems = [
{ label: 'Desktop app', body: 'packaged Electron build for macOS, Windows, Linux.' },
{ label: 'Daemon (od)', body: 'local HTTP daemon and CLI for agents, shell, or CI.' },
{ label: 'Skills + Systems', body: 'Markdown bundles you can fork, edit, and ship.' },
];
const nextItems = [
{ label: 'Quickstart', href: '/quickstart/', body: 'install in three commands.' },
{ label: 'Agents', href: '/agents/', body: 'Claude Code, Codex, Cursor, Gemini, OpenCode, Qwen.' },
{ label: 'Claude Design alternative', href: '/alternatives/claude-design/', body: 'comparison and migration.' },
{ label: 'Skills catalog', href: '/skills/', body: 'every shippable design skill.' },
{ label: 'Systems catalog', href: '/systems/', body: 'every portable DESIGN.md brand system.' },
{ ...page.sources[0], href: SITE },
{ ...page.sources[1], href: REPO },
{ ...page.sources[2], name: github.versionLabel, href: REPO_RELEASES },
{ ...page.sources[3], href: REPO_ISSUES },
{ ...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/') },
];
const jsonLd = [
@ -70,84 +56,92 @@ const jsonLd = [
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: SITE },
{ '@type': 'ListItem', position: 2, name: 'Official', item: `${SITE}official/` },
{ '@type': 'ListItem', position: 2, name: page.breadcrumb, item: `${SITE}official/` },
],
},
{
// The Organization entity is fully defined on `/` with the canonical
// `@id: '<site>#organization'`. Schema.org `@id`s should be unique,
// so we only REFERENCE the entity here (this WebPage is "about"
// that Organization), not redefine it. Google merges the two
// mentions through the shared `@id`.
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Open Design',
alternateName: ['OpenDesign', 'open-design', 'opendesign', 'Open Design AI', 'OD'],
url: SITE,
sameAs: [REPO, DISCORD],
logo: `${SITE}android-chrome-512x512.png`,
'@type': 'WebPage',
'@id': `${SITE}official/#webpage`,
name: title,
description,
url: `${SITE}official/`,
isPartOf: { '@id': `${SITE}#website` },
about: { '@id': `${SITE}#organization` },
mainEntity: { '@id': `${SITE}#organization` },
},
];
---
<Layout title={title} description={description} active="home" jsonLd={jsonLd}>
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/">Open Design</a>
<nav class="breadcrumb" aria-label={common.breadcrumbAria}>
<a href={href('/')}>Open Design</a>
<span>/</span>
<span aria-current="page">Official</span>
<span aria-current="page">{page.breadcrumb}</span>
</nav>
<article class="info-page">
<header class="catalog-head">
<span class="label">Source · Nº 00</span>
<h1 class="display">Official Open Design source page<span class="dot">.</span></h1>
<p class="lead">
Open Design (also searched as OpenDesign, open-design, opendesign, or Open Design AI) is the official open-source AI design workspace from the <a class="inline-link" href={REPO} target="_blank" rel="noreferrer noopener">nexu-io/open-design</a> project. This page lists every canonical surface so you can verify the source for yourself.
</p>
<span class="label">{page.label}</span>
<h1 class="display">{page.heading}</h1>
<p class="lead">{page.lead}</p>
</header>
<section class="info-section">
<h2>Canonical surfaces</h2>
<p>Bookmark <code>open-design.ai</code> and the GitHub repo. Everything else points back to one of these two.</p>
<h2>{page.canonicalTitle}</h2>
<p>{page.canonicalBody}</p>
<div class="source-card">
{sources.map((s) => (
<a href={s.href} target={s.href.startsWith('http') ? '_blank' : undefined} rel={s.href.startsWith('http') ? 'noreferrer noopener' : undefined}>
<span><span class="label">{s.label}</span><br />{s.name}</span>
<span class="arrow">→</span>
<span>
<span class="label">{s.label}</span><br />
<strong>{s.name}</strong>
</span>
<span class="arrow" aria-hidden="true">→</span>
</a>
))}
</div>
</section>
<section class="info-section">
<h2>Naming &amp; aliases</h2>
<p>The project is searched and written several ways depending on the tool, audience, and locale:</p>
<section class="info-section" id="aliases">
<h2>{page.aliasesTitle}</h2>
<p>{page.aliasesLead}</p>
<ul>
{aliases.map((a) => (
<li><strong>{a.label}</strong> — {a.body}</li>
{page.aliases.map((item) => (
<li><strong>{item.label}</strong> — {item.body}</li>
))}
</ul>
<p>All six names refer to this same project. The canonical URL is always <code>open-design.ai</code>.</p>
<p>{page.aliasesClosing}</p>
</section>
<section class="info-section" id="maintainer">
<h2>{page.maintainerTitle}</h2>
<p>{page.maintainerBody}</p>
</section>
<section class="info-section">
<h2>Maintainer &amp; license</h2>
<p>
Open Design is developed in the open at <a class="inline-link" href={REPO} target="_blank" rel="noreferrer noopener">github.com/nexu-io/open-design</a> and released under the Apache-2.0 license. Issues, RFCs, and roadmap conversations happen on <a class="inline-link" href={REPO_ISSUES} target="_blank" rel="noreferrer noopener">GitHub Issues</a> and <a class="inline-link" href={DISCORD} target="_blank" rel="noreferrer noopener">Discord</a>.
</p>
</section>
<section class="info-section">
<h2>What runs on your machine</h2>
<p>Open Design ships three runnable surfaces — all open source, all local-first:</p>
<h2>{page.runtimeTitle}</h2>
<p>{page.runtimeBody}</p>
<ul>
{runtimeItems.map((r) => (
<li><strong>{r.label}</strong> — {r.body}</li>
{page.runtimeItems.map((item) => (
<li><strong>{item.label}</strong> — {item.body}</li>
))}
</ul>
</section>
<section class="info-section">
<h2>Where to go next</h2>
<h2>{page.nextTitle}</h2>
<ul>
{nextItems.map((n) => (
<li><a class="inline-link" href={n.href}>{n.label}</a> — {n.body}</li>
))}
<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>
</ul>
</section>
</article>

View file

@ -2,12 +2,26 @@
import '../../globals.css';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import FaviconLinks from '../../_components/favicon-links.astro';
import FontStylesheet from '../../_components/font-stylesheet.astro';
import GoogleAnalytics from '../../_components/google-analytics.astro';
import { Header } from '../../_components/header';
import HeaderEnhancer from '../../_components/header-enhancer.astro';
import LazyImg from '../../_components/lazy-img.astro';
import LocaleSwitcherScript from '../../_components/locale-switcher-script.astro';
import PreciseLazyload from '../../_components/precise-lazyload.astro';
import Topbar from '../../_components/topbar.astro';
import { getCatalogCounts } from '../../_lib/catalog';
import { getGithubRepoMeta } from '../../_lib/github';
import { localizeContentTag } from '../../content-i18n';
import {
LANDING_LOCALES,
alternateLinksForPath,
getLandingUiCopy,
getLocaleDefinition,
localeFromPath,
localizedHref,
} from '../../i18n';
import type { PublicPluginEntry } from '../../plugin-registry';
import { getPublicPlugins } from '../../plugin-registry';
@ -18,48 +32,70 @@ export function getStaticPaths() {
}));
}
const { plugin } = Astro.props as { plugin: PublicPluginEntry };
const { plugin: routePlugin } = Astro.props as { plugin: PublicPluginEntry };
const locale = localeFromPath(Astro.url.pathname);
const localeDef = getLocaleDefinition(locale);
const ui = getLandingUiCopy(locale);
const href = (path: string) => localizedHref(path, locale);
const localePlugins = getPublicPlugins(locale);
const plugin = localePlugins.find((item) => item.id === routePlugin.id) ?? routePlugin;
const site = Astro.site ?? new URL('https://open-design.ai');
const canonical = new URL(plugin.detailHref, site).toString();
const title = `${plugin.title} — Open Design Plugin`;
const description = `${plugin.description} Install with ${plugin.installCommand}.`;
const catalogCounts = await getCatalogCounts();
const headerHtml = renderToStaticMarkup(
createElement(Header, { counts: catalogCounts, brandHref: '/' }),
const canonical = new URL(Astro.url.pathname, site).toString();
const alternateLinks = alternateLinksForPath(Astro.url.pathname).map((entry) => ({
...entry,
href: new URL(entry.hrefPath, site).toString(),
}));
const xDefaultHref = new URL(alternateLinks[0]!.hrefPath, site).toString();
const title = ui.plugins.detailTitle(plugin.title);
const description = ui.plugins.detailDescription(
plugin.description,
plugin.installCommand,
);
const related = getPublicPlugins()
const catalogCounts = await getCatalogCounts();
const github = await getGithubRepoMeta();
const headerHtml = renderToStaticMarkup(
createElement(Header, { counts: catalogCounts, brandHref: '/', locale }),
);
const related = localePlugins
.filter((item) => item.id !== plugin.id && item.registryId === plugin.registryId)
.slice(0, 6);
const previewLabel = plugin.preview?.frameHref
? 'Interactive preview'
? ui.plugins.interactivePreview
: plugin.preview?.poster
? plugin.preview.label
: `${plugin.visualKind} preview`;
: ui.plugins.preview;
const previewFrameHref = plugin.preview?.frameHref
? plugin.preview.frameHref.startsWith('/')
? href(plugin.preview.frameHref)
: plugin.preview.frameHref
: undefined;
const trustLabel = (entry: PublicPluginEntry) =>
ui.plugins.trustLabels[entry.trust] ?? entry.trust;
const detailLinks = [
{
href: plugin.registryUrl,
label: 'Marketplace JSON',
label: ui.plugins.marketplaceJson,
},
plugin.sourceUrl
? {
href: plugin.sourceUrl,
label: 'Source repository',
label: ui.plugins.sourceRepository,
}
: undefined,
plugin.homepage && plugin.homepage !== plugin.sourceUrl
? {
href: plugin.homepage,
label: 'Homepage',
label: ui.plugins.homepage,
}
: undefined,
].filter((item): item is { href: string; label: string } => Boolean(item));
const factRows = [
['Plugin ID', plugin.id],
['Version', plugin.version],
['Registry', plugin.registryName],
['Mode', plugin.mode ?? plugin.surface ?? plugin.visualKind],
['License', plugin.license ?? 'Not specified'],
plugin.publisher ? ['Publisher', plugin.publisher] : undefined,
[ui.plugins.facts.pluginId, plugin.id],
[ui.plugins.facts.version, plugin.version],
[ui.plugins.facts.registry, plugin.registryName],
[ui.plugins.facts.mode, localizeContentTag(plugin.mode ?? plugin.surface ?? plugin.visualKind, locale) ?? plugin.mode ?? plugin.surface ?? plugin.visualKind],
[ui.plugins.facts.license, plugin.license ?? ui.plugins.facts.notSpecified],
plugin.publisher ? [ui.plugins.facts.publisher, plugin.publisher] : undefined,
].filter((item): item is [string, string] => Boolean(item));
const pluginJsonLd = {
'@context': 'https://schema.org',
@ -70,6 +106,7 @@ const pluginJsonLd = {
applicationCategory: 'DesignApplication',
operatingSystem: 'macOS, Windows, Linux',
softwareVersion: plugin.version,
inLanguage: localeDef.htmlLang,
url: canonical,
installUrl: plugin.sourceUrl,
codeRepository: plugin.sourceUrl,
@ -102,8 +139,8 @@ const breadcrumbJsonLd = {
{
'@type': 'ListItem',
position: 2,
name: 'Plugins',
item: new URL('/plugins/', site).toString(),
name: ui.plugins.registry,
item: new URL(href('/plugins/'), site).toString(),
},
{
'@type': 'ListItem',
@ -116,7 +153,7 @@ const breadcrumbJsonLd = {
---
<!doctype html>
<html lang='en'>
<html lang={localeDef.htmlLang} dir={localeDef.dir}>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
@ -124,12 +161,21 @@ const breadcrumbJsonLd = {
<meta name='description' content={description} />
<meta name='robots' content={plugin.yanked ? 'noindex,follow' : 'index,follow'} />
<link rel='canonical' href={canonical} />
{alternateLinks.map((entry) => (
<link rel='alternate' hreflang={entry.hreflang} href={entry.href} />
))}
<link rel='alternate' hreflang='x-default' href={xDefaultHref} />
<FaviconLinks />
<FontStylesheet />
<meta property='og:type' content='article' />
<meta property='og:site_name' content='Open Design' />
<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
<meta property='og:url' content={canonical} />
<meta property='og:locale' content={localeDef.ogLocale} />
{LANDING_LOCALES.filter((entry) => entry.code !== locale).map((entry) => (
<meta property='og:locale:alternate' content={entry.ogLocale} />
))}
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:title' content={title} />
<meta name='twitter:description' content={description} />
@ -139,63 +185,59 @@ const breadcrumbJsonLd = {
</head>
<body>
<div class='side-rail right' aria-hidden='true'>
<span class='rail-text'>Open Design Plugin · {plugin.id}</span>
<span class='rail-text'>{ui.plugins.detailRailRight(plugin.id)}</span>
</div>
<div class='side-rail left' aria-hidden='true'>
<span class='rail-text'>{plugin.registryName} · {plugin.trust}</span>
<span class='rail-text'>{plugin.registryName} · {trustLabel(plugin)}</span>
</div>
<div class='shell plugin-shell'>
<div class='topbar'>
<div class='container topbar-inner'>
<span><b>OD / PLUGIN</b>&nbsp;·&nbsp;{plugin.registryName}</span>
<span class='mid'>
<span>{plugin.id}</span>
<span>{plugin.version}</span>
</span>
<span class='right'>
<a class='topbar-link' href='/plugins/'>All plugins</a>
</span>
</div>
<div class='site-chrome' data-chrome-headroom>
<Topbar github={github} locale={locale} />
<Fragment set:html={headerHtml} />
</div>
<Fragment set:html={headerHtml} />
<main id='top' class='plugin-detail'>
<section class='plugin-detail-hero'>
<div class='container plugin-detail-hero__grid'>
<div>
<a class='plugin-back-link' href='/plugins/'>Registry</a>
<a class='plugin-back-link' href={href('/plugins/')}>{ui.plugins.registry}</a>
<div class='plugin-detail__badges'>
<span class={`plugin-badge plugin-badge--${plugin.registryId}`}>
{plugin.registryName}
</span>
<span>{plugin.trust}</span>
{plugin.deprecated && <span>Deprecated</span>}
{plugin.yanked && <span>Yanked</span>}
<span>{trustLabel(plugin)}</span>
{plugin.deprecated && <span>{ui.plugins.deprecated}</span>}
{plugin.yanked && <span>{ui.plugins.yanked}</span>}
</div>
<h1>{plugin.title}</h1>
<p>{plugin.description}</p>
<div class='plugin-detail__commands'>
<div>
<span>Install from registry</span>
<span>{ui.plugins.installFromRegistry}</span>
<code>{plugin.installCommand}</code>
</div>
<button type='button' data-copy-command={plugin.installCommand}>
Copy
<button
type='button'
data-copy-command={plugin.installCommand}
data-copy-label={ui.plugins.copy}
data-copied-label={ui.plugins.copied}
data-select-label={ui.plugins.select}
>
{ui.plugins.copy}
</button>
</div>
</div>
<aside class='plugin-detail-side' aria-label='Plugin preview and facts'>
<aside class='plugin-detail-side' aria-label={ui.plugins.previewAndFacts}>
<div class={`plugin-detail-preview plugin-detail-preview--${plugin.visualKind}`}>
<div class='plugin-detail-preview__head'>
<span>{previewLabel}</span>
<small>{plugin.preview?.type ?? plugin.visualKind}</small>
</div>
<div class='plugin-detail-preview__frame'>
{plugin.preview?.frameHref ? (
{previewFrameHref ? (
<iframe
src={plugin.preview.frameHref}
src={previewFrameHref}
title={`${plugin.title} preview`}
loading='lazy'
sandbox='allow-scripts allow-same-origin'
@ -228,13 +270,10 @@ const breadcrumbJsonLd = {
<section class='plugin-detail-section'>
<div class='container plugin-detail-grid'>
<div class='plugin-detail-panel'>
<span class='label'>How it resolves</span>
<h2>Registry provenance</h2>
<span class='label'>{ui.plugins.howItResolves}</span>
<h2>{ui.plugins.provenance}</h2>
<p>
This entry is discovered from a marketplace catalog and resolves
to the transport source below. The product can group it by source
while the CLI keeps the install target stable through the
vendor/plugin-name ID.
{ui.plugins.provenanceBody}
</p>
<div class='plugin-source-list'>
{detailLinks.map((link) => (
@ -246,17 +285,17 @@ const breadcrumbJsonLd = {
</div>
<div class='plugin-detail-panel'>
<span class='label'>Capabilities</span>
<h2>Workflow surface</h2>
<span class='label'>{ui.plugins.capabilities}</span>
<h2>{ui.plugins.workflowSurface}</h2>
<div class='plugin-tags plugin-tags--large'>
{plugin.tags.map((tag) => <span>{tag}</span>)}
{plugin.capabilities.map((capability) => <span>{capability}</span>)}
{plugin.mode && <span>{plugin.mode}</span>}
{plugin.taskKind && <span>{plugin.taskKind}</span>}
{plugin.tags.slice(0, 8).map((tag) => <span>{localizeContentTag(tag, locale) ?? tag}</span>)}
{plugin.capabilities.slice(0, 4).map((capability) => <span>{localizeContentTag(capability, locale) ?? capability}</span>)}
{plugin.mode && <span>{localizeContentTag(plugin.mode, locale) ?? plugin.mode}</span>}
{plugin.taskKind && <span>{localizeContentTag(plugin.taskKind, locale) ?? plugin.taskKind}</span>}
</div>
<div class='plugin-detail__commands compact'>
<div>
<span>Direct source fallback</span>
<span>{ui.plugins.directSourceFallback}</span>
<code>{plugin.directInstallCommand}</code>
</div>
</div>
@ -264,11 +303,10 @@ const breadcrumbJsonLd = {
{plugin.exampleQuery && (
<div class='plugin-detail-panel plugin-detail-panel--wide'>
<span class='label'>Example prompt</span>
<h2>How people use it</h2>
<span class='label'>{ui.plugins.examplePrompt}</span>
<h2>{ui.plugins.howPeopleUseIt}</h2>
<p>
The registry entry includes a ready-to-run prompt seed so the
plugin can be evaluated without guessing its expected workflow.
{ui.plugins.examplePromptBody}
</p>
<pre class='plugin-example-query'>{plugin.exampleQuery}</pre>
</div>
@ -280,8 +318,8 @@ const breadcrumbJsonLd = {
<section class='plugin-detail-section plugin-related'>
<div class='container'>
<div class='section-header'>
<span class='label'>More from {plugin.registryName}</span>
<h2>Related plugins</h2>
<span class='label'>{ui.plugins.moreFrom(plugin.registryName)}</span>
<h2>{ui.plugins.related}</h2>
</div>
<div class='plugin-card-grid compact'>
{related.map((item) => (
@ -293,7 +331,7 @@ const breadcrumbJsonLd = {
<span>{item.version}</span>
</div>
<h3>
<a href={item.detailHref}>{item.title}</a>
<a href={href(item.detailHref)}>{item.title}</a>
</h3>
<code>{item.id}</code>
<p>{item.description}</p>
@ -310,14 +348,17 @@ const breadcrumbJsonLd = {
document.querySelectorAll('[data-copy-command]').forEach((button) => {
button.addEventListener('click', async () => {
const command = button.getAttribute('data-copy-command') ?? '';
const copyLabel = button.getAttribute('data-copy-label') ?? 'Copy';
const copiedLabel = button.getAttribute('data-copied-label') ?? 'Copied';
const selectLabel = button.getAttribute('data-select-label') ?? 'Select';
try {
await navigator.clipboard.writeText(command);
button.textContent = 'Copied';
button.textContent = copiedLabel;
window.setTimeout(() => {
button.textContent = 'Copy';
button.textContent = copyLabel;
}, 1400);
} catch {
button.textContent = 'Select';
button.textContent = selectLabel;
}
});
});
@ -334,6 +375,8 @@ const breadcrumbJsonLd = {
})
.catch(() => {});
</script>
<HeaderEnhancer />
<LocaleSwitcherScript />
<PreciseLazyload />
</body>
</html>

View file

@ -2,17 +2,36 @@
import '../../globals.css';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import FaviconLinks from '../../_components/favicon-links.astro';
import FontStylesheet from '../../_components/font-stylesheet.astro';
import GoogleAnalytics from '../../_components/google-analytics.astro';
import { Header } from '../../_components/header';
import HeaderEnhancer from '../../_components/header-enhancer.astro';
import LazyImg from '../../_components/lazy-img.astro';
import LocaleSwitcherScript from '../../_components/locale-switcher-script.astro';
import PreciseLazyload from '../../_components/precise-lazyload.astro';
import Topbar from '../../_components/topbar.astro';
import { getCatalogCounts } from '../../_lib/catalog';
import { getGithubRepoMeta } from '../../_lib/github';
import { localizeContentTag } from '../../content-i18n';
import {
LANDING_LOCALES,
alternateLinksForPath,
getLandingUiCopy,
getLocaleDefinition,
localeFromPath,
localizedHref,
} from '../../i18n';
import { getPublicPlugins, getRegistryCounts } from '../../plugin-registry';
const plugins = getPublicPlugins();
const locale = localeFromPath(Astro.url.pathname);
const localeDef = getLocaleDefinition(locale);
const ui = getLandingUiCopy(locale);
const href = (path: string) => localizedHref(path, locale);
const plugins = getPublicPlugins(locale);
const counts = getRegistryCounts(plugins);
const catalogCounts = await getCatalogCounts();
const github = await getGithubRepoMeta();
const previewCount = plugins.filter((plugin) => plugin.preview).length;
const surfaceCount = new Set(
plugins.map((plugin) => plugin.mode ?? plugin.surface ?? plugin.visualKind),
@ -28,11 +47,16 @@ const featuredPlugins = plugins
const showcasePlugins =
featuredPlugins.length >= 3 ? featuredPlugins : plugins.slice(0, 3);
const site = Astro.site ?? new URL('https://open-design.ai');
const canonical = new URL('/plugins/', site).toString();
const title = 'Open Design Plugins — Official and community registries';
const description = `Browse ${counts.all} Open Design plugins from official and community registries. Search installable agent-native design workflows with stable vendor/plugin IDs.`;
const canonical = new URL(Astro.url.pathname, site).toString();
const alternateLinks = alternateLinksForPath(Astro.url.pathname).map((entry) => ({
...entry,
href: new URL(entry.hrefPath, site).toString(),
}));
const xDefaultHref = new URL(alternateLinks[0]!.hrefPath, site).toString();
const title = ui.plugins.registryTitle;
const description = ui.plugins.registryDescription(counts.all);
const headerHtml = renderToStaticMarkup(
createElement(Header, { counts: catalogCounts, brandHref: '/' }),
createElement(Header, { counts: catalogCounts, brandHref: '/', locale }),
);
const itemListJsonLd = {
'@context': 'https://schema.org',
@ -44,7 +68,7 @@ const itemListJsonLd = {
itemListElement: plugins.slice(0, 120).map((plugin, index) => ({
'@type': 'ListItem',
position: index + 1,
url: new URL(plugin.detailHref, site).toString(),
url: new URL(href(plugin.detailHref), site).toString(),
name: plugin.title,
description: plugin.description,
})),
@ -62,23 +86,29 @@ const breadcrumbJsonLd = {
{
'@type': 'ListItem',
position: 2,
name: 'Plugins',
name: ui.plugins.registry,
item: canonical,
},
],
};
const displayKind = (plugin: (typeof plugins)[number]) =>
plugin.mode ?? plugin.surface ?? plugin.visualKind;
localizeContentTag(plugin.mode ?? plugin.surface ?? plugin.visualKind, locale) ??
plugin.mode ??
plugin.surface ??
plugin.visualKind;
const primaryMeta = (plugin: (typeof plugins)[number]) =>
[displayKind(plugin), plugin.preview?.label]
.filter(Boolean)
.join(' · ');
const trustLabel = (plugin: (typeof plugins)[number]) =>
ui.plugins.trustLabels[plugin.trust] ?? plugin.trust;
---
<!doctype html>
<html lang='en'>
<html lang={localeDef.htmlLang} dir={localeDef.dir}>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
@ -86,12 +116,21 @@ const primaryMeta = (plugin: (typeof plugins)[number]) =>
<meta name='description' content={description} />
<meta name='robots' content='index,follow' />
<link rel='canonical' href={canonical} />
{alternateLinks.map((entry) => (
<link rel='alternate' hreflang={entry.hreflang} href={entry.href} />
))}
<link rel='alternate' hreflang='x-default' href={xDefaultHref} />
<FaviconLinks />
<FontStylesheet />
<meta property='og:type' content='website' />
<meta property='og:site_name' content='Open Design' />
<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
<meta property='og:url' content={canonical} />
<meta property='og:locale' content={localeDef.ogLocale} />
{LANDING_LOCALES.filter((entry) => entry.code !== locale).map((entry) => (
<meta property='og:locale:alternate' content={entry.ogLocale} />
))}
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:title' content={title} />
<meta name='twitter:description' content={description} />
@ -101,63 +140,47 @@ const primaryMeta = (plugin: (typeof plugins)[number]) =>
</head>
<body>
<div class='side-rail right' aria-hidden='true'>
<span class='rail-text'>Open Design Registry · Official · Community</span>
<span class='rail-text'>{ui.plugins.directoryRailRight}</span>
</div>
<div class='side-rail left' aria-hidden='true'>
<span class='rail-text'>vendor/plugin-name · marketplace.json</span>
<span class='rail-text'>{ui.plugins.directoryRailLeft}</span>
</div>
<div class='shell plugin-shell'>
<div class='topbar'>
<div class='container topbar-inner'>
<span><b>OD / REGISTRY</b>&nbsp;·&nbsp;Public index</span>
<span class='mid'>
<span>Official · Community · Self-hosted</span>
<span>GitHub-backed today · database-ready later</span>
</span>
<span class='right'>
<a class='topbar-link' href='https://github.com/nexu-io/open-design/tree/main/plugins/registry' target='_blank' rel='noreferrer noopener'>
Source JSON
</a>
</span>
</div>
<div class='site-chrome' data-chrome-headroom>
<Topbar github={github} locale={locale} />
<Fragment set:html={headerHtml} />
</div>
<Fragment set:html={headerHtml} />
<main id='top' class='plugin-directory'>
<section class='plugin-hero'>
<div class='container plugin-hero__grid'>
<div>
<span class='label'>Plugin Registry · public ecosystem</span>
<h1>Browse agent-native design plugins with live previews.</h1>
<span class='label'>{ui.plugins.heroLabel}</span>
<h1>{ui.plugins.heroTitle}</h1>
<p>
Discover installable workflows, decks, image templates, design
systems, and atomic capabilities. Each entry keeps a stable
vendor/plugin ID, clear provenance, and a visual cue so browsing
the registry feels closer to choosing a creative tool than reading
a manifest dump.
{ui.plugins.heroBody}
</p>
<div class='plugin-hero__actions'>
<a class='btn btn-primary' href='#registry-results'>
Browse registry
{ui.plugins.browseRegistry}
</a>
<a class='btn btn-ghost' href='https://raw.githubusercontent.com/nexu-io/open-design/main/plugins/registry/community/open-design-marketplace.json' target='_blank' rel='noreferrer noopener'>
Community marketplace.json
{ui.plugins.communityMarketplace}
</a>
</div>
</div>
<aside class='plugin-hero__panel' aria-label='Registry preview'>
<aside class='plugin-hero__panel' aria-label={ui.plugins.preview}>
<div class='plugin-showcase__head'>
<span>Registry preview</span>
<span>{ui.plugins.preview}</span>
<strong>{counts.all}</strong>
<small>installable entries</small>
<small>{ui.plugins.installableEntries}</small>
</div>
<div class='plugin-showcase__list'>
{showcasePlugins.map((plugin, index) => (
<a
class={`plugin-showcase-item plugin-showcase-item--${plugin.visualKind}`}
href={plugin.detailHref}
href={href(plugin.detailHref)}
>
<span class='plugin-showcase-item__visual' aria-hidden='true'>
{plugin.preview?.poster ? (
@ -185,15 +208,15 @@ const primaryMeta = (plugin: (typeof plugins)[number]) =>
<dl class='plugin-hero__stats'>
<div>
<dt>{counts.official}</dt>
<dd>official</dd>
<dd>{ui.plugins.official.toLowerCase()}</dd>
</div>
<div>
<dt>{previewCount}</dt>
<dd>with preview</dd>
<dd>{ui.plugins.withPreview}</dd>
</div>
<div>
<dt>{surfaceCount}</dt>
<dd>surfaces</dd>
<dd>{ui.plugins.surfaces}</dd>
</div>
</dl>
</aside>
@ -204,29 +227,29 @@ const primaryMeta = (plugin: (typeof plugins)[number]) =>
<div class='container'>
<div class='plugin-toolbar' data-plugin-toolbar>
<div>
<span class='label'>Available from sources</span>
<h2>Registry entries</h2>
<span class='label'>{ui.plugins.availableFromSources}</span>
<h2>{ui.plugins.registryEntries}</h2>
</div>
<label class='plugin-search'>
<span class='sr-only'>Search plugins</span>
<input data-plugin-search type='search' placeholder='Search plugins, workflows, vendors...' autocomplete='off' />
<span class='sr-only'>{ui.plugins.searchPlugins}</span>
<input data-plugin-search type='search' placeholder={ui.plugins.searchPlaceholder} autocomplete='off' />
</label>
</div>
<div class='plugin-filter-row' aria-label='Registry filters'>
<div class='plugin-filter-row' aria-label={ui.plugins.filtersLabel}>
<button class='plugin-filter is-active' type='button' data-plugin-filter='all' aria-pressed='true'>
All <span>{counts.all}</span>
{ui.plugins.all} <span>{counts.all}</span>
</button>
<button class='plugin-filter' type='button' data-plugin-filter='official' aria-pressed='false'>
Official <span>{counts.official}</span>
{ui.plugins.official} <span>{counts.official}</span>
</button>
<button class='plugin-filter' type='button' data-plugin-filter='community' aria-pressed='false'>
Community <span>{counts.community}</span>
{ui.plugins.community} <span>{counts.community}</span>
</button>
</div>
<p class='plugin-result-count'>
<span data-plugin-visible-count>{counts.all}</span> visible plugins
<span data-plugin-visible-count>{counts.all}</span> {ui.plugins.visiblePlugins}
</p>
<div class='plugin-card-grid'>
@ -239,8 +262,8 @@ const primaryMeta = (plugin: (typeof plugins)[number]) =>
>
<a
class={`plugin-card__preview plugin-card__preview--${plugin.visualKind}`}
href={plugin.detailHref}
aria-label={`Open ${plugin.title} details`}
href={href(plugin.detailHref)}
aria-label={ui.plugins.openDetails(plugin.title)}
>
{plugin.preview?.poster ? (
<LazyImg src={plugin.preview.poster} alt='' loading='precise' />
@ -262,7 +285,7 @@ const primaryMeta = (plugin: (typeof plugins)[number]) =>
<span>{plugin.version}</span>
</div>
<h3>
<a href={plugin.detailHref}>{plugin.title}</a>
<a href={href(plugin.detailHref)}>{plugin.title}</a>
</h3>
<code>{plugin.id}</code>
<p>{plugin.description}</p>
@ -273,8 +296,8 @@ const primaryMeta = (plugin: (typeof plugins)[number]) =>
))}
</div>
<div class='plugin-card__footer'>
<a href={plugin.detailHref}>Details</a>
<span>{plugin.trust}</span>
<a href={href(plugin.detailHref)}>{ui.plugins.details}</a>
<span>{trustLabel(plugin)}</span>
</div>
</article>
))}
@ -331,6 +354,8 @@ const primaryMeta = (plugin: (typeof plugins)[number]) =>
})
.catch(() => {});
</script>
<HeaderEnhancer />
<LocaleSwitcherScript />
<PreciseLazyload />
</body>
</html>

View file

@ -1,6 +1,951 @@
---
import { injectGoogleAnalytics } from '../../../_lib/google-analytics';
import { getPluginPreviewHtml, getPublicPlugins } from '../../../plugin-registry';
import {
DEFAULT_LOCALE,
getLocaleDefinition,
localeFromPath,
type LandingLocaleCode,
} from '../../../i18n';
type PreviewTextMap = Record<string, string>;
type PreviewTerms = {
howItWorks: string;
viewOnGithub: string;
quickstart: string;
gettingStarted: string;
docs: string;
onThisPage: string;
roadmap: string;
showcase: string;
search: string;
concepts: string;
authentication: string;
syncEngine: string;
blockLevelDeltas: string;
conflictResolution: string;
resumableUploads: string;
install: string;
configuration: string;
subcommands: string;
syncFirstFolder: string;
installCli: string;
cliDistributed: string;
verifyInstall: string;
authenticateStep: string;
signIn: string;
opensBrowser: string;
loggedIn: string;
note: string;
noBrowser: string;
syncFolder: string;
pickLocal: string;
excludingFiles: string;
addIgnore: string;
whereNext: string;
readConflict: string;
previous: string;
next: string;
};
const PREVIEW_TERMS: Record<Exclude<LandingLocaleCode, 'en'>, PreviewTerms> = {
zh: {
howItWorks: '工作方式',
viewOnGithub: '在 GitHub 查看',
quickstart: '快速开始',
gettingStarted: '入门',
docs: '文档',
onThisPage: '本页内容',
roadmap: '路线图',
showcase: '示例',
search: '搜索',
concepts: '概念',
authentication: '身份验证',
syncEngine: '同步引擎',
blockLevelDeltas: '块级增量',
conflictResolution: '冲突解决',
resumableUploads: '可续传上传',
install: '安装',
configuration: '配置',
subcommands: '子命令',
syncFirstFolder: '五分钟内同步第一个文件夹',
installCli: '安装 CLI',
cliDistributed: 'CLI 以单一二进制文件发布,支持 macOS、Linux 和 Windows。',
verifyInstall: '验证安装:',
authenticateStep: '登录认证',
signIn: '使用 Filebase 账号登录。令牌会存储在 <code>~/.config/filebase/credentials</code>。',
opensBrowser: '# → 打开浏览器',
loggedIn: '# ✓ 已以 you@example.com 登录',
note: '说明',
noBrowser: '在没有浏览器的服务器上,可以使用 <code>filebase auth login --device</code> 走设备码流程。',
syncFolder: '同步文件夹',
pickLocal: '选择本地目录并关联到远端根目录。Filebase 会监听变更,并在后台推送块级差异。',
excludingFiles: '排除文件',
addIgnore: '在同步文件夹根目录添加 <code>.filebaseignore</code>。语法与 <code>.gitignore</code> 相同:',
whereNext: '下一步',
readConflict: '阅读 <a href="#">冲突解决</a> 了解 Filebase 如何合并并发编辑,或直接查看 <a href="#">CLI 参考</a> 获取完整子命令列表。',
previous: '上一页',
next: '下一页',
},
'zh-tw': {
howItWorks: '運作方式',
viewOnGithub: '在 GitHub 查看',
quickstart: '快速開始',
gettingStarted: '入門',
docs: '文件',
onThisPage: '本頁內容',
roadmap: '路線圖',
showcase: '示例',
search: '搜尋',
concepts: '概念',
authentication: '身份驗證',
syncEngine: '同步引擎',
blockLevelDeltas: '區塊級增量',
conflictResolution: '衝突解決',
resumableUploads: '可續傳上傳',
install: '安裝',
configuration: '設定',
subcommands: '子命令',
syncFirstFolder: '五分鐘內同步第一個資料夾',
installCli: '安裝 CLI',
cliDistributed: 'CLI 以單一二進位檔發布,支援 macOS、Linux 和 Windows。',
verifyInstall: '驗證安裝:',
authenticateStep: '登入認證',
signIn: '使用 Filebase 帳號登入。權杖會儲存在 <code>~/.config/filebase/credentials</code>。',
opensBrowser: '# → 開啟瀏覽器',
loggedIn: '# ✓ 已以 you@example.com 登入',
note: '說明',
noBrowser: '在沒有瀏覽器的伺服器上,可以使用 <code>filebase auth login --device</code> 走裝置碼流程。',
syncFolder: '同步資料夾',
pickLocal: '選擇本地目錄並連結到遠端根目錄。Filebase 會監聽變更,並在背景推送區塊級差異。',
excludingFiles: '排除檔案',
addIgnore: '在同步資料夾根目錄新增 <code>.filebaseignore</code>。語法與 <code>.gitignore</code> 相同:',
whereNext: '下一步',
readConflict: '閱讀 <a href="#">衝突解決</a> 了解 Filebase 如何合併並行編輯,或直接查看 <a href="#">CLI 參考</a> 取得完整子命令列表。',
previous: '上一頁',
next: '下一頁',
},
ja: {
howItWorks: '仕組み',
viewOnGithub: 'GitHubで見る',
quickstart: 'クイックスタート',
gettingStarted: 'はじめに',
docs: 'ドキュメント',
onThisPage: 'このページ',
roadmap: 'ロードマップ',
showcase: 'ショーケース',
search: '検索',
concepts: 'コンセプト',
authentication: '認証',
syncEngine: '同期エンジン',
blockLevelDeltas: 'ブロック単位の差分',
conflictResolution: '競合解決',
resumableUploads: '再開可能アップロード',
install: 'インストール',
configuration: '設定',
subcommands: 'サブコマンド',
syncFirstFolder: '最初のフォルダを5分以内に同期します',
installCli: 'CLIをインストール',
cliDistributed: 'CLIはmacOS、Linux、Windows向けの単一バイナリとして配布されます。',
verifyInstall: 'インストールを確認:',
authenticateStep: '認証',
signIn: 'Filebaseアカウントでサインインします。トークンは<code>~/.config/filebase/credentials</code>に保存されます。',
opensBrowser: '# → ブラウザを開きます',
loggedIn: '# ✓ you@example.comとしてログインしました',
note: '注記',
noBrowser: 'ブラウザのないサーバーでは、<code>filebase auth login --device</code>でデバイスコードフローを使います。',
syncFolder: 'フォルダを同期',
pickLocal: 'ローカルディレクトリを選び、リモートルートにリンクします。Filebaseは変更を監視し、差分をバックグラウンドで送信します。',
excludingFiles: 'ファイルを除外',
addIgnore: '同期フォルダのルートに<code>.filebaseignore</code>を追加します。構文は<code>.gitignore</code>と同じです:',
whereNext: '次のステップ',
readConflict: '<a href="#">競合解決</a>を読んで同時編集のマージを理解するか、<a href="#">CLIリファレンス</a>で全サブコマンドを確認します。',
previous: '前へ',
next: '次へ',
},
ko: {
howItWorks: '작동 방식',
viewOnGithub: 'GitHub에서 보기',
quickstart: '빠른 시작',
gettingStarted: '시작하기',
docs: '문서',
onThisPage: '이 페이지',
roadmap: '로드맵',
showcase: '쇼케이스',
search: '검색',
concepts: '개념',
authentication: '인증',
syncEngine: '동기화 엔진',
blockLevelDeltas: '블록 단위 변경',
conflictResolution: '충돌 해결',
resumableUploads: '이어 올리기',
install: '설치',
configuration: '설정',
subcommands: '하위 명령',
syncFirstFolder: '첫 폴더를 5분 안에 동기화합니다',
installCli: 'CLI 설치',
cliDistributed: 'CLI는 macOS, Linux, Windows용 단일 바이너리로 배포됩니다.',
verifyInstall: '설치 확인:',
authenticateStep: '인증',
signIn: 'Filebase 계정으로 로그인합니다. 토큰은 <code>~/.config/filebase/credentials</code>에 저장됩니다.',
opensBrowser: '# → 브라우저를 엽니다',
loggedIn: '# ✓ you@example.com으로 로그인됨',
note: '참고',
noBrowser: '브라우저가 없는 서버에서는 <code>filebase auth login --device</code>로 디바이스 코드 흐름을 사용합니다.',
syncFolder: '폴더 동기화',
pickLocal: '로컬 디렉터리를 선택해 원격 루트에 연결합니다. Filebase는 변경을 감시하고 블록 단위 차이를 백그라운드에서 전송합니다.',
excludingFiles: '파일 제외',
addIgnore: '동기화 폴더 루트에 <code>.filebaseignore</code>를 추가합니다. 문법은 <code>.gitignore</code>와 같습니다:',
whereNext: '다음 단계',
readConflict: '<a href="#">충돌 해결</a>을 읽어 동시 편집 병합을 이해하거나 <a href="#">CLI 참조</a>에서 전체 하위 명령을 확인합니다.',
previous: '이전',
next: '다음',
},
de: {
howItWorks: 'So funktioniert es',
viewOnGithub: 'Auf GitHub ansehen',
quickstart: 'Schnellstart',
gettingStarted: 'Erste Schritte',
docs: 'Dokumentation',
onThisPage: 'Auf dieser Seite',
roadmap: 'Fahrplan',
showcase: 'Beispiele',
search: 'Suche',
concepts: 'Konzepte',
authentication: 'Authentifizierung',
syncEngine: 'Sync-Engine',
blockLevelDeltas: 'Blockbasierte Deltas',
conflictResolution: 'Konfliktlösung',
resumableUploads: 'Fortsetzbare Uploads',
install: 'Installation',
configuration: 'Konfiguration',
subcommands: 'Unterbefehle',
syncFirstFolder: 'Synchronisiere den ersten Ordner in unter fünf Minuten',
installCli: 'CLI installieren',
cliDistributed: 'Die CLI wird als einzelne Binärdatei für macOS, Linux und Windows verteilt.',
verifyInstall: 'Installation prüfen:',
authenticateStep: 'Authentifizieren',
signIn: 'Melde dich mit deinem Filebase-Konto an. Das Token wird unter <code>~/.config/filebase/credentials</code> gespeichert.',
opensBrowser: '# → öffnet den Browser',
loggedIn: '# ✓ Angemeldet als you@example.com',
note: 'Hinweis',
noBrowser: 'Auf Servern ohne Browser nutze <code>filebase auth login --device</code> für den Gerätecode-Flow.',
syncFolder: 'Ordner synchronisieren',
pickLocal: 'Wähle ein lokales Verzeichnis und verknüpfe es mit einem Remote-Root. Filebase beobachtet Änderungen und sendet blockbasierte Diffs im Hintergrund.',
excludingFiles: 'Dateien ausschließen',
addIgnore: 'Lege eine <code>.filebaseignore</code> im Root des Sync-Ordners an. Die Syntax entspricht <code>.gitignore</code>:',
whereNext: 'Nächste Schritte',
readConflict: 'Lies <a href="#">Konfliktlösung</a>, um parallele Bearbeitungen zu verstehen, oder gehe zur <a href="#">CLI-Referenz</a> mit allen Unterbefehlen.',
previous: 'Zurück',
next: 'Weiter',
},
fr: {
howItWorks: 'Fonctionnement',
viewOnGithub: 'Voir sur GitHub',
quickstart: 'Démarrage rapide',
gettingStarted: 'Premiers pas',
docs: 'Documentation',
onThisPage: 'Sur cette page',
roadmap: 'Feuille de route',
showcase: 'Exemples',
search: 'Recherche',
concepts: 'Concepts',
authentication: 'Authentification',
syncEngine: 'Moteur de synchronisation',
blockLevelDeltas: 'Deltas par bloc',
conflictResolution: 'Résolution des conflits',
resumableUploads: 'Uploads reprenables',
install: 'Installation',
configuration: 'Configuration',
subcommands: 'Sous-commandes',
syncFirstFolder: 'Synchronisez votre premier dossier en moins de cinq minutes',
installCli: 'Installer la CLI',
cliDistributed: 'La CLI est distribuée comme un binaire unique pour macOS, Linux et Windows.',
verifyInstall: "Vérifier l'installation :",
authenticateStep: "S'authentifier",
signIn: 'Connectez-vous avec votre compte Filebase. Le jeton est stocké dans <code>~/.config/filebase/credentials</code>.',
opensBrowser: '# → ouvre votre navigateur',
loggedIn: '# ✓ Connecté en tant que you@example.com',
note: 'Note',
noBrowser: 'Sur un serveur sans navigateur, utilisez <code>filebase auth login --device</code> pour le flux par code appareil.',
syncFolder: 'Synchroniser un dossier',
pickLocal: 'Choisissez un dossier local et liez-le à une racine distante. Filebase surveille les changements et pousse les différences par bloc en arrière-plan.',
excludingFiles: 'Exclure des fichiers',
addIgnore: 'Ajoutez un <code>.filebaseignore</code> à la racine du dossier synchronisé. Même syntaxe que <code>.gitignore</code> :',
whereNext: 'Étapes suivantes',
readConflict: 'Lisez <a href="#">Résolution des conflits</a> ou ouvrez la <a href="#">référence CLI</a> pour la liste complète des sous-commandes.',
previous: 'Précédent',
next: 'Suivant',
},
ru: {
howItWorks: 'Как это работает',
viewOnGithub: 'Посмотреть на GitHub',
quickstart: 'Быстрый старт',
gettingStarted: 'Начало работы',
docs: 'Документация',
onThisPage: 'На этой странице',
roadmap: 'Дорожная карта',
showcase: 'Примеры',
search: 'Поиск',
concepts: 'Концепции',
authentication: 'Аутентификация',
syncEngine: 'Движок синхронизации',
blockLevelDeltas: 'Блочные изменения',
conflictResolution: 'Разрешение конфликтов',
resumableUploads: 'Возобновляемые загрузки',
install: 'Установка',
configuration: 'Настройка',
subcommands: 'Подкоманды',
syncFirstFolder: 'Синхронизируйте первую папку менее чем за пять минут',
installCli: 'Установите CLI',
cliDistributed: 'CLI распространяется как один бинарный файл для macOS, Linux и Windows.',
verifyInstall: 'Проверьте установку:',
authenticateStep: 'Войдите',
signIn: 'Войдите в аккаунт Filebase. Токен хранится в <code>~/.config/filebase/credentials</code>.',
opensBrowser: '# → открывает браузер',
loggedIn: '# ✓ вход выполнен как you@example.com',
note: 'Примечание',
noBrowser: 'На серверах без браузера используйте <code>filebase auth login --device</code> для входа по коду устройства.',
syncFolder: 'Синхронизируйте папку',
pickLocal: 'Выберите локальный каталог и привяжите его к удаленному корню. Filebase отслеживает изменения и отправляет блочные различия в фоне.',
excludingFiles: 'Исключение файлов',
addIgnore: 'Добавьте <code>.filebaseignore</code> в корень синхронизируемой папки. Синтаксис такой же, как у <code>.gitignore</code>:',
whereNext: 'Что дальше',
readConflict: 'Прочитайте <a href="#">разрешение конфликтов</a> или перейдите к <a href="#">справке CLI</a> со всеми подкомандами.',
previous: 'Назад',
next: 'Далее',
},
es: {
howItWorks: 'Cómo funciona',
viewOnGithub: 'Ver en GitHub',
quickstart: 'Inicio rápido',
gettingStarted: 'Primeros pasos',
docs: 'Documentación',
onThisPage: 'En esta página',
roadmap: 'Hoja de ruta',
showcase: 'Ejemplos',
search: 'Buscar',
concepts: 'Conceptos',
authentication: 'Autenticación',
syncEngine: 'Motor de sincronización',
blockLevelDeltas: 'Deltas por bloque',
conflictResolution: 'Resolución de conflictos',
resumableUploads: 'Subidas reanudables',
install: 'Instalación',
configuration: 'Configuración',
subcommands: 'Subcomandos',
syncFirstFolder: 'Sincroniza tu primera carpeta en menos de cinco minutos',
installCli: 'Instala la CLI',
cliDistributed: 'La CLI se distribuye como un único binario para macOS, Linux y Windows.',
verifyInstall: 'Verifica la instalación:',
authenticateStep: 'Autentícate',
signIn: 'Inicia sesión con tu cuenta de Filebase. El token se guarda en <code>~/.config/filebase/credentials</code>.',
opensBrowser: '# → abre tu navegador',
loggedIn: '# ✓ Sesión iniciada como you@example.com',
note: 'Nota',
noBrowser: 'En servidores sin navegador, usa <code>filebase auth login --device</code> para el flujo con código de dispositivo.',
syncFolder: 'Sincroniza una carpeta',
pickLocal: 'Elige un directorio local y enlázalo con una raíz remota. Filebase observa cambios y envía diferencias por bloque en segundo plano.',
excludingFiles: 'Excluir archivos',
addIgnore: 'Agrega <code>.filebaseignore</code> en la raíz de la carpeta sincronizada. Misma sintaxis que <code>.gitignore</code>:',
whereNext: 'Siguiente paso',
readConflict: 'Lee <a href="#">resolución de conflictos</a> o abre la <a href="#">referencia CLI</a> con todos los subcomandos.',
previous: 'Anterior',
next: 'Siguiente',
},
'pt-br': {
howItWorks: 'Como funciona',
viewOnGithub: 'Ver no GitHub',
quickstart: 'Início rápido',
gettingStarted: 'Primeiros passos',
docs: 'Documentação',
onThisPage: 'Nesta página',
roadmap: 'Roteiro',
showcase: 'Exemplos',
search: 'Buscar',
concepts: 'Conceitos',
authentication: 'Autenticação',
syncEngine: 'Motor de sincronização',
blockLevelDeltas: 'Deltas por bloco',
conflictResolution: 'Resolução de conflitos',
resumableUploads: 'Uploads retomáveis',
install: 'Instalação',
configuration: 'Configuração',
subcommands: 'Subcomandos',
syncFirstFolder: 'Sincronize sua primeira pasta em menos de cinco minutos',
installCli: 'Instale a CLI',
cliDistributed: 'A CLI é distribuída como um binário único para macOS, Linux e Windows.',
verifyInstall: 'Verifique a instalação:',
authenticateStep: 'Autentique-se',
signIn: 'Entre com sua conta Filebase. O token fica em <code>~/.config/filebase/credentials</code>.',
opensBrowser: '# → abre seu navegador',
loggedIn: '# ✓ Login feito como you@example.com',
note: 'Nota',
noBrowser: 'Em servidores sem navegador, use <code>filebase auth login --device</code> para o fluxo por código de dispositivo.',
syncFolder: 'Sincronize uma pasta',
pickLocal: 'Escolha um diretório local e conecte-o a uma raiz remota. Filebase observa alterações e envia diffs por bloco em segundo plano.',
excludingFiles: 'Excluir arquivos',
addIgnore: 'Adicione <code>.filebaseignore</code> na raiz da pasta sincronizada. Mesma sintaxe de <code>.gitignore</code>:',
whereNext: 'Próximo passo',
readConflict: 'Leia <a href="#">resolução de conflitos</a> ou abra a <a href="#">referência da CLI</a> com todos os subcomandos.',
previous: 'Anterior',
next: 'Próximo',
},
it: {
howItWorks: 'Come funziona',
viewOnGithub: 'Vedi su GitHub',
quickstart: 'Avvio rapido',
gettingStarted: 'Primi passi',
docs: 'Documentazione',
onThisPage: 'In questa pagina',
roadmap: 'Tabella di marcia',
showcase: 'Esempi',
search: 'Cerca',
concepts: 'Concetti',
authentication: 'Autenticazione',
syncEngine: 'Motore di sincronizzazione',
blockLevelDeltas: 'Delta per blocco',
conflictResolution: 'Risoluzione dei conflitti',
resumableUploads: 'Upload riprendibili',
install: 'Installazione',
configuration: 'Configurazione',
subcommands: 'Sottocomandi',
syncFirstFolder: 'Sincronizza la prima cartella in meno di cinque minuti',
installCli: 'Installa la CLI',
cliDistributed: 'La CLI è distribuita come binario unico per macOS, Linux e Windows.',
verifyInstall: "Verifica l'installazione:",
authenticateStep: 'Autenticati',
signIn: "Accedi con il tuo account Filebase. Il token viene salvato in <code>~/.config/filebase/credentials</code>.",
opensBrowser: '# → apre il browser',
loggedIn: '# ✓ Accesso effettuato come you@example.com',
note: 'Nota',
noBrowser: 'Su server senza browser usa <code>filebase auth login --device</code> per il flusso con codice dispositivo.',
syncFolder: 'Sincronizza una cartella',
pickLocal: 'Scegli una directory locale e collegala a una radice remota. Filebase osserva le modifiche e invia differenze per blocco in background.',
excludingFiles: 'Escludere file',
addIgnore: 'Aggiungi <code>.filebaseignore</code> alla radice della cartella sincronizzata. Stessa sintassi di <code>.gitignore</code>:',
whereNext: 'Prossimo passo',
readConflict: 'Leggi <a href="#">risoluzione dei conflitti</a> o apri la <a href="#">referenza CLI</a> con tutti i sottocomandi.',
previous: 'Precedente',
next: 'Successivo',
},
vi: {
howItWorks: 'Cách hoạt động',
viewOnGithub: 'Xem trên GitHub',
quickstart: 'Bắt đầu nhanh',
gettingStarted: 'Bắt đầu',
docs: 'Tài liệu',
onThisPage: 'Trong trang này',
roadmap: 'Lộ trình',
showcase: 'Ví dụ',
search: 'Tìm kiếm',
concepts: 'Khái niệm',
authentication: 'Xác thực',
syncEngine: 'Bộ máy đồng bộ',
blockLevelDeltas: 'Delta theo khối',
conflictResolution: 'Giải quyết xung đột',
resumableUploads: 'Tải lên có thể tiếp tục',
install: 'Cài đặt',
configuration: 'Cấu hình',
subcommands: 'Lệnh con',
syncFirstFolder: 'Đồng bộ thư mục đầu tiên trong chưa đầy năm phút',
installCli: 'Cài đặt CLI',
cliDistributed: 'CLI được phân phối dưới dạng một binary cho macOS, Linux và Windows.',
verifyInstall: 'Kiểm tra cài đặt:',
authenticateStep: 'Xác thực',
signIn: 'Đăng nhập bằng tài khoản Filebase. Token được lưu tại <code>~/.config/filebase/credentials</code>.',
opensBrowser: '# → mở trình duyệt',
loggedIn: '# ✓ Đã đăng nhập dưới tên you@example.com',
note: 'Ghi chú',
noBrowser: 'Trên máy chủ không có trình duyệt, dùng <code>filebase auth login --device</code> cho luồng mã thiết bị.',
syncFolder: 'Đồng bộ thư mục',
pickLocal: 'Chọn một thư mục cục bộ và liên kết với gốc từ xa. Filebase theo dõi thay đổi và đẩy diff theo khối ở nền.',
excludingFiles: 'Loại trừ tệp',
addIgnore: 'Thêm <code>.filebaseignore</code> ở gốc thư mục đồng bộ. Cú pháp giống <code>.gitignore</code>:',
whereNext: 'Bước tiếp theo',
readConflict: 'Đọc <a href="#">giải quyết xung đột</a> hoặc mở <a href="#">tham chiếu CLI</a> để xem toàn bộ lệnh con.',
previous: 'Trước',
next: 'Tiếp',
},
pl: {
howItWorks: 'Jak to działa',
viewOnGithub: 'Zobacz na GitHub',
quickstart: 'Szybki start',
gettingStarted: 'Pierwsze kroki',
docs: 'Dokumentacja',
onThisPage: 'Na tej stronie',
roadmap: 'Plan',
showcase: 'Przykłady',
search: 'Szukaj',
concepts: 'Koncepcje',
authentication: 'Uwierzytelnianie',
syncEngine: 'Silnik synchronizacji',
blockLevelDeltas: 'Zmiany blokowe',
conflictResolution: 'Rozwiązywanie konfliktów',
resumableUploads: 'Wznawialne uploady',
install: 'Instalacja',
configuration: 'Konfiguracja',
subcommands: 'Podkomendy',
syncFirstFolder: 'Zsynchronizuj pierwszy folder w mniej niż pięć minut',
installCli: 'Zainstaluj CLI',
cliDistributed: 'CLI jest dystrybuowane jako pojedynczy plik binarny dla macOS, Linux i Windows.',
verifyInstall: 'Sprawdź instalację:',
authenticateStep: 'Uwierzytelnij się',
signIn: 'Zaloguj się kontem Filebase. Token jest zapisany w <code>~/.config/filebase/credentials</code>.',
opensBrowser: '# → otwiera przeglądarkę',
loggedIn: '# ✓ Zalogowano jako you@example.com',
note: 'Uwaga',
noBrowser: 'Na serwerach bez przeglądarki użyj <code>filebase auth login --device</code> dla przepływu z kodem urządzenia.',
syncFolder: 'Synchronizuj folder',
pickLocal: 'Wybierz lokalny katalog i połącz go ze zdalnym katalogiem głównym. Filebase obserwuje zmiany i wysyła różnice blokowe w tle.',
excludingFiles: 'Wykluczanie plików',
addIgnore: 'Dodaj <code>.filebaseignore</code> w katalogu głównym synchronizowanego folderu. Składnia jak w <code>.gitignore</code>:',
whereNext: 'Co dalej',
readConflict: 'Przeczytaj <a href="#">rozwiązywanie konfliktów</a> albo otwórz <a href="#">referencję CLI</a> z pełną listą podkomend.',
previous: 'Poprzednia',
next: 'Następna',
},
id: {
howItWorks: 'Cara kerja',
viewOnGithub: 'Lihat di GitHub',
quickstart: 'Mulai cepat',
gettingStarted: 'Mulai',
docs: 'Dokumentasi',
onThisPage: 'Di halaman ini',
roadmap: 'Peta jalan',
showcase: 'Contoh',
search: 'Cari',
concepts: 'Konsep',
authentication: 'Autentikasi',
syncEngine: 'Mesin sinkronisasi',
blockLevelDeltas: 'Delta per blok',
conflictResolution: 'Resolusi konflik',
resumableUploads: 'Unggahan yang dapat dilanjutkan',
install: 'Instalasi',
configuration: 'Konfigurasi',
subcommands: 'Subperintah',
syncFirstFolder: 'Sinkronkan folder pertama dalam kurang dari lima menit',
installCli: 'Instal CLI',
cliDistributed: 'CLI didistribusikan sebagai satu binary untuk macOS, Linux, dan Windows.',
verifyInstall: 'Verifikasi instalasi:',
authenticateStep: 'Autentikasi',
signIn: 'Masuk dengan akun Filebase. Token disimpan di <code>~/.config/filebase/credentials</code>.',
opensBrowser: '# → membuka browser',
loggedIn: '# ✓ Masuk sebagai you@example.com',
note: 'Catatan',
noBrowser: 'Di server tanpa browser, gunakan <code>filebase auth login --device</code> untuk alur kode perangkat.',
syncFolder: 'Sinkronkan folder',
pickLocal: 'Pilih direktori lokal dan hubungkan ke root remote. Filebase memantau perubahan dan mengirim diff per blok di latar belakang.',
excludingFiles: 'Mengecualikan file',
addIgnore: 'Tambahkan <code>.filebaseignore</code> di root folder yang disinkronkan. Sintaksnya sama dengan <code>.gitignore</code>:',
whereNext: 'Langkah berikutnya',
readConflict: 'Baca <a href="#">resolusi konflik</a> atau buka <a href="#">referensi CLI</a> untuk daftar lengkap subperintah.',
previous: 'Sebelumnya',
next: 'Berikutnya',
},
nl: {
howItWorks: 'Hoe het werkt',
viewOnGithub: 'Bekijk op GitHub',
quickstart: 'Snelstart',
gettingStarted: 'Aan de slag',
docs: 'Documentatie',
onThisPage: 'Op deze pagina',
roadmap: 'Routekaart',
showcase: 'Voorbeelden',
search: 'Zoeken',
concepts: 'Concepten',
authentication: 'Authenticatie',
syncEngine: 'Synchronisatie-engine',
blockLevelDeltas: 'Blok-deltas',
conflictResolution: 'Conflictoplossing',
resumableUploads: 'Hervatbare uploads',
install: 'Installatie',
configuration: 'Configuratie',
subcommands: 'Subcommandos',
syncFirstFolder: 'Synchroniseer je eerste map binnen vijf minuten',
installCli: 'Installeer de CLI',
cliDistributed: 'De CLI wordt geleverd als één binary voor macOS, Linux en Windows.',
verifyInstall: 'Controleer de installatie:',
authenticateStep: 'Authenticeer',
signIn: 'Log in met je Filebase-account. Het token wordt opgeslagen in <code>~/.config/filebase/credentials</code>.',
opensBrowser: '# → opent je browser',
loggedIn: '# ✓ Ingelogd als you@example.com',
note: 'Opmerking',
noBrowser: 'Gebruik op servers zonder browser <code>filebase auth login --device</code> voor een apparaatcode-flow.',
syncFolder: 'Synchroniseer een map',
pickLocal: 'Kies een lokale map en koppel die aan een remote root. Filebase bewaakt wijzigingen en stuurt blok-diffs op de achtergrond.',
excludingFiles: 'Bestanden uitsluiten',
addIgnore: 'Voeg <code>.filebaseignore</code> toe aan de root van de gesynchroniseerde map. Zelfde syntaxis als <code>.gitignore</code>:',
whereNext: 'Volgende stap',
readConflict: 'Lees <a href="#">conflictoplossing</a> of open de <a href="#">CLI-referentie</a> voor alle subcommandos.',
previous: 'Vorige',
next: 'Volgende',
},
ar: {
howItWorks: 'كيف يعمل',
viewOnGithub: 'عرض على GitHub',
quickstart: 'بدء سريع',
gettingStarted: 'البدء',
docs: 'الوثائق',
onThisPage: 'في هذه الصفحة',
roadmap: 'خارطة الطريق',
showcase: 'أمثلة',
search: 'بحث',
concepts: 'المفاهيم',
authentication: 'المصادقة',
syncEngine: 'محرك المزامنة',
blockLevelDeltas: 'فروق على مستوى الكتل',
conflictResolution: 'حل التعارضات',
resumableUploads: 'رفع قابل للاستئناف',
install: 'التثبيت',
configuration: 'الإعداد',
subcommands: 'الأوامر الفرعية',
syncFirstFolder: 'زامن أول مجلد خلال أقل من خمس دقائق',
installCli: 'ثبّت CLI',
cliDistributed: 'يوزع CLI كملف تنفيذي واحد لنظام macOS وLinux وWindows.',
verifyInstall: 'تحقق من التثبيت:',
authenticateStep: 'المصادقة',
signIn: 'سجّل الدخول بحساب Filebase. يتم حفظ الرمز في <code>~/.config/filebase/credentials</code>.',
opensBrowser: '# → يفتح المتصفح',
loggedIn: '# ✓ تم تسجيل الدخول باسم you@example.com',
note: 'ملاحظة',
noBrowser: 'على الخوادم بلا متصفح، استخدم <code>filebase auth login --device</code> لتدفق رمز الجهاز.',
syncFolder: 'زامن مجلدا',
pickLocal: 'اختر دليلا محليا واربطه بجذر بعيد. يراقب Filebase التغييرات ويدفع فروق الكتل في الخلفية.',
excludingFiles: 'استبعاد الملفات',
addIgnore: 'أضف <code>.filebaseignore</code> في جذر المجلد المتزامن. نفس صياغة <code>.gitignore</code>:',
whereNext: 'الخطوة التالية',
readConflict: 'اقرأ <a href="#">حل التعارضات</a> أو انتقل إلى <a href="#">مرجع CLI</a> لقائمة الأوامر الفرعية.',
previous: 'السابق',
next: 'التالي',
},
tr: {
howItWorks: 'Nasıl çalışır',
viewOnGithub: 'GitHubda görüntüle',
quickstart: 'Hızlı başlangıç',
gettingStarted: 'Başlarken',
docs: 'Belgeler',
onThisPage: 'Bu sayfada',
roadmap: 'Yol haritası',
showcase: 'Örnekler',
search: 'Ara',
concepts: 'Kavramlar',
authentication: 'Kimlik doğrulama',
syncEngine: 'Senkronizasyon motoru',
blockLevelDeltas: 'Blok düzeyi deltalar',
conflictResolution: 'Çakışma çözümü',
resumableUploads: 'Sürdürülebilir yüklemeler',
install: 'Kurulum',
configuration: 'Yapılandırma',
subcommands: 'Alt komutlar',
syncFirstFolder: 'İlk klasörünüzü beş dakikadan kısa sürede eşitleyin',
installCli: 'CLI kur',
cliDistributed: 'CLI, macOS, Linux ve Windows için tek bir binary olarak dağıtılır.',
verifyInstall: 'Kurulumu doğrula:',
authenticateStep: 'Kimlik doğrula',
signIn: 'Filebase hesabınızla giriş yapın. Token <code>~/.config/filebase/credentials</code> içinde saklanır.',
opensBrowser: '# → tarayıcınızı açar',
loggedIn: '# ✓ you@example.com olarak giriş yapıldı',
note: 'Not',
noBrowser: 'Tarayıcısı olmayan sunucularda cihaz kodu akışı için <code>filebase auth login --device</code> kullanın.',
syncFolder: 'Klasör eşitle',
pickLocal: 'Yerel bir dizin seçip uzak köke bağlayın. Filebase değişiklikleri izler ve blok farklarını arka planda gönderir.',
excludingFiles: 'Dosya hariç tutma',
addIgnore: 'Eşitlenen klasörün köküne <code>.filebaseignore</code> ekleyin. Söz dizimi <code>.gitignore</code> ile aynıdır:',
whereNext: 'Sonraki adım',
readConflict: '<a href="#">Çakışma çözümü</a> bölümünü okuyun veya tüm alt komutlar için <a href="#">CLI başvurusuna</a> geçin.',
previous: 'Önceki',
next: 'Sonraki',
},
uk: {
howItWorks: 'Як це працює',
viewOnGithub: 'Переглянути на GitHub',
quickstart: 'Швидкий старт',
gettingStarted: 'Початок роботи',
docs: 'Документація',
onThisPage: 'На цій сторінці',
roadmap: 'Дорожня карта',
showcase: 'Приклади',
search: 'Пошук',
concepts: 'Концепції',
authentication: 'Автентифікація',
syncEngine: 'Рушій синхронізації',
blockLevelDeltas: 'Блокові зміни',
conflictResolution: 'Вирішення конфліктів',
resumableUploads: 'Відновлювані завантаження',
install: 'Встановлення',
configuration: 'Налаштування',
subcommands: 'Підкоманди',
syncFirstFolder: 'Синхронізуйте першу теку менш ніж за п’ять хвилин',
installCli: 'Встановіть CLI',
cliDistributed: 'CLI постачається як один виконуваний файл для macOS, Linux і Windows.',
verifyInstall: 'Перевірте встановлення:',
authenticateStep: 'Увійдіть',
signIn: 'Увійдіть у свій обліковий запис Filebase. Токен зберігається в <code>~/.config/filebase/credentials</code>.',
opensBrowser: '# → відкриває браузер',
loggedIn: '# ✓ виконано вхід як you@example.com',
note: 'Примітка',
noBrowser: 'На серверах без браузера використовуйте <code>filebase auth login --device</code> для входу через код пристрою.',
syncFolder: 'Синхронізуйте теку',
pickLocal: 'Виберіть локальну директорію та прив’яжіть її до віддаленого кореня. Filebase стежить за змінами й у фоновому режимі надсилає блокові різниці.',
excludingFiles: 'Виключення файлів',
addIgnore: 'Додайте <code>.filebaseignore</code> у корінь синхронізованої теки. Синтаксис такий самий, як у <code>.gitignore</code>:',
whereNext: 'Куди рухатися далі',
readConflict: 'Прочитайте <a href="#">вирішення конфліктів</a>, щоб зрозуміти, як Filebase об’єднує паралельні зміни, або перейдіть до <a href="#">довідника CLI</a> з повним списком підкоманд.',
previous: 'Попередня',
next: 'Наступна',
},
};
const COMMON_PLUGIN_PREVIEW_COPY: Partial<Record<LandingLocaleCode, PreviewTextMap>> = {
zh: {
'How it works': '工作方式',
'View on GitHub →': '在 GitHub 查看 →',
'View on GitHub': '在 GitHub 查看',
Quickstart: '快速开始',
'Getting started': '入门',
Docs: '文档',
'On this page': '本页内容',
},
'zh-tw': {
'How it works': '運作方式',
'View on GitHub →': '在 GitHub 查看 →',
'View on GitHub': '在 GitHub 查看',
Quickstart: '快速開始',
'Getting started': '入門',
Docs: '文件',
'On this page': '本頁內容',
},
uk: {
'How it works': 'Як це працює',
'View on GitHub →': 'Переглянути на GitHub →',
'View on GitHub': 'Переглянути на GitHub',
Quickstart: 'Швидкий старт',
'Getting started': 'Початок роботи',
Docs: 'Документація',
'On this page': 'На цій сторінці',
},
};
const FILEBASE_DOCS_PREVIEW_COPY: Partial<Record<LandingLocaleCode, PreviewTextMap>> = {
zh: {
'Filebase docs — Quickstart': 'Filebase 文档 — 快速开始',
'◰ Filebase docs': '◰ Filebase 文档',
'Search · ⌘K': '搜索 · ⌘K',
'Getting started': '入门',
Quickstart: '快速开始',
Concepts: '概念',
Authentication: '身份验证',
'Sync engine': '同步引擎',
'Block-level deltas': '块级增量',
'Conflict resolution': '冲突解决',
'Resumable uploads': '可续传上传',
Install: '安装',
Configuration: '配置',
Subcommands: '子命令',
'Docs Getting started Quickstart': '文档 入门 快速开始',
'Sync your first folder in under five minutes. The CLI is the fastest path; the desktop app and the API client all wrap the same engine.':
'五分钟内同步第一个文件夹。CLI 是最快路径;桌面端和 API 客户端都封装同一套引擎。',
'1. Install the CLI': '1. 安装 CLI',
'The CLI is distributed as a single binary for macOS, Linux, and Windows.':
'CLI 以单一二进制文件发布,支持 macOS、Linux 和 Windows。',
'Verify the install:': '验证安装:',
'2. Authenticate': '2. 登录认证',
'Sign in with your Filebase account. The token is stored in <code>~/.config/filebase/credentials</code>.':
'使用 Filebase 账号登录。令牌会存储在 <code>~/.config/filebase/credentials</code>。',
'# → opens your browser': '# → 打开浏览器',
'# ✓ Logged in as you@example.com': '# ✓ 已以 you@example.com 登录',
Note: '说明',
'On servers without a browser, use <code>filebase auth login --device</code> for a device-code flow.':
'在没有浏览器的服务器上,可以使用 <code>filebase auth login --device</code> 走设备码流程。',
'3. Sync a folder': '3. 同步文件夹',
'Pick a local directory and link it to a remote root. Filebase watches it for changes and pushes block-level diffs in the background.':
'选择一个本地目录并关联到远端根目录。Filebase 会监听变更,并在后台推送块级差异。',
'Excluding files': '排除文件',
'Add a <code>.filebaseignore</code> at the root of the synced folder. Same syntax as <code>.gitignore</code>:':
'在同步文件夹根目录添加 <code>.filebaseignore</code>。语法与 <code>.gitignore</code> 相同:',
'4. Where to go next': '4. 下一步',
'Read <a href="#">Conflict resolution</a> to understand how Filebase merges concurrent edits, or skip to the <a href="#">CLI reference</a> for the full subcommand list.':
'阅读 <a href="#">冲突解决</a> 了解 Filebase 如何合并并发编辑,或直接查看 <a href="#">CLI 参考</a> 获取完整子命令列表。',
'← Previous': '← 上一页',
'Next →': '下一页 →',
'On this page': '本页内容',
},
'zh-tw': {
'Filebase docs — Quickstart': 'Filebase 文件 — 快速開始',
'◰ Filebase docs': '◰ Filebase 文件',
'Search · ⌘K': '搜尋 · ⌘K',
'Getting started': '入門',
Quickstart: '快速開始',
Concepts: '概念',
Authentication: '身份驗證',
'Sync engine': '同步引擎',
'Block-level deltas': '區塊級增量',
'Conflict resolution': '衝突解決',
'Resumable uploads': '可續傳上傳',
Install: '安裝',
Configuration: '設定',
Subcommands: '子命令',
'Docs Getting started Quickstart': '文件 入門 快速開始',
'Sync your first folder in under five minutes. The CLI is the fastest path; the desktop app and the API client all wrap the same engine.':
'五分鐘內同步第一個資料夾。CLI 是最快路徑;桌面端和 API 客戶端都封裝同一套引擎。',
'1. Install the CLI': '1. 安裝 CLI',
'The CLI is distributed as a single binary for macOS, Linux, and Windows.':
'CLI 以單一二進位檔發布,支援 macOS、Linux 和 Windows。',
'Verify the install:': '驗證安裝:',
'2. Authenticate': '2. 登入認證',
'Sign in with your Filebase account. The token is stored in <code>~/.config/filebase/credentials</code>.':
'使用 Filebase 帳號登入。權杖會儲存在 <code>~/.config/filebase/credentials</code>。',
'# → opens your browser': '# → 開啟瀏覽器',
'# ✓ Logged in as you@example.com': '# ✓ 已以 you@example.com 登入',
Note: '說明',
'On servers without a browser, use <code>filebase auth login --device</code> for a device-code flow.':
'在沒有瀏覽器的伺服器上,可以使用 <code>filebase auth login --device</code> 走裝置碼流程。',
'3. Sync a folder': '3. 同步資料夾',
'Pick a local directory and link it to a remote root. Filebase watches it for changes and pushes block-level diffs in the background.':
'選擇一個本地目錄並連結到遠端根目錄。Filebase 會監聽變更,並在背景推送區塊級差異。',
'Excluding files': '排除檔案',
'Add a <code>.filebaseignore</code> at the root of the synced folder. Same syntax as <code>.gitignore</code>:':
'在同步資料夾根目錄新增 <code>.filebaseignore</code>。語法與 <code>.gitignore</code> 相同:',
'4. Where to go next': '4. 下一步',
'Read <a href="#">Conflict resolution</a> to understand how Filebase merges concurrent edits, or skip to the <a href="#">CLI reference</a> for the full subcommand list.':
'閱讀 <a href="#">衝突解決</a> 了解 Filebase 如何合併並行編輯,或直接查看 <a href="#">CLI 參考</a> 取得完整子命令列表。',
'← Previous': '← 上一頁',
'Next →': '下一頁 →',
'On this page': '本頁內容',
},
uk: {
'Filebase docs — Quickstart': 'Документація Filebase — Швидкий старт',
'◰ Filebase docs': '◰ Документація Filebase',
'Search · ⌘K': 'Пошук · ⌘K',
'Getting started': 'Початок роботи',
Quickstart: 'Швидкий старт',
Concepts: 'Концепції',
Authentication: 'Автентифікація',
'Sync engine': 'Рушій синхронізації',
'Block-level deltas': 'Блокові зміни',
'Conflict resolution': 'Вирішення конфліктів',
'Resumable uploads': 'Відновлювані завантаження',
Install: 'Встановлення',
Configuration: 'Налаштування',
Subcommands: 'Підкоманди',
'Docs Getting started Quickstart': 'Документація Початок роботи Швидкий старт',
'Sync your first folder in under five minutes. The CLI is the fastest path; the desktop app and the API client all wrap the same engine.':
'Синхронізуйте першу теку менш ніж за п’ять хвилин. CLI — найшвидший шлях; десктопний застосунок і API-клієнт використовують той самий рушій.',
'1. Install the CLI': '1. Встановіть CLI',
'The CLI is distributed as a single binary for macOS, Linux, and Windows.':
'CLI постачається як один виконуваний файл для macOS, Linux і Windows.',
'Verify the install:': 'Перевірте встановлення:',
'2. Authenticate': '2. Увійдіть',
'Sign in with your Filebase account. The token is stored in <code>~/.config/filebase/credentials</code>.':
'Увійдіть у свій обліковий запис Filebase. Токен зберігається в <code>~/.config/filebase/credentials</code>.',
'# → opens your browser': '# → відкриває браузер',
'# ✓ Logged in as you@example.com': '# ✓ виконано вхід як you@example.com',
Note: 'Примітка',
'On servers without a browser, use <code>filebase auth login --device</code> for a device-code flow.':
'На серверах без браузера використовуйте <code>filebase auth login --device</code> для входу через код пристрою.',
'3. Sync a folder': '3. Синхронізуйте теку',
'Pick a local directory and link it to a remote root. Filebase watches it for changes and pushes block-level diffs in the background.':
'Виберіть локальну директорію та прив’яжіть її до віддаленого кореня. Filebase стежить за змінами й у фоновому режимі надсилає блокові різниці.',
'Excluding files': 'Виключення файлів',
'Add a <code>.filebaseignore</code> at the root of the synced folder. Same syntax as <code>.gitignore</code>:':
'Додайте <code>.filebaseignore</code> у корінь синхронізованої теки. Синтаксис такий самий, як у <code>.gitignore</code>:',
'4. Where to go next': '4. Куди рухатися далі',
'Read <a href="#">Conflict resolution</a> to understand how Filebase merges concurrent edits, or skip to the <a href="#">CLI reference</a> for the full subcommand list.':
'Прочитайте <a href="#">вирішення конфліктів</a>, щоб зрозуміти, як Filebase об’єднує паралельні зміни, або перейдіть до <a href="#">довідника CLI</a> з повним списком підкоманд.',
'← Previous': '← Попередня',
'Next →': 'Наступна →',
'On this page': 'На цій сторінці',
},
};
const generatedCommonPreviewCopy = (locale: LandingLocaleCode): PreviewTextMap => {
if (locale === DEFAULT_LOCALE) return {};
const terms = PREVIEW_TERMS[locale as Exclude<LandingLocaleCode, 'en'>];
if (!terms) return {};
return {
'How it works': terms.howItWorks,
'View on GitHub →': `${terms.viewOnGithub} →`,
'View on GitHub': terms.viewOnGithub,
Quickstart: terms.quickstart,
'Getting started': terms.gettingStarted,
Docs: terms.docs,
'On this page': terms.onThisPage,
Roadmap: terms.roadmap,
Showcase: terms.showcase,
};
};
const generatedFilebaseDocsPreviewCopy = (locale: LandingLocaleCode): PreviewTextMap => {
if (locale === DEFAULT_LOCALE) return {};
const terms = PREVIEW_TERMS[locale as Exclude<LandingLocaleCode, 'en'>];
if (!terms) return {};
return {
'Filebase docs — Quickstart': `Filebase ${terms.docs} — ${terms.quickstart}`,
'◰ Filebase docs': `◰ Filebase ${terms.docs}`,
'Search · ⌘K': `${terms.search} · ⌘K`,
'Getting started': terms.gettingStarted,
Quickstart: terms.quickstart,
Concepts: terms.concepts,
Authentication: terms.authentication,
'Sync engine': terms.syncEngine,
'Block-level deltas': terms.blockLevelDeltas,
'Conflict resolution': terms.conflictResolution,
'Resumable uploads': terms.resumableUploads,
Install: terms.install,
Configuration: terms.configuration,
Subcommands: terms.subcommands,
'Docs Getting started Quickstart':
`${terms.docs} ${terms.gettingStarted} ${terms.quickstart}`,
'Sync your first folder in under five minutes. The CLI is the fastest path; the desktop app and the API client all wrap the same engine.':
`${terms.syncFirstFolder}. Filebase CLI / desktop / API.`,
'1. Install the CLI': `1. ${terms.installCli}`,
'The CLI is distributed as a single binary for macOS, Linux, and Windows.':
terms.cliDistributed,
'Verify the install:': terms.verifyInstall,
'2. Authenticate': `2. ${terms.authenticateStep}`,
'Sign in with your Filebase account. The token is stored in <code>~/.config/filebase/credentials</code>.':
terms.signIn,
'# → opens your browser': terms.opensBrowser,
'# ✓ Logged in as you@example.com': terms.loggedIn,
Note: terms.note,
'On servers without a browser, use <code>filebase auth login --device</code> for a device-code flow.':
terms.noBrowser,
'3. Sync a folder': `3. ${terms.syncFolder}`,
'Pick a local directory and link it to a remote root. Filebase watches it for changes and pushes block-level diffs in the background.':
terms.pickLocal,
'Excluding files': terms.excludingFiles,
'Add a <code>.filebaseignore</code> at the root of the synced folder. Same syntax as <code>.gitignore</code>:':
terms.addIgnore,
'4. Where to go next': `4. ${terms.whereNext}`,
'Read <a href="#">Conflict resolution</a> to understand how Filebase merges concurrent edits, or skip to the <a href="#">CLI reference</a> for the full subcommand list.':
terms.readConflict,
'← Previous': `← ${terms.previous}`,
'Next →': `${terms.next} →`,
'On this page': terms.onThisPage,
};
};
function localizePluginPreviewHtml(html: string, locale: LandingLocaleCode): string {
if (locale === DEFAULT_LOCALE) return html;
const copy = {
...generatedCommonPreviewCopy(locale),
...(COMMON_PLUGIN_PREVIEW_COPY[locale] ?? {}),
...(html.includes('Filebase docs') ? generatedFilebaseDocsPreviewCopy(locale) : {}),
...(html.includes('Filebase docs') ? FILEBASE_DOCS_PREVIEW_COPY[locale] ?? {} : {}),
};
if (Object.keys(copy).length === 0) return html;
const localeDef = getLocaleDefinition(locale);
let localized = html.replace(
'<html lang="en">',
`<html lang="${localeDef.htmlLang}" dir="${localeDef.dir}">`,
);
for (const [source, target] of Object.entries(copy).sort(
([left], [right]) => right.length - left.length,
)) {
localized = localized.replaceAll(source, target);
}
return localized;
}
export function getStaticPaths() {
return getPublicPlugins()
@ -14,8 +959,9 @@ export function getStaticPaths() {
.filter((entry) => Boolean(entry.props.html));
}
const { html } = Astro.props as { html: string };
const pageHtml = injectGoogleAnalytics(html);
const { html: rawHtml } = Astro.props as { html: string };
const locale = localeFromPath(Astro.url.pathname);
const html = localizePluginPreviewHtml(rawHtml, locale);
---
<Fragment set:html={pageHtml} />
<Fragment set:html={html} />

View file

@ -2,68 +2,44 @@
/*
* /quickstart/ — install-and-run intent.
*
* Captures "open design install", "open design quickstart", "open-design
* getting started", "od cli" queries and lands them directly on a page
* that gives commands, expected output, and the troubleshooting checklist
* most users need.
* Captures "open design install", "open design quickstart",
* "open-design getting started", "od cli" queries and lands them
* directly on a page that gives commands, expected output, and the
* troubleshooting checklist most users need.
*
* HowTo JSON-LD describes the three visible install steps; the rest of
* the page is reference material that complements the GitHub README
* without duplicating it.
* HowTo JSON-LD describes the three visible install steps; the rest
* of the page is reference material that complements the GitHub
* README without duplicating it.
*/
import Layout from '../../_components/sub-page-layout.astro';
import { getGithubRepoMeta } from '../../_lib/github';
import { getInfoPageCopy } from '../../info-page-i18n';
import { localeFromPath, localizedHref } from '../../i18n';
import { heroImage } from '../../image-assets';
const github = await getGithubRepoMeta();
const locale = localeFromPath(Astro.url.pathname);
const href = (path: string) => localizedHref(path, locale);
const copy = getInfoPageCopy(locale);
const page = copy.quickstart;
const common = copy.common;
// Derive SITE from Astro.site so preview/staging JSON-LD matches the
// layout's canonical link. Falls back to the production origin.
const SITE = Astro.site?.toString() ?? 'https://open-design.ai/';
const REPO = 'https://github.com/nexu-io/open-design';
const REPO_RELEASES = `${REPO}/releases`;
const DISCORD = 'https://discord.gg/9ptkbbqRu';
const QUICKSTART_DOC = `${REPO}/blob/main/QUICKSTART.md`;
const github = await getGithubRepoMeta();
// Google's HowTo rich result is image-biased: a HowTo with no images is
// near-invisible in the SERP. We attach the canonical homepage hero as
// the top-level HowTo image and reuse it per-step as a fallback until
// we render per-step screenshots through `scripts/generate-previews.ts`.
const HOWTO_IMAGE = heroImage;
const title = 'Open Design quickstart — Install in three commands (Node 24, pnpm)';
const description =
'Install Open Design locally with three commands. Requirements (Node 24, pnpm 10.33.2), commands, expected output, troubleshooting, and how to generate your first design artifact with Claude Code, Codex, Cursor, Gemini, OpenCode, or Qwen.';
const requirements = [
{ label: 'Node.js 24', body: 'install via your platform package manager or nodejs.org. Node 22 is not supported.' },
{ label: 'pnpm 10.33.2', body: 'enabled through Corepack so the lockfile-pinned version is used.' },
{ label: 'git', body: 'any recent version.' },
{ label: 'An agent', body: 'Claude Code, Codex, Cursor, Gemini CLI, OpenCode, or Qwen.' },
];
const steps = [
{
name: 'Clone and install',
text: 'Clone the open-design repository and install workspace dependencies with pnpm. Requires Node 24 and pnpm 10.33.2.',
code: 'git clone https://github.com/nexu-io/open-design\ncd open-design\npnpm install',
},
{
name: 'Start the daemon and web UI',
text: 'Run tools-dev to start the local daemon and web runtime. This is the only lifecycle entry point.',
code: 'pnpm tools-dev',
},
{
name: 'Generate your first artifact',
text: 'Open the web UI, pick a skill from the catalog, and let your agent render it. Or drive the daemon directly with the od CLI.',
code: 'od skill run open-design-landing --output ./artifact.html',
},
];
const troubleshooting = [
{ label: 'EBADENGINE on pnpm install', body: 'wrong Node major. Switch to Node 24.' },
{ label: 'better-sqlite3 build hangs on Windows', body: 'expected on Node 24; install Visual Studio Build Tools first.' },
{ label: 'Port already in use', body: 'pass --daemon-port and --web-port, or stop the previous run.' },
{ label: 'Agent does not show up', body: 'check /agents/ and your .od/media-config.json credentials.' },
{ label: 'Permission prompt loops', body: 'pnpm tools-dev check verifies the environment and prints missing setup.' },
];
const nextItems = [
{ label: 'Browse the skill catalog', href: '/skills/', body: 'and pick one to render.' },
{ label: 'Pick a DESIGN.md system', href: '/systems/', body: 'so generated artifacts inherit a brand.' },
{ label: 'Compare Open Design', href: '/compare/', body: 'with Claude Design, Figma Make, v0, and Lovable.' },
{ label: 'Subscribe to GitHub releases', href: REPO_RELEASES, body: 'for new versions.' },
];
const title = page.title;
const description = page.description;
const steps = page.steps;
const jsonLd = [
{
@ -71,109 +47,124 @@ const jsonLd = [
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: SITE },
{ '@type': 'ListItem', position: 2, name: 'Quickstart', item: `${SITE}quickstart/` },
{ '@type': 'ListItem', position: 2, name: page.breadcrumb, item: `${SITE}quickstart/` },
],
},
{
'@context': 'https://schema.org',
'@type': 'HowTo',
name: 'Install Open Design in three commands',
name: page.heading,
description,
totalTime: 'PT5M',
// Google's HowTo rich result requires an image at the top level
// for SERP eligibility. Until per-step screenshots ship, we attach
// the canonical hero image at both the root and each step.
image: { '@type': 'ImageObject', url: HOWTO_IMAGE },
tool: [
{ '@type': 'HowToTool', name: 'Node.js 24' },
{ '@type': 'HowToTool', name: 'pnpm 10.33.2 (Corepack)' },
{ '@type': 'HowToTool', name: 'git' },
],
step: steps.map((s, idx) => ({
'@type': 'HowToStep',
position: idx + 1,
name: s.name,
text: s.text,
image: { '@type': 'ImageObject', url: HOWTO_IMAGE },
url: `${SITE}quickstart/#commands`,
})),
},
];
---
<Layout title={title} description={description} active="home" jsonLd={jsonLd}>
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/">Open Design</a>
<nav class="breadcrumb" aria-label={common.breadcrumbAria}>
<a href={href('/')}>Open Design</a>
<span>/</span>
<span aria-current="page">Quickstart</span>
<span aria-current="page">{page.breadcrumb}</span>
</nav>
<article class="info-page">
<header class="catalog-head">
<span class="label">Install · Nº 01</span>
<h1 class="display">Open Design quickstart<span class="dot">.</span></h1>
<span class="label">{page.label}</span>
<h1 class="display">{page.heading}</h1>
<p class="lead">
Open Design runs entirely on your machine. Three commands gets you from a clean checkout to a running daemon, web UI, and your first generated design artifact.
{page.lead} {page.latestRelease}{' '}
<a class="inline-link" href={REPO_RELEASES} target="_blank" rel="noreferrer noopener">{github.versionLabel}</a>.
</p>
<p>Latest stable release: <a class="inline-link" href={REPO_RELEASES} target="_blank" rel="noreferrer noopener">{github.versionLabel}</a></p>
</header>
<section class="info-section">
<h2>Requirements</h2>
<section class="info-section" id="requirements">
<h2>{page.requirementsTitle}</h2>
<ul>
{requirements.map((r) => (
<li><strong>{r.label}</strong> — {r.body}</li>
{page.requirements.map((item) => (
<li><strong>{item.label}</strong> — {item.body}</li>
))}
</ul>
</section>
<section class="info-section">
<h2>Three commands to ship</h2>
<p>Run these commands from a clean shell:</p>
{steps.map((s, idx) => (
<>
<h3>{idx + 1}. {s.name}</h3>
<p>{s.text}</p>
<code class="code-block">{s.code}</code>
</>
))}
<p>Full notes live in <a class="inline-link" href={`${REPO}/blob/main/QUICKSTART.md`} target="_blank" rel="noreferrer noopener">QUICKSTART.md</a>.</p>
<section class="info-section" id="commands">
<h2>{page.commandsTitle}</h2>
<p>{page.commandsLead}</p>
<ol>
{steps.map((s) => (
<li>
<strong>{s.name}.</strong> {s.text}
<pre class="code-block"><code>{s.code}</code></pre>
</li>
))}
</ol>
<p>
<a class="inline-link" href={QUICKSTART_DOC} target="_blank" rel="noreferrer noopener">{page.fullNotes}</a>
</p>
</section>
<section class="info-section">
<h2>What you should see</h2>
<p>When <code>pnpm tools-dev</code> is healthy, the terminal reports the daemon, web runtime, and sidecar IPC namespace as ready.</p>
<p>The exact ports come from your <code>tools-dev</code> flags (<code>--daemon-port</code>, <code>--web-port</code>); defaults are stable across runs.</p>
<section class="info-section" id="expected-output">
<h2>{page.expectedTitle}</h2>
<p>{page.expectedBody}</p>
<pre class="code-block"><code><span class="comment"># tools-dev — startup</span>
<span class="prompt">→</span> daemon listening on http://127.0.0.1:17456 (namespace tools-dev/main)
<span class="prompt">→</span> web listening on http://127.0.0.1:17573 (proxy → daemon)
<span class="prompt">→</span> sidecar /tmp/open-design/ipc/tools-dev-main/daemon.sock
<span class="prompt">→</span> ready in 1.4s</code></pre>
<p>{page.expectedPorts}</p>
</section>
<section class="info-section">
<h2>Troubleshooting</h2>
<section class="info-section" id="troubleshooting">
<h2>{page.troubleshootingTitle}</h2>
<ul>
{troubleshooting.map((t) => (
<li><strong>{t.label}</strong> — {t.body}</li>
{page.troubleshooting.map((item) => (
<li><strong>{item.label}:</strong> {item.body}</li>
))}
</ul>
</section>
<section class="info-section">
<h2>Next steps</h2>
<section class="info-section" id="next">
<h2>{page.nextTitle}</h2>
<ul>
{nextItems.map((n) => {
const isExt = n.href.startsWith('http');
return (
<li>
<a class="inline-link" href={n.href} target={isExt ? '_blank' : undefined} rel={isExt ? 'noreferrer noopener' : undefined}>
{n.label}
</a> — {n.body}
</li>
);
})}
<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('/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>
</section>
{/* Closing CTA — keeps the documentation-heavy page from ending in
* silence and gives Google a clear conversion signal. */}
<section class="info-cta" aria-label="Open Design call to action">
<div>
<h2>Three commands. <em>Yours to keep.</em></h2>
<p>You have the install path. Star the repo, grab the desktop build, or join Discord if anything breaks on first run.</p>
<h2>{page.ctaTitle}</h2>
<p>{page.ctaBody}</p>
</div>
<div class="info-cta-actions">
<a class="btn btn-primary" href={REPO} target="_blank" rel="noreferrer noopener">Star on GitHub</a>
<a class="btn btn-ghost" href={REPO_RELEASES} target="_blank" rel="noreferrer noopener">Download desktop</a>
<a class="btn btn-ghost" href={DISCORD} target="_blank" rel="noreferrer noopener">Join Discord</a>
<a class="btn btn-primary" href="https://github.com/nexu-io/open-design" target="_blank" rel="noreferrer noopener">{common.starOnGithub}</a>
<a class="btn btn-ghost" href={REPO_RELEASES} target="_blank" rel="noreferrer noopener">{common.downloadDesktop}</a>
<a class="btn btn-ghost" href="https://discord.gg/9ptkbbqRu" target="_blank" rel="noreferrer noopener">{common.joinDiscord}</a>
</div>
<div class="info-cta-meta">
<span class="stamp">● Local-first</span>
<span>BYOK · Apache-2.0</span>
<span>macOS · Windows · Linux</span>
<span class="stamp">● {common.live}</span>
<span>{github.versionLabel} · Apache-2.0</span>
<span>{common.macWinLinux}</span>
</div>
</section>
</article>

View file

@ -11,6 +11,7 @@
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 } from '../../i18n';
export async function getStaticPaths() {
const skills = await getSkillRecords();
@ -25,12 +26,17 @@ interface Props {
all: ReadonlyArray<SkillRecord>;
}
const { skill, all } = Astro.props as Props;
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 = `${skill.name} — Open Design skill`;
const title = ui.catalog.skills.detailTitle(skill.name);
const description = skill.description.length > 0
? skill.description
: `Open Design skill bundle: ${skill.name}.`;
: ui.catalog.skills.detailFallbackDescription(skill.name);
const related = all
.filter((s) => s.slug !== skill.slug)
@ -43,7 +49,7 @@ const jsonLd = [
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: Astro.site?.toString() },
{ '@type': 'ListItem', position: 2, name: 'Skills', item: new URL('/skills/', 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() },
],
},
@ -61,10 +67,10 @@ const jsonLd = [
---
<Layout title={title} description={description} active="skills" jsonLd={jsonLd}>
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/">Open Design</a>
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
<a href={href('/')}>Open Design</a>
<span>/</span>
<a href="/skills/">Skills</a>
<a href={href('/skills/')}>{ui.catalog.skills.detailLabel}</a>
<span>/</span>
<span aria-current="page">{skill.name}</span>
</nav>
@ -72,20 +78,20 @@ const jsonLd = [
<article class="detail">
<header class="detail-head">
<span class="label">
Skill
{ui.catalog.skills.detailLabel}
{typeof skill.featured === 'number' && (
<span class="ix">· Featured Nº {String(skill.featured).padStart(2, '0')}</span>
<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">
<a class="btn btn-primary" href={skill.source} target="_blank" rel="noopener">
View on GitHub
{ui.catalog.skills.viewOnGithub}
</a>
{skill.upstream && (
<a class="btn btn-ghost" href={skill.upstream} target="_blank" rel="noopener">
Upstream
{ui.catalog.skills.upstream}
</a>
)}
</div>
@ -99,7 +105,7 @@ const jsonLd = [
loading="priority"
/>
<figcaption>
Rendered from <code>skills/{skill.slug}/example.html</code>
{ui.catalog.skills.previewCaption(skill.slug)}
</figcaption>
</figure>
)}
@ -107,35 +113,35 @@ const jsonLd = [
<dl class="detail-meta">
{skill.mode && (
<Fragment>
<dt>Mode</dt>
<dd>{skill.mode}</dd>
<dt>{ui.catalog.skills.mode}</dt>
<dd>{skill.modeLabel ?? skill.mode}</dd>
</Fragment>
)}
{skill.scenario && (
<Fragment>
<dt>Scenario</dt>
<dd>{skill.scenario}</dd>
<dt>{ui.catalog.skills.scenario}</dt>
<dd>{skill.scenarioLabel ?? skill.scenario}</dd>
</Fragment>
)}
{skill.platform && (
<Fragment>
<dt>Platform</dt>
<dd>{skill.platform}</dd>
<dt>{ui.catalog.skills.platform}</dt>
<dd>{skill.platformLabel ?? skill.platform}</dd>
</Fragment>
)}
{skill.category && (
<Fragment>
<dt>Category</dt>
<dd>{skill.category}</dd>
<dt>{ui.catalog.systems.category}</dt>
<dd>{skill.categoryLabel ?? skill.category}</dd>
</Fragment>
)}
</dl>
{skill.triggers.length > 0 && (
<section class="detail-block">
<h2>Triggers</h2>
<h2>{ui.catalog.skills.triggers}</h2>
<p class="block-lead">
The picker matches these prompts to the skill. Copy one and adapt it to your brief.
{ui.catalog.skills.triggersLead}
</p>
<ul class="trigger-list">
{skill.triggers.map((t) => <li><code>{t}</code></li>)}
@ -145,23 +151,23 @@ const jsonLd = [
{skill.examplePrompt && (
<section class="detail-block">
<h2>Example prompt</h2>
<h2>{ui.catalog.skills.examplePrompt}</h2>
<pre class="example-prompt">{skill.examplePrompt}</pre>
</section>
)}
{related.length > 0 && (
<section class="detail-block">
<h2>Related skills</h2>
<h2>{ui.catalog.skills.related}</h2>
<ul class="related-grid">
{related.map((r) => (
<li>
<a href={`/skills/${r.slug}/`}>
<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.mode && <span class="meta-tag">{r.mode}</span>}
{r.scenario && <span class="meta-tag muted">{r.scenario}</span>}
{r.modeLabel && <span class="meta-tag">{r.modeLabel}</span>}
{r.scenarioLabel && <span class="meta-tag muted">{r.scenarioLabel}</span>}
</span>
</a>
</li>

View file

@ -15,25 +15,28 @@ import {
getSkillScenarioIndex,
tally,
} from '../../_lib/catalog';
import { getLandingUiCopy, localeFromPath, localizedHref } from '../../i18n';
const skills = await getSkillRecords();
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();
const scenarioTags = await getSkillScenarioIndex();
const modeTags = await getSkillModeIndex(locale);
const scenarioTags = await getSkillScenarioIndex(locale);
const platformTally = tally(
skills.map((s) => s.platform).filter((p): p is string => Boolean(p)),
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 = `Skills — ${skills.length} composable design capabilities | Open Design`;
const description =
'Browse the full Open Design skills catalog: 100+ file-based SKILL.md bundles spanning decks, prototypes, dashboards, mobile flows, video, and live artifacts. Each skill is a folder you drop into the daemon.';
const title = ui.catalog.skills.title(skills.length);
const description = ui.catalog.skills.description;
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Open Design Skills catalog',
name: title,
description,
url: new URL('/skills/', Astro.site).toString(),
isPartOf: {
@ -47,24 +50,22 @@ const jsonLd = {
<Layout title={title} description={description} active="skills" jsonLd={jsonLd}>
<header class="catalog-head">
<span class="label">Catalog · Nº 01</span>
<span class="label">{ui.catalog.skills.label}</span>
<h1 class="display">
<em>Skills</em> — {skills.length} composable design capabilities<span class="dot">.</span>
{ui.catalog.skills.heading(skills.length)}
</h1>
<p class="lead">
Each skill is a single folder with one <code>SKILL.md</code>. Drop it in,
restart the daemon, the picker shows it. Filter by surface, scenario,
or platform below to find the one that matches your brief.
{ui.catalog.skills.lead}
</p>
</header>
<section class="filter-strip" aria-label="Skill filters">
<section class="filter-strip" aria-label={ui.catalog.skills.allAria}>
<div class="filter-group">
<span class="filter-label">Mode</span>
<span class="filter-label">{ui.catalog.skills.mode}</span>
<ul>
{modeTags.map((tag) => (
<li>
<a class="chip chip-link" href={`/skills/mode/${tag.slug}/`}>
<a class="chip chip-link" href={href(`/skills/mode/${tag.slug}/`)}>
{tag.label}<span class="chip-num">{tag.count}</span>
</a>
</li>
@ -72,11 +73,11 @@ const jsonLd = {
</ul>
</div>
<div class="filter-group">
<span class="filter-label">Scenario</span>
<span class="filter-label">{ui.catalog.skills.scenario}</span>
<ul>
{scenarioTags.slice(0, 12).map((tag) => (
<li>
<a class="chip chip-link" href={`/skills/scenario/${tag.slug}/`}>
<a class="chip chip-link" href={href(`/skills/scenario/${tag.slug}/`)}>
{tag.label}<span class="chip-num">{tag.count}</span>
</a>
</li>
@ -85,7 +86,7 @@ const jsonLd = {
</div>
{platformTally.length > 0 && (
<div class="filter-group">
<span class="filter-label">Platform</span>
<span class="filter-label">{ui.catalog.skills.platform}</span>
<ul>
{platformTally.map(([key, count]) => (
<li>
@ -101,11 +102,11 @@ const jsonLd = {
{featured.length > 0 && (
<section class="featured-strip" aria-labelledby="featured-skills">
<h2 id="featured-skills" class="strip-title">Featured</h2>
<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={`/skills/${s.slug}/`}>
<a href={href(`/skills/${s.slug}/`)}>
{s.previewUrl ? (
<span class="featured-thumb">
<LazyImg src={s.previewUrl} alt="" loading={i < 4 ? 'eager' : 'precise'} />
@ -116,7 +117,7 @@ const jsonLd = {
<span class="featured-num">Nº {String(s.featured).padStart(2, '0')}</span>
<span class="featured-name">{s.name}</span>
<p>{s.description}</p>
{s.mode && <span class="meta-tag">{s.mode}</span>}
{s.modeLabel && <span class="meta-tag">{s.modeLabel}</span>}
</a>
</li>
))}
@ -124,7 +125,7 @@ const jsonLd = {
</section>
)}
<section class="catalog-grid catalog-grid-skills" aria-label="All skills">
<section class="catalog-grid catalog-grid-skills" aria-label={ui.catalog.skills.allAria}>
<ol>
{skills.map((s, idx) => <SkillRow skill={s} index={idx} />)}
</ol>

View file

@ -14,6 +14,7 @@ import {
getSkillsForMode,
type TagDescriptor,
} from '../../../_lib/catalog';
import { getLandingUiCopy, localeFromPath, localizedHref } from '../../../i18n';
export async function getStaticPaths() {
const tags = await getSkillModeIndex();
@ -28,20 +29,20 @@ interface Props {
}
const { tag } = Astro.props as Props;
const { records, label } = await getSkillsForMode(tag.slug);
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 = `${heading} skills — ${records.length} Open Design ${heading.toLowerCase()} agents`;
const description =
`Every Open Design skill that produces ${heading.toLowerCase()} artifacts. ` +
`${records.length} ready-to-run, system-aware agents — installable through ` +
`the daemon, brand-locked through any DESIGN.md system.`;
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: `${heading} skills · Open Design`,
name: title,
description,
url,
numberOfItems: records.length,
@ -50,28 +51,26 @@ const jsonLd = {
<Layout title={title} description={description} active="skills" jsonLd={jsonLd}>
<header class="catalog-head">
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/skills/">Skills</a>
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
<a href={href('/skills/')}>{ui.catalog.skills.detailLabel}</a>
<span aria-hidden="true">/</span>
<span>Mode</span>
<span>{ui.catalog.skills.mode}</span>
<span aria-hidden="true">/</span>
<span class="crumb-active">{heading}</span>
</nav>
<span class="label">Catalog · Nº 01 · Filter</span>
<span class="label">{ui.catalog.skills.label}</span>
<h1 class="display">
<em>{heading}</em> — {records.length} brand-grade {heading.toLowerCase()} agents<span class="dot">.</span>
{ui.catalog.skills.modeHeading(heading, records.length)}
</h1>
<p class="lead">
Filtered to <code>od.mode: {label ?? tag.label}</code>. Every skill below
reads the active <code>DESIGN.md</code> as a system prompt, so it inherits
colors, type, and spacing from any portable system you pair it with.
{ui.catalog.skills.modeLead(label ?? tag.label)}
</p>
<p class="filter-clear">
<a href="/skills/">← All skills ({tag.count} of total)</a>
<a href={href('/skills/')}>{ui.catalog.skills.allSkills(tag.count)}</a>
</p>
</header>
<section class="catalog-grid catalog-grid-skills" aria-label={`${heading} skills`}>
<section class="catalog-grid catalog-grid-skills" aria-label={ui.catalog.skills.allAria}>
<ol>
{records.map((s, idx) => <SkillRow skill={s} index={idx} />)}
</ol>

View file

@ -13,6 +13,7 @@ import {
getSkillsForScenario,
type TagDescriptor,
} from '../../../_lib/catalog';
import { getLandingUiCopy, localeFromPath, localizedHref } from '../../../i18n';
export async function getStaticPaths() {
const tags = await getSkillScenarioIndex();
@ -27,20 +28,20 @@ interface Props {
}
const { tag } = Astro.props as Props;
const { records, label } = await getSkillsForScenario(tag.slug);
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 = `${heading} skills — ${records.length} Open Design ${heading.toLowerCase()} agents`;
const description =
`Every Open Design skill in the ${heading.toLowerCase()} scenario. ` +
`${records.length} ready-to-run agents covering decks, prototypes, ` +
`templates, and live artifacts — all brand-locked through any DESIGN.md.`;
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: `${heading} skills · Open Design`,
name: title,
description,
url,
numberOfItems: records.length,
@ -49,28 +50,26 @@ const jsonLd = {
<Layout title={title} description={description} active="skills" jsonLd={jsonLd}>
<header class="catalog-head">
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/skills/">Skills</a>
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
<a href={href('/skills/')}>{ui.catalog.skills.detailLabel}</a>
<span aria-hidden="true">/</span>
<span>Scenario</span>
<span>{ui.catalog.skills.scenario}</span>
<span aria-hidden="true">/</span>
<span class="crumb-active">{heading}</span>
</nav>
<span class="label">Catalog · Nº 01 · Filter</span>
<span class="label">{ui.catalog.skills.label}</span>
<h1 class="display">
<em>{heading}</em> — {records.length} {heading.toLowerCase()} skills<span class="dot">.</span>
{ui.catalog.skills.scenarioHeading(heading, records.length)}
</h1>
<p class="lead">
Filtered to <code>od.scenario: {label ?? tag.label}</code>. Pair any of
these with a portable design system and the daemon orchestrates the rest —
one prompt, one branded artifact.
{ui.catalog.skills.scenarioLead(label ?? tag.label)}
</p>
<p class="filter-clear">
<a href="/skills/">← All skills</a>
<a href={href('/skills/')}>{ui.catalog.skills.allSkills()}</a>
</p>
</header>
<section class="catalog-grid catalog-grid-skills" aria-label={`${heading} skills`}>
<section class="catalog-grid catalog-grid-skills" aria-label={ui.catalog.skills.allAria}>
<ol>
{records.map((s, idx) => <SkillRow skill={s} index={idx} />)}
</ol>

View file

@ -1,6 +1,7 @@
---
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();
@ -15,12 +16,17 @@ interface Props {
all: ReadonlyArray<SystemRecord>;
}
const { system, all } = Astro.props as Props;
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 = `${system.name} — Open Design design system`;
const title = ui.catalog.systems.detailTitle(system.name);
const description = system.tagline
? `${system.name} (${system.category}) — ${system.tagline}`
: `Open Design system bundle: ${system.name}, ${system.category}.`;
? `${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)
@ -32,7 +38,7 @@ const jsonLd = [
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: Astro.site?.toString() },
{ '@type': 'ListItem', position: 2, name: 'Design Systems', item: new URL('/systems/', 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() },
],
},
@ -43,16 +49,16 @@ const jsonLd = [
description,
url: new URL(`/systems/${system.slug}/`, Astro.site).toString(),
license: 'https://www.apache.org/licenses/LICENSE-2.0',
genre: system.category,
genre: system.categoryLabel,
},
];
---
<Layout title={title} description={description} active="systems" jsonLd={jsonLd}>
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/">Open Design</a>
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
<a href={href('/')}>Open Design</a>
<span>/</span>
<a href="/systems/">Design Systems</a>
<a href={href('/systems/')}>{ui.catalog.systems.detailLabel}</a>
<span>/</span>
<span aria-current="page">{system.name}</span>
</nav>
@ -60,24 +66,23 @@ const jsonLd = [
<article class="detail">
<header class="detail-head">
<span class="label">
Design system
<span class="ix">· {system.category}</span>
{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">
View DESIGN.md on GitHub
{ui.catalog.systems.viewOnGithub}
</a>
</div>
</header>
{system.palette.length > 0 && (
<section class="detail-block">
<h2>Palette sample</h2>
<h2>{ui.catalog.systems.paletteSample}</h2>
<p class="block-lead">
First {system.palette.length} hex codes lifted from the DESIGN.md
color sections. The full palette and roles live in the source spec.
{ui.catalog.systems.paletteLead(system.palette.length)}
</p>
<div class="palette-row">
{system.palette.map((hex) => (
@ -92,18 +97,18 @@ const jsonLd = [
{system.atmosphere && (
<section class="detail-block">
<h2>Visual theme</h2>
<h2>{ui.catalog.systems.visualTheme}</h2>
<p class="atmosphere">{system.atmosphere}</p>
</section>
)}
{related.length > 0 && (
<section class="detail-block">
<h2>Related systems in {system.category}</h2>
<h2>{ui.catalog.systems.related(system.categoryLabel)}</h2>
<ul class="related-grid">
{related.map((r) => (
<li>
<a href={`/systems/${r.slug}/`}>
<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">

View file

@ -10,6 +10,7 @@ import {
getSystemsForCategory,
type TagDescriptor,
} from '../../../_lib/catalog';
import { getLandingUiCopy, localeFromPath, localizedHref } from '../../../i18n';
export async function getStaticPaths() {
const tags = await getSystemCategoryIndex();
@ -24,20 +25,20 @@ interface Props {
}
const { tag } = Astro.props as Props;
const { records, label } = await getSystemsForCategory(tag.slug);
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 = `${heading} design systems — ${records.length} portable visual systems · Open Design`;
const description =
`Every Open Design design system tagged ${heading.toLowerCase()}. ` +
`${records.length} portable DESIGN.md token bundles — ready to pair with ` +
`any skill in the catalog for instant brand-grade output.`;
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: `${heading} design systems · Open Design`,
name: title,
description,
url,
numberOfItems: records.length,
@ -46,28 +47,26 @@ const jsonLd = {
<Layout title={title} description={description} active="systems" jsonLd={jsonLd}>
<header class="catalog-head">
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/systems/">Design Systems</a>
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
<a href={href('/systems/')}>{ui.catalog.systems.detailLabel}</a>
<span aria-hidden="true">/</span>
<span>Category</span>
<span>{ui.catalog.systems.category}</span>
<span aria-hidden="true">/</span>
<span class="crumb-active">{heading}</span>
</nav>
<span class="label">Catalog · Nº 02 · Filter</span>
<span class="label">{ui.catalog.systems.label}</span>
<h1 class="display">
<em>{heading}</em> — {records.length} portable visual systems<span class="dot">.</span>
{ui.catalog.systems.categoryHeading(heading, records.length)}
</h1>
<p class="lead">
Filtered to category <code>{label ?? tag.label}</code>. Pick any of these
in the daemon top-bar and every skill in the catalog reads its tokens —
colors, type, spacing, voice — as part of its system prompt.
{ui.catalog.systems.categoryLead(label ?? tag.label)}
</p>
<p class="filter-clear">
<a href="/systems/">← All design systems</a>
<a href={href('/systems/')}>{ui.catalog.systems.allSystems}</a>
</p>
</header>
<section class="catalog-grid systems-grid" aria-label={`${heading} systems`}>
<section class="catalog-grid systems-grid" aria-label={ui.catalog.systems.allAria}>
<ul>
{records.map((s) => <SystemCard system={s} />)}
</ul>

View file

@ -5,19 +5,22 @@
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 systems = await getSystemRecords();
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();
const categoryTags = await getSystemCategoryIndex(locale);
const title = `Design Systems — ${systems.length} portable visual systems | Open Design`;
const description =
'Browse the full Open Design design systems catalog: 100+ DESIGN.md token bundles spanning editorial, productivity, brand, futuristic, and minimalist systems. Pick one in the daemon top-bar and every skill renders in that visual language.';
const title = ui.catalog.systems.title(systems.length);
const description = ui.catalog.systems.description;
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Open Design Design Systems catalog',
name: title,
description,
url: new URL('/systems/', Astro.site).toString(),
numberOfItems: systems.length,
@ -26,24 +29,22 @@ const jsonLd = {
<Layout title={title} description={description} active="systems" jsonLd={jsonLd}>
<header class="catalog-head">
<span class="label">Catalog · Nº 02</span>
<span class="label">{ui.catalog.systems.label}</span>
<h1 class="display">
<em>Design Systems</em> — {systems.length} portable visual systems<span class="dot">.</span>
{ui.catalog.systems.heading(systems.length)}
</h1>
<p class="lead">
Each system is a single <code>DESIGN.md</code> token spec. Pick one in
the daemon top-bar and every skill reads it as part of its system
prompt — colors, type, spacing, components, all consistent.
{ui.catalog.systems.lead}
</p>
</header>
<section class="filter-strip" aria-label="Design system filters">
<section class="filter-strip" aria-label={ui.catalog.systems.allAria}>
<div class="filter-group">
<span class="filter-label">Category</span>
<span class="filter-label">{ui.catalog.systems.category}</span>
<ul>
{categoryTags.map((tag) => (
<li>
<a class="chip chip-link" href={`/systems/category/${tag.slug}/`}>
<a class="chip chip-link" href={href(`/systems/category/${tag.slug}/`)}>
{tag.label}<span class="chip-num">{tag.count}</span>
</a>
</li>
@ -52,7 +53,7 @@ const jsonLd = {
</div>
</section>
<section class="catalog-grid systems-grid" aria-label="All systems">
<section class="catalog-grid systems-grid" aria-label={ui.catalog.systems.allAria}>
<ul>
{systems.map((s) => <SystemCard system={s} />)}
</ul>

View file

@ -6,6 +6,7 @@
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 } from '../../i18n';
export async function getStaticPaths() {
const records = await getTemplateRecords();
@ -19,23 +20,32 @@ interface Props {
template: TemplateRecord;
}
const { template } = Astro.props as Props;
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 = `${template.name} — Open Design template`;
const title = ui.catalog.templates.detailTitle(template.name);
const description = template.summary;
const originLabel = template.origin === 'live-artifact' ? 'Live Artifact' : 'Design Template';
const originLabel =
template.origin === 'live-artifact'
? ui.catalog.templates.liveArtifact
: ui.catalog.templates.skillTemplate;
const files =
template.origin === 'live-artifact'
? [
['template.html', 'the artifact renderer'],
['data.json', 'seed values for offline / preview rendering'],
['README.md', 'connector wiring, refresh cadence, customization notes'],
['template.html', ui.catalog.templates.renderer],
['data.json', ui.catalog.templates.seedData],
['README.md', ui.catalog.templates.readme],
]
: [
['SKILL.md', 'agent instructions, triggers, and template metadata'],
['example.html', 'the baked preview rendered into the catalog thumbnail'],
['assets/', 'optional reusable source files, renderers, or media'],
['references/', 'optional checklist and implementation notes'],
['SKILL.md', ui.catalog.skills.detailLabel],
['example.html', ui.catalog.templates.previewCaption],
['assets/', ui.catalog.templates.detailLabel],
['references/', ui.catalog.craft.detailLabel],
];
const jsonLd = {
@ -43,17 +53,17 @@ const jsonLd = {
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: Astro.site?.toString() },
{ '@type': 'ListItem', position: 2, name: 'Templates', item: new URL('/templates/', 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="Breadcrumb">
<a href="/">Open Design</a>
<nav class="breadcrumb" aria-label={ui.catalog.breadcrumbLabel}>
<a href={href('/')}>Open Design</a>
<span>/</span>
<a href="/templates/">Templates</a>
<a href={href('/templates/')}>{ui.catalog.templates.detailLabel}</a>
<span>/</span>
<span aria-current="page">{template.name}</span>
</nav>
@ -61,7 +71,7 @@ const jsonLd = {
<article class="detail">
<header class="detail-head">
<span class="label">
Template
{ui.catalog.templates.detailLabel}
<span class="ix">· {originLabel}</span>
</span>
<h1 class="display">{template.name}<span class="dot">.</span></h1>
@ -70,27 +80,27 @@ const jsonLd = {
<dl class="detail-meta">
{template.mode && (
<>
<dt>Mode</dt>
<dd>{template.mode}</dd>
<dt>{ui.catalog.skills.mode}</dt>
<dd>{template.modeLabel ?? template.mode}</dd>
</>
)}
{template.platform && (
<>
<dt>Platform</dt>
<dd>{template.platform}</dd>
<dt>{ui.catalog.skills.platform}</dt>
<dd>{template.platformLabel ?? template.platform}</dd>
</>
)}
{template.scenario && (
<>
<dt>Scenario</dt>
<dd>{template.scenario}</dd>
<dt>{ui.catalog.skills.scenario}</dt>
<dd>{template.scenarioLabel ?? template.scenario}</dd>
</>
)}
</dl>
)}
<div class="detail-actions">
<a class="btn btn-primary" href={template.source} target="_blank" rel="noopener">
View on GitHub
{ui.catalog.templates.forkOnGithub}
</a>
</div>
</header>
@ -102,26 +112,14 @@ const jsonLd = {
alt={`${template.name} preview`}
loading="priority"
/>
<figcaption>Rendered from the template's seed data.</figcaption>
<figcaption>{ui.catalog.templates.previewCaption}</figcaption>
</figure>
)}
<section class="detail-block">
<h2>What's in this template</h2>
<h2>{ui.catalog.templates.whatsInside}</h2>
<p class="block-lead">
{template.origin === 'live-artifact' ? (
<>
Live Artifact templates ship as a folder you can copy verbatim
into your workspace. They include a renderer, seed values, and
connector notes for refreshable artifacts.
</>
) : (
<>
Design templates ship as renderable folders under <code>design-templates/</code>.
Each bundle pairs agent-facing instructions with a baked example so
you can preview the visual system before adapting it.
</>
)}
{ui.catalog.templates.whatsInsideLead}
</p>
<ul class="trigger-list">
{files.map(([name, copy]) => (

View file

@ -2,17 +2,20 @@
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 templates = await getTemplateRecords();
const locale = localeFromPath(Astro.url.pathname);
const ui = getLandingUiCopy(locale);
const href = (path: string) => localizedHref(path, locale);
const templates = await getTemplateRecords(locale);
const title = `Templates — ${templates.length} ready-to-fork artifact templates | Open Design`;
const description =
'Ready-to-fork Open Design templates: decks, prototypes, dashboards, live artifacts, image/video systems, and other renderable starting points from the design-templates catalog.';
const title = ui.catalog.templates.title(templates.length);
const description = ui.catalog.templates.description;
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Open Design Templates catalog',
name: title,
description,
url: new URL('/templates/', Astro.site).toString(),
numberOfItems: templates.length,
@ -21,22 +24,20 @@ const jsonLd = {
<Layout title={title} description={description} active="templates" jsonLd={jsonLd}>
<header class="catalog-head">
<span class="label">Catalog · Nº 04</span>
<span class="label">{ui.catalog.templates.label}</span>
<h1 class="display">
<em>Templates</em> — {templates.length} ready-to-fork artifacts<span class="dot">.</span>
{ui.catalog.templates.heading(templates.length)}
</h1>
<p class="lead">
Pre-wired design-template bundles with a known-good visual language,
baked examples, and agent instructions. Fork the folder, adapt the
source data or prompt, and ship.
{ui.catalog.templates.lead}
</p>
</header>
<section class="template-grid" aria-label="All templates">
<section class="template-grid" aria-label={ui.catalog.templates.allAria}>
<ul>
{templates.map((t, i) => (
<li class="template-card">
<a href={t.detailHref}>
<a href={href(t.detailHref)}>
{t.previewUrl ? (
<span class="template-thumb">
<LazyImg src={t.previewUrl} alt="" loading={i < 4 ? 'eager' : 'precise'} />
@ -45,13 +46,13 @@ const jsonLd = {
<span class="template-thumb template-thumb-empty" aria-hidden="true" />
)}
<span class={`meta-tag ${t.origin === 'live-artifact' ? 'coral' : ''}`}>
{t.origin === 'live-artifact' ? 'Live Artifact' : (t.mode ?? 'Design template')}
{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.platform, t.scenario].filter(Boolean).join(' · ')}
{[t.platformLabel ?? t.platform, t.scenarioLabel ?? t.scenario].filter(Boolean).join(' · ')}
</span>
)}
</a>

View file

@ -11,14 +11,22 @@
import { getCollection, render } from 'astro:content';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import GoogleAnalytics from '../../_components/google-analytics.astro';
import FontStylesheet from '../../_components/font-stylesheet.astro';
import HeaderEnhancer from '../../_components/header-enhancer.astro';
import { Header, type HeaderProps } from '../../_components/header';
import LocaleSwitcherScript from '../../_components/locale-switcher-script.astro';
import SeoHead from '../../_components/seo-head.astro';
import SiteFooter from '../../_components/site-footer.astro';
import Topbar from '../../_components/topbar.astro';
import { getCatalogCounts } from '../../_lib/catalog';
import { getGithubRepoMeta } from '../../_lib/github';
import {
DEFAULT_LOCALE,
getLandingUiCopy,
getLocaleDefinition,
localeFromPath,
localizedHref,
} from '../../i18n';
import '../../globals.css';
import '../../sub-pages.css';
@ -32,6 +40,10 @@ export async function getStaticPaths() {
const { entry } = Astro.props;
const { Content } = await render(entry);
const locale = localeFromPath(Astro.url.pathname);
const localeDef = getLocaleDefinition(locale);
const ui = getLandingUiCopy(locale);
const href = (path: string) => localizedHref(path, locale);
const sourceUrl = `https://github.com/nexu-io/open-design/blob/main/apps/landing-page/app/content/tutorials/${entry.id}.md`;
const youtubeUrl = `https://www.youtube.com/watch?v=${entry.data.youtubeId}`;
const thumbnailUrl =
@ -45,11 +57,12 @@ const headerHtml = renderToStaticMarkup(
github,
brandHref: '/',
active: 'tutorials',
locale,
}),
);
const fmtDate = (d: Date) =>
d.toLocaleDateString('en-US', {
d.toLocaleDateString(localeDef.htmlLang, {
year: 'numeric',
month: 'long',
day: 'numeric',
@ -62,44 +75,83 @@ const fmtDuration = (seconds: number) => {
if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
return `${m}:${String(s).padStart(2, '0')}`;
};
const categoryValue = (category: string) => category.toLowerCase().replace(/\s+/g, '-');
const localizedCategory = (category: string) => {
const value = categoryValue(category);
if (value === 'getting-started') return ui.tutorials.categories.gettingStarted;
if (value === 'tutorial') return ui.tutorials.categories.tutorial;
if (value === 'demo') return ui.tutorials.categories.demo;
if (value === 'review') return ui.tutorials.categories.review;
if (value === 'community') return ui.tutorials.categories.community;
return category;
};
const tutorialCopy =
locale === DEFAULT_LOCALE
? {
title: entry.data.title,
summary: entry.data.summary,
category: entry.data.category,
bodyHtml: undefined as string | undefined,
}
: {
title: ui.tutorials.localizedTitle(entry.data.title, entry.data.author),
summary: ui.tutorials.localizedSummary(
entry.data.title,
entry.data.author,
localizedCategory(entry.data.category),
),
category: localizedCategory(entry.data.category),
bodyHtml: ui.tutorials.localizedBodyHtml(
entry.data.title,
entry.data.author,
ui.tutorials.localizedSummary(
entry.data.title,
entry.data.author,
localizedCategory(entry.data.category),
),
),
};
---
<!doctype html>
<html lang='en'>
<html lang={localeDef.htmlLang} dir={localeDef.dir}>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<SeoHead
kind='article'
title={`${entry.data.title} — Open Design Tutorials`}
description={entry.data.summary}
title={ui.tutorials.detailTitle(tutorialCopy.title)}
description={tutorialCopy.summary}
pathname={Astro.url.pathname}
datePublished={entry.data.date}
category={entry.data.category}
category={tutorialCopy.category}
/>
<GoogleAnalytics />
<FontStylesheet />
</head>
<body>
<div class='shell'>
<Topbar github={github} />
<Fragment set:html={headerHtml} />
<div class='site-chrome' data-chrome-headroom>
<Topbar github={github} locale={locale} />
<Fragment set:html={headerHtml} />
</div>
<main class='tut-detail-shell'>
<article class='tut-detail'>
<div class='container'>
<a class='tut-back' href='/tutorials/'>← Back to Tutorials</a>
<a class='tut-back' href={href('/tutorials/')}>{ui.tutorials.backToTutorials}</a>
<a
class='tut-player'
href={youtubeUrl}
target='_blank'
rel='noreferrer noopener'
aria-label={`Watch "${entry.data.title}" on YouTube`}
aria-label={`${ui.tutorials.watch}: ${tutorialCopy.title}`}
>
<img
class='tut-player-thumb'
src={thumbnailUrl}
alt={`Thumbnail for ${entry.data.title}`}
alt={ui.tutorials.thumbnailAlt(tutorialCopy.title)}
loading='eager'
/>
<span class='tut-player-overlay' aria-hidden='true'>
@ -112,19 +164,19 @@ const fmtDuration = (seconds: number) => {
<path d='M27 34V14l18 10z' fill='#fff' />
</svg>
</span>
<span class='tut-player-cta'>Watch on YouTube ↗</span>
<span class='tut-player-cta'>{ui.tutorials.watchOnYouTube}</span>
</span>
</a>
<div class='tut-detail-head'>
<span class='tut-card-cat'>
{entry.data.category}
{tutorialCopy.category}
{entry.data.official && (
<span class='tut-badge-official'>Official</span>
<span class='tut-badge-official'>{ui.tutorials.official}</span>
)}
</span>
<h1 class='tut-detail-title'>{entry.data.title}</h1>
<p class='tut-detail-summary'>{entry.data.summary}</p>
<h1 class='tut-detail-title'>{tutorialCopy.title}</h1>
<p class='tut-detail-summary'>{tutorialCopy.summary}</p>
<div class='tut-detail-meta'>
<span>{entry.data.author}</span>
@ -136,7 +188,7 @@ const fmtDuration = (seconds: number) => {
target='_blank'
rel='noreferrer noopener'
>
Open on YouTube ↗
{ui.tutorials.openOnYouTube}
</a>
</div>
</div>
@ -144,29 +196,30 @@ const fmtDuration = (seconds: number) => {
<hr class='tut-rule' />
<div class='tut-body'>
<Content />
{tutorialCopy.bodyHtml ? <Fragment set:html={tutorialCopy.bodyHtml} /> : <Content />}
</div>
<hr class='tut-rule' />
<div class='tut-foot'>
<a class='tut-back' href='/tutorials/'>← Back to Tutorials</a>
<a class='tut-back' href={href('/tutorials/')}>{ui.tutorials.backToTutorials}</a>
<a
class='tut-source'
href={sourceUrl}
target='_blank'
rel='noreferrer noopener'
>
View source on GitHub ↗
{ui.tutorials.viewSource}
</a>
</div>
</div>
</article>
</main>
<SiteFooter counts={counts} />
<SiteFooter counts={counts} locale={locale} />
</div>
<HeaderEnhancer />
<LocaleSwitcherScript />
<style>
.tut-detail-shell {

View file

@ -13,17 +13,29 @@
import { getCollection } from 'astro:content';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import GoogleAnalytics from '../../_components/google-analytics.astro';
import FontStylesheet from '../../_components/font-stylesheet.astro';
import HeaderEnhancer from '../../_components/header-enhancer.astro';
import { Header, type HeaderProps } from '../../_components/header';
import LocaleSwitcherScript from '../../_components/locale-switcher-script.astro';
import SeoHead from '../../_components/seo-head.astro';
import SiteFooter from '../../_components/site-footer.astro';
import Topbar from '../../_components/topbar.astro';
import { getCatalogCounts } from '../../_lib/catalog';
import { getGithubRepoMeta } from '../../_lib/github';
import {
DEFAULT_LOCALE,
getLandingUiCopy,
getLocaleDefinition,
localeFromPath,
localizedHref,
} from '../../i18n';
import '../../globals.css';
import '../../sub-pages.css';
const locale = localeFromPath(Astro.url.pathname);
const localeDef = getLocaleDefinition(locale);
const ui = getLandingUiCopy(locale);
const href = (path: string) => localizedHref(path, locale);
const tutorials = (await getCollection('tutorials')).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
);
@ -36,24 +48,24 @@ const headerHtml = renderToStaticMarkup(
github,
brandHref: '/',
active: 'tutorials',
locale,
}),
);
const seoTitle = 'Tutorials — Open Design';
const seoDescription =
'Watch Open Design from the inside — getting-started walkthroughs, plugin tutorials, demos, and community deep-dives, curated from YouTube and embedded for in-page playback.';
const seoTitle = ui.tutorials.seoTitle;
const seoDescription = ui.tutorials.description;
const categoryChips = [
{ label: 'All', value: 'all' },
{ label: 'Getting started', value: 'getting-started' },
{ label: 'Tutorials', value: 'tutorial' },
{ label: 'Demos', value: 'demo' },
{ label: 'Reviews', value: 'review' },
{ label: 'Community', value: 'community' },
{ label: ui.tutorials.categories.all, value: 'all' },
{ label: ui.tutorials.categories.gettingStarted, value: 'getting-started' },
{ label: ui.tutorials.categories.tutorial, value: 'tutorial' },
{ label: ui.tutorials.categories.demo, value: 'demo' },
{ label: ui.tutorials.categories.review, value: 'review' },
{ label: ui.tutorials.categories.community, value: 'community' },
];
const fmtDate = (d: Date) =>
d.toLocaleDateString('en-US', {
d.toLocaleDateString(localeDef.htmlLang, {
year: 'numeric',
month: 'long',
day: 'numeric',
@ -68,15 +80,43 @@ const fmtDuration = (seconds: number) => {
};
const categoryValue = (category: string) => category.toLowerCase().replace(/\s+/g, '-');
const localizedCategory = (category: string) => {
const value = categoryValue(category);
if (value === 'getting-started') return ui.tutorials.categories.gettingStarted;
if (value === 'tutorial') return ui.tutorials.categories.tutorial;
if (value === 'demo') return ui.tutorials.categories.demo;
if (value === 'review') return ui.tutorials.categories.review;
if (value === 'community') return ui.tutorials.categories.community;
return category;
};
const localizedTutorial = (entry: (typeof tutorials)[number]) => {
if (locale === DEFAULT_LOCALE) {
return {
title: entry.data.title,
summary: entry.data.summary,
category: entry.data.category,
};
}
return {
title: ui.tutorials.localizedTitle(entry.data.title, entry.data.author),
summary: ui.tutorials.localizedSummary(
entry.data.title,
entry.data.author,
localizedCategory(entry.data.category),
),
category: localizedCategory(entry.data.category),
};
};
const ytThumb = (id: string) => `https://i.ytimg.com/vi/${id}/hqdefault.jpg`;
const featuredTutorial = tutorials[0];
const cardTutorials = tutorials.slice(1);
const featuredCopy = featuredTutorial ? localizedTutorial(featuredTutorial) : undefined;
---
<!doctype html>
<html lang='en'>
<html lang={localeDef.htmlLang} dir={localeDef.dir}>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
@ -86,28 +126,27 @@ const cardTutorials = tutorials.slice(1);
description={seoDescription}
pathname={Astro.url.pathname}
/>
<GoogleAnalytics />
<FontStylesheet />
</head>
<body>
<div class='shell'>
<Topbar github={github} />
<Fragment set:html={headerHtml} />
<div class='site-chrome' data-chrome-headroom>
<Topbar github={github} locale={locale} />
<Fragment set:html={headerHtml} />
</div>
<main class='tut-shell'>
<section class='tut-masthead'>
<div class='container'>
<h1 class='tut-title'>Tutorials</h1>
<h1 class='tut-title'>{ui.tutorials.title}</h1>
<p class='tut-sub'>
Watch Open Design from the inside — getting-started walkthroughs,
plugin tutorials, demos, and community deep-dives. Each video
plays in-page; the source link points back to the original
YouTube channel.
{ui.tutorials.description}
</p>
</div>
</section>
{
featuredTutorial && (
featuredTutorial && featuredCopy && (
<section
class='tut-feature'
data-featured
@ -116,13 +155,13 @@ const cardTutorials = tutorials.slice(1);
<div class='container'>
<a
class='tut-feature-card'
href={`/tutorials/${featuredTutorial.id}/`}
href={href(`/tutorials/${featuredTutorial.id}/`)}
style={`--accent-index: 1;`}
>
<span class='tut-feature-thumb'>
<img
src={featuredTutorial.data.thumbnail ?? ytThumb(featuredTutorial.data.youtubeId)}
alt={`Thumbnail for ${featuredTutorial.data.title}`}
alt={ui.tutorials.thumbnailAlt(featuredCopy.title)}
loading='eager'
/>
<span class='tut-play' aria-hidden='true'>▶</span>
@ -132,18 +171,18 @@ const cardTutorials = tutorials.slice(1);
</span>
<span class='tut-feature-copy'>
<span class='tut-card-cat'>
{featuredTutorial.data.category}
{featuredCopy.category}
{featuredTutorial.data.official && (
<span class='tut-badge-official'>Official</span>
<span class='tut-badge-official'>{ui.tutorials.official}</span>
)}
</span>
<h2 class='tut-feature-title'>{featuredTutorial.data.title}</h2>
<p class='tut-feature-summary'>{featuredTutorial.data.summary}</p>
<h2 class='tut-feature-title'>{featuredCopy.title}</h2>
<p class='tut-feature-summary'>{featuredCopy.summary}</p>
<span class='tut-feature-meta'>
<span>{featuredTutorial.data.author}</span>
<span>·</span>
<span>{fmtDate(featuredTutorial.data.date)}</span>
<span class='tut-feature-cta'>Watch →</span>
<span class='tut-feature-cta'>{ui.tutorials.watchCta}</span>
</span>
</span>
</a>
@ -154,11 +193,11 @@ const cardTutorials = tutorials.slice(1);
<section class='tut-categories'>
<div class='container'>
<div class='tut-category-strip' aria-label='Tutorial categories'>
<div class='tut-category-strip' aria-label={ui.tutorials.categoriesLabel}>
{categoryChips.map((category, index) => (
<a
class:list={['tut-chip', index === 0 && 'is-active']}
href={category.value === 'all' ? '/tutorials/' : `/tutorials/?category=${category.value}`}
href={category.value === 'all' ? href('/tutorials/') : href(`/tutorials/?category=${category.value}`)}
data-category-filter={category.value}
>
{category.label}
@ -173,63 +212,66 @@ const cardTutorials = tutorials.slice(1);
{
tutorials.length === 0 ? (
<p class='tut-empty'>
No tutorials yet. We're curating the first batch — check back soon, or
{ui.tutorials.noEntries}
<a href='https://github.com/nexu-io/open-design/issues/new' target='_blank' rel='noreferrer noopener'>
{' '}suggest a video
{' '}{ui.tutorials.suggestVideo}
</a>.
</p>
) : cardTutorials.length === 0 ? null : (
<ol class='tut-grid'>
{cardTutorials.map((entry, idx) => (
<li
class='tut-card'
data-tut-item
data-category={categoryValue(entry.data.category)}
>
<a
class='tut-card-link'
href={`/tutorials/${entry.id}/`}
style={`--accent-index: ${idx + 2};`}
{cardTutorials.map((entry, idx) => {
const copy = localizedTutorial(entry);
return (
<li
class='tut-card'
data-tut-item
data-category={categoryValue(entry.data.category)}
>
<span class='tut-card-thumb'>
<img
src={entry.data.thumbnail ?? ytThumb(entry.data.youtubeId)}
alt={`Thumbnail for ${entry.data.title}`}
loading='lazy'
/>
<span class='tut-play' aria-hidden='true'>▶</span>
<span class='tut-duration-pill'>
{fmtDuration(entry.data.durationSeconds)}
<a
class='tut-card-link'
href={href(`/tutorials/${entry.id}/`)}
style={`--accent-index: ${idx + 2};`}
>
<span class='tut-card-thumb'>
<img
src={entry.data.thumbnail ?? ytThumb(entry.data.youtubeId)}
alt={ui.tutorials.thumbnailAlt(copy.title)}
loading='lazy'
/>
<span class='tut-play' aria-hidden='true'>▶</span>
<span class='tut-duration-pill'>
{fmtDuration(entry.data.durationSeconds)}
</span>
</span>
</span>
<span class='tut-card-body'>
<span class='tut-card-cat'>
{entry.data.category}
{entry.data.official && (
<span class='tut-badge-official'>Official</span>
)}
<span class='tut-card-body'>
<span class='tut-card-cat'>
{copy.category}
{entry.data.official && (
<span class='tut-badge-official'>{ui.tutorials.official}</span>
)}
</span>
<h2 class='tut-card-title'>{copy.title}</h2>
<p class='tut-card-summary'>{copy.summary}</p>
<span class='tut-card-foot'>
<span class='tut-card-author'>{entry.data.author}</span>
<span class='tut-card-date'>{fmtDate(entry.data.date)}</span>
<span class='tut-card-cta'>{ui.tutorials.watchCta}</span>
</span>
</span>
<h2 class='tut-card-title'>{entry.data.title}</h2>
<p class='tut-card-summary'>{entry.data.summary}</p>
<span class='tut-card-foot'>
<span class='tut-card-author'>{entry.data.author}</span>
<span class='tut-card-date'>{fmtDate(entry.data.date)}</span>
<span class='tut-card-cta'>Watch →</span>
</span>
</span>
</a>
</li>
))}
</a>
</li>
);
})}
</ol>
)
}
<p class='tut-empty is-filter-empty' data-tut-empty hidden>
No tutorials in this category yet. More are on the way.
{ui.tutorials.noCategory}
</p>
</div>
</section>
</main>
<SiteFooter counts={counts} />
<SiteFooter counts={counts} locale={locale} />
</div>
<script is:inline>
@ -267,6 +309,7 @@ const cardTutorials = tutorials.slice(1);
})();
</script>
<HeaderEnhancer />
<LocaleSwitcherScript />
<style>
.tut-shell {

View file

@ -1,6 +1,16 @@
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
DEFAULT_LOCALE,
getLandingUiCopy,
getLocalizedString,
type LandingLocaleCode,
} from './i18n';
import {
explicitLocalizedString,
localizePluginText,
} from './content-i18n';
type TrustTier = 'official' | 'trusted' | 'restricted';
@ -188,32 +198,36 @@ const sourceUrlFromSource = (source: string): string | undefined => {
const registryUrlFor = (registryId: string) =>
`${RAW_REPO}/plugins/registry/${registryId}/open-design-marketplace.json`;
const previewLabelFor = (type: string | undefined) => {
if (type === 'image') return 'Image preview';
if (type === 'video') return 'Video poster';
return 'Live HTML preview';
const previewLabelFor = (
type: string | undefined,
locale: LandingLocaleCode = DEFAULT_LOCALE,
) => {
const copy = getLandingUiCopy(locale).plugins;
if (type === 'image') return copy.imagePreview;
if (type === 'video') return copy.videoPoster;
return copy.liveHtmlPreview;
};
const localizedString = (value: unknown): string | undefined => {
const text = asString(value);
if (text) {
return text;
}
const record = asRecord(value);
return (
asString(record?.en) ??
asString(record?.['zh-CN']) ??
asString(record?.['zh']) ??
Object.values(record ?? {})
.map((item) => asString(item))
.find(Boolean)
const localizedString = (
value: unknown,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): string | undefined => {
const text = getLocalizedString(
value as Parameters<typeof getLocalizedString>[0],
locale,
);
return text || undefined;
};
const useCaseQuery = (value: unknown): string | undefined => {
const useCaseQuery = (
value: unknown,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): string | undefined => {
const record = asRecord(value);
return localizedString(record?.query);
return explicitLocalizedString(
record?.query as Parameters<typeof explicitLocalizedString>[0],
locale,
);
};
const localPluginPath = (pluginDir: string, entry: string): string | undefined => {
@ -238,6 +252,7 @@ const previewFrom = (
pluginDir: string | undefined,
id: string,
rawPreview: unknown,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): PublicPluginPreview | undefined => {
const preview = asRecord(rawPreview);
const rawType = asString(preview?.type);
@ -248,7 +263,7 @@ const previewFrom = (
if (rawType === 'image' || (!rawType && poster)) {
return {
type: 'image',
label: previewLabelFor('image'),
label: previewLabelFor('image', locale),
poster,
frameHref: undefined,
localHtmlPath: undefined,
@ -258,7 +273,7 @@ const previewFrom = (
if (rawType === 'video') {
return {
type: 'video',
label: previewLabelFor('video'),
label: previewLabelFor('video', locale),
poster,
frameHref: undefined,
localHtmlPath: undefined,
@ -274,7 +289,7 @@ const previewFrom = (
: undefined;
return {
type: 'html',
label: previewLabelFor('html'),
label: previewLabelFor('html', locale),
poster,
frameHref,
localHtmlPath: frameHref ? localHtmlPath : undefined,
@ -285,7 +300,7 @@ const previewFrom = (
if (url && (url.startsWith('http://') || url.startsWith('https://'))) {
return {
type: 'html',
label: previewLabelFor('html'),
label: previewLabelFor('html', locale),
poster,
frameHref: url,
localHtmlPath: undefined,
@ -361,6 +376,7 @@ const entryFromMarketplace = (
registryName: string,
registryTrust: TrustTier,
rawEntry: RawPluginEntry,
locale: LandingLocaleCode,
): PublicPluginEntry | undefined => {
const id = asString(rawEntry.name);
const source = asString(rawEntry.source ?? rawEntry.dist);
@ -368,20 +384,39 @@ const entryFromMarketplace = (
return undefined;
}
const trust = registryTrust;
const title = asString(rawEntry.title) ?? titleize(id.split('/').at(-1) ?? id);
const description =
asString(rawEntry.description) ??
'Agent-native Open Design workflow packaged as a portable plugin.';
const tags = asStringArray(rawEntry.tags);
const capabilities = asStringArray(rawEntry.capabilitiesSummary);
const version = asString(rawEntry.version) ?? '0.1.0';
const publisher = publisherLabel(rawEntry.publisher);
const publisher = publisherLabel(rawEntry.publisher, locale);
const detailHref = detailHrefFor(id);
const mode = asString(rawEntry.mode);
const taskKind = asString(rawEntry.taskKind);
const surface = undefined;
const preview = previewFrom(undefined, id, rawEntry.preview);
const preview = previewFrom(undefined, id, rawEntry.preview, locale);
const trust = registryTrust;
const rawTitle =
explicitLocalizedString(
rawEntry.title as Parameters<typeof explicitLocalizedString>[0],
locale,
) ?? titleize(id.split('/').at(-1) ?? id);
const rawDescription =
explicitLocalizedString(
rawEntry.description as Parameters<typeof explicitLocalizedString>[0],
locale,
) ?? 'Agent-native Open Design workflow packaged as a portable plugin.';
const localized = localizePluginText({
id,
title: rawTitle,
description: rawDescription,
locale,
mode,
taskKind,
surface,
visualKind: preview?.type,
labels: [...tags, ...capabilities],
});
const title = localized.title;
const description = localized.description;
return {
id,
@ -430,8 +465,11 @@ const entryFromMarketplace = (
};
};
const publisherLabel = (publisher: unknown): string | undefined => {
const text = asString(publisher);
const publisherLabel = (
publisher: unknown,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): string | undefined => {
const text = localizedString(publisher, locale);
if (text) {
return text;
}
@ -439,7 +477,9 @@ const publisherLabel = (publisher: unknown): string | undefined => {
return asString(record?.name) ?? asString(record?.id) ?? asString(record?.github);
};
const loadRegistryEntries = (): PublicPluginEntry[] => {
const loadRegistryEntries = (
locale: LandingLocaleCode = DEFAULT_LOCALE,
): PublicPluginEntry[] => {
if (!existsSync(REGISTRY_ROOT)) {
return [];
}
@ -454,7 +494,10 @@ const loadRegistryEntries = (): PublicPluginEntry[] => {
const manifestPath = path.join(REGISTRY_ROOT, registryId, 'open-design-marketplace.json');
const manifest = readJson<RawMarketplace>(manifestPath);
const rawPlugins = Array.isArray(manifest?.plugins) ? manifest.plugins : [];
const registryName = asString(manifest?.name) ?? titleize(registryId);
const registryName =
registryId === 'official'
? getLandingUiCopy(locale).plugins.official
: localizedString(manifest?.name, locale) ?? titleize(registryId);
const registryTrust = normalizeTrust(
manifest?.trust,
registryTrustFallback(registryId),
@ -470,6 +513,7 @@ const loadRegistryEntries = (): PublicPluginEntry[] => {
registryName,
registryTrust,
rawEntry,
locale,
);
if (entry) {
entries.push(entry);
@ -480,7 +524,11 @@ const loadRegistryEntries = (): PublicPluginEntry[] => {
return entries;
};
const officialEntryFromManifest = (manifestPath: string): PublicPluginEntry | undefined => {
const officialEntryFromManifest = (
manifestPath: string,
locale: LandingLocaleCode = DEFAULT_LOCALE,
): PublicPluginEntry | undefined => {
const copy = getLandingUiCopy(locale).plugins;
const manifest = readJson<RawPluginManifest>(manifestPath);
const pluginName = asString(manifest?.name) ?? path.basename(path.dirname(manifestPath));
const id = `open-design/${pluginName}`;
@ -490,16 +538,38 @@ const officialEntryFromManifest = (manifestPath: string): PublicPluginEntry | un
const od = asRecord(manifest?.od) as RawOdMetadata | undefined;
const capabilities = asStringArray(od?.capabilities);
const tags = asStringArray(manifest?.tags);
const title = asString(manifest?.title) ?? titleize(pluginName);
const description =
asString(manifest?.description) ??
'First-party Open Design workflow packaged as a portable plugin.';
const detailHref = detailHrefFor(id);
const mode = asString(od?.mode);
const taskKind = asString(od?.taskKind);
const surface = asString(od?.surface);
const preview = previewFrom(pluginDir, id, od?.preview);
const exampleQuery = useCaseQuery(od?.useCase);
const preview = previewFrom(pluginDir, id, od?.preview, locale);
const rawTitle =
explicitLocalizedString(
manifest?.title as Parameters<typeof explicitLocalizedString>[0],
locale,
) ?? titleize(pluginName);
const rawDescription =
explicitLocalizedString(
manifest?.description as Parameters<typeof explicitLocalizedString>[0],
locale,
) ?? 'First-party Open Design workflow packaged as a portable plugin.';
const localized = localizePluginText({
id,
title: rawTitle,
description: rawDescription,
locale,
mode,
taskKind,
surface,
visualKind: preview?.type,
labels: [...tags, ...capabilities],
});
const title = localized.title;
const description = localized.description;
const exampleQuery =
locale === DEFAULT_LOCALE
? useCaseQuery(od?.useCase, locale) ?? localized.exampleQuery
: localized.exampleQuery ?? useCaseQuery(od?.useCase, locale);
return {
id,
@ -508,7 +578,7 @@ const officialEntryFromManifest = (manifestPath: string): PublicPluginEntry | un
description,
version: asString(manifest?.version) ?? '0.1.0',
registryId: 'official',
registryName: 'Official',
registryName: copy.official,
trust: 'official',
source,
sourceUrl: `${REPO}/tree/main/${repoPath}`,
@ -534,8 +604,8 @@ const officialEntryFromManifest = (manifestPath: string): PublicPluginEntry | un
id,
title,
description,
'Official',
'official',
copy.official,
copy.trustLabels.official,
mode,
taskKind,
surface,
@ -549,19 +619,23 @@ const officialEntryFromManifest = (manifestPath: string): PublicPluginEntry | un
};
};
const loadBundledOfficialEntries = (): PublicPluginEntry[] =>
const loadBundledOfficialEntries = (
locale: LandingLocaleCode = DEFAULT_LOCALE,
): PublicPluginEntry[] =>
findManifestFiles(OFFICIAL_PLUGINS_ROOT)
.map(officialEntryFromManifest)
.map((manifestPath) => officialEntryFromManifest(manifestPath, locale))
.filter((entry): entry is PublicPluginEntry => Boolean(entry));
export const getPublicPlugins = (): PublicPluginEntry[] => {
export const getPublicPlugins = (
locale: LandingLocaleCode = DEFAULT_LOCALE,
): PublicPluginEntry[] => {
const byId = new Map<string, PublicPluginEntry>();
for (const entry of loadRegistryEntries()) {
for (const entry of loadRegistryEntries(locale)) {
byId.set(entry.id, entry);
}
for (const entry of loadBundledOfficialEntries()) {
for (const entry of loadBundledOfficialEntries(locale)) {
const existing = byId.get(entry.id);
if (existing) {
const preview = existing.preview ?? entry.preview;
@ -602,7 +676,7 @@ export const getPublicPlugins = (): PublicPluginEntry[] => {
if (order !== 0) {
return order;
}
return left.title.localeCompare(right.title, 'en');
return left.title.localeCompare(right.title, locale);
});
};

View file

@ -18,12 +18,20 @@ body.sub-page {
}
.sub-main {
padding: 140px 0 96px;
/* Only set vertical padding here. The element also has the `.container`
* class which provides the responsive horizontal gutter; using the
* `padding` shorthand instead would zero out left/right and let
* content touch the screen edge on mobile (issue #review-fix-8). */
padding-top: 140px;
padding-bottom: 96px;
min-height: 60vh;
}
@media (max-width: 720px) {
.sub-main { padding: 120px 0 72px; }
.sub-main {
padding-top: 120px;
padding-bottom: 72px;
}
}
/* ---------- nav active state ---------- */
@ -876,7 +884,7 @@ body.sub-page {
}
.sub-footer-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
grid-template-columns: 1.6fr 1fr 1fr 1fr;
gap: 48px;
margin-bottom: 36px;
}
@ -940,15 +948,15 @@ body.sub-page {
.sub-footer { padding: 40px 0 24px; }
}
/* =========================================================
* SEO landing pages /official/, /quickstart/, /compare/,
* /alternatives/claude-design/, /agents/
* =========================================================
*
* Shared layout primitives for the high-intent, mostly-textual
* pages. These reuse Atelier Zero tokens; they avoid the editorial
* side rails and full hero, since these pages are query-legible first.
* pages added in growth/seo-opendesigner-analysis.md. These reuse
* Atelier Zero tokens; they avoid the editorial side rails and
* full hero, since these pages are query-legible first.
*/
.info-page {
max-width: 920px;

View file

@ -3,6 +3,11 @@ import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import type { AstroUserConfig } from 'astro';
import { defineConfig } from 'astro/config';
import {
DEFAULT_LOCALE,
LANDING_LOCALES,
stripLocaleFromPath,
} from './app/i18n';
// Pull the Shiki theme shape off Astro's own config typing rather than
// importing from `shiki` directly — Shiki is a transitive dependency of
@ -104,7 +109,9 @@ const editorialPaperTheme: ShikiThemeObject = {
// builds (Cloudflare Pages preview deployments, local previews on a
// different host) can stamp their own URL without forking the config.
const site = process.env.OD_LANDING_SITE ?? 'https://open-design.ai';
const locales = ['en', 'id', 'de', 'zh-CN', 'zh-TW', 'pt-BR', 'es-ES', 'ru', 'fa', 'ar', 'ja', 'ko', 'pl', 'hu', 'fr', 'uk', 'tr', 'th', 'it'];
const sitemapLocales = Object.fromEntries(
LANDING_LOCALES.map((locale) => [locale.code, locale.htmlLang]),
);
const changefreq = {
daily: 'daily' as SitemapItem['changefreq'],
weekly: 'weekly' as SitemapItem['changefreq'],
@ -124,12 +131,6 @@ for (const file of readdirSync(blogDir)) {
}
}
const localePrefixPattern = new RegExp(`^/(${locales.join('|').replaceAll('-', '\\-')})(?=/|$)`);
const stripLocalePrefix = (path: string) => {
const stripped = path.replace(localePrefixPattern, '');
return stripped.length > 0 ? stripped : '/';
};
export default defineConfig({
output: 'static',
site,
@ -151,29 +152,51 @@ export default defineConfig({
},
integrations: [
sitemap({
i18n: {
defaultLocale: DEFAULT_LOCALE,
locales: sitemapLocales,
},
namespaces: {
xhtml: true,
},
// `/og/` is a screenshot surface for the 1200x630 Open Graph
// image — it already carries `<meta name="robots" content="noindex">`
// and is `Disallow`-ed from `public/robots.txt`. Filtering it
// out of the sitemap keeps the index strictly canonical pages.
filter: (page) => !page.includes('/og/'),
serialize(item: SitemapItem) {
const path = new URL(item.url).pathname;
const canonicalPath = stripLocalePrefix(path);
if (canonicalPath === '/') {
const path = stripLocaleFromPath(new URL(item.url).pathname).pathname;
if (path === '/') {
item.priority = 1.0;
item.changefreq = changefreq.daily;
} else if (canonicalPath === '/blog/') {
} else if (path === '/blog/') {
item.priority = 0.9;
item.changefreq = changefreq.daily;
} else if (canonicalPath.startsWith('/blog/')) {
} else if (path.startsWith('/blog/')) {
item.priority = 0.8;
item.changefreq = changefreq.weekly;
const date = blogDates.get(canonicalPath);
const date = blogDates.get(path);
if (date) item.lastmod = date;
} else if (
canonicalPath === '/skills/' ||
canonicalPath === '/systems/' ||
canonicalPath === '/craft/'
// High-intent landing pages — these are the brand defense
// and commercial-intent surfaces from
// growth/seo-opendesigner-analysis.md. They should be
// crawled more often than the catalog and prioritized
// above generic detail pages.
path === '/official/' ||
path === '/quickstart/' ||
path === '/compare/' ||
path === '/agents/' ||
path === '/alternatives/claude-design/'
) {
item.priority = 0.9;
item.changefreq = changefreq.weekly;
} else if (
path === '/skills/' ||
path === '/systems/' ||
path === '/templates/' ||
path === '/craft/' ||
path === '/plugins/'
) {
item.priority = 0.7;
item.changefreq = changefreq.weekly;

View file

@ -53,8 +53,20 @@
# a moderate browser TTL and rely on edge purge from CF Pages on deploy.
/favicon.png
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
/favicon.ico
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
/favicon-16x16.png
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
/favicon-32x32.png
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
/apple-touch-icon.png
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
/android-chrome-192x192.png
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
/android-chrome-512x512.png
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
/site.webmanifest
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
/logo.webp
Cache-Control: public, max-age=86400, stale-while-revalidate=604800

View file

@ -7,3 +7,5 @@
# /blog/old-slug/ /blog/ 301
#
# Keep this file committed even when empty; it documents the contract.
/sitemap.xml /sitemap-index.xml 301

View file

@ -0,0 +1,5 @@
<svg width="444" height="444" viewBox="0 0 444 444" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M212.059 291.897L166.327 171.549C164.847 167.654 168.651 163.836 172.52 165.333L292.925 211.915C297.88 213.832 296.514 221.229 291.205 221.229H221.322V290.177C221.322 295.523 213.957 296.891 212.059 291.897Z" fill="#202020"/>
<path d="M222 82C299.32 82 362 144.68 362 222C362 299.32 299.32 362 222 362C144.68 362 82 299.32 82 222C82 144.68 144.68 82 222 82Z" stroke="#202020" stroke-width="20"/>
<path d="M72 222C72 304.843 139.157 372 222 372H82C76.4772 372 72 367.523 72 362V222Z" fill="#202020"/>
</svg>

After

Width:  |  Height:  |  Size: 616 B

View file

@ -31,9 +31,7 @@ https://open-design.ai/ and developed at github.com/nexu-io/open-design.
- Templates catalog: https://open-design.ai/templates/
- Craft principles: https://open-design.ai/craft/
- Plugins catalog: https://open-design.ai/plugins/
- Tutorials: https://open-design.ai/tutorials/
- Blog: https://open-design.ai/blog/
- HTML Anything (sister project): https://open-design.ai/html-anything/
- RSS: https://open-design.ai/blog/rss.xml
- Sitemap: https://open-design.ai/sitemap-index.xml
@ -44,18 +42,6 @@ https://open-design.ai/ and developed at github.com/nexu-io/open-design.
- GitHub issues: https://github.com/nexu-io/open-design/issues
- Discord: https://discord.gg/9ptkbbqRu
## Sister Projects
HTML Anything (alternately written as "html-anything" or "htmlanything")
is the agentic HTML editor from the same team. It reuses whichever
coding-agent CLI you already authenticated — Claude Code, Cursor Agent,
Codex, Gemini CLI, GitHub Copilot CLI, OpenCode, Qwen Coder, or Aider —
to turn Markdown, CSV, JSON, or Excel into ship-ready HTML for WeChat,
X, Zhihu, and Xiaohongshu. Apache-2.0 open source.
- Landing page: https://open-design.ai/html-anything/
- Source: https://github.com/nexu-io/html-anything
## Key Blog Posts
- https://open-design.ai/blog/open-source-alternative-to-claude-design/