feat(landing-page): agent design pages — rich layout, hero illustrations, and full localization (#3972)
* feat(landing-page): add agent detail pages for top coding agents Build the first batch of per-agent detail pages under /agents/ for Claude Code, Codex, Cursor, and OpenCode, and turn the /agents/ hub into a supporting page that links into them. Each page renders from copy.agentGuides[slug]: a TL;DR, what-the-agent-is section, design patterns, real externally-sourced resource links, a with-Open-Design drive section, and an FAQ backed by FAQPage JSON-LD. en is hand-written and zh is hand-reviewed; the remaining locales reuse the en guide via the compact-locale fallback so no locale renders undefined. Targets long-tail intent like "claude code for design", "codex design", "cursor for designers", and "opencode design". * feat(landing-page): upgrade the Codex agent page to a rich long-form layout Replace the compact /agents/codex/ page with /agents/codex-design/, an opt-in long-form layout: a product-led hero band + CTA, an on-this-page TOC, deep how-to sections mixing prose, steps, code, comparison tables, and three custom illustrations, plus an expanded FAQ backed by FAQPage JSON-LD. The route carries the "design" keyword to target "codex design" and "codex for design" intent; the /agents/ hub now links there. The rich layout is gated behind an optional `rich` payload on copy.agentGuides[slug]: pages without it keep the compact shape, so this doubles as the template other agent pages can opt into later. en is hand-written and zh is hand-reviewed; the remaining locales reuse the en guide via the compact-locale fallback so none renders undefined. * feat(landing-page): rework agent pages into a single-column rich layout with hero illustrations Replace the two-column hero band with one centered full-measure column so the heading, hero banner, and body all share a single left and right edge, instead of stranding prose in a narrow left-aligned column while the heading wrapped mid-word. Widen the body to fill the measure and justify it. Drop the inherited 70ch max-width caps that were also catching the solution-page how-to steps, FAQ list, and template/system grids, so they fill the section width instead of stranding at ~707px on /solutions/ pages. Swap the placeholder line-art for warm editorial hero illustrations whose central hub carries the agent's real mark (the official OpenAI Codex emblem fused into the artwork), so the page is identifiable at a glance rather than relying on a pasted-on logo badge. * feat(landing-page): add Cursor, Claude Code, and OpenCode design pages and localize all agent pages Add rich long-form detail pages for Cursor, Claude Code, and OpenCode that mirror the Codex page: a hand-structured how-to (what the agent is, why it is good at design, setup, the screenshot/preview loop, Figma, a comparison table, pitfalls, and how Open Design wraps it), an FAQ with JSON-LD, and a hero illustration with the agent's real logo fused into the artwork. Rename the per-agent routes to the `/agents/<agent>-design/` form to capture "<agent> design" search intent, point the /agents/ hub at them via DETAIL_ROUTES, and remove the old compact routes. Localize the agent guides across every landing locale. The new agent-guides.i18n.ts builds per-locale guides by spreading the English copy (so non-rendered compact fields stay type-complete) and overriding the rendered fields plus the long-form rich payload with translations; it is wired into compactInfoPageCopy so a compact locale renders fully localized agent pages instead of falling back to English. en and zh are authored directly; the other 16 locales are machine-translated and welcome native review. * feat(landing-page): surface agent pages in the nav and flatten the /agents/ hub Add the four agent design pages (Codex, Cursor, Claude Code, OpenCode) under AMR in the header's Agent dropdown, so they are reachable from the top nav and not only from the hub. Brand names are locale-invariant, so they are listed directly while the routes stay in lockstep with the hub's DETAIL_ROUTES. On the /agents/ hub, drop the first-party / supported / community tier split and list all adapters in one grid under a single description, and tighten the section rhythm so the long run of short sections reads less airy. * fix(landing-page): emit locale-correct JSON-LD URLs on agent pages The agent detail pages' BreadcrumbList and Article JSON-LD hard-coded the English /agents/<route>/ URLs, so every localized route (e.g. /ja/agents/codex-design/) emitted structured data pointing back to the English document — contradicting the locale-specific canonical, hreflang, and <html lang> the layout already emits. Build the JSON-LD URLs from the localized path instead, on all four detail pages. Also point the /agents/ hub ItemList at each agent's dedicated leaf page (/agents/<route>/) instead of the in-hub #slug anchor when one exists, so the structured data matches the cards' real link targets. * fix(landing-page): localize the /agents/ hub JSON-LD URLs too The hub's BreadcrumbList and ItemList structured data still emitted English /agents/... URLs on the localized /[locale]/agents/ routes, contradicting the locale-specific canonical the layout renders. Build them from the localized path (href / Astro.url.pathname) so the metadata matches the rendered route, mirroring the detail-page fix. * fix(landing-page): scope prose-list coral markers away from layout components The .solution-page .info-section ul/ol normalization added a coral dash / counter ::before and an indent to every list under a solution section, which double-numbered the FAQ (.faq-list) and how-to steps (.solution-steps) on top of their own indices, and prepended a stray coral dash that shifted the template / system gallery cards (.solution-tpl-grid / .solution-system-grid). Keep the list-style reset broad but apply the marker + indent only to plain prose lists, excluding those four components. * fix(landing-page): split localized agent guides under the blob-size guard The single agent-guides.i18n.ts (~1.4 MB of machine-translated agent copy for 16 locales) tripped the repo's 1 MiB changed-file blob guard. Split the data into four part files (~350 KB each) merged by a thin index, keeping the same buildLocalizedAgentGuides API and behavior. * fix(landing-page): redirect old /agents/<slug>/ routes to the -design pages The agent detail pages were renamed from /agents/<slug>/ to /agents/<slug>-design/ and the old generators removed. Add 301 redirects (root and locale-prefixed) so any inbound link or indexed URL for the old paths transfers to the new page instead of 404ing. --------- Co-authored-by: Joey <276262049+xne998808-ai@users.noreply.github.com>
@@ -302,10 +302,11 @@ export function Header({
|
||||
</ul>
|
||||
</li>
|
||||
{/*
|
||||
Agent — for now this dropdown lists only AMR (the design Agent).
|
||||
The 17 first-party coding-agent adapters and their per-agent
|
||||
/agents/ hub anchors are intentionally held back for a later
|
||||
pass; the trigger already links to the /agents/ hub.
|
||||
Agent — AMR (the design Agent) heads the dropdown, followed by the
|
||||
coding agents that have a dedicated long-form design page. Brand
|
||||
names are locale-invariant, so they are listed here directly; the
|
||||
routes stay in lockstep with DETAIL_ROUTES on the /agents/ hub. The
|
||||
trigger still links to the /agents/ hub.
|
||||
*/}
|
||||
<li className='has-dropdown'>
|
||||
<a
|
||||
@@ -324,6 +325,18 @@ export function Header({
|
||||
<span className='dropdown-blurb'>{productMenuCopy.amrBlurb}</span>
|
||||
</a>
|
||||
</li>
|
||||
{[
|
||||
{ route: 'codex-design', name: 'Codex' },
|
||||
{ route: 'cursor-design', name: 'Cursor' },
|
||||
{ route: 'claude-code-design', name: 'Claude Code' },
|
||||
{ route: 'opencode-design', name: 'OpenCode' },
|
||||
].map((agentItem) => (
|
||||
<li role='none' key={agentItem.route}>
|
||||
<a role='menuitem' href={href(`/agents/${agentItem.route}/`)}>
|
||||
<span className='dropdown-name'>{agentItem.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
{/*
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Localized agent-guide copy for the landing-page agent detail pages.
|
||||
*
|
||||
* en + zh live directly in info-page-i18n.ts. Every other landing locale
|
||||
* gets its agent pages from the part files here (split to stay under the
|
||||
* repo changed-file blob guard). buildLocalizedAgentGuides() spreads the
|
||||
* English guide first then overrides with the translation; it is wired into
|
||||
* compactInfoPageCopy so a compact locale renders fully localized agent
|
||||
* pages instead of falling back to English. Machine-translated; native
|
||||
* review welcome.
|
||||
*/
|
||||
import type { InfoPageCopy } from './info-page-i18n';
|
||||
import type { LandingLocaleCode } from './i18n';
|
||||
import { localizedAgentGuidesPartA } from './agent-guides.part-a.i18n';
|
||||
import { localizedAgentGuidesPartB } from './agent-guides.part-b.i18n';
|
||||
import { localizedAgentGuidesPartC } from './agent-guides.part-c.i18n';
|
||||
import { localizedAgentGuidesPartD } from './agent-guides.part-d.i18n';
|
||||
|
||||
type Guides = NonNullable<InfoPageCopy['agentGuides']>;
|
||||
|
||||
export function buildLocalizedAgentGuides(en: Guides): Partial<Record<LandingLocaleCode, Guides>> {
|
||||
return {
|
||||
...localizedAgentGuidesPartA(en),
|
||||
...localizedAgentGuidesPartB(en),
|
||||
...localizedAgentGuidesPartC(en),
|
||||
...localizedAgentGuidesPartD(en),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
import AgentDetailPage from '../../../agents/claude-code-design/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 } }),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<AgentDetailPage />
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
import AgentDetailPage from '../../../agents/codex-design/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 } }),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<AgentDetailPage />
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
import AgentDetailPage from '../../../agents/cursor-design/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 } }),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<AgentDetailPage />
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
import AgentDetailPage from '../../../agents/opencode-design/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 } }),
|
||||
);
|
||||
}
|
||||
---
|
||||
|
||||
<AgentDetailPage />
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
/*
|
||||
* /agents/claude-code-design/ — agent detail page (industrial long-form).
|
||||
*
|
||||
* Captures "claude code design", "claude code for design", "design with claude code" intent and drives to Open Design. When the
|
||||
* agent guide carries a `rich` payload this renders the deep how-to
|
||||
* layout (hero CTA + sectioned long-form + images); otherwise it falls
|
||||
* back to the compact layout. Linked from the `/agents/` hub.
|
||||
*
|
||||
* All copy and resource links come from `copy.agentGuides['claude-code']`;
|
||||
* never fabricate URLs or facts here.
|
||||
*/
|
||||
import Layout from '../../../_components/sub-page-layout.astro';
|
||||
import { getInfoPageCopy } from '../../../info-page-i18n';
|
||||
import { localeFromPath, localizedHref } from '../../../i18n';
|
||||
|
||||
const SLUG = 'claude-code';
|
||||
const ROUTE = 'claude-code-design';
|
||||
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 common = copy.common;
|
||||
// Compact locales fall back to the English guide rather than crash on undefined.
|
||||
const page = copy.agentGuides[SLUG] ?? getInfoPageCopy('en').agentGuides[SLUG]!;
|
||||
const rich = page.rich;
|
||||
|
||||
// Localize CTA action hrefs while leaving external links untouched.
|
||||
const ctaHref = (a: { href: string; external?: boolean }) =>
|
||||
a.external ? a.href : href(a.href);
|
||||
|
||||
const faqEntries = (rich?.faq ?? page.faq).map(({ name, text }) => ({ name, text }));
|
||||
|
||||
// JSON-LD URLs must match the locale-specific canonical the Layout emits, so
|
||||
// build them from the localized path rather than the hard-coded English route.
|
||||
const homeUrl = new URL(href('/'), SITE).toString();
|
||||
const agentsHubUrl = new URL(href('/agents/'), SITE).toString();
|
||||
const pageUrl = new URL(Astro.url.pathname, SITE).toString();
|
||||
|
||||
const jsonLd = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: homeUrl },
|
||||
{ '@type': 'ListItem', position: 2, name: 'Agents', item: agentsHubUrl },
|
||||
{ '@type': 'ListItem', position: 3, name: page.breadcrumb, item: pageUrl },
|
||||
],
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: page.title,
|
||||
description: page.description,
|
||||
url: pageUrl,
|
||||
isPartOf: { '@id': `${SITE}#website` },
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqEntries.map(({ name, text }) => ({
|
||||
'@type': 'Question',
|
||||
name,
|
||||
acceptedAnswer: { '@type': 'Answer', text },
|
||||
})),
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<Layout title={page.title} description={page.description} active="home" jsonLd={jsonLd}>
|
||||
<nav class="breadcrumb" aria-label={common.breadcrumbAria}>
|
||||
<a href={href('/')}>Open Design</a>
|
||||
<span>/</span>
|
||||
<a href={href('/agents/')}>Agents</a>
|
||||
<span>/</span>
|
||||
<span aria-current="page">{page.breadcrumb}</span>
|
||||
</nav>
|
||||
|
||||
<article class={`info-page${rich ? ' solution-page agent-rich' : ''}`}>
|
||||
{rich ? (
|
||||
<>
|
||||
{/* Single-column article header: full-measure heading + lead + CTA,
|
||||
then a full-width hero banner. One measure / one left edge so the
|
||||
heading never wraps mid-word and the body never feels offset. */}
|
||||
<header class="catalog-head agent-rich-head">
|
||||
<span class="label">{page.label}</span>
|
||||
<h1 class="display">{page.heading}</h1>
|
||||
<p class="lead">{page.lead}</p>
|
||||
<div class="solution-hero-cta">
|
||||
{rich.heroCtaActions.map((a) => (
|
||||
<a
|
||||
class={`btn ${a.variant === 'primary' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
href={ctaHref(a)}
|
||||
{...(a.external ? { target: '_blank', rel: 'noreferrer noopener' } : {})}
|
||||
>{a.label}</a>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{rich.heroImage ? (
|
||||
<figure class="agent-hero-banner">
|
||||
<img src={rich.heroImage.src} alt={rich.heroImage.alt} loading="eager" decoding="async" width={1280} height={720} />
|
||||
</figure>
|
||||
) : null}
|
||||
|
||||
<div class="agent-intro">
|
||||
<p class="agent-hero-lead">{rich.heroCtaLead}</p>
|
||||
{rich.intro.map((p) => (<p>{p}</p>))}
|
||||
</div>
|
||||
|
||||
<nav class="info-toc" aria-label={rich.tocLabel}>
|
||||
<span>{rich.tocLabel}</span>
|
||||
{rich.toc.map((t) => (<a href={`#${t.id}`}>{t.label}</a>))}
|
||||
</nav>
|
||||
|
||||
{rich.sections.map((section) => (
|
||||
<section class="info-section" id={section.id}>
|
||||
<h2>{section.heading}</h2>
|
||||
{section.blocks.map((block) => {
|
||||
if (block.kind === 'p') return <p>{block.text}</p>;
|
||||
if (block.kind === 'ul') return <ul>{block.items.map((i) => (<li>{i}</li>))}</ul>;
|
||||
if (block.kind === 'ol') return <ol>{block.items.map((i) => (<li>{i}</li>))}</ol>;
|
||||
if (block.kind === 'steps')
|
||||
return (
|
||||
<ul class="agent-steps">
|
||||
{block.items.map((i) => (<li><strong>{i.label}:</strong> {i.body}</li>))}
|
||||
</ul>
|
||||
);
|
||||
if (block.kind === 'code')
|
||||
return (
|
||||
<pre class="agent-code"><code>{block.code}</code></pre>
|
||||
);
|
||||
if (block.kind === 'image')
|
||||
return (
|
||||
<figure class="agent-section-img">
|
||||
<img src={block.src} alt={block.alt} loading="lazy" decoding="async" />
|
||||
{block.caption ? <figcaption>{block.caption}</figcaption> : null}
|
||||
</figure>
|
||||
);
|
||||
if (block.kind === 'table')
|
||||
return (
|
||||
<div class="compare-table-wrap">
|
||||
<table class="compare-table">
|
||||
<thead>
|
||||
<tr>{block.columns.map((c) => (<th>{c}</th>))}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{block.rows.map((row) => (
|
||||
<tr>
|
||||
{row.map((cell, ci) => (
|
||||
ci === 0 ? <th scope="row">{cell}</th> : <td>{cell}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
})}
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section class="info-section" id="faq">
|
||||
<h2>{rich.faqTitle}</h2>
|
||||
<ol class="faq-list">
|
||||
{faqEntries.map(({ name, text }, idx) => (
|
||||
<li class="faq-item">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="faq-index">{String(idx + 1).padStart(2, '0')}</span>
|
||||
<span class="faq-q">{name}</span>
|
||||
<span class="faq-toggle" aria-hidden="true">+</span>
|
||||
</summary>
|
||||
<p class="faq-a">{text}</p>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-cta" aria-label="Open Design call to action">
|
||||
<div>
|
||||
<h2>{rich.ctaTitle}</h2>
|
||||
<p>{rich.ctaBody}</p>
|
||||
</div>
|
||||
<div class="info-cta-actions">
|
||||
{rich.ctaActions.map((a) => (
|
||||
<a
|
||||
class={`btn ${a.variant === 'primary' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
href={ctaHref(a)}
|
||||
{...(a.external ? { target: '_blank', rel: 'noreferrer noopener' } : {})}
|
||||
>{a.label}</a>
|
||||
))}
|
||||
</div>
|
||||
<div class="info-cta-meta">
|
||||
<span class="stamp">● {common.apache}</span>
|
||||
<span>{common.localFirst} · {common.byok}</span>
|
||||
<span><a class="inline-link" href={href('/agents/')}>{rich.hubLinkLabel}</a></span>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<header class="catalog-head">
|
||||
<span class="label">{page.label}</span>
|
||||
<h1 class="display">{page.heading}</h1>
|
||||
<p class="lead">{page.lead}</p>
|
||||
</header>
|
||||
|
||||
<div class="tldr-card">
|
||||
<h3>{page.tldrTitle}</h3>
|
||||
<p>{page.tldrBody}</p>
|
||||
</div>
|
||||
|
||||
<nav class="info-toc" aria-label={common.onThisPage}>
|
||||
<span>{common.onThisPage}</span>
|
||||
<a href="#about">{page.toc[0]}</a>
|
||||
<a href="#design">{page.toc[1]}</a>
|
||||
<a href="#resources">{page.toc[2]}</a>
|
||||
<a href="#with-od">{page.toc[3]}</a>
|
||||
<a href="#faq">{page.toc[4]}</a>
|
||||
</nav>
|
||||
|
||||
<section class="info-section" id="about">
|
||||
<h2>{page.aboutTitle}</h2>
|
||||
{page.aboutBody.map((paragraph) => (<p>{paragraph}</p>))}
|
||||
<ul>
|
||||
<li><strong>{page.vendorLabel}:</strong> {page.vendor}</li>
|
||||
<li><strong>{page.credentialLabel}:</strong> {page.credential}</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="design">
|
||||
<h2>{page.designTitle}</h2>
|
||||
<p>{page.designLead}</p>
|
||||
<ol>
|
||||
{page.designPoints.map((item) => (<li><strong>{item.label}</strong> {item.body}</li>))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="resources">
|
||||
<h2>{page.linksTitle}</h2>
|
||||
<p>{page.linksLead}</p>
|
||||
<ul class="resource-list">
|
||||
{page.links.map((l) => (
|
||||
<li>
|
||||
<a href={l.href} target="_blank" rel="noreferrer noopener">{l.label}</a>
|
||||
<span class="resource-source">{l.source}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="with-od">
|
||||
<h2>{page.withOdTitle}</h2>
|
||||
<p>{page.withOdLead}</p>
|
||||
<ol>{page.withOdSteps.map((step) => (<li>{step}</li>))}</ol>
|
||||
<p>{page.withOdClosing}</p>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="faq">
|
||||
<h2>{page.faqTitle}</h2>
|
||||
<ol class="faq-list">
|
||||
{page.faq.map(({ name, text }, idx) => (
|
||||
<li class="faq-item">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="faq-index">{String(idx + 1).padStart(2, '0')}</span>
|
||||
<span class="faq-q">{name}</span>
|
||||
<span class="faq-toggle" aria-hidden="true">+</span>
|
||||
</summary>
|
||||
<p class="faq-a">{text}</p>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-cta" aria-label="Open Design call to action">
|
||||
<div>
|
||||
<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">{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">● {common.apache}</span>
|
||||
<span>{common.localFirst} · {common.byok}</span>
|
||||
<span><a class="inline-link" href={href('/agents/')}>{copy.agents.breadcrumb}</a></span>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
</Layout>
|
||||
@@ -0,0 +1,302 @@
|
||||
---
|
||||
/*
|
||||
* /agents/codex-design/ — agent detail page (industrial long-form).
|
||||
*
|
||||
* Captures "codex design", "codex for design", "build UI with codex",
|
||||
* "openai codex frontend" intent and drives to Open Design. When the
|
||||
* agent guide carries a `rich` payload this renders the deep how-to
|
||||
* layout (hero CTA + sectioned long-form + images); otherwise it falls
|
||||
* back to the compact layout. Linked from the `/agents/` hub.
|
||||
*
|
||||
* All copy and resource links come from `copy.agentGuides['codex']`;
|
||||
* never fabricate URLs or facts here.
|
||||
*/
|
||||
import Layout from '../../../_components/sub-page-layout.astro';
|
||||
import { getInfoPageCopy } from '../../../info-page-i18n';
|
||||
import { localeFromPath, localizedHref } from '../../../i18n';
|
||||
|
||||
const SLUG = 'codex';
|
||||
const ROUTE = 'codex-design';
|
||||
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 common = copy.common;
|
||||
// Compact locales fall back to the English guide rather than crash on undefined.
|
||||
const page = copy.agentGuides[SLUG] ?? getInfoPageCopy('en').agentGuides[SLUG]!;
|
||||
const rich = page.rich;
|
||||
|
||||
// Localize CTA action hrefs while leaving external links untouched.
|
||||
const ctaHref = (a: { href: string; external?: boolean }) =>
|
||||
a.external ? a.href : href(a.href);
|
||||
|
||||
const faqEntries = (rich?.faq ?? page.faq).map(({ name, text }) => ({ name, text }));
|
||||
|
||||
// JSON-LD URLs must match the locale-specific canonical the Layout emits, so
|
||||
// build them from the localized path rather than the hard-coded English route.
|
||||
const homeUrl = new URL(href('/'), SITE).toString();
|
||||
const agentsHubUrl = new URL(href('/agents/'), SITE).toString();
|
||||
const pageUrl = new URL(Astro.url.pathname, SITE).toString();
|
||||
|
||||
const jsonLd = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: homeUrl },
|
||||
{ '@type': 'ListItem', position: 2, name: 'Agents', item: agentsHubUrl },
|
||||
{ '@type': 'ListItem', position: 3, name: page.breadcrumb, item: pageUrl },
|
||||
],
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: page.title,
|
||||
description: page.description,
|
||||
url: pageUrl,
|
||||
isPartOf: { '@id': `${SITE}#website` },
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqEntries.map(({ name, text }) => ({
|
||||
'@type': 'Question',
|
||||
name,
|
||||
acceptedAnswer: { '@type': 'Answer', text },
|
||||
})),
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<Layout title={page.title} description={page.description} active="home" jsonLd={jsonLd}>
|
||||
<nav class="breadcrumb" aria-label={common.breadcrumbAria}>
|
||||
<a href={href('/')}>Open Design</a>
|
||||
<span>/</span>
|
||||
<a href={href('/agents/')}>Agents</a>
|
||||
<span>/</span>
|
||||
<span aria-current="page">{page.breadcrumb}</span>
|
||||
</nav>
|
||||
|
||||
<article class={`info-page${rich ? ' solution-page agent-rich' : ''}`}>
|
||||
{rich ? (
|
||||
<>
|
||||
{/* Single-column article header: full-measure heading + lead + CTA,
|
||||
then a full-width hero banner. One measure / one left edge so the
|
||||
heading never wraps mid-word and the body never feels offset. */}
|
||||
<header class="catalog-head agent-rich-head">
|
||||
<span class="label">{page.label}</span>
|
||||
<h1 class="display">{page.heading}</h1>
|
||||
<p class="lead">{page.lead}</p>
|
||||
<div class="solution-hero-cta">
|
||||
{rich.heroCtaActions.map((a) => (
|
||||
<a
|
||||
class={`btn ${a.variant === 'primary' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
href={ctaHref(a)}
|
||||
{...(a.external ? { target: '_blank', rel: 'noreferrer noopener' } : {})}
|
||||
>{a.label}</a>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{rich.heroImage ? (
|
||||
<figure class="agent-hero-banner">
|
||||
<img src={rich.heroImage.src} alt={rich.heroImage.alt} loading="eager" decoding="async" width={1280} height={720} />
|
||||
</figure>
|
||||
) : null}
|
||||
|
||||
<div class="agent-intro">
|
||||
<p class="agent-hero-lead">{rich.heroCtaLead}</p>
|
||||
{rich.intro.map((p) => (<p>{p}</p>))}
|
||||
</div>
|
||||
|
||||
<nav class="info-toc" aria-label={rich.tocLabel}>
|
||||
<span>{rich.tocLabel}</span>
|
||||
{rich.toc.map((t) => (<a href={`#${t.id}`}>{t.label}</a>))}
|
||||
</nav>
|
||||
|
||||
{rich.sections.map((section) => (
|
||||
<section class="info-section" id={section.id}>
|
||||
<h2>{section.heading}</h2>
|
||||
{section.blocks.map((block) => {
|
||||
if (block.kind === 'p') return <p>{block.text}</p>;
|
||||
if (block.kind === 'ul') return <ul>{block.items.map((i) => (<li>{i}</li>))}</ul>;
|
||||
if (block.kind === 'ol') return <ol>{block.items.map((i) => (<li>{i}</li>))}</ol>;
|
||||
if (block.kind === 'steps')
|
||||
return (
|
||||
<ul class="agent-steps">
|
||||
{block.items.map((i) => (<li><strong>{i.label}:</strong> {i.body}</li>))}
|
||||
</ul>
|
||||
);
|
||||
if (block.kind === 'code')
|
||||
return (
|
||||
<pre class="agent-code"><code>{block.code}</code></pre>
|
||||
);
|
||||
if (block.kind === 'image')
|
||||
return (
|
||||
<figure class="agent-section-img">
|
||||
<img src={block.src} alt={block.alt} loading="lazy" decoding="async" />
|
||||
{block.caption ? <figcaption>{block.caption}</figcaption> : null}
|
||||
</figure>
|
||||
);
|
||||
if (block.kind === 'table')
|
||||
return (
|
||||
<div class="compare-table-wrap">
|
||||
<table class="compare-table">
|
||||
<thead>
|
||||
<tr>{block.columns.map((c) => (<th>{c}</th>))}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{block.rows.map((row) => (
|
||||
<tr>
|
||||
{row.map((cell, ci) => (
|
||||
ci === 0 ? <th scope="row">{cell}</th> : <td>{cell}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
})}
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section class="info-section" id="faq">
|
||||
<h2>{rich.faqTitle}</h2>
|
||||
<ol class="faq-list">
|
||||
{faqEntries.map(({ name, text }, idx) => (
|
||||
<li class="faq-item">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="faq-index">{String(idx + 1).padStart(2, '0')}</span>
|
||||
<span class="faq-q">{name}</span>
|
||||
<span class="faq-toggle" aria-hidden="true">+</span>
|
||||
</summary>
|
||||
<p class="faq-a">{text}</p>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-cta" aria-label="Open Design call to action">
|
||||
<div>
|
||||
<h2>{rich.ctaTitle}</h2>
|
||||
<p>{rich.ctaBody}</p>
|
||||
</div>
|
||||
<div class="info-cta-actions">
|
||||
{rich.ctaActions.map((a) => (
|
||||
<a
|
||||
class={`btn ${a.variant === 'primary' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
href={ctaHref(a)}
|
||||
{...(a.external ? { target: '_blank', rel: 'noreferrer noopener' } : {})}
|
||||
>{a.label}</a>
|
||||
))}
|
||||
</div>
|
||||
<div class="info-cta-meta">
|
||||
<span class="stamp">● {common.apache}</span>
|
||||
<span>{common.localFirst} · {common.byok}</span>
|
||||
<span><a class="inline-link" href={href('/agents/')}>{rich.hubLinkLabel}</a></span>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<header class="catalog-head">
|
||||
<span class="label">{page.label}</span>
|
||||
<h1 class="display">{page.heading}</h1>
|
||||
<p class="lead">{page.lead}</p>
|
||||
</header>
|
||||
|
||||
<div class="tldr-card">
|
||||
<h3>{page.tldrTitle}</h3>
|
||||
<p>{page.tldrBody}</p>
|
||||
</div>
|
||||
|
||||
<nav class="info-toc" aria-label={common.onThisPage}>
|
||||
<span>{common.onThisPage}</span>
|
||||
<a href="#about">{page.toc[0]}</a>
|
||||
<a href="#design">{page.toc[1]}</a>
|
||||
<a href="#resources">{page.toc[2]}</a>
|
||||
<a href="#with-od">{page.toc[3]}</a>
|
||||
<a href="#faq">{page.toc[4]}</a>
|
||||
</nav>
|
||||
|
||||
<section class="info-section" id="about">
|
||||
<h2>{page.aboutTitle}</h2>
|
||||
{page.aboutBody.map((paragraph) => (<p>{paragraph}</p>))}
|
||||
<ul>
|
||||
<li><strong>{page.vendorLabel}:</strong> {page.vendor}</li>
|
||||
<li><strong>{page.credentialLabel}:</strong> {page.credential}</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="design">
|
||||
<h2>{page.designTitle}</h2>
|
||||
<p>{page.designLead}</p>
|
||||
<ol>
|
||||
{page.designPoints.map((item) => (<li><strong>{item.label}</strong> {item.body}</li>))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="resources">
|
||||
<h2>{page.linksTitle}</h2>
|
||||
<p>{page.linksLead}</p>
|
||||
<ul class="resource-list">
|
||||
{page.links.map((l) => (
|
||||
<li>
|
||||
<a href={l.href} target="_blank" rel="noreferrer noopener">{l.label}</a>
|
||||
<span class="resource-source">{l.source}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="with-od">
|
||||
<h2>{page.withOdTitle}</h2>
|
||||
<p>{page.withOdLead}</p>
|
||||
<ol>{page.withOdSteps.map((step) => (<li>{step}</li>))}</ol>
|
||||
<p>{page.withOdClosing}</p>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="faq">
|
||||
<h2>{page.faqTitle}</h2>
|
||||
<ol class="faq-list">
|
||||
{page.faq.map(({ name, text }, idx) => (
|
||||
<li class="faq-item">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="faq-index">{String(idx + 1).padStart(2, '0')}</span>
|
||||
<span class="faq-q">{name}</span>
|
||||
<span class="faq-toggle" aria-hidden="true">+</span>
|
||||
</summary>
|
||||
<p class="faq-a">{text}</p>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-cta" aria-label="Open Design call to action">
|
||||
<div>
|
||||
<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">{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">● {common.apache}</span>
|
||||
<span>{common.localFirst} · {common.byok}</span>
|
||||
<span><a class="inline-link" href={href('/agents/')}>{copy.agents.breadcrumb}</a></span>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
</Layout>
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
/*
|
||||
* /agents/cursor-design/ — agent detail page (industrial long-form).
|
||||
*
|
||||
* Captures "cursor design", "cursor for designers", "build UI with cursor" intent and drives to Open Design. When the
|
||||
* agent guide carries a `rich` payload this renders the deep how-to
|
||||
* layout (hero CTA + sectioned long-form + images); otherwise it falls
|
||||
* back to the compact layout. Linked from the `/agents/` hub.
|
||||
*
|
||||
* All copy and resource links come from `copy.agentGuides['cursor']`;
|
||||
* never fabricate URLs or facts here.
|
||||
*/
|
||||
import Layout from '../../../_components/sub-page-layout.astro';
|
||||
import { getInfoPageCopy } from '../../../info-page-i18n';
|
||||
import { localeFromPath, localizedHref } from '../../../i18n';
|
||||
|
||||
const SLUG = 'cursor';
|
||||
const ROUTE = 'cursor-design';
|
||||
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 common = copy.common;
|
||||
// Compact locales fall back to the English guide rather than crash on undefined.
|
||||
const page = copy.agentGuides[SLUG] ?? getInfoPageCopy('en').agentGuides[SLUG]!;
|
||||
const rich = page.rich;
|
||||
|
||||
// Localize CTA action hrefs while leaving external links untouched.
|
||||
const ctaHref = (a: { href: string; external?: boolean }) =>
|
||||
a.external ? a.href : href(a.href);
|
||||
|
||||
const faqEntries = (rich?.faq ?? page.faq).map(({ name, text }) => ({ name, text }));
|
||||
|
||||
// JSON-LD URLs must match the locale-specific canonical the Layout emits, so
|
||||
// build them from the localized path rather than the hard-coded English route.
|
||||
const homeUrl = new URL(href('/'), SITE).toString();
|
||||
const agentsHubUrl = new URL(href('/agents/'), SITE).toString();
|
||||
const pageUrl = new URL(Astro.url.pathname, SITE).toString();
|
||||
|
||||
const jsonLd = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: homeUrl },
|
||||
{ '@type': 'ListItem', position: 2, name: 'Agents', item: agentsHubUrl },
|
||||
{ '@type': 'ListItem', position: 3, name: page.breadcrumb, item: pageUrl },
|
||||
],
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: page.title,
|
||||
description: page.description,
|
||||
url: pageUrl,
|
||||
isPartOf: { '@id': `${SITE}#website` },
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqEntries.map(({ name, text }) => ({
|
||||
'@type': 'Question',
|
||||
name,
|
||||
acceptedAnswer: { '@type': 'Answer', text },
|
||||
})),
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<Layout title={page.title} description={page.description} active="home" jsonLd={jsonLd}>
|
||||
<nav class="breadcrumb" aria-label={common.breadcrumbAria}>
|
||||
<a href={href('/')}>Open Design</a>
|
||||
<span>/</span>
|
||||
<a href={href('/agents/')}>Agents</a>
|
||||
<span>/</span>
|
||||
<span aria-current="page">{page.breadcrumb}</span>
|
||||
</nav>
|
||||
|
||||
<article class={`info-page${rich ? ' solution-page agent-rich' : ''}`}>
|
||||
{rich ? (
|
||||
<>
|
||||
{/* Single-column article header: full-measure heading + lead + CTA,
|
||||
then a full-width hero banner. One measure / one left edge so the
|
||||
heading never wraps mid-word and the body never feels offset. */}
|
||||
<header class="catalog-head agent-rich-head">
|
||||
<span class="label">{page.label}</span>
|
||||
<h1 class="display">{page.heading}</h1>
|
||||
<p class="lead">{page.lead}</p>
|
||||
<div class="solution-hero-cta">
|
||||
{rich.heroCtaActions.map((a) => (
|
||||
<a
|
||||
class={`btn ${a.variant === 'primary' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
href={ctaHref(a)}
|
||||
{...(a.external ? { target: '_blank', rel: 'noreferrer noopener' } : {})}
|
||||
>{a.label}</a>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{rich.heroImage ? (
|
||||
<figure class="agent-hero-banner">
|
||||
<img src={rich.heroImage.src} alt={rich.heroImage.alt} loading="eager" decoding="async" width={1280} height={720} />
|
||||
</figure>
|
||||
) : null}
|
||||
|
||||
<div class="agent-intro">
|
||||
<p class="agent-hero-lead">{rich.heroCtaLead}</p>
|
||||
{rich.intro.map((p) => (<p>{p}</p>))}
|
||||
</div>
|
||||
|
||||
<nav class="info-toc" aria-label={rich.tocLabel}>
|
||||
<span>{rich.tocLabel}</span>
|
||||
{rich.toc.map((t) => (<a href={`#${t.id}`}>{t.label}</a>))}
|
||||
</nav>
|
||||
|
||||
{rich.sections.map((section) => (
|
||||
<section class="info-section" id={section.id}>
|
||||
<h2>{section.heading}</h2>
|
||||
{section.blocks.map((block) => {
|
||||
if (block.kind === 'p') return <p>{block.text}</p>;
|
||||
if (block.kind === 'ul') return <ul>{block.items.map((i) => (<li>{i}</li>))}</ul>;
|
||||
if (block.kind === 'ol') return <ol>{block.items.map((i) => (<li>{i}</li>))}</ol>;
|
||||
if (block.kind === 'steps')
|
||||
return (
|
||||
<ul class="agent-steps">
|
||||
{block.items.map((i) => (<li><strong>{i.label}:</strong> {i.body}</li>))}
|
||||
</ul>
|
||||
);
|
||||
if (block.kind === 'code')
|
||||
return (
|
||||
<pre class="agent-code"><code>{block.code}</code></pre>
|
||||
);
|
||||
if (block.kind === 'image')
|
||||
return (
|
||||
<figure class="agent-section-img">
|
||||
<img src={block.src} alt={block.alt} loading="lazy" decoding="async" />
|
||||
{block.caption ? <figcaption>{block.caption}</figcaption> : null}
|
||||
</figure>
|
||||
);
|
||||
if (block.kind === 'table')
|
||||
return (
|
||||
<div class="compare-table-wrap">
|
||||
<table class="compare-table">
|
||||
<thead>
|
||||
<tr>{block.columns.map((c) => (<th>{c}</th>))}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{block.rows.map((row) => (
|
||||
<tr>
|
||||
{row.map((cell, ci) => (
|
||||
ci === 0 ? <th scope="row">{cell}</th> : <td>{cell}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
})}
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section class="info-section" id="faq">
|
||||
<h2>{rich.faqTitle}</h2>
|
||||
<ol class="faq-list">
|
||||
{faqEntries.map(({ name, text }, idx) => (
|
||||
<li class="faq-item">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="faq-index">{String(idx + 1).padStart(2, '0')}</span>
|
||||
<span class="faq-q">{name}</span>
|
||||
<span class="faq-toggle" aria-hidden="true">+</span>
|
||||
</summary>
|
||||
<p class="faq-a">{text}</p>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-cta" aria-label="Open Design call to action">
|
||||
<div>
|
||||
<h2>{rich.ctaTitle}</h2>
|
||||
<p>{rich.ctaBody}</p>
|
||||
</div>
|
||||
<div class="info-cta-actions">
|
||||
{rich.ctaActions.map((a) => (
|
||||
<a
|
||||
class={`btn ${a.variant === 'primary' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
href={ctaHref(a)}
|
||||
{...(a.external ? { target: '_blank', rel: 'noreferrer noopener' } : {})}
|
||||
>{a.label}</a>
|
||||
))}
|
||||
</div>
|
||||
<div class="info-cta-meta">
|
||||
<span class="stamp">● {common.apache}</span>
|
||||
<span>{common.localFirst} · {common.byok}</span>
|
||||
<span><a class="inline-link" href={href('/agents/')}>{rich.hubLinkLabel}</a></span>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<header class="catalog-head">
|
||||
<span class="label">{page.label}</span>
|
||||
<h1 class="display">{page.heading}</h1>
|
||||
<p class="lead">{page.lead}</p>
|
||||
</header>
|
||||
|
||||
<div class="tldr-card">
|
||||
<h3>{page.tldrTitle}</h3>
|
||||
<p>{page.tldrBody}</p>
|
||||
</div>
|
||||
|
||||
<nav class="info-toc" aria-label={common.onThisPage}>
|
||||
<span>{common.onThisPage}</span>
|
||||
<a href="#about">{page.toc[0]}</a>
|
||||
<a href="#design">{page.toc[1]}</a>
|
||||
<a href="#resources">{page.toc[2]}</a>
|
||||
<a href="#with-od">{page.toc[3]}</a>
|
||||
<a href="#faq">{page.toc[4]}</a>
|
||||
</nav>
|
||||
|
||||
<section class="info-section" id="about">
|
||||
<h2>{page.aboutTitle}</h2>
|
||||
{page.aboutBody.map((paragraph) => (<p>{paragraph}</p>))}
|
||||
<ul>
|
||||
<li><strong>{page.vendorLabel}:</strong> {page.vendor}</li>
|
||||
<li><strong>{page.credentialLabel}:</strong> {page.credential}</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="design">
|
||||
<h2>{page.designTitle}</h2>
|
||||
<p>{page.designLead}</p>
|
||||
<ol>
|
||||
{page.designPoints.map((item) => (<li><strong>{item.label}</strong> {item.body}</li>))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="resources">
|
||||
<h2>{page.linksTitle}</h2>
|
||||
<p>{page.linksLead}</p>
|
||||
<ul class="resource-list">
|
||||
{page.links.map((l) => (
|
||||
<li>
|
||||
<a href={l.href} target="_blank" rel="noreferrer noopener">{l.label}</a>
|
||||
<span class="resource-source">{l.source}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="with-od">
|
||||
<h2>{page.withOdTitle}</h2>
|
||||
<p>{page.withOdLead}</p>
|
||||
<ol>{page.withOdSteps.map((step) => (<li>{step}</li>))}</ol>
|
||||
<p>{page.withOdClosing}</p>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="faq">
|
||||
<h2>{page.faqTitle}</h2>
|
||||
<ol class="faq-list">
|
||||
{page.faq.map(({ name, text }, idx) => (
|
||||
<li class="faq-item">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="faq-index">{String(idx + 1).padStart(2, '0')}</span>
|
||||
<span class="faq-q">{name}</span>
|
||||
<span class="faq-toggle" aria-hidden="true">+</span>
|
||||
</summary>
|
||||
<p class="faq-a">{text}</p>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-cta" aria-label="Open Design call to action">
|
||||
<div>
|
||||
<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">{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">● {common.apache}</span>
|
||||
<span>{common.localFirst} · {common.byok}</span>
|
||||
<span><a class="inline-link" href={href('/agents/')}>{copy.agents.breadcrumb}</a></span>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
</Layout>
|
||||
@@ -5,8 +5,8 @@
|
||||
* Captures "claude code design", "codex design", "cursor agent design",
|
||||
* "gemini cli design", "opencode design", "qwen design" queries. Each
|
||||
* agent gets a short card explaining how it plugs into Open Design.
|
||||
* Per-agent detail pages (e.g. `/agents/claude-code/`) will follow as
|
||||
* P2 work — this hub is the SEO entry point.
|
||||
* Agents with a dedicated long-form page link into `/agents/<agent>-design/`
|
||||
* (see DETAIL_ROUTES below); this hub is the SEO entry point.
|
||||
*/
|
||||
import Layout from '../../_components/sub-page-layout.astro';
|
||||
import { getInfoPageCopy } from '../../info-page-i18n';
|
||||
@@ -69,6 +69,17 @@ const tiers = [
|
||||
|
||||
const allAgents = tiers.flatMap((t) => t.agents);
|
||||
|
||||
// Agents with a dedicated detail page, mapped slug -> route segment.
|
||||
// The route can differ from the slug (e.g. `codex` -> `codex-design`)
|
||||
// to put the design keyword in the URL. Keep in lockstep with the
|
||||
// `pages/agents/<route>/index.astro` directories.
|
||||
const DETAIL_ROUTES: Record<string, string> = {
|
||||
'claude-code': 'claude-code-design',
|
||||
codex: 'codex-design',
|
||||
cursor: 'cursor-design',
|
||||
opencode: 'opencode-design',
|
||||
};
|
||||
|
||||
const localizedCredentialLabels: Partial<Record<LandingLocaleCode, Record<string, string>>> = {
|
||||
zh: {
|
||||
'claude-code': 'Anthropic API key(BYOK)或 Claude 订阅',
|
||||
@@ -134,13 +145,24 @@ const credentialLabel = (agent: (typeof allAgents)[number]) => {
|
||||
return localizedCredentialLabels[locale]?.[agent.slug] ?? `${common.byok} · ${agent.vendor}`;
|
||||
};
|
||||
|
||||
// JSON-LD URLs must match the locale-specific canonical the Layout emits, so
|
||||
// build them from the localized path rather than the hard-coded English route.
|
||||
const homeUrl = new URL(href('/'), SITE).toString();
|
||||
const hubUrl = new URL(Astro.url.pathname, SITE).toString();
|
||||
// Agents with a dedicated leaf page advertise it as the canonical destination;
|
||||
// the rest keep the in-hub anchor until they get one. Both stay locale-aware.
|
||||
const agentItemUrl = (slug: string) =>
|
||||
DETAIL_ROUTES[slug]
|
||||
? new URL(href(`/agents/${DETAIL_ROUTES[slug]}/`), SITE).toString()
|
||||
: `${hubUrl}#${slug}`;
|
||||
|
||||
const jsonLd = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: SITE },
|
||||
{ '@type': 'ListItem', position: 2, name: page.breadcrumb, item: `${SITE}agents/` },
|
||||
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: homeUrl },
|
||||
{ '@type': 'ListItem', position: 2, name: page.breadcrumb, item: hubUrl },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -152,7 +174,7 @@ const jsonLd = [
|
||||
'@type': 'ListItem',
|
||||
position: idx + 1,
|
||||
name: a.name,
|
||||
url: `${SITE}agents/#${a.slug}`,
|
||||
url: agentItemUrl(a.slug),
|
||||
})),
|
||||
},
|
||||
];
|
||||
@@ -165,7 +187,7 @@ const jsonLd = [
|
||||
<span aria-current="page">{page.breadcrumb}</span>
|
||||
</nav>
|
||||
|
||||
<article class="info-page">
|
||||
<article class="info-page agents-hub">
|
||||
<header class="catalog-head">
|
||||
<span class="label">{page.label}</span>
|
||||
<h1 class="display">{page.heading(allAgents.length)}</h1>
|
||||
@@ -175,24 +197,17 @@ const jsonLd = [
|
||||
<section class="info-section">
|
||||
<h2>{page.adaptersTitle}</h2>
|
||||
<p>{page.adaptersBody}</p>
|
||||
<ul class="agent-grid">
|
||||
{allAgents.map((a) => (
|
||||
<li class="agent-card" id={a.slug}>
|
||||
<h3>{DETAIL_ROUTES[a.slug] ? <a href={href(`/agents/${DETAIL_ROUTES[a.slug]}/`)}>{a.name}</a> : a.name}</h3>
|
||||
<p><strong>{page.vendor}:</strong> {a.vendor}</p>
|
||||
<p><strong>{page.credential}:</strong> {credentialLabel(a)}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{tiers.map((tier) => (
|
||||
<section class="info-section">
|
||||
<h2>{tier.label}</h2>
|
||||
<p>{tier.blurb}</p>
|
||||
<ul class="agent-grid">
|
||||
{tier.agents.map((a) => (
|
||||
<li class="agent-card" id={a.slug}>
|
||||
<h3>{a.name}</h3>
|
||||
<p><strong>{page.vendor}:</strong> {a.vendor}</p>
|
||||
<p><strong>{page.credential}:</strong> {credentialLabel(a)}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section class="info-section" id="byok">
|
||||
<h2>{page.byokTitle}</h2>
|
||||
<p>{page.byokLead}</p>
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
/*
|
||||
* /agents/opencode-design/ — agent detail page (industrial long-form).
|
||||
*
|
||||
* Captures "opencode design", "opencode for frontend", "design with opencode" intent and drives to Open Design. When the
|
||||
* agent guide carries a `rich` payload this renders the deep how-to
|
||||
* layout (hero CTA + sectioned long-form + images); otherwise it falls
|
||||
* back to the compact layout. Linked from the `/agents/` hub.
|
||||
*
|
||||
* All copy and resource links come from `copy.agentGuides['opencode']`;
|
||||
* never fabricate URLs or facts here.
|
||||
*/
|
||||
import Layout from '../../../_components/sub-page-layout.astro';
|
||||
import { getInfoPageCopy } from '../../../info-page-i18n';
|
||||
import { localeFromPath, localizedHref } from '../../../i18n';
|
||||
|
||||
const SLUG = 'opencode';
|
||||
const ROUTE = 'opencode-design';
|
||||
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 common = copy.common;
|
||||
// Compact locales fall back to the English guide rather than crash on undefined.
|
||||
const page = copy.agentGuides[SLUG] ?? getInfoPageCopy('en').agentGuides[SLUG]!;
|
||||
const rich = page.rich;
|
||||
|
||||
// Localize CTA action hrefs while leaving external links untouched.
|
||||
const ctaHref = (a: { href: string; external?: boolean }) =>
|
||||
a.external ? a.href : href(a.href);
|
||||
|
||||
const faqEntries = (rich?.faq ?? page.faq).map(({ name, text }) => ({ name, text }));
|
||||
|
||||
// JSON-LD URLs must match the locale-specific canonical the Layout emits, so
|
||||
// build them from the localized path rather than the hard-coded English route.
|
||||
const homeUrl = new URL(href('/'), SITE).toString();
|
||||
const agentsHubUrl = new URL(href('/agents/'), SITE).toString();
|
||||
const pageUrl = new URL(Astro.url.pathname, SITE).toString();
|
||||
|
||||
const jsonLd = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Open Design', item: homeUrl },
|
||||
{ '@type': 'ListItem', position: 2, name: 'Agents', item: agentsHubUrl },
|
||||
{ '@type': 'ListItem', position: 3, name: page.breadcrumb, item: pageUrl },
|
||||
],
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: page.title,
|
||||
description: page.description,
|
||||
url: pageUrl,
|
||||
isPartOf: { '@id': `${SITE}#website` },
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqEntries.map(({ name, text }) => ({
|
||||
'@type': 'Question',
|
||||
name,
|
||||
acceptedAnswer: { '@type': 'Answer', text },
|
||||
})),
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<Layout title={page.title} description={page.description} active="home" jsonLd={jsonLd}>
|
||||
<nav class="breadcrumb" aria-label={common.breadcrumbAria}>
|
||||
<a href={href('/')}>Open Design</a>
|
||||
<span>/</span>
|
||||
<a href={href('/agents/')}>Agents</a>
|
||||
<span>/</span>
|
||||
<span aria-current="page">{page.breadcrumb}</span>
|
||||
</nav>
|
||||
|
||||
<article class={`info-page${rich ? ' solution-page agent-rich' : ''}`}>
|
||||
{rich ? (
|
||||
<>
|
||||
{/* Single-column article header: full-measure heading + lead + CTA,
|
||||
then a full-width hero banner. One measure / one left edge so the
|
||||
heading never wraps mid-word and the body never feels offset. */}
|
||||
<header class="catalog-head agent-rich-head">
|
||||
<span class="label">{page.label}</span>
|
||||
<h1 class="display">{page.heading}</h1>
|
||||
<p class="lead">{page.lead}</p>
|
||||
<div class="solution-hero-cta">
|
||||
{rich.heroCtaActions.map((a) => (
|
||||
<a
|
||||
class={`btn ${a.variant === 'primary' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
href={ctaHref(a)}
|
||||
{...(a.external ? { target: '_blank', rel: 'noreferrer noopener' } : {})}
|
||||
>{a.label}</a>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{rich.heroImage ? (
|
||||
<figure class="agent-hero-banner">
|
||||
<img src={rich.heroImage.src} alt={rich.heroImage.alt} loading="eager" decoding="async" width={1280} height={720} />
|
||||
</figure>
|
||||
) : null}
|
||||
|
||||
<div class="agent-intro">
|
||||
<p class="agent-hero-lead">{rich.heroCtaLead}</p>
|
||||
{rich.intro.map((p) => (<p>{p}</p>))}
|
||||
</div>
|
||||
|
||||
<nav class="info-toc" aria-label={rich.tocLabel}>
|
||||
<span>{rich.tocLabel}</span>
|
||||
{rich.toc.map((t) => (<a href={`#${t.id}`}>{t.label}</a>))}
|
||||
</nav>
|
||||
|
||||
{rich.sections.map((section) => (
|
||||
<section class="info-section" id={section.id}>
|
||||
<h2>{section.heading}</h2>
|
||||
{section.blocks.map((block) => {
|
||||
if (block.kind === 'p') return <p>{block.text}</p>;
|
||||
if (block.kind === 'ul') return <ul>{block.items.map((i) => (<li>{i}</li>))}</ul>;
|
||||
if (block.kind === 'ol') return <ol>{block.items.map((i) => (<li>{i}</li>))}</ol>;
|
||||
if (block.kind === 'steps')
|
||||
return (
|
||||
<ul class="agent-steps">
|
||||
{block.items.map((i) => (<li><strong>{i.label}:</strong> {i.body}</li>))}
|
||||
</ul>
|
||||
);
|
||||
if (block.kind === 'code')
|
||||
return (
|
||||
<pre class="agent-code"><code>{block.code}</code></pre>
|
||||
);
|
||||
if (block.kind === 'image')
|
||||
return (
|
||||
<figure class="agent-section-img">
|
||||
<img src={block.src} alt={block.alt} loading="lazy" decoding="async" />
|
||||
{block.caption ? <figcaption>{block.caption}</figcaption> : null}
|
||||
</figure>
|
||||
);
|
||||
if (block.kind === 'table')
|
||||
return (
|
||||
<div class="compare-table-wrap">
|
||||
<table class="compare-table">
|
||||
<thead>
|
||||
<tr>{block.columns.map((c) => (<th>{c}</th>))}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{block.rows.map((row) => (
|
||||
<tr>
|
||||
{row.map((cell, ci) => (
|
||||
ci === 0 ? <th scope="row">{cell}</th> : <td>{cell}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
})}
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section class="info-section" id="faq">
|
||||
<h2>{rich.faqTitle}</h2>
|
||||
<ol class="faq-list">
|
||||
{faqEntries.map(({ name, text }, idx) => (
|
||||
<li class="faq-item">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="faq-index">{String(idx + 1).padStart(2, '0')}</span>
|
||||
<span class="faq-q">{name}</span>
|
||||
<span class="faq-toggle" aria-hidden="true">+</span>
|
||||
</summary>
|
||||
<p class="faq-a">{text}</p>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-cta" aria-label="Open Design call to action">
|
||||
<div>
|
||||
<h2>{rich.ctaTitle}</h2>
|
||||
<p>{rich.ctaBody}</p>
|
||||
</div>
|
||||
<div class="info-cta-actions">
|
||||
{rich.ctaActions.map((a) => (
|
||||
<a
|
||||
class={`btn ${a.variant === 'primary' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
href={ctaHref(a)}
|
||||
{...(a.external ? { target: '_blank', rel: 'noreferrer noopener' } : {})}
|
||||
>{a.label}</a>
|
||||
))}
|
||||
</div>
|
||||
<div class="info-cta-meta">
|
||||
<span class="stamp">● {common.apache}</span>
|
||||
<span>{common.localFirst} · {common.byok}</span>
|
||||
<span><a class="inline-link" href={href('/agents/')}>{rich.hubLinkLabel}</a></span>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<header class="catalog-head">
|
||||
<span class="label">{page.label}</span>
|
||||
<h1 class="display">{page.heading}</h1>
|
||||
<p class="lead">{page.lead}</p>
|
||||
</header>
|
||||
|
||||
<div class="tldr-card">
|
||||
<h3>{page.tldrTitle}</h3>
|
||||
<p>{page.tldrBody}</p>
|
||||
</div>
|
||||
|
||||
<nav class="info-toc" aria-label={common.onThisPage}>
|
||||
<span>{common.onThisPage}</span>
|
||||
<a href="#about">{page.toc[0]}</a>
|
||||
<a href="#design">{page.toc[1]}</a>
|
||||
<a href="#resources">{page.toc[2]}</a>
|
||||
<a href="#with-od">{page.toc[3]}</a>
|
||||
<a href="#faq">{page.toc[4]}</a>
|
||||
</nav>
|
||||
|
||||
<section class="info-section" id="about">
|
||||
<h2>{page.aboutTitle}</h2>
|
||||
{page.aboutBody.map((paragraph) => (<p>{paragraph}</p>))}
|
||||
<ul>
|
||||
<li><strong>{page.vendorLabel}:</strong> {page.vendor}</li>
|
||||
<li><strong>{page.credentialLabel}:</strong> {page.credential}</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="design">
|
||||
<h2>{page.designTitle}</h2>
|
||||
<p>{page.designLead}</p>
|
||||
<ol>
|
||||
{page.designPoints.map((item) => (<li><strong>{item.label}</strong> {item.body}</li>))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="resources">
|
||||
<h2>{page.linksTitle}</h2>
|
||||
<p>{page.linksLead}</p>
|
||||
<ul class="resource-list">
|
||||
{page.links.map((l) => (
|
||||
<li>
|
||||
<a href={l.href} target="_blank" rel="noreferrer noopener">{l.label}</a>
|
||||
<span class="resource-source">{l.source}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="with-od">
|
||||
<h2>{page.withOdTitle}</h2>
|
||||
<p>{page.withOdLead}</p>
|
||||
<ol>{page.withOdSteps.map((step) => (<li>{step}</li>))}</ol>
|
||||
<p>{page.withOdClosing}</p>
|
||||
</section>
|
||||
|
||||
<section class="info-section" id="faq">
|
||||
<h2>{page.faqTitle}</h2>
|
||||
<ol class="faq-list">
|
||||
{page.faq.map(({ name, text }, idx) => (
|
||||
<li class="faq-item">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="faq-index">{String(idx + 1).padStart(2, '0')}</span>
|
||||
<span class="faq-q">{name}</span>
|
||||
<span class="faq-toggle" aria-hidden="true">+</span>
|
||||
</summary>
|
||||
<p class="faq-a">{text}</p>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="info-cta" aria-label="Open Design call to action">
|
||||
<div>
|
||||
<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">{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">● {common.apache}</span>
|
||||
<span>{common.localFirst} · {common.byok}</span>
|
||||
<span><a class="inline-link" href={href('/agents/')}>{copy.agents.breadcrumb}</a></span>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
</Layout>
|
||||
@@ -1120,7 +1120,7 @@ body.sub-page {
|
||||
.info-page {
|
||||
max-width: 920px;
|
||||
}
|
||||
.info-page .info-section + .info-section { margin-top: 64px; }
|
||||
.info-page .info-section + .info-section { margin-top: 44px; }
|
||||
.info-page h2 {
|
||||
font-family: var(--serif);
|
||||
font-weight: 600;
|
||||
@@ -2358,3 +2358,238 @@ dialog.tpl-share-popover .detail-share-dialog-text {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Agent detail — industrial long-form layout (`/agents/<slug>-design/`).
|
||||
* Opt-in: only rendered when an agent guide carries a `rich` payload. Hero
|
||||
* CTA strip leads with the product, deep sections carry prose + code + images.
|
||||
* ------------------------------------------------------------------------- */
|
||||
.agent-hero-cta {
|
||||
margin: 4px 0 8px;
|
||||
padding: 22px 24px;
|
||||
border: 1px solid var(--ink);
|
||||
background: var(--paper-warm);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 16px 20px;
|
||||
}
|
||||
.agent-hero-cta p {
|
||||
flex: 1 1 320px;
|
||||
margin: 0;
|
||||
font-family: var(--sans);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: var(--ink-soft);
|
||||
max-width: 60ch;
|
||||
}
|
||||
.agent-hero-cta-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.agent-intro p {
|
||||
font-size: 16.5px;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.agent-section-img {
|
||||
margin: 22px 0 6px;
|
||||
}
|
||||
.agent-section-img img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--line, rgba(20, 18, 11, 0.12));
|
||||
border-radius: 8px;
|
||||
background: var(--paper-warm);
|
||||
}
|
||||
.agent-section-img figcaption {
|
||||
margin-top: 8px;
|
||||
font-family: var(--sans);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
.agent-code {
|
||||
margin: 0 0 18px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid var(--line, rgba(20, 18, 11, 0.12));
|
||||
border-radius: 8px;
|
||||
background: #14120b;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.agent-code code {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
color: #f0ede4;
|
||||
white-space: pre;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.agent-steps {
|
||||
list-style: none;
|
||||
margin: 0 0 18px;
|
||||
padding: 0;
|
||||
}
|
||||
.agent-steps li {
|
||||
position: relative;
|
||||
padding-left: 1.4em;
|
||||
margin-bottom: 10px;
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.agent-steps li::before {
|
||||
content: "—";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--coral);
|
||||
}
|
||||
.agent-steps li strong { color: var(--ink); }
|
||||
@media (max-width: 600px) {
|
||||
.agent-hero-cta { padding: 18px; }
|
||||
.agent-hero-cta-actions .btn { flex: 1 1 auto; text-align: center; }
|
||||
}
|
||||
|
||||
/* Unify list styling inside agent long-form sections: bare <ul>/<ol> share the
|
||||
* same rhythm and coral marker as `.agent-steps`, so a section that mixes both
|
||||
* (steps + facts list) reads as one consistent list family rather than two. */
|
||||
.solution-page .info-section ul,
|
||||
.solution-page .info-section ol {
|
||||
list-style: none;
|
||||
margin: 0 0 18px;
|
||||
padding-left: 0;
|
||||
max-width: 70ch;
|
||||
}
|
||||
/* The coral dash / number markers are only for plain prose lists. Components
|
||||
* that ship their own layout and numbering — the FAQ (.faq-list), the how-to
|
||||
* steps (.solution-steps), and the template / system galleries
|
||||
* (.solution-tpl-grid / .solution-system-grid) — keep the list-style reset
|
||||
* above but opt OUT of the marker + indent below, so they don't get a
|
||||
* duplicate coral counter or a stray dash shifting their cards. */
|
||||
.solution-page .info-section ul:not(.solution-tpl-grid):not(.solution-system-grid) li,
|
||||
.solution-page .info-section ol:not(.faq-list):not(.solution-steps) li {
|
||||
position: relative;
|
||||
padding-left: 1.4em;
|
||||
margin-bottom: 10px;
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.solution-page .info-section ul:not(.solution-tpl-grid):not(.solution-system-grid) li::before {
|
||||
content: "—";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--coral);
|
||||
}
|
||||
/* Ordered prose lists keep their number, coral, in a monospace marker. */
|
||||
.solution-page .info-section ol:not(.faq-list):not(.solution-steps) {
|
||||
counter-reset: agent-ol;
|
||||
}
|
||||
.solution-page .info-section ol:not(.faq-list):not(.solution-steps) li {
|
||||
padding-left: 1.9em;
|
||||
counter-increment: agent-ol;
|
||||
}
|
||||
.solution-page .info-section ol:not(.faq-list):not(.solution-steps) li::before {
|
||||
content: counter(agent-ol) ".";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--coral);
|
||||
}
|
||||
/* steps and the unified lists now share one look; keep steps' bottom margin
|
||||
* tight when it directly precedes a fact list in the same section. */
|
||||
.solution-page .info-section .agent-steps + ul,
|
||||
.solution-page .info-section .agent-steps + ol { margin-top: 2px; }
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Solution + agent pages: single-column text content fills the section and
|
||||
* lines up with the full-width hero / feature rows / tables / template grid,
|
||||
* instead of stranding in a ~707px column pinned hard left (the 70ch ul/ol
|
||||
* prose cap above is meant for genuine bullet lists but also catches the FAQ
|
||||
* list and how-to steps, which should fill). Prose is justified so both edges
|
||||
* sit flush. Genuine prose bullet lists keep the readable 70ch measure.
|
||||
* ------------------------------------------------------------------------- */
|
||||
.solution-page .info-section > p,
|
||||
.solution-page .info-section .faq-list,
|
||||
.solution-page .info-section .solution-steps,
|
||||
.solution-page .solution-step-text p { max-width: none; }
|
||||
.solution-page .info-section > p {
|
||||
text-align: justify;
|
||||
text-justify: inter-character;
|
||||
}
|
||||
/* Let the step's text column grow to the section's right edge so the step body
|
||||
* lines up with the FAQ rows and the feature/compare tables above and below. */
|
||||
.solution-page .solution-step-text { flex: 1 1 auto; min-width: 0; }
|
||||
/* The template grid inherits the narrow ~706px cap from the catalog `.tpl-grid`
|
||||
* (built for the dense /templates/ index), which strands its three columns in
|
||||
* the left half of the wide solution canvas. Drop the cap so the grid fills the
|
||||
* section and its cards line up with everything else. */
|
||||
.solution-page .info-section ul.solution-tpl-grid,
|
||||
.solution-page .info-section ul.solution-system-grid { max-width: none; }
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Agent rich page is a long-form ARTICLE, not a solution marketing page: ONE
|
||||
* centered single-column measure for everything — heading, hero banner, intro,
|
||||
* TOC, how-to sections, FAQ, CTA all share the same width and the same left
|
||||
* edge. This avoids the two failure modes of a wide solution canvas: prose
|
||||
* stranded in a narrow column pinned hard left (huge empty right), and a wide
|
||||
* 2-column hero whose heading wraps mid-word and whose left edge doesn't line
|
||||
* up with the narrower body below it.
|
||||
* ------------------------------------------------------------------------- */
|
||||
.info-page.solution-page.agent-rich {
|
||||
max-width: 1080px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
/* Everything fills the single measure — drop the inherited 70ch prose caps
|
||||
* (.info-page p, .solution-page .info-section > p) so the intro, section
|
||||
* paragraphs, and lists all reach the column's right edge and line up with the
|
||||
* full-width hero banner instead of re-stranding in a ~640px column. */
|
||||
.agent-rich .agent-intro p,
|
||||
.agent-rich .info-section > p,
|
||||
.agent-rich .info-section ul,
|
||||
.agent-rich .info-section ol { max-width: none; }
|
||||
|
||||
/* Justify body prose so both the left AND right edge sit flush — the ragged
|
||||
* right edge of plain left-aligned text reads as sloppy for CJK copy. Lists
|
||||
* and captions stay left-aligned (their markers want a hard left edge). */
|
||||
.agent-rich .agent-intro p,
|
||||
.agent-rich .info-section > p {
|
||||
text-align: justify;
|
||||
text-justify: inter-character;
|
||||
}
|
||||
|
||||
/* Header fills the full measure too — override the shared .catalog-head 880px
|
||||
* cap and its 720px lead cap so the heading, lead, hero banner, and body all
|
||||
* share one left edge and one right edge. Nothing in the article is narrower
|
||||
* than anything else. */
|
||||
.agent-rich-head { margin-bottom: 4px; max-width: none; }
|
||||
.agent-rich-head .lead { max-width: none; }
|
||||
|
||||
/* Hero banner: full-width editorial illustration under the header. The agent's
|
||||
* own mark is baked into the illustration's central hub, so no overlay needed. */
|
||||
.agent-hero-banner {
|
||||
margin: 28px 0 6px;
|
||||
}
|
||||
.agent-hero-banner > img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: var(--paper-warm);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Agents hub (/agents/) — all adapters in one grid, no tier split. Tighten the
|
||||
* vertical rhythm: the page is a long list of short sections, so the shared
|
||||
* 44px info-section gap reads as too airy here.
|
||||
* ------------------------------------------------------------------------- */
|
||||
.agents-hub .info-section + .info-section { margin-top: 30px; }
|
||||
.agents-hub .info-section > p { margin-bottom: 14px; }
|
||||
.agents-hub .agent-grid { margin-top: 18px; }
|
||||
|
||||
@@ -162,3 +162,17 @@
|
||||
/ko/skills/* /ko/plugins/skills/ 301
|
||||
/ko/systems/* /ko/plugins/systems/ 301
|
||||
/ko/templates/* /ko/plugins/templates/ 301
|
||||
|
||||
# Agent detail route migration: the per-agent pages moved from
|
||||
# `/agents/<slug>/` to `/agents/<slug>-design/` (design keyword in the URL) and
|
||||
# the old Astro generators were removed. These 301s preserve any inbound links
|
||||
# / SEO equity for the old paths, at the root and under every locale prefix.
|
||||
# trailingSlash:'always', so every source and target ends in '/'.
|
||||
/agents/codex/ /agents/codex-design/ 301
|
||||
/agents/cursor/ /agents/cursor-design/ 301
|
||||
/agents/claude-code/ /agents/claude-code-design/ 301
|
||||
/agents/opencode/ /agents/opencode-design/ 301
|
||||
/:locale/agents/codex/ /:locale/agents/codex-design/ 301
|
||||
/:locale/agents/cursor/ /:locale/agents/cursor-design/ 301
|
||||
/:locale/agents/claude-code/ /:locale/agents/claude-code-design/ 301
|
||||
/:locale/agents/opencode/ /:locale/agents/opencode-design/ 301
|
||||
|
||||
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 108 KiB |
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path d="M19.503 0H4.496A4.496 4.496 0 000 4.496v15.007A4.496 4.496 0 004.496 24h15.007A4.496 4.496 0 0024 19.503V4.496A4.496 4.496 0 0019.503 0z" fill="#fff"></path><path d="M9.064 3.344a4.578 4.578 0 012.285-.312c1 .115 1.891.54 2.673 1.275.01.01.024.017.037.021a.09.09 0 00.043 0 4.55 4.55 0 013.046.275l.047.022.116.057a4.581 4.581 0 012.188 2.399c.209.51.313 1.041.315 1.595a4.24 4.24 0 01-.134 1.223.123.123 0 00.03.115c.594.607.988 1.33 1.183 2.17.289 1.425-.007 2.71-.887 3.854l-.136.166a4.548 4.548 0 01-2.201 1.388.123.123 0 00-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838-1.187-.006-2.239-.44-3.157-1.302a.107.107 0 00-.105-.024c-.388.125-.78.143-1.204.138a4.441 4.441 0 01-1.945-.466 4.544 4.544 0 01-1.61-1.335c-.152-.202-.303-.392-.414-.617a5.81 5.81 0 01-.37-.961 4.582 4.582 0 01-.014-2.298.124.124 0 00.006-.056.085.085 0 00-.027-.048 4.467 4.467 0 01-1.034-1.651 3.896 3.896 0 01-.251-1.192 5.189 5.189 0 01.141-1.6c.337-1.112.982-1.985 1.933-2.618.212-.141.413-.251.601-.33.215-.089.43-.164.646-.227a.098.098 0 00.065-.066 4.51 4.51 0 01.829-1.615 4.535 4.535 0 011.837-1.388zm3.482 10.565a.637.637 0 000 1.272h3.636a.637.637 0 100-1.272h-3.636zM8.462 9.23a.637.637 0 00-1.106.631l1.272 2.224-1.266 2.136a.636.636 0 101.095.649l1.454-2.455a.636.636 0 00.005-.64L8.462 9.23z" fill="url(#lobe-icons-codex-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-codex-_R_0_" x1="12" x2="12" y1="3" y2="21"><stop stop-color="#B1A7FF"></stop><stop offset=".5" stop-color="#7A9DFF"></stop><stop offset="1" stop-color="#3941FF"></stop></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 113 KiB |