Skip to content

Commit 5ab8cfc

Browse files
authored
feat(seo): add BreadcrumbList, SoftwareApplication schema, and image alt text (#2029)
1 parent f02644b commit 5ab8cfc

File tree

5 files changed

+151
-1
lines changed

5 files changed

+151
-1
lines changed

config/breadcrumb.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
const SITE_URL = 'https://apisix.apache.org';
2+
3+
/**
4+
* Generate BreadcrumbList JSON-LD structured data from a URL path.
5+
*
6+
* Example for /docs/apisix/plugins/limit-req/:
7+
* Home > Docs > APISIX > Plugins > limit-req
8+
*/
9+
function buildBreadcrumbs(urlPath) {
10+
// Remove trailing index.html and normalize
11+
const cleanPath = urlPath
12+
.replace(/\/index\.html$/, '/')
13+
.replace(/\.html$/, '/');
14+
15+
// Split into segments, filter empties
16+
const segments = cleanPath.split('/').filter(Boolean);
17+
18+
if (segments.length === 0) return null; // homepage, no breadcrumb needed
19+
20+
// Build breadcrumb items
21+
const items = [
22+
{
23+
'@type': 'ListItem',
24+
position: 1,
25+
name: 'Home',
26+
item: SITE_URL + '/',
27+
},
28+
];
29+
30+
// Human-readable labels for known path segments
31+
const labels = {
32+
docs: 'Docs',
33+
apisix: 'APISIX',
34+
blog: 'Blog',
35+
plugins: 'Plugins',
36+
'learning-center': 'Learning Center',
37+
'ingress-controller': 'Ingress Controller',
38+
'helm-chart': 'Helm Chart',
39+
docker: 'Docker',
40+
'ai-gateway': 'AI Gateway',
41+
downloads: 'Downloads',
42+
team: 'Team',
43+
contribute: 'Contribute',
44+
showcase: 'Showcase',
45+
help: 'Help',
46+
articles: 'Articles',
47+
events: 'Events',
48+
general: 'General',
49+
zh: 'Chinese',
50+
};
51+
52+
let currentPath = '';
53+
for (let i = 0; i < segments.length; i++) {
54+
const seg = segments[i];
55+
currentPath += '/' + seg;
56+
57+
// Skip 'zh' locale prefix in breadcrumb display
58+
if (seg === 'zh' && i === 0) continue;
59+
60+
const name = labels[seg] || seg.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
61+
62+
items.push({
63+
'@type': 'ListItem',
64+
position: items.length + 1,
65+
name,
66+
item: SITE_URL + currentPath + '/',
67+
});
68+
}
69+
70+
// Don't generate breadcrumbs for single-level pages (just Home > Page)
71+
if (items.length <= 1) return null;
72+
73+
return {
74+
'@context': 'https://schema.org',
75+
'@type': 'BreadcrumbList',
76+
itemListElement: items,
77+
};
78+
}
79+
80+
/**
81+
* Docusaurus plugin that injects BreadcrumbList JSON-LD into every page
82+
* during the post-build phase.
83+
*/
84+
module.exports = function breadcrumbPlugin() {
85+
return {
86+
name: 'breadcrumb-jsonld',
87+
88+
async postBuild({ outDir }) {
89+
const fs = require('fs');
90+
const path = require('path');
91+
92+
function findHtmlFiles(dir) {
93+
const results = [];
94+
const entries = fs.readdirSync(dir, { withFileTypes: true });
95+
for (const entry of entries) {
96+
const fullPath = path.join(dir, entry.name);
97+
if (entry.isDirectory()) {
98+
results.push(...findHtmlFiles(fullPath));
99+
} else if (entry.name.endsWith('.html')) {
100+
results.push(fullPath);
101+
}
102+
}
103+
return results;
104+
}
105+
106+
const htmlFiles = findHtmlFiles(outDir);
107+
let injected = 0;
108+
109+
for (const filePath of htmlFiles) {
110+
let html = fs.readFileSync(filePath, 'utf-8');
111+
112+
const relativePath = path.relative(outDir, filePath);
113+
const urlPath = '/' + relativePath.split(path.sep).join('/');
114+
115+
const breadcrumbs = buildBreadcrumbs(urlPath);
116+
if (!breadcrumbs) continue;
117+
118+
const script = `<script type="application/ld+json">${JSON.stringify(breadcrumbs)}</script>`;
119+
html = html.replace('</head>', ` ${script}\n </head>`);
120+
fs.writeFileSync(filePath, html, 'utf-8');
121+
injected++;
122+
}
123+
124+
console.log(` [breadcrumb] Injected BreadcrumbList into ${injected} pages`);
125+
},
126+
};
127+
};

doc/docusaurus.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ module.exports = {
205205
],
206206
['docusaurus-plugin-sass', {}],
207207
require.resolve('../config/schema-org'),
208+
require.resolve('../config/breadcrumb'),
208209
],
209210
themeConfig: {
210211
navbar: {

website/docusaurus.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ module.exports = {
9797
],
9898
['docusaurus-plugin-sass', {}],
9999
require.resolve('../config/schema-org'),
100+
require.resolve('../config/breadcrumb'),
100101
],
101102
themeConfig: {
102103
navbar: {

website/src/pages/downloads.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,27 @@ const Downloads: FC = () => (
6161
<Head>
6262
<meta name="description" content={translate({ id: 'download.meta.description', message: 'Download Apache APISIX, the cloud-native API Gateway and AI Gateway. Get the latest release, verify signatures, and access historical versions.' })} />
6363
<meta property="og:description" content={translate({ id: 'download.meta.ogDescription', message: 'Download Apache APISIX, the cloud-native API Gateway and AI Gateway. Get the latest release and historical versions.' })} />
64+
<script type="application/ld+json">
65+
{JSON.stringify({
66+
'@context': 'https://schema.org',
67+
'@type': 'SoftwareApplication',
68+
name: 'Apache APISIX',
69+
applicationCategory: 'DeveloperApplication',
70+
operatingSystem: 'Linux, macOS, Docker, Kubernetes',
71+
license: 'https://www.apache.org/licenses/LICENSE-2.0',
72+
url: 'https://apisix.apache.org/downloads/',
73+
author: {
74+
'@type': 'Organization',
75+
name: 'Apache Software Foundation',
76+
url: 'https://www.apache.org/',
77+
},
78+
offers: {
79+
'@type': 'Offer',
80+
price: '0',
81+
priceCurrency: 'USD',
82+
},
83+
})}
84+
</script>
6485
</Head>
6586
<DownloadsPage>
6687
<PageTitle><Translate id="download.website.title">Downloads</Translate></PageTitle>

website/src/pages/team.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ const Team: FC = () => {
256256
href={`https://github.com/${member.githubUsername}`}
257257
target="_blank"
258258
>
259-
<Avatar src={member.avatarUrl} />
259+
<Avatar src={member.avatarUrl} alt={member.name || member.username} width={108} height={108} loading="lazy" />
260260
<MemberName>{member.name}</MemberName>
261261
<Username>
262262
@

0 commit comments

Comments
 (0)