Skip to content

Commit b8734c0

Browse files
authored
refactor(site): gallery page support seo (#152)
* refactor(site): gallery page support seo * fix: fix cr issues * docs(site): remove useless docs * docs(site): update docs title * docs(site): optimize i18n * chore: add script to create llms.txt * fix: fix cr issues
1 parent ad3fa9a commit b8734c0

12 files changed

Lines changed: 215 additions & 39 deletions

File tree

site/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
"dev": "run-p dev:next dev:watch-content",
99
"dev:next": "next dev",
1010
"dev:watch-content": "node scripts/watch-content.js",
11-
"build": "next build",
11+
"build": "next build && npm run generate:llms",
1212
"build:static": "next build",
13+
"generate:llms": "node scripts/generate-llms.js",
1314
"lint": "next lint",
1415
"lint:fix": "next lint --fix",
1516
"format:source": "prettier --config .prettierrc --write \"{plugins,src}/**/*.{js,ts,jsx,tsx,css}\"",

site/scripts/generate-llms.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
const fs = require('fs').promises;
2+
const path = require('path');
3+
const matter = require('gray-matter');
4+
const globby = require('globby');
5+
6+
const CONTENT_DIR = path.join(__dirname, '../src/content');
7+
const OUTPUT_DIR = path.join(__dirname, '../out');
8+
9+
const PAGES_DIR = path.join(__dirname, '../src/pages');
10+
11+
async function generate() {
12+
// 1. Process Markdown files
13+
const patterns = ['**/*.en.md', '**/*.en.mdx'];
14+
const files = await globby(patterns, {cwd: CONTENT_DIR});
15+
16+
let llmsFull = '';
17+
let llms = '# Documentation Index\n\n';
18+
const seenUrls = new Set();
19+
20+
// Sort files for consistent order
21+
files.sort();
22+
23+
for (const file of files) {
24+
const filePath = path.join(CONTENT_DIR, file);
25+
const content = await fs.readFile(filePath, 'utf8');
26+
const {data, content: body} = matter(content);
27+
28+
const title = data.title || file;
29+
const description = data.description || '';
30+
31+
// Clean URL path
32+
// Remove extension (.en.md or .en.mdx)
33+
let urlPath = file.replace(/\.en\.mdx?$/, '');
34+
// Remove "index" at the end (e.g. foo/index -> foo, or index -> /)
35+
if (urlPath === 'index' || urlPath.endsWith('/index')) {
36+
urlPath = urlPath.replace(/\/index$/, '').replace(/^index$/, '');
37+
}
38+
// Ensure root slash if empty (though usually we want relative or absolute paths, let's keep it clean)
39+
if (!urlPath.startsWith('/')) urlPath = '/' + urlPath;
40+
41+
seenUrls.add(urlPath);
42+
43+
llms += `- [${title}](${urlPath}) ${
44+
description ? '- ' + description : ''
45+
}\n`;
46+
47+
// Append to llms-full.txt
48+
llmsFull += `\n\n# ${title}\n\n`;
49+
llmsFull += `> File: ${file}\n\n`;
50+
llmsFull += body;
51+
llmsFull += '\n\n---\n';
52+
}
53+
54+
// 2. Process Page files
55+
// Scan for pages, ignoring api, _*, and dynamic routes with brackets [ ]
56+
const pageFiles = await globby(['**/*.tsx'], {
57+
cwd: PAGES_DIR,
58+
ignore: ['_*', 'api/**', '**/*\\[*'],
59+
});
60+
pageFiles.sort();
61+
62+
for (const file of pageFiles) {
63+
const filePath = path.join(PAGES_DIR, file);
64+
const content = await fs.readFile(filePath, 'utf8');
65+
66+
// Try to extract title from meta={{title: '...'}}
67+
const titleMatch = content.match(
68+
/meta=\{\{.*?title:\s*['"]([^'"]+)['"].*?\}\}/s
69+
);
70+
let title = titleMatch
71+
? titleMatch[1]
72+
: path.basename(file, path.extname(file));
73+
// Capitalize if fallback
74+
if (!titleMatch && title !== 'index') {
75+
title = title.charAt(0).toUpperCase() + title.slice(1);
76+
} else if (title === 'index') {
77+
// For index files, try to use dirname as title if no meta
78+
const dir = path.dirname(file);
79+
if (dir !== '.') {
80+
title = dir.charAt(0).toUpperCase() + dir.slice(1);
81+
} else {
82+
title = 'Home';
83+
}
84+
}
85+
86+
let urlPath = file.replace(/\.tsx$/, '');
87+
// Remove "index" at the end (e.g. foo/index -> foo)
88+
if (urlPath === 'index' || urlPath.endsWith('/index')) {
89+
urlPath = urlPath.replace(/\/index$/, '').replace(/^index$/, '');
90+
}
91+
// Ensure properly formatted path
92+
if (!urlPath.startsWith('/')) urlPath = '/' + urlPath;
93+
94+
// Avoid duplicates if markdown already covered it (unlikely for pages dir vs content dir overlap unless mapped)
95+
// But check if path already in llms (simple check)
96+
if (seenUrls.has(urlPath)) continue;
97+
98+
llms += `- [${title}](${urlPath})\n`;
99+
}
100+
101+
// Write files
102+
await fs.mkdir(OUTPUT_DIR, {recursive: true});
103+
await fs.writeFile(path.join(OUTPUT_DIR, 'llms.txt'), llms);
104+
await fs.writeFile(path.join(OUTPUT_DIR, 'llms-full.txt'), llmsFull);
105+
106+
console.log(
107+
`Successfully generated llms.txt and llms-full.txt in ${OUTPUT_DIR}`
108+
);
109+
}
110+
111+
generate().catch(console.error);

site/src/components/Gallery/DetailPage.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,24 @@ const TRANSLATIONS = {
4545
},
4646
};
4747

48-
export default function DetailPage() {
48+
export default function DetailPage({templateId}: {templateId?: string}) {
4949
const router = useRouter();
50-
const {template} = router.query;
50+
const template = templateId || router.query.template;
5151

5252
const initialTemplate =
5353
TEMPLATES.find((t) => t.template === template) || TEMPLATES[0];
5454

55-
const [code, setCode] = useState('');
55+
const [code, setCode] = useState(initialTemplate?.syntax || '');
5656
const [error, setError] = useState<string | null>(null);
5757
const [copied, setCopied] = useState(false);
5858
const detailTexts = useLocaleBundle(TRANSLATIONS);
5959

60-
// Initialize code template
60+
// Initialize code template only when template ID changes (client-side)
6161
useEffect(() => {
62-
setCode(initialTemplate.syntax);
63-
setError(null);
62+
if (initialTemplate) {
63+
setCode(initialTemplate.syntax);
64+
setError(null);
65+
}
6466
}, [initialTemplate]);
6567

6668
const handleCodeChange = (e: string) => {
@@ -195,6 +197,10 @@ export default function DetailPage() {
195197
onChange={handleCodeChange}
196198
value={code}
197199
/>
200+
{/* Hidden pre tag for SEO */}
201+
<pre className="sr-only" aria-hidden="true">
202+
{code}
203+
</pre>
198204
</div>
199205

200206
<div className="absolute bottom-0 left-0 right-0 px-4 py-2 bg-card/80 dark:bg-card-dark/80 backdrop-blur border-t border-primary/10 dark:border-primary-dark/10 text-[10px] text-tertiary dark:text-tertiary-dark flex justify-between items-center">

site/src/components/Gallery/GalleryPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ export default function GalleryPage() {
250250

251251
// Jump to detail page
252252
const handleCardClick = (template: string) => {
253-
router.push(`/gallery/example?template=${template}`);
253+
router.push(`/gallery/${template}`);
254254
};
255255

256256
return (

site/src/content/examples/index.en.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

site/src/content/examples/index.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

site/src/content/reference/index.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: AntV Infographic Overview
2+
title: Architecture Design
33
---
44

55
AntV Infographic uses a three-layer architecture: JSX rendering engine, runtime, and external API. The overall structure is shown below:

site/src/content/reference/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: AntV Infographic 简介
2+
title: 架构设计
33
---
44

55
AntV Infographic 采用三层架构:JSX 渲染引擎、运行时、对外 API。下图为整体结构:

site/src/pages/editor.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
import {Page} from 'components/Layout/Page';
22
import {EditorContent} from 'components/LiveEditor';
3+
import {useLocaleBundle} from '../hooks/useTranslation';
4+
5+
const TRANSLATIONS = {
6+
'zh-CN': {
7+
title: '编辑器',
8+
},
9+
'en-US': {
10+
title: 'Live Editor',
11+
},
12+
};
313

414
export default function LiveEditorPage() {
15+
const t = useLocaleBundle(TRANSLATIONS);
16+
517
return (
618
<Page
719
toc={[]}
8-
routeTree={{title: 'Live Editor', path: '/editor', routes: []}}
9-
meta={{titleForTitleTag: 'Live Editor'}}
20+
routeTree={{title: t.title, path: '/editor', routes: []}}
21+
meta={{titleForTitleTag: t.title}}
1022
topNavOptions={{
1123
hideBrandWhenHeroVisible: true,
1224
overlayOnHome: true,

site/src/pages/gallery.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
import {Page} from 'components/Layout/Page';
22
import GalleryPage from '../components/Gallery/GalleryPage';
3+
import {useLocaleBundle} from '../hooks/useTranslation';
4+
5+
const TRANSLATIONS = {
6+
'zh-CN': {
7+
title: '示例',
8+
},
9+
'en-US': {
10+
title: 'Gallery',
11+
},
12+
};
313

414
export default function Gallery() {
15+
const t = useLocaleBundle(TRANSLATIONS);
16+
517
return (
618
<Page
719
toc={[]}
8-
routeTree={{title: '示例', path: '/gallery', routes: []}}
9-
meta={{title: '示例'}}
20+
routeTree={{title: t.title, path: '/gallery', routes: []}}
21+
meta={{title: t.title}}
1022
section="gallery"
1123
topNavOptions={{
1224
hideBrandWhenHeroVisible: true,

0 commit comments

Comments
 (0)