|
| 1 | +import { join } from 'node:path'; |
| 2 | +import { ELEMENTS_SITE_URL } from '../utils/env.js'; |
| 3 | +import { siteData } from '../../index.11tydata.js'; |
| 4 | + |
| 5 | +export const BASE_URL = join('/', process.env.PAGES_BASE_URL ?? '', '/'); |
| 6 | + |
| 7 | +const SITE_ORIGIN = ELEMENTS_SITE_URL.replace(/\/$/, ''); |
| 8 | +const PATH_PREFIX = BASE_URL.replace(/\/$/, ''); |
| 9 | + |
| 10 | +const BREADCRUMB_TERMS = { |
| 11 | + api: 'API', |
| 12 | + cli: 'CLI', |
| 13 | + mcp: 'MCP', |
| 14 | + ssr: 'SSR', |
| 15 | + i18n: 'i18n', |
| 16 | + ui: 'UI', |
| 17 | + nextjs: 'Next.js', |
| 18 | + solidjs: 'SolidJS', |
| 19 | + nuxt: 'Nuxt', |
| 20 | + typescript: 'TypeScript', |
| 21 | + go: 'Go', |
| 22 | + vue: 'Vue', |
| 23 | + react: 'React', |
| 24 | + svelte: 'Svelte', |
| 25 | + preact: 'Preact', |
| 26 | + lit: 'Lit', |
| 27 | + hugo: 'Hugo', |
| 28 | + angular: 'Angular', |
| 29 | + monaco: 'Monaco', |
| 30 | + jsdoc: 'JSDoc', |
| 31 | + json: 'JSON', |
| 32 | + html: 'HTML', |
| 33 | + css: 'CSS', |
| 34 | + nvidia: 'NVIDIA', |
| 35 | + npm: 'npm' |
| 36 | +}; |
| 37 | + |
| 38 | +export function escapeAttr(value) { |
| 39 | + return String(value ?? '') |
| 40 | + .replace(/&/g, '&') |
| 41 | + .replace(/</g, '<') |
| 42 | + .replace(/>/g, '>') |
| 43 | + .replace(/"/g, '"') |
| 44 | + .replace(/'/g, '''); |
| 45 | +} |
| 46 | + |
| 47 | +function titleCaseSegment(segment) { |
| 48 | + if (BREADCRUMB_TERMS[segment]) return BREADCRUMB_TERMS[segment]; |
| 49 | + return segment |
| 50 | + .split('-') |
| 51 | + .map(part => BREADCRUMB_TERMS[part] ?? part.charAt(0).toUpperCase() + part.slice(1)) |
| 52 | + .join(' '); |
| 53 | +} |
| 54 | + |
| 55 | +function findElementByTag(tag) { |
| 56 | + if (!tag) return null; |
| 57 | + return siteData.elements.find(e => e.name === tag) ?? null; |
| 58 | +} |
| 59 | + |
| 60 | +export function resolvePageMeta(data) { |
| 61 | + const url = data.page?.url ?? '/'; |
| 62 | + const title = data.title ?? 'NVIDIA Elements'; |
| 63 | + const tag = data.tag ?? data.component?.data?.tag; |
| 64 | + const element = findElementByTag(tag); |
| 65 | + const componentDescription = element?.manifest?.description?.trim(); |
| 66 | + |
| 67 | + let description; |
| 68 | + if (data.description) { |
| 69 | + description = data.description; |
| 70 | + } else if (data.isApiTab && componentDescription) { |
| 71 | + description = `${componentDescription} — API reference for <${tag}>.`; |
| 72 | + } else if (data.isExamplesTab && componentDescription) { |
| 73 | + description = `${componentDescription} — Interactive examples for <${tag}>.`; |
| 74 | + } else if (componentDescription) { |
| 75 | + description = componentDescription; |
| 76 | + } else { |
| 77 | + description = `Documentation for ${title} in NVIDIA Elements, the framework-agnostic design system for AI/ML factories.`; |
| 78 | + } |
| 79 | + |
| 80 | + const canonicalUrl = `${SITE_ORIGIN}${PATH_PREFIX}${url}`; |
| 81 | + const ogImage = `${SITE_ORIGIN}${PATH_PREFIX}/favicon.svg`; |
| 82 | + return { title, description, canonicalUrl, ogImage, url }; |
| 83 | +} |
| 84 | + |
| 85 | +function jsonLdEncode(value) { |
| 86 | + return JSON.stringify(value).replace(/<\//g, '<\\/'); |
| 87 | +} |
| 88 | + |
| 89 | +export function renderJsonLd(data, meta) { |
| 90 | + const isDocs = meta.url.startsWith('/docs/'); |
| 91 | + const articleType = isDocs ? 'TechArticle' : 'WebPage'; |
| 92 | + const date = data.page?.date instanceof Date ? data.page.date.toISOString() : new Date().toISOString(); |
| 93 | + |
| 94 | + const article = { |
| 95 | + '@context': 'https://schema.org', |
| 96 | + '@type': articleType, |
| 97 | + headline: meta.title, |
| 98 | + description: meta.description, |
| 99 | + url: meta.canonicalUrl, |
| 100 | + mainEntityOfPage: meta.canonicalUrl, |
| 101 | + inLanguage: 'en', |
| 102 | + image: meta.ogImage, |
| 103 | + datePublished: date, |
| 104 | + dateModified: date, |
| 105 | + publisher: { '@type': 'Organization', name: 'NVIDIA', url: SITE_ORIGIN }, |
| 106 | + author: { '@type': 'Organization', name: 'NVIDIA' } |
| 107 | + }; |
| 108 | + |
| 109 | + const segments = meta.url.split('/').filter(Boolean); |
| 110 | + const articleScript = `<script type="application/ld+json">${jsonLdEncode(article)}</script>`; |
| 111 | + if (segments.length === 0) { |
| 112 | + return articleScript; |
| 113 | + } |
| 114 | + |
| 115 | + const itemListElement = [{ '@type': 'ListItem', position: 1, name: 'Home', item: `${SITE_ORIGIN}${PATH_PREFIX}/` }]; |
| 116 | + let cumulative = ''; |
| 117 | + segments.forEach((seg, i) => { |
| 118 | + cumulative += `/${seg}`; |
| 119 | + itemListElement.push({ |
| 120 | + '@type': 'ListItem', |
| 121 | + position: i + 2, |
| 122 | + name: titleCaseSegment(seg), |
| 123 | + item: `${SITE_ORIGIN}${PATH_PREFIX}${cumulative}/` |
| 124 | + }); |
| 125 | + }); |
| 126 | + |
| 127 | + const breadcrumb = { |
| 128 | + '@context': 'https://schema.org', |
| 129 | + '@type': 'BreadcrumbList', |
| 130 | + itemListElement |
| 131 | + }; |
| 132 | + return `${articleScript}\n <script type="application/ld+json">${jsonLdEncode(breadcrumb)}</script>`; |
| 133 | +} |
0 commit comments