diff --git a/README.md b/README.md index 3883329..02e2f9b 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,14 @@ The example gallery loads canonical `.stack` sources from the public specificati The Playground publishes canonical, Open Graph, Twitter Card, and `WebApplication` JSON-LD metadata from `index.html`. [`public/ogp.png`](./public/ogp.png) is the shared 1200×630 social image for the Playground and documentation. +The pinned Docs homepage metadata supplies the canonical tagline, localized benefits, +and supporting description. Vite substitutes the tagline into the Playground's HTML +and JSON-LD placeholders with context-specific escaping; development retrieves the +same verified Docs input before starting. VitePress displays the supporting paragraph +below the tagline while keeping the H1 product-only, and generates its homepage +Markdown alternatives from that same metadata. Do not maintain a second homepage +story in Web or add pre-rendered example SVGs to illustrate it. + [`public/robots.txt`](./public/robots.txt) permits public search crawling and advertises both sitemaps. [`public/llms.txt`](./public/llms.txt) and [`docs/public/llms.txt`](./docs/public/llms.txt) provide curated agent entry points. The VitePress build also emits clean Markdown alternatives for every documentation page and generates `/llms-full.txt` from the complete English documentation, so the agent-facing content stays synchronized with its public source. ## Keeping CLI documentation current diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 6fc896b..dc5bf47 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,4 +1,5 @@ import { mkdir, readFile, writeFile } from "node:fs/promises" +import { readFileSync } from "node:fs" import { createRequire } from "node:module" import path from "node:path" @@ -7,50 +8,21 @@ import type { LanguageRegistration } from "@shikijs/core" import type { DefaultTheme } from "vitepress" import { documentationContract } from "../../scripts/docs-validation.config.mjs" +import { productHomeMarkdown, readProductHome } from "../../scripts/product-home.mjs" const require = createRequire(import.meta.url) const stackGrammar = require("@stack-sh/language/grammar") as LanguageRegistration const siteOrigin = "https://stack-diagram.com" -const siteDescription = "Write your Technical Stack, Get beautiful diagram" +const siteDescription = readProductHome(readFileSync(path.resolve("docs/index.md"), "utf8")).hero + .tagline const socialImageUrl = `${siteOrigin}/ogp.png` -const documentationHomeMarkdown: Record = { - "index.md": `# Stack Documentation - -Stack is a declarative language for writing static software-architecture and technical-stack diagrams as concise, reviewable source. - -- [Getting started](./guide/getting-started.md) -- [Language reference](./language/syntax.md) -- [Diagnostics and limits](./reference/diagnostics-and-limits.md) -`, - "ja/index.md": `# Stackドキュメント - -Stackは、静的なsoftware architecture diagramとtechnical stack diagramを簡潔でreview可能なsourceとして記述するための宣言的言語です。 - -- [はじめる](./guide/getting-started.md) -- [言語リファレンス](./language/syntax.md) -- [Diagnosticとlimit](./reference/diagnostics-and-limits.md) -`, - "zh/index.md": `# Stack 文档 - -Stack 是一种声明式语言,用简洁、可审查的源代码编写静态软件架构图和技术栈图。 - -- [快速开始](./guide/getting-started.md) -- [语言参考](./language/syntax.md) -- [诊断与限制](./reference/diagnostics-and-limits.md) -`, - "ko/index.md": `# Stack 문서 - -Stack은 정적 소프트웨어 아키텍처 및 기술 스택 다이어그램을 간결하고 검토 가능한 소스로 작성하는 선언적 언어입니다. - -- [시작하기](./guide/getting-started.md) -- [언어 레퍼런스](./language/syntax.md) -- [진단과 제한](./reference/diagnostics-and-limits.md) -`, -} - function agentMarkdown(relativePath: string, source: string): string { const content = source.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim() - return `${content || documentationHomeMarkdown[relativePath] || ""}\n` + if (content) return `${content}\n` + if (/^(?:(?:ja|zh|ko)\/)?index\.md$/.test(relativePath)) { + return productHomeMarkdown(readProductHome(source)) + } + throw new Error(`Empty documentation page: ${relativePath}`) } function documentationUrl(relativePath: string): string { @@ -390,7 +362,7 @@ export default defineConfig({ logo: { light: "/favicon.svg", dark: "/favicon.svg", - alt: "Stack", + alt: "", }, i18nRouting: true, externalLinkIcon: true, diff --git a/docs/.vitepress/theme/components/DocsLayout.vue b/docs/.vitepress/theme/components/DocsLayout.vue new file mode 100644 index 0000000..c24da4e --- /dev/null +++ b/docs/.vitepress/theme/components/DocsLayout.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 6d0560e..6ea62dd 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -3,12 +3,14 @@ import "@fontsource-variable/ibm-plex-sans" import "@fontsource/ibm-plex-mono/400.css" import ExampleGallery from "./components/ExampleGallery.vue" +import DocsLayout from "./components/DocsLayout.vue" import IconCatalog from "./components/IconCatalog.vue" import ProviderCatalog from "./components/ProviderCatalog.vue" import "./style.css" export default { extends: DefaultTheme, + Layout: DocsLayout, enhanceApp({ app }) { app.component("ExampleGallery", ExampleGallery) app.component("IconCatalog", IconCatalog) diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index 69c0b99..431fb7f 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -55,6 +55,22 @@ body { text-rendering: optimizeLegibility; } +.stack-home-description { + max-width: 36rem; + margin: 1rem auto 0; + color: var(--vp-c-text-2); + font-size: 1rem; + line-height: 1.6; + text-wrap: pretty; +} + +@media (min-width: 960px) { + .stack-home-description { + margin-left: 0; + margin-right: 0; + } +} + .stack-provider-catalog .sr-only { position: absolute; width: 1px; diff --git a/index.html b/index.html index b376502..999ad04 100644 --- a/index.html +++ b/index.html @@ -7,13 +7,13 @@ - + - + @@ -23,7 +23,7 @@ - + ' + const rendered = renderProductMetadata( + '', + changed, + ) + assert.ok(rendered.includes('content="Text "quotes" & </script>"')) + const json = rendered.match(/")) +}) diff --git a/scripts/validate-docs-output.mjs b/scripts/validate-docs-output.mjs index 1da3d08..1176995 100644 --- a/scripts/validate-docs-output.mjs +++ b/scripts/validate-docs-output.mjs @@ -4,6 +4,7 @@ import path from "node:path" import { validateDocumentationContract } from "./docs-contract.mjs" import { documentationContract } from "./docs-validation.config.mjs" import { digest, readDocsManifest } from "./docs-source.mjs" +import { readProductHome, validateBuiltProductHome, productHomeMarkdown } from "./product-home.mjs" const outputRoot = path.resolve("dist/docs") const siteOutputRoot = path.resolve("dist") @@ -41,6 +42,7 @@ const [playgroundHtml, sourceSocialImage, builtSocialImage, rootAgentIndex, robo for (const metadata of [ "Stack", + `name="description" content="${readProductHome(await readFile(path.join(docsRoot, "index.md"), "utf8")).hero.tagline}"`, 'rel="canonical" href="https://stack-diagram.com/"', 'property="og:image" content="https://stack-diagram.com/ogp.png"', 'name="twitter:card" content="summary_large_image"', @@ -50,6 +52,8 @@ for (const metadata of [ throw new Error(`Built Playground metadata is missing: ${metadata}`) } } +if (playgroundHtml.includes("__STACK_DESCRIPTION_")) + throw new Error("Unresolved product metadata token") if (!sourceSocialImage.equals(builtSocialImage)) { throw new Error("Built social image does not match public/ogp.png") @@ -69,6 +73,12 @@ if (!rootSitemap.includes("https://stack-diagram.com/")) { for (const [page, language] of localePages) { const html = await readFile(path.join(outputRoot, page), "utf8") + const markdownPage = page.replace(/\.html$/, ".md") + const copy = readProductHome(await readFile(path.join(docsRoot, markdownPage), "utf8")) + validateBuiltProductHome(html, copy) + const agentCopy = await readFile(path.join(outputRoot, markdownPage), "utf8") + if (agentCopy !== productHomeMarkdown(copy)) + throw new Error(`${page} agent copy diverges from the visible product story`) if (!html.includes(`Stack", - `name="description" content="${siteDescription}"`, + 'name="description" content="__STACK_DESCRIPTION_HTML__"', 'rel="canonical" href="https://stack-diagram.com/"', 'rel="describedby" href="/llms.txt"', 'property="og:title" content="Stack"', @@ -156,14 +156,14 @@ if (!rootSitemap.includes("https://stack-diagram.com/")) { } for (const [page, source] of englishPages) { - if (page === "index.md" && !source.includes("light: /favicon.svg")) { + if (page === "index.md" && readProductHome(source).hero.image.light !== "/favicon.svg") { throw new Error("English documentation home does not use the Stack logo") } } for (const locale of locales) { const source = await readFile(path.join(docsRoot, locale, "index.md"), "utf8") - if (!source.includes("light: /favicon.svg")) { + if (readProductHome(source).hero.image.light !== "/favicon.svg") { throw new Error(`${locale}/index.md does not use the Stack logo`) } } diff --git a/src/App.test.tsx b/src/App.test.tsx index 885b159..67e70af 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -133,6 +133,7 @@ describe("Stack Playground", () => { it("loads the engine and renders the initial source", async () => { render() + expect(screen.getByRole("heading", { level: 1, name: "Stack" })).toBeInTheDocument() expect(screen.getByRole("textbox", { name: "Stack source" })).toBeInTheDocument() expect(screen.getByRole("link", { name: "Stack" }).querySelector("img")).toHaveAttribute( "src", diff --git a/src/App.tsx b/src/App.tsx index 0c60510..5368cfd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -252,7 +252,7 @@ export default function App() {
-

Stack Playground

+

Stack