-
Notifications
You must be signed in to change notification settings - Fork 7
Add AI-friendly meta tags and enhanced structured data #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
f6dcbe7
Add AI-friendly meta tags and structured data
JakeSCahill 7d094be
Update applicationCategory to Agentic Data Plane
JakeSCahill 07f18f5
Remove non-standard ai-content-declaration meta tag
JakeSCahill 44aa8b3
Fix JSON escaping and date metadata in structured data
JakeSCahill 6f7ec37
Fix markdown-url helper to generate proper .md URLs
JakeSCahill a672f66
Enhance TechArticle schema with additional fields
JakeSCahill 395d2da
Update structured data to use Git-based dates
JakeSCahill e04fe30
Add 10s timeout to Bloblang grammar HTTP requests to prevent CI hangs
JakeSCahill 9e62347
Add Handlebars helpers to access Git dates from contentCatalog
JakeSCahill 42109ab
Address structured data and URL handling issues from PR review
JakeSCahill 1a01cb4
Revert to hardcoded 'Agentic Data Plane' applicationCategory
JakeSCahill 5482eb0
Apply suggestion from @JakeSCahill
JakeSCahill 192dfb2
Apply suggestion from @JakeSCahill
JakeSCahill c61ff36
Fix structured data attribute access and correct topic type field
JakeSCahill 2fffce7
Fix structured data helpers for git dates, descriptions, and HTML ent…
JakeSCahill af898af
Remove redundant git date helpers, use page.attributes directly
JakeSCahill 66f88c5
Optimize nav helpers with O(1) URL lookups
JakeSCahill 6da03de
Enhance TechArticle schema with additional properties
JakeSCahill fb47535
Remove duplicate genre field from TechArticle schema
JakeSCahill 08c0245
Trigger CI rebuild
JakeSCahill 87bcb69
Use direct page.attributes access, reduce helper calls
JakeSCahill 5389822
Add FAQ structured data to schema.org JSON-LD
JakeSCahill d78ef8d
Generate FAQPage JSON-LD from structured attributes
JakeSCahill c3c703e
Optimize validate-build workflow with caching
JakeSCahill File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| :!example-caption: | ||
| :!table-caption: | ||
| :page-pagination: | ||
| :page-has-markdown: | ||
|
|
||
| {description} | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,36 +1,26 @@ | ||
| 'use strict' | ||
|
|
||
| // Cache for beta status lookups (cleared between components) | ||
| const cache = new Map() | ||
| let currentComponent = null | ||
| // Cache: Map<componentName, Map<url, isBeta>> | ||
| let urlCache = null | ||
| let cachedComponent = null | ||
|
|
||
| module.exports = (navUrl, { data: { root } }) => { | ||
| const { contentCatalog, page } = root | ||
| if (page.layout === '404') return false | ||
| if (!contentCatalog) return false | ||
|
|
||
| // Reset cache if component changed | ||
| if (currentComponent !== page.component.name) { | ||
| cache.clear() | ||
| currentComponent = page.component.name | ||
| } | ||
|
|
||
| // Check cache first | ||
| if (cache.has(navUrl)) { | ||
| return cache.get(navUrl) | ||
| } | ||
|
|
||
| // Query and cache result | ||
| const pages = contentCatalog.findBy({ component: page.component.name, family: 'page' }) | ||
| // Build URL map once per component (O(n) once, then O(1) lookups) | ||
| if (cachedComponent !== page.component.name) { | ||
| urlCache = new Map() | ||
| cachedComponent = page.component.name | ||
|
|
||
| for (const navGroup of pages) { | ||
| if (navGroup.pub.url === navUrl) { | ||
| const isBeta = !!navGroup.asciidoc.attributes['page-beta'] | ||
| cache.set(navUrl, isBeta) | ||
| return isBeta | ||
| const pages = contentCatalog.findBy({ component: page.component.name, family: 'page' }) | ||
| for (const p of pages) { | ||
| if (p.pub?.url) { | ||
| urlCache.set(p.pub.url, !!p.asciidoc?.attributes?.['page-beta']) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| cache.set(navUrl, false) | ||
| return false | ||
| return urlCache.get(navUrl) || false | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,47 +1,25 @@ | ||
| 'use strict' | ||
|
|
||
| /* Put this in nav-tree.hbs | ||
| {{#if navigation.length}} | ||
| <ul class="nav-list"> | ||
| {{#each navigation}} | ||
| <li class="nav-item{{#if (eq ./url @root.page.url)}} is-current-page{{/if}}" data-depth="{{or ../level 0}}"> | ||
| {{#if ./content}} | ||
| {{#if ./url}} | ||
| <a class="nav-link | ||
| {{~#if (is-enterprise ./url)}} enterprise{{/if}} | ||
| */ | ||
|
|
||
| // Cache for component pages (cleared between render cycles) | ||
| const cache = new Map() | ||
| let currentComponent = null | ||
| // Cache: Map<url, isEnterprise> | ||
| let urlCache = null | ||
| let cachedComponent = null | ||
|
|
||
| module.exports = (navUrl, { data: { root } }) => { | ||
| const { contentCatalog, page } = root | ||
|
|
||
| // Reset cache if component changed | ||
| if (currentComponent !== page.component.name) { | ||
| cache.clear() | ||
| currentComponent = page.component.name | ||
| } | ||
|
|
||
| // Check cache first | ||
| if (cache.has(navUrl)) { | ||
| return cache.get(navUrl) | ||
| } | ||
|
|
||
| // Query and cache result | ||
| const pages = contentCatalog.findBy({ component: page.component.name, family: 'page' }) | ||
|
|
||
| for (let i = 0; i < pages.length; i++) { | ||
| const isEnterprise = pages[i].pub.url === navUrl && | ||
| pages[i].asciidoc.attributes['page-enterprise'] === 'true' | ||
|
|
||
| if (pages[i].pub.url === navUrl) { | ||
| cache.set(navUrl, isEnterprise) | ||
| return isEnterprise | ||
| if (!contentCatalog) return false | ||
|
|
||
| // Build URL map once per component (O(n) once, then O(1) lookups) | ||
| if (cachedComponent !== page.component.name) { | ||
| urlCache = new Map() | ||
| cachedComponent = page.component.name | ||
|
|
||
| const pages = contentCatalog.findBy({ component: page.component.name, family: 'page' }) | ||
| for (const p of pages) { | ||
| if (p.pub?.url) { | ||
| urlCache.set(p.pub.url, p.asciidoc?.attributes?.['page-enterprise'] === 'true') | ||
| } | ||
| } | ||
| } | ||
|
|
||
| cache.set(navUrl, false) | ||
| return false | ||
| return urlCache.get(navUrl) || false | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,33 @@ | ||
| 'use strict' | ||
|
|
||
| let navGroupCache = null | ||
| let componentCache = null | ||
| // Cache: Map<url, isLimitedAvailability> | ||
| let urlCache = null | ||
| let cachedComponent = null | ||
|
|
||
| module.exports = (navUrl, { data: { root } }) => { | ||
| const { contentCatalog, page } = root | ||
| if (page.layout === '404') return false | ||
|
|
||
| // Preview mode: contentCatalog doesn't contain preview pages | ||
| // Fall back to checking if this nav item is the current page | ||
| if (!contentCatalog) { | ||
| const currentPageUrl = page.url | ||
| // Check if this navigation URL matches the current page | ||
| const isCurrentPage = navUrl === currentPageUrl || | ||
| (navUrl && page.src && navUrl.endsWith(page.src.basename.replace('.adoc', '.html'))) | ||
| // Return the current page's limited availability status | ||
| return isCurrentPage && page.attributes && page.attributes['limited-availability'] | ||
| } | ||
|
|
||
| // Only perform lookup if caches are invalid or stale | ||
| if (!navGroupCache || componentCache !== page.component.name) { | ||
| navGroupCache = contentCatalog.findBy({ component: page.component.name, family: 'page' }) | ||
| componentCache = page.component.name | ||
| } | ||
| // Build URL map once per component (O(n) once, then O(1) lookups) | ||
| if (cachedComponent !== page.component.name) { | ||
| urlCache = new Map() | ||
| cachedComponent = page.component.name | ||
|
|
||
| // Iterate through cached pages and check for limited availability status | ||
| for (const navGroup of navGroupCache) { | ||
| // Guard against missing properties | ||
| if (navGroup.pub && navGroup.pub.url === navUrl && | ||
| navGroup.asciidoc && navGroup.asciidoc.attributes && | ||
| navGroup.asciidoc.attributes['page-limited-availability']) { | ||
| return true | ||
| const pages = contentCatalog.findBy({ component: page.component.name, family: 'page' }) | ||
| for (const p of pages) { | ||
| if (p.pub?.url) { | ||
| urlCache.set(p.pub.url, !!p.asciidoc?.attributes?.['page-limited-availability']) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| return urlCache.get(navUrl) || false | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| 'use strict' | ||
|
|
||
| /** | ||
| * Decodes HTML entities to their actual characters. | ||
| * Handles both named entities (e.g., &) and numeric entities (e.g., ’) | ||
| * | ||
| * @param {string} str - String with HTML entities | ||
| * @returns {string} - Decoded string | ||
| */ | ||
| function decodeHtmlEntities (str) { | ||
| // Named entities mapped to their Unicode code points | ||
| const namedEntities = { | ||
| '&': '\u0026', // & | ||
| '<': '\u003C', // < | ||
| '>': '\u003E', // > | ||
| '"': '\u0022', // " | ||
| ''': '\u0027', // ' | ||
| ''': '\u0027', // ' | ||
| ' ': '\u00A0', // non-breaking space | ||
| '–': '\u2013', // en dash | ||
| '—': '\u2014', // em dash | ||
| '‘': '\u2018', // left single quote | ||
| '’': '\u2019', // right single quote | ||
| '“': '\u201C', // left double quote | ||
| '”': '\u201D', // right double quote | ||
| '…': '\u2026', // ellipsis | ||
| '©': '\u00A9', // copyright | ||
| '®': '\u00AE', // registered | ||
| '™': '\u2122', // trademark | ||
| } | ||
|
|
||
| // Replace named entities | ||
| let result = str.replace(/&[a-zA-Z]+;/g, (match) => namedEntities[match] || match) | ||
|
|
||
| // Replace numeric entities (decimal: ’ and hex: ’) | ||
| result = result.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10))) | ||
| result = result.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCharCode(parseInt(code, 16))) | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| /** | ||
| * Escapes a string for safe inclusion in JSON-LD script blocks. | ||
| * | ||
| * This helper addresses several issues when embedding dynamic content in JSON: | ||
| * 1. HTML entities are decoded to actual characters (e.g., ’ becomes right single quote) | ||
| * 2. Backslashes must be escaped to avoid invalid JSON | ||
| * 3. Double quotes must be escaped to avoid breaking JSON strings | ||
| * 4. </script> substrings must be escaped to avoid breaking out of script tags | ||
| * | ||
| * @param {string} value - The string to make JSON-safe | ||
| * @returns {string} - The JSON-safe string | ||
| */ | ||
| module.exports = (value) => { | ||
| if (!value || typeof value !== 'string') return '' | ||
|
|
||
| // First decode HTML entities to their actual characters | ||
| const decoded = decodeHtmlEntities(value) | ||
|
|
||
| return decoded | ||
| // Escape backslashes first (must be done before escaping quotes) | ||
| .replace(/\\/g, '\\\\') | ||
| // Escape double quotes | ||
| .replace(/"/g, '\\"') | ||
| // Escape </script> to prevent breaking out of script block | ||
| .replace(/<\/script>/gi, '<\\/script>') | ||
| // Escape other control characters that could break JSON | ||
| .replace(/\n/g, '\\n') | ||
| .replace(/\r/g, '\\r') | ||
| .replace(/\t/g, '\\t') | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| 'use strict' | ||
|
|
||
| /** | ||
| * Get a page attribute from contentCatalog | ||
| * | ||
| * This helper queries contentCatalog to access page.asciidoc.attributes, | ||
| * which contains AsciiDoc document attributes set in the page or by extensions. | ||
| * | ||
| * Special handling for intrinsic attributes: | ||
| * - "description": Falls back to page.description (Antora intrinsic property) | ||
| * - "keywords": Falls back to page.keywords (Antora intrinsic property) | ||
| * | ||
| * Usage: {{page-attribute "description"}} or {{page-attribute "page-topic-type"}} | ||
| */ | ||
|
|
||
| module.exports = (attributeName, { data: { root } }) => { | ||
| const { contentCatalog, page } = root | ||
|
|
||
| // Safety checks | ||
| if (!contentCatalog || !page || !page.component || page.version === undefined) { | ||
| return null | ||
| } | ||
|
|
||
| // Query contentCatalog for full page object | ||
| const pageInfo = contentCatalog.getById({ | ||
| component: page.component.name, | ||
| version: page.version, | ||
| module: page.module, | ||
| family: 'page', | ||
| relative: page.relativeSrcPath, | ||
| }) | ||
|
|
||
| // First try asciidoc.attributes | ||
| const attrValue = pageInfo?.asciidoc?.attributes?.[attributeName] | ||
| if (attrValue) return attrValue | ||
|
|
||
| // Fall back to intrinsic page properties for special attributes | ||
| // Antora stores :description: and :keywords: as intrinsic properties | ||
| if (attributeName === 'description' && page.description) { | ||
| return page.description | ||
| } | ||
| if (attributeName === 'keywords' && page.keywords) { | ||
| return page.keywords | ||
| } | ||
|
|
||
| return null | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.