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>
             @

Reply via email to