Add SEO and agent-readable documentation
This commit is contained in:
@@ -9,3 +9,4 @@ site/decodal-site/dist
|
||||
|
||||
# Astro
|
||||
site/decodal-site/.astro
|
||||
site/decodal-site/.wrangler
|
||||
|
||||
@@ -40,6 +40,18 @@ npm --prefix site/decodal-site run build
|
||||
|
||||
Use `npm --prefix site/decodal-site run build:wasm` when Rust bindings or formatter behavior changed.
|
||||
|
||||
## Documentation site delivery
|
||||
|
||||
The canonical site origin is `https://decodal.hareworks.net`, configured through Astro's `site` option. A normal site build generates the sitemap, robots policy, social metadata, structured data, 404 page, and source-backed Markdown endpoints. `/llms.txt` indexes the individual English and Japanese Markdown pages; `/llms-full.txt` and `/ja/llms-full.txt` provide complete manuals. These resources are built directly from `doc/manual/source` and do not use Cloudflare's HTML-to-Markdown conversion.
|
||||
|
||||
Deploy the already prepared site with:
|
||||
|
||||
```sh
|
||||
npm --prefix site/decodal-site run deploy
|
||||
```
|
||||
|
||||
Cloudflare Pages `_redirects` handles legacy paths within the site, but not hostname redirects. In the Cloudflare account, configure a Bulk Redirect from `https://decodal-site.pages.dev/` to `https://decodal.hareworks.net/` with status `301`, subpath matching, path-suffix preservation, and query-string preservation enabled. This prevents the Pages project hostname from becoming a second public origin.
|
||||
|
||||
## Syntax changes
|
||||
|
||||
When syntax changes:
|
||||
|
||||
@@ -1,7 +1,47 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
const site = 'https://decodal.hareworks.net';
|
||||
|
||||
function alternateLinks(url) {
|
||||
const pathname = new URL(url).pathname;
|
||||
if (pathname === '/' || pathname === '/ja/') {
|
||||
return [
|
||||
{ url: `${site}/`, lang: 'en' },
|
||||
{ url: `${site}/ja/`, lang: 'ja' },
|
||||
{ url: `${site}/`, lang: 'x-default' },
|
||||
];
|
||||
}
|
||||
|
||||
const match = pathname.match(/^\/docs\/(en|ja)\/(.*)$/);
|
||||
if (!match) return undefined;
|
||||
const localizedPath = match[2];
|
||||
return [
|
||||
{ url: `${site}/docs/en/${localizedPath}`, lang: 'en' },
|
||||
{ url: `${site}/docs/ja/${localizedPath}`, lang: 'ja' },
|
||||
{ url: `${site}/docs/en/${localizedPath}`, lang: 'x-default' },
|
||||
];
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
site,
|
||||
output: 'static',
|
||||
integrations: [
|
||||
sitemap({
|
||||
filter: (page) => {
|
||||
const pathname = new URL(page).pathname;
|
||||
return pathname !== '/docs/'
|
||||
&& pathname !== '/404.html'
|
||||
&& pathname !== '/404/'
|
||||
&& !pathname.endsWith('.md')
|
||||
&& !pathname.endsWith('.txt');
|
||||
},
|
||||
serialize(item) {
|
||||
const links = alternateLinks(item.url);
|
||||
return links ? { ...item, links } : item;
|
||||
},
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
assets: 'assets',
|
||||
},
|
||||
|
||||
Generated
+82
@@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/sitemap": "3.2.1",
|
||||
"@codemirror/language": "^6.12.4",
|
||||
"@codemirror/view": "^6.43.6",
|
||||
"@fontsource/ibm-plex-mono": "^5.3.0",
|
||||
@@ -156,6 +157,17 @@
|
||||
"node": "^18.17.1 || ^20.3.0 || >=21.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@astrojs/sitemap": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.2.1.tgz",
|
||||
"integrity": "sha512-uxMfO8f7pALq0ADL6Lk68UV6dNYjJ2xGUzyjjVj60JLBs5a6smtlkBYv3tQ0DzoqwS7c9n4FUx5lgv0yPo/fgA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sitemap": "^8.0.0",
|
||||
"stream-replace-string": "^2.0.0",
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@astrojs/telemetry": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.1.0.tgz",
|
||||
@@ -2452,6 +2464,24 @@
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
|
||||
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sax": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz",
|
||||
"integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
@@ -2670,6 +2700,12 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -6422,6 +6458,15 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
|
||||
"integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/section-matter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||
@@ -6531,6 +6576,31 @@
|
||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sitemap": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sitemap/-/sitemap-8.0.3.tgz",
|
||||
"integrity": "sha512-9Ew1tR2WYw8RGE2XLy7GjkusvYXy8Rg6y8TYuBuQMfIEdGcWoJpY2Wr5DzsEiL/TKCw56+YKTCCUHglorEYK+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^17.0.5",
|
||||
"@types/sax": "^1.2.1",
|
||||
"arg": "^5.0.0",
|
||||
"sax": "^1.4.1"
|
||||
},
|
||||
"bin": {
|
||||
"sitemap": "dist/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0",
|
||||
"npm": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sitemap/node_modules/@types/node": {
|
||||
"version": "17.0.45",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
|
||||
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -6568,6 +6638,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-replace-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz",
|
||||
"integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
@@ -6784,6 +6860,12 @@
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unenv": {
|
||||
"version": "2.0.0-rc.24",
|
||||
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/sitemap": "3.2.1",
|
||||
"@codemirror/language": "^6.12.4",
|
||||
"@codemirror/view": "^6.43.6",
|
||||
"@fontsource/ibm-plex-mono": "^5.3.0",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Content-Signal: search=yes, ai-input=yes, ai-train=no
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
/*.md
|
||||
Content-Type: text/markdown; charset=utf-8
|
||||
X-Robots-Tag: noindex
|
||||
|
||||
/llms.txt
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Robots-Tag: noindex
|
||||
|
||||
/llms-full.txt
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Robots-Tag: noindex
|
||||
|
||||
/ja/llms-full.txt
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Robots-Tag: noindex
|
||||
@@ -0,0 +1,37 @@
|
||||
/docs /docs/en/ 301
|
||||
/docs/ /docs/en/ 301
|
||||
/docs/components/ /docs/en/components/ 301
|
||||
/docs/embedding/ /docs/en/embedding/ 301
|
||||
/docs/introduction/ /docs/en/introduction/ 301
|
||||
/docs/language/ /docs/en/language/ 301
|
||||
/docs/language/constraints-and-defaults/ /docs/en/language/constraints-and-defaults/ 301
|
||||
/docs/language/evaluation/ /docs/en/language/evaluation/ 301
|
||||
/docs/language/examples/ /docs/en/language/examples/ 301
|
||||
/docs/language/expression/ /docs/en/language/expression/ 301
|
||||
/docs/language/expression/arithmetic/ /docs/en/language/expression/arithmetic/ 301
|
||||
/docs/language/expression/array/ /docs/en/language/expression/array/ 301
|
||||
/docs/language/expression/ascription/ /docs/en/language/expression/ascription/ 301
|
||||
/docs/language/expression/composition/ /docs/en/language/expression/composition/ 301
|
||||
/docs/language/expression/default/ /docs/en/language/expression/default/ 301
|
||||
/docs/language/expression/function/ /docs/en/language/expression/function/ 301
|
||||
/docs/language/expression/function-call/ /docs/en/language/expression/function-call/ 301
|
||||
/docs/language/expression/identifier/ /docs/en/language/expression/identifier/ 301
|
||||
/docs/language/expression/import/ /docs/en/language/expression/import/ 301
|
||||
/docs/language/expression/let/ /docs/en/language/expression/let/ 301
|
||||
/docs/language/expression/literal/ /docs/en/language/expression/literal/ 301
|
||||
/docs/language/expression/logical-and-comparison/ /docs/en/language/expression/logical-and-comparison/ 301
|
||||
/docs/language/expression/match/ /docs/en/language/expression/match/ 301
|
||||
/docs/language/expression/object/ /docs/en/language/expression/object/ 301
|
||||
/docs/language/expression/path-reference/ /docs/en/language/expression/path-reference/ 301
|
||||
/docs/language/functions/ /docs/en/language/functions/ 301
|
||||
/docs/language/grammar/ /docs/en/language/grammar/ 301
|
||||
/docs/language/materialization-and-errors/ /docs/en/language/materialization-and-errors/ 301
|
||||
/docs/language/modules-and-imports/ /docs/en/language/modules-and-imports/ 301
|
||||
/docs/language/naming/ /docs/en/language/naming/ 301
|
||||
/docs/language/operators/ /docs/en/language/operators/ 301
|
||||
/docs/language/syntax/ /docs/en/language/syntax/ 301
|
||||
/docs/language/value/ /docs/en/language/value/ 301
|
||||
/docs/language/value/bool/ /docs/en/language/value/bool/ 301
|
||||
/docs/language/value/float/ /docs/en/language/value/float/ 301
|
||||
/docs/language/value/int/ /docs/en/language/value/int/ 301
|
||||
/docs/language/value/string/ /docs/en/language/value/string/ 301
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" fill="#1a73e8"/>
|
||||
<path d="M17 13h14c12 0 20 7 20 19s-8 19-20 19H17V13Zm10 9v20h4c6 0 10-3 10-10s-4-10-10-10h-4Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 228 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
||||
<rect width="1200" height="630" fill="#f6f7f8"/>
|
||||
<rect x="72" y="72" width="1056" height="486" fill="#fff" stroke="#dadce0" stroke-width="2"/>
|
||||
<rect x="120" y="118" width="72" height="72" fill="#1a73e8"/>
|
||||
<path d="M139 135h16c14 0 23 8 23 19s-9 21-23 21h-16v-40Zm11 10v20h5c7 0 11-3 11-10s-4-10-11-10h-5Z" fill="#fff"/>
|
||||
<text x="216" y="170" fill="#171717" font-family="IBM Plex Sans, Arial, sans-serif" font-size="54" font-weight="600">Decodal</text>
|
||||
<text x="120" y="300" fill="#171717" font-family="IBM Plex Sans, Arial, sans-serif" font-size="68" font-weight="600">Deferred Constraint</text>
|
||||
<text x="120" y="380" fill="#171717" font-family="IBM Plex Sans, Arial, sans-serif" font-size="68" font-weight="600">Data Language</text>
|
||||
<text x="120" y="465" fill="#5f6368" font-family="IBM Plex Mono, monospace" font-size="30">configuration · constraints · defaults</text>
|
||||
<rect x="120" y="505" width="160" height="6" fill="#1a73e8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -11,9 +11,12 @@ const {
|
||||
playground = false,
|
||||
frontpage = false,
|
||||
locale,
|
||||
noindex = false,
|
||||
standalone = false,
|
||||
} = Astro.props;
|
||||
|
||||
const pathname = Astro.url.pathname;
|
||||
const site = Astro.site ?? new URL('https://decodal.hareworks.net');
|
||||
const language = locale ?? 'en';
|
||||
const pageDescription = description ?? (language === 'ja'
|
||||
? 'Decodalは、設定、制約、デフォルトのための決定的なデータ言語です。'
|
||||
@@ -22,6 +25,20 @@ const docsLocale = language === 'ja' ? 'ja' : 'en';
|
||||
const otherDocsLocale = docsLocale === 'ja' ? 'en' : 'ja';
|
||||
const isDocsPage = pathname.startsWith('/docs/');
|
||||
const localizedDocPath = active && active !== 'index' ? `${active}/` : '';
|
||||
const canonicalUrl = new URL(pathname, site).href;
|
||||
const englishUrl = new URL(frontpage ? '/' : `/docs/en/${localizedDocPath}`, site).href;
|
||||
const japaneseUrl = new URL(frontpage ? '/ja/' : `/docs/ja/${localizedDocPath}`, site).href;
|
||||
const markdownPath = frontpage
|
||||
? language === 'ja' ? '/ja.md' : '/index.md'
|
||||
: isDocsPage
|
||||
? active === 'index'
|
||||
? `/docs/${docsLocale}.md`
|
||||
: `/docs/${docsLocale}/${active}.md`
|
||||
: undefined;
|
||||
const markdownUrl = markdownPath ? new URL(markdownPath, site).href : undefined;
|
||||
const sitemapUrl = new URL('/sitemap-index.xml', site).href;
|
||||
const llmsUrl = new URL('/llms.txt', site).href;
|
||||
const socialImageUrl = new URL('/og.png', site).href;
|
||||
const homeHref = language === 'ja' ? '/ja/' : '/';
|
||||
const alternateHref = frontpage
|
||||
? language === 'ja' ? '/' : '/ja/'
|
||||
@@ -32,6 +49,53 @@ const manualLabel = language === 'ja' ? 'マニュアル' : 'Manual';
|
||||
const manualHref = `/docs/${docsLocale}/introduction/`;
|
||||
const manualIndexHref = `/docs/${docsLocale}/`;
|
||||
const nav = getNav(docsLocale);
|
||||
const localizedPage = !noindex && (frontpage || isDocsPage);
|
||||
const structuredData = isDocsPage
|
||||
? {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'TechArticle',
|
||||
headline: title,
|
||||
description: pageDescription,
|
||||
inLanguage: language,
|
||||
url: canonicalUrl,
|
||||
isPartOf: {
|
||||
'@type': 'WebSite',
|
||||
name: 'Decodal',
|
||||
url: new URL('/', site).href,
|
||||
},
|
||||
}
|
||||
: playground
|
||||
? {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebApplication',
|
||||
name: 'Decodal Playground',
|
||||
description: pageDescription,
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
operatingSystem: 'Web',
|
||||
url: canonicalUrl,
|
||||
}
|
||||
: {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
{
|
||||
'@type': 'WebSite',
|
||||
name: 'Decodal',
|
||||
description: pageDescription,
|
||||
inLanguage: language,
|
||||
url: canonicalUrl,
|
||||
},
|
||||
{
|
||||
'@type': 'SoftwareSourceCode',
|
||||
name: 'Decodal',
|
||||
description: pageDescription,
|
||||
codeRepository: 'https://gitea.hareworks.net/Hare/Decodal',
|
||||
license: [
|
||||
'https://opensource.org/license/mit',
|
||||
'https://www.apache.org/licenses/LICENSE-2.0',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function isCurrent(path) {
|
||||
return pathname === path || (path !== '/' && pathname.startsWith(path));
|
||||
@@ -55,9 +119,33 @@ function renderNav(items, prefix = []) {
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content={pageDescription} />
|
||||
{(frontpage || isDocsPage) && <link rel="alternate" hreflang="en" href={frontpage ? '/' : `/docs/en/${localizedDocPath}`} />}
|
||||
{(frontpage || isDocsPage) && <link rel="alternate" hreflang="ja" href={frontpage ? '/ja/' : `/docs/ja/${localizedDocPath}`} />}
|
||||
{(frontpage || isDocsPage) && <link rel="alternate" hreflang="x-default" href={frontpage ? '/' : `/docs/en/${localizedDocPath}`} />}
|
||||
{noindex && <meta name="robots" content="noindex" />}
|
||||
{!noindex && <link rel="canonical" href={canonicalUrl} />}
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="sitemap" href={sitemapUrl} />
|
||||
<link rel="describedby" href={llmsUrl} type="text/markdown" />
|
||||
{markdownUrl && <link rel="alternate" type="text/markdown" href={markdownUrl} />}
|
||||
{localizedPage && <link rel="alternate" hreflang="en" href={englishUrl} />}
|
||||
{localizedPage && <link rel="alternate" hreflang="ja" href={japaneseUrl} />}
|
||||
{localizedPage && <link rel="alternate" hreflang="x-default" href={englishUrl} />}
|
||||
<meta property="og:type" content={isDocsPage ? 'article' : 'website'} />
|
||||
<meta property="og:site_name" content="Decodal" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={pageDescription} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:locale" content={language === 'ja' ? 'ja_JP' : 'en_US'} />
|
||||
{localizedPage && <meta property="og:locale:alternate" content={language === 'ja' ? 'en_US' : 'ja_JP'} />}
|
||||
<meta property="og:image" content={socialImageUrl} />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="Decodal — Deferred Constraint Data Language" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={pageDescription} />
|
||||
<meta name="twitter:image" content={socialImageUrl} />
|
||||
<meta name="twitter:image:alt" content="Decodal — Deferred Constraint Data Language" />
|
||||
{!noindex && <script type="application/ld+json" is:inline set:html={JSON.stringify(structuredData)} />}
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
@@ -73,8 +161,8 @@ function renderNav(items, prefix = []) {
|
||||
{(frontpage || isDocsPage) && <a class="language-switch" href={alternateHref} hreflang={otherDocsLocale} aria-label={alternateAriaLabel}>{alternateLabel}</a>}
|
||||
</nav>
|
||||
</header>
|
||||
<div class:list={{ layout: true, 'wide-layout': playground || frontpage, 'playground-layout': playground }}>
|
||||
{!playground && !frontpage && (
|
||||
<div class:list={{ layout: true, 'wide-layout': playground || frontpage || standalone, 'playground-layout': playground }}>
|
||||
{!playground && !frontpage && !standalone && (
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-content">
|
||||
<a class="sidebar-title" href={manualIndexHref}>{manualLabel}</a>
|
||||
@@ -82,7 +170,7 @@ function renderNav(items, prefix = []) {
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
<main class={playground ? 'playground' : frontpage ? 'frontpage' : ''}>
|
||||
<main class={playground ? 'playground' : frontpage ? 'frontpage' : standalone ? 'standalone' : ''}>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { docTitle, orderedDocSlugs, sourceMarkdown } from './docs.js';
|
||||
|
||||
export const SITE_URL = 'https://decodal.hareworks.net';
|
||||
|
||||
const homePages = {
|
||||
en: `# Decodal
|
||||
|
||||
Decodal is the Deferred Constraint Data Language: a small deterministic language for describing, composing, validating, and materializing structured data.
|
||||
|
||||
Configuration values, constraints, defaults, and derived values share one expression system. The host owns filesystems and external I/O; Decodal owns deterministic evaluation.
|
||||
|
||||
\`\`\`dcdl
|
||||
let
|
||||
Port = Int & >= 1 & <= 65535;
|
||||
Service = {
|
||||
host = String;
|
||||
port = Port default 8080;
|
||||
};
|
||||
in
|
||||
{
|
||||
host = "127.0.0.1";
|
||||
} as Service;
|
||||
\`\`\`
|
||||
|
||||
## Documentation
|
||||
|
||||
- [English manual](/docs/en.md)
|
||||
- [Embedding guide](/docs/en/embedding.md)
|
||||
- [Packages and integrations](/docs/en/components.md)
|
||||
- [Japanese site](/ja.md)
|
||||
|
||||
## Resources
|
||||
|
||||
- [Browser playground](/playground/)
|
||||
- [Source repository](https://gitea.hareworks.net/Hare/Decodal)
|
||||
`,
|
||||
ja: `# Decodal
|
||||
|
||||
Decodalは、構造化データの記述、合成、検証、具体化のための小さく決定的な言語、Deferred Constraint Data Languageである。
|
||||
|
||||
設定値、制約、デフォルト、派生値をひとつの式体系で扱う。filesystemや外部I/Oはホストが担い、Decodalは決定的な評価を担う。
|
||||
|
||||
\`\`\`dcdl
|
||||
let
|
||||
Port = Int & >= 1 & <= 65535;
|
||||
Service = {
|
||||
host = String;
|
||||
port = Port default 8080;
|
||||
};
|
||||
in
|
||||
{
|
||||
host = "127.0.0.1";
|
||||
} as Service;
|
||||
\`\`\`
|
||||
|
||||
## ドキュメント
|
||||
|
||||
- [日本語マニュアル](/docs/ja.md)
|
||||
- [組み込みガイド](/docs/ja/embedding.md)
|
||||
- [パッケージと統合](/docs/ja/components.md)
|
||||
- [English site](/index.md)
|
||||
|
||||
## リソース
|
||||
|
||||
- [ブラウザPlayground](/playground/)
|
||||
- [ソースリポジトリ](https://gitea.hareworks.net/Hare/Decodal)
|
||||
`,
|
||||
};
|
||||
|
||||
export function homeMarkdown(locale = 'en') {
|
||||
return homePages[locale] ?? homePages.en;
|
||||
}
|
||||
|
||||
function markdownUrl(slug, locale) {
|
||||
return slug === 'index'
|
||||
? `${SITE_URL}/docs/${locale}.md`
|
||||
: `${SITE_URL}/docs/${locale}/${slug}.md`;
|
||||
}
|
||||
|
||||
function manualLinks(locale) {
|
||||
return orderedDocSlugs(locale, { includeIndex: true })
|
||||
.map((slug) => `- [${docTitle(slug, locale)}](${markdownUrl(slug, locale)})`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function llmsText() {
|
||||
return `# Decodal
|
||||
|
||||
> Decodal is a deterministic data language for configuration, constraints, defaults, validation, and materialization.
|
||||
|
||||
Use the English manual as the default reference. Japanese translations cover the same document structure. Markdown endpoints are generated directly from the manual sources rather than converted from HTML.
|
||||
|
||||
## English documentation
|
||||
|
||||
${manualLinks('en')}
|
||||
|
||||
## 日本語ドキュメント
|
||||
|
||||
${manualLinks('ja')}
|
||||
|
||||
## Complete manuals
|
||||
|
||||
- [Complete English manual](${SITE_URL}/llms-full.txt): All English manual pages in one text resource.
|
||||
- [日本語マニュアル全文](${SITE_URL}/ja/llms-full.txt): 日本語マニュアルの全ページをまとめたテキスト。
|
||||
|
||||
## Resources
|
||||
|
||||
- [Project overview](${SITE_URL}/index.md): Concise English overview.
|
||||
- [プロジェクト概要](${SITE_URL}/ja.md): 日本語の概要。
|
||||
- [Browser playground](${SITE_URL}/playground/): Interactive Decodal runtime and editor.
|
||||
- [Source repository](https://gitea.hareworks.net/Hare/Decodal): Rust, JavaScript, language service, and website source.
|
||||
`;
|
||||
}
|
||||
|
||||
export function fullManual(locale = 'en') {
|
||||
const heading = locale === 'ja' ? 'Decodalマニュアル全文' : 'Complete Decodal Manual';
|
||||
const note = locale === 'ja'
|
||||
? 'このファイルは日本語マニュアルの原本から生成されている。各章の個別Markdownは /llms.txt を参照。'
|
||||
: 'This file is generated from the English manual sources. See /llms.txt for links to individual Markdown pages.';
|
||||
const pages = orderedDocSlugs(locale, { includeIndex: true })
|
||||
.map((slug) => sourceMarkdown(slug, locale).trim())
|
||||
.join('\n\n---\n\n');
|
||||
return `# ${heading}\n\n${note}\n\n${pages}\n`;
|
||||
}
|
||||
|
||||
export function textResponse(content, language = 'en', contentType = 'text/markdown') {
|
||||
return new Response(content, {
|
||||
headers: {
|
||||
'Content-Type': `${contentType}; charset=utf-8`,
|
||||
'Content-Language': language,
|
||||
'Content-Signal': 'search=yes, ai-input=yes, ai-train=no',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -173,6 +173,16 @@ export function allDocSlugs(locale = 'en') {
|
||||
return Object.keys(docsByLocale[locale] ?? {}).filter((slug) => slug !== 'index');
|
||||
}
|
||||
|
||||
function flattenNav(items) {
|
||||
return items.flatMap((item) => [item.slug, ...(item.children ? flattenNav(item.children) : [])]);
|
||||
}
|
||||
|
||||
export function orderedDocSlugs(locale = 'en', { includeIndex = false } = {}) {
|
||||
const localizedDocs = docsByLocale[locale] ?? docsByLocale.en;
|
||||
const slugs = flattenNav(getNav(locale)).filter((slug) => localizedDocs[slug]);
|
||||
return includeIndex ? ['index', ...slugs] : slugs;
|
||||
}
|
||||
|
||||
function findDocTitle(slug, items) {
|
||||
for (const item of items) {
|
||||
if (item.slug === slug) return item.title;
|
||||
@@ -185,13 +195,51 @@ function findDocTitle(slug, items) {
|
||||
}
|
||||
|
||||
export function docTitle(slug, locale = 'en') {
|
||||
if (slug === 'index') return locale === 'ja' ? 'Decodalマニュアル' : 'Decodal Manual';
|
||||
return findDocTitle(slug, getNav(locale)) ?? slug;
|
||||
}
|
||||
|
||||
export function renderMarkdown(slug, locale = 'en') {
|
||||
function getSource(slug, locale) {
|
||||
const localizedDocs = docsByLocale[locale] ?? docsByLocale.en;
|
||||
const source = localizedDocs[slug] ?? localizedDocs.index;
|
||||
const html = marked.parse(source ?? '# Not found\n');
|
||||
return localizedDocs[slug] ?? localizedDocs.index ?? '# Not found\n';
|
||||
}
|
||||
|
||||
function stripInlineMarkdown(value) {
|
||||
return value
|
||||
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
||||
.replace(/[`*_~]/g, '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function docDescription(slug, locale = 'en') {
|
||||
const blocks = getSource(slug, locale)
|
||||
.replace(/```[\s\S]*?```/g, '')
|
||||
.split(/\n\s*\n/)
|
||||
.map((block) => block.trim())
|
||||
.filter((block) => block && !block.startsWith('#') && !/^[-*]\s/.test(block));
|
||||
const description = stripInlineMarkdown(blocks[0] ?? '');
|
||||
if (description.length <= 180) return description;
|
||||
const shortened = description.slice(0, 177).replace(/\s+\S*$/, '').trimEnd();
|
||||
return `${shortened || description.slice(0, 177)}…`;
|
||||
}
|
||||
|
||||
export function sourceMarkdown(slug, locale = 'en') {
|
||||
return getSource(slug, locale).replace(/\]\(([^)\s]+)\)/g, (all, destination) => {
|
||||
const [href, hash = ''] = destination.split('#', 2);
|
||||
if (!href.endsWith('.md')) return all;
|
||||
const target = normalizeDocLink(locale, slug, href.slice(0, -3));
|
||||
const pathname = target === 'index'
|
||||
? `/docs/${locale}.md`
|
||||
: `/docs/${locale}/${target}.md`;
|
||||
return `](${pathname}${hash ? `#${hash}` : ''})`;
|
||||
});
|
||||
}
|
||||
|
||||
export function renderMarkdown(slug, locale = 'en') {
|
||||
const html = marked.parse(getSource(slug, locale));
|
||||
return html.replace(/href="([^"#][^"]*)\.md(#[^"]*)?"/g, (_all, href, hash = '') => {
|
||||
const target = normalizeDocLink(locale, slug, href);
|
||||
const pathname = target === 'index' ? `/docs/${locale}/` : `/docs/${locale}/${target}/`;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import ManualLayout from '../layouts/ManualLayout.astro';
|
||||
---
|
||||
<ManualLayout
|
||||
title="Page not found — Decodal"
|
||||
description="The requested Decodal page could not be found."
|
||||
noindex
|
||||
standalone
|
||||
>
|
||||
<article class="markdown not-found">
|
||||
<h1>Page not found</h1>
|
||||
<p>The requested page does not exist or has moved.</p>
|
||||
<p><a href="/">Return home</a> or <a href="/docs/en/">open the manual</a>.</p>
|
||||
</article>
|
||||
</ManualLayout>
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import ManualLayout from '../../../layouts/ManualLayout.astro';
|
||||
import { allDocSlugs, docTitle, renderMarkdown } from '../../../lib/docs.js';
|
||||
import { allDocSlugs, docDescription, docTitle, renderMarkdown } from '../../../lib/docs.js';
|
||||
import '@fontsource/ibm-plex-sans-jp/latin-400.css';
|
||||
import '@fontsource/ibm-plex-sans-jp/latin-600.css';
|
||||
|
||||
@@ -14,7 +14,8 @@ export function getStaticPaths() {
|
||||
const { slug } = Astro.props;
|
||||
const html = renderMarkdown(slug, 'en');
|
||||
const title = docTitle(slug, 'en');
|
||||
const description = docDescription(slug, 'en');
|
||||
---
|
||||
<ManualLayout title={`${title} — Decodal Manual`} active={slug} locale="en">
|
||||
<ManualLayout title={`${title} — Decodal Manual`} description={description} active={slug} locale="en">
|
||||
<article class="markdown" set:html={html} />
|
||||
</ManualLayout>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { textResponse } from '../../../../lib/agent-content.js';
|
||||
import { allDocSlugs, sourceMarkdown } from '../../../../lib/docs.js';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return allDocSlugs('en').map((slug) => ({ params: { slug }, props: { slug } }));
|
||||
}
|
||||
|
||||
export function GET({ props }) {
|
||||
return textResponse(sourceMarkdown(props.slug, 'en'), 'en');
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { textResponse } from '../../../lib/agent-content.js';
|
||||
import { sourceMarkdown } from '../../../lib/docs.js';
|
||||
|
||||
export function GET() {
|
||||
return textResponse(sourceMarkdown('index', 'en'), 'en');
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import ManualLayout from '../../../layouts/ManualLayout.astro';
|
||||
import { allDocSlugs, docTitle, renderMarkdown } from '../../../lib/docs.js';
|
||||
import { allDocSlugs, docDescription, docTitle, renderMarkdown } from '../../../lib/docs.js';
|
||||
import '@fontsource/ibm-plex-sans-jp/400.css';
|
||||
import '@fontsource/ibm-plex-sans-jp/600.css';
|
||||
|
||||
@@ -14,7 +14,8 @@ export function getStaticPaths() {
|
||||
const { slug } = Astro.props;
|
||||
const html = renderMarkdown(slug, 'ja');
|
||||
const title = docTitle(slug, 'ja');
|
||||
const description = docDescription(slug, 'ja');
|
||||
---
|
||||
<ManualLayout title={`${title} — Decodalマニュアル`} active={slug} locale="ja">
|
||||
<ManualLayout title={`${title} — Decodalマニュアル`} description={description} active={slug} locale="ja">
|
||||
<article class="markdown" set:html={html} />
|
||||
</ManualLayout>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { textResponse } from '../../../../lib/agent-content.js';
|
||||
import { allDocSlugs, sourceMarkdown } from '../../../../lib/docs.js';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return allDocSlugs('ja').map((slug) => ({ params: { slug }, props: { slug } }));
|
||||
}
|
||||
|
||||
export function GET({ props }) {
|
||||
return textResponse(sourceMarkdown(props.slug, 'ja'), 'ja');
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { textResponse } from '../../../lib/agent-content.js';
|
||||
import { sourceMarkdown } from '../../../lib/docs.js';
|
||||
|
||||
export function GET() {
|
||||
return textResponse(sourceMarkdown('index', 'ja'), 'ja');
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { homeMarkdown, textResponse } from '../lib/agent-content.js';
|
||||
|
||||
export function GET() {
|
||||
return textResponse(homeMarkdown('en'), 'en');
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { homeMarkdown, textResponse } from '../../lib/agent-content.js';
|
||||
|
||||
export function GET() {
|
||||
return textResponse(homeMarkdown('ja'), 'ja');
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { fullManual, textResponse } from '../../lib/agent-content.js';
|
||||
|
||||
export function GET() {
|
||||
return textResponse(fullManual('ja'), 'ja', 'text/plain');
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { fullManual, textResponse } from '../lib/agent-content.js';
|
||||
|
||||
export function GET() {
|
||||
return textResponse(fullManual('en'), 'en', 'text/plain');
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { llmsText, textResponse } from '../lib/agent-content.js';
|
||||
|
||||
export function GET() {
|
||||
return textResponse(llmsText(), 'en', 'text/plain');
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { SITE_URL, textResponse } from '../lib/agent-content.js';
|
||||
|
||||
const robots = `User-agent: *
|
||||
Content-Signal: search=yes, ai-input=yes, ai-train=no
|
||||
Allow: /
|
||||
|
||||
Sitemap: ${SITE_URL}/sitemap-index.xml
|
||||
`;
|
||||
|
||||
export function GET() {
|
||||
return textResponse(robots, 'en', 'text/plain');
|
||||
}
|
||||
Reference in New Issue
Block a user