This is an automated email from the ASF dual-hosted git repository. wenming pushed a commit to branch feat/seo-structured-data-titles in repository https://gitbox.apache.org/repos/asf/apisix-website.git
commit 5254be04b407fdb5f0824c9bc332279a64b4592f Author: Ming Wen <[email protected]> AuthorDate: Tue Apr 14 11:16:25 2026 +0800 feat(seo): add BreadcrumbList, SoftwareApplication schema, and image alt text BreadcrumbList structured data (#13): - New config/breadcrumb.js Docusaurus plugin that injects BreadcrumbList JSON-LD into every page during post-build - Generates breadcrumb hierarchy from URL path (e.g., Home > Docs > APISIX > Plugins) - Registered in both website and doc site configs - Improves Google SERP display with breadcrumb navigation SoftwareApplication structured data (#14): - Added SoftwareApplication JSON-LD to the Downloads page with name, category, OS, license, and free pricing - BlogPosting schema already implemented in master Image alt text fixes (#29): - Added alt={member.name} to team page avatar images - Added width/height/loading="lazy" attributes Note: Plugin title optimization (#15) and Getting Started expansion (#16) require changes in the upstream apache/apisix repository, not this repo. --- config/breadcrumb.js | 127 ++++++++++++++++++++++++++++++++++++++++ doc/docusaurus.config.js | 1 + website/docusaurus.config.js | 1 + website/src/pages/downloads.tsx | 21 +++++++ website/src/pages/team.tsx | 2 +- 5 files changed, 151 insertions(+), 1 deletion(-) diff --git a/config/breadcrumb.js b/config/breadcrumb.js new file mode 100644 index 00000000000..24d1f2d2d69 --- /dev/null +++ b/config/breadcrumb.js @@ -0,0 +1,127 @@ +const SITE_URL = 'https://apisix.apache.org'; + +/** + * Generate BreadcrumbList JSON-LD structured data from a URL path. + * + * Example for /docs/apisix/plugins/limit-req/: + * Home > Docs > APISIX > Plugins > limit-req + */ +function buildBreadcrumbs(urlPath) { + // Remove trailing index.html and normalize + const cleanPath = urlPath + .replace(/\/index\.html$/, '/') + .replace(/\.html$/, '/'); + + // Split into segments, filter empties + const segments = cleanPath.split('/').filter(Boolean); + + if (segments.length === 0) return null; // homepage, no breadcrumb needed + + // Build breadcrumb items + const items = [ + { + '@type': 'ListItem', + position: 1, + name: 'Home', + item: SITE_URL + '/', + }, + ]; + + // Human-readable labels for known path segments + const labels = { + docs: 'Docs', + apisix: 'APISIX', + blog: 'Blog', + plugins: 'Plugins', + 'learning-center': 'Learning Center', + 'ingress-controller': 'Ingress Controller', + 'helm-chart': 'Helm Chart', + docker: 'Docker', + 'ai-gateway': 'AI Gateway', + downloads: 'Downloads', + team: 'Team', + contribute: 'Contribute', + showcase: 'Showcase', + help: 'Help', + articles: 'Articles', + events: 'Events', + general: 'General', + zh: 'Chinese', + }; + + let currentPath = ''; + for (let i = 0; i < segments.length; i++) { + const seg = segments[i]; + currentPath += '/' + seg; + + // Skip 'zh' locale prefix in breadcrumb display + if (seg === 'zh' && i === 0) continue; + + const name = labels[seg] || seg.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); + + items.push({ + '@type': 'ListItem', + position: items.length + 1, + name, + item: SITE_URL + currentPath + '/', + }); + } + + // Don't generate breadcrumbs for single-level pages (just Home > Page) + if (items.length <= 1) return null; + + return { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: items, + }; +} + +/** + * Docusaurus plugin that injects BreadcrumbList JSON-LD into every page + * during the post-build phase. + */ +module.exports = function breadcrumbPlugin() { + return { + name: 'breadcrumb-jsonld', + + async postBuild({ outDir }) { + const fs = require('fs'); + const path = require('path'); + + function findHtmlFiles(dir) { + const results = []; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + results.push(...findHtmlFiles(fullPath)); + } else if (entry.name.endsWith('.html')) { + results.push(fullPath); + } + } + return results; + } + + const htmlFiles = findHtmlFiles(outDir); + let injected = 0; + + for (const filePath of htmlFiles) { + let html = fs.readFileSync(filePath, 'utf-8'); + + const relativePath = path.relative(outDir, filePath); + const urlPath = '/' + relativePath.split(path.sep).join('/'); + + const breadcrumbs = buildBreadcrumbs(urlPath); + if (!breadcrumbs) continue; + + const script = `<script type="application/ld+json">${JSON.stringify(breadcrumbs)}</script>`; + html = html.replace('</head>', ` ${script}\n </head>`); + fs.writeFileSync(filePath, html, 'utf-8'); + injected++; + } + + console.log(` [breadcrumb] Injected BreadcrumbList into ${injected} pages`); + }, + }; +}; diff --git a/doc/docusaurus.config.js b/doc/docusaurus.config.js index 009a71bfbe2..80324e4e15d 100644 --- a/doc/docusaurus.config.js +++ b/doc/docusaurus.config.js @@ -205,6 +205,7 @@ module.exports = { ], ['docusaurus-plugin-sass', {}], require.resolve('../config/schema-org'), + require.resolve('../config/breadcrumb'), ], themeConfig: { navbar: { diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 5cb908525a7..c7c596b08b6 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -97,6 +97,7 @@ module.exports = { ], ['docusaurus-plugin-sass', {}], require.resolve('../config/schema-org'), + require.resolve('../config/breadcrumb'), ], themeConfig: { navbar: { diff --git a/website/src/pages/downloads.tsx b/website/src/pages/downloads.tsx index 0ae16685f81..058ed97a239 100644 --- a/website/src/pages/downloads.tsx +++ b/website/src/pages/downloads.tsx @@ -61,6 +61,27 @@ const Downloads: FC = () => ( <Head> <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.' })} /> <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.' })} /> + <script type="application/ld+json"> + {JSON.stringify({ + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: 'Apache APISIX', + applicationCategory: 'DeveloperApplication', + operatingSystem: 'Linux, macOS, Docker, Kubernetes', + license: 'https://www.apache.org/licenses/LICENSE-2.0', + url: 'https://apisix.apache.org/downloads/', + author: { + '@type': 'Organization', + name: 'Apache Software Foundation', + url: 'https://www.apache.org/', + }, + offers: { + '@type': 'Offer', + price: '0', + priceCurrency: 'USD', + }, + })} + </script> </Head> <DownloadsPage> <PageTitle><Translate id="download.website.title">Downloads</Translate></PageTitle> diff --git a/website/src/pages/team.tsx b/website/src/pages/team.tsx index 0ec75721582..fd4fdac033a 100644 --- a/website/src/pages/team.tsx +++ b/website/src/pages/team.tsx @@ -256,7 +256,7 @@ const Team: FC = () => { href={`https://github.com/${member.githubUsername}`} target="_blank" > - <Avatar src={member.avatarUrl} /> + <Avatar src={member.avatarUrl} alt={member.name || member.username} width={108} height={108} loading="lazy" /> <MemberName>{member.name}</MemberName> <Username> @
