Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions projects/site/eleventy.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EleventyRenderPlugin, IdAttributePlugin } from '@11ty/eleventy';
import EleventyPluginVite from '@11ty/eleventy-plugin-vite';
import litPlugin from '@lit-labs/eleventy-plugin-lit';

import { BASE_URL } from './src/_11ty/layouts/common.js';
import { BASE_URL } from './src/_11ty/layouts/metadata.js';
import {
ELEMENTS_PAGES_BASE_URL,
ELEMENTS_REPO_BASE_URL,
Expand All @@ -16,6 +16,7 @@ import {
} from './src/_11ty/utils/env.js';
import { searchPlugin } from './src/_11ty/plugins/search.js';
import { llmsTxtPlugin } from './src/_11ty/plugins/llms-txt.js';
import { sitemapPlugin } from './src/_11ty/plugins/sitemap-xml.js';
import { elementLoaderTransform } from './src/_11ty/transforms/element-loader.js';
import { anchorGeneratorTransform } from './src/_11ty/transforms/anchor-generator.js';
import { htmlMinifyTransform } from './src/_11ty/transforms/html-minify.js';
Expand Down Expand Up @@ -104,8 +105,6 @@ export default function (eleventyConfig) {
eleventyConfig.addPassthroughCopy({ '../themes/dist/bundles': 'local-bundles/themes' });
eleventyConfig.addPassthroughCopy({ '../styles/dist/bundles': 'local-bundles/styles' });

eleventyConfig.addPassthroughCopy('src/robots.txt');

// Configure Lit SSR plugin for web components
eleventyConfig.addPlugin(litPlugin, {
mode: 'worker',
Expand Down Expand Up @@ -174,6 +173,11 @@ export default function (eleventyConfig) {
eleventyConfig.addPlugin(llmsTxtPlugin);
}

// https://www.sitemaps.org
if (process.env.ELEVENTY_RUN_MODE === 'build') {
eleventyConfig.addPlugin(sitemapPlugin);
}

// Set custom markdown library
eleventyConfig.setLibrary('md', markdown);

Expand Down
4 changes: 4 additions & 0 deletions projects/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
"PAGES_BASE_URL": {
"external": true,
"default": "/elements/"
},
"ELEMENTS_SITE_URL": {
"external": true,
"default": "https://nvidia.github.io"
}
Comment on lines +67 to 70
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Document ELEMENTS_SITE_URL in build docs.

ELEMENTS_SITE_URL was added to build-time Wireit env; please ensure /projects/internals/BUILD.md documents this variable and default value so CI/CD and local build docs stay aligned.

Based on learnings: "Review /projects/internals/BUILD.md when modifying build configuration, Wireit scripts, or CI/CD pipeline".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/site/package.json` around lines 67 - 70, Add documentation for the
new build-time Wireit environment variable ELEMENTS_SITE_URL to the BUILD.md
docs: describe its purpose (overriding the site URL during builds), list its
default value ("https://nvidia.github.io"), indicate that it is a
build-time/Wireit environment variable, and show an example of how to set it for
local CI/CD usage and in the Wireit/CI configuration; update the BUILD.md
section that covers build env vars so ELEMENTS_SITE_URL appears alongside the
other documented variables.

}
},
Expand Down
6 changes: 5 additions & 1 deletion projects/site/public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
User-agent: *
Disallow:
Allow: /
Disallow: /assets/
Disallow: /.pagefind/

Sitemap: https://nvidia.github.io/elements/sitemap.xml
30 changes: 17 additions & 13 deletions projects/site/src/_11ty/layouts/common.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
/* eslint-env node */
/* global process */

import { join } from 'node:path';
import { ELEMENTS_PAGES_BASE_URL, ELEMENTS_PLAYGROUND_BASE_URL, ELEMENTS_REPO_BASE_URL } from '../utils/env.js';

export const BASE_URL = join('/', process.env.PAGES_BASE_URL ?? '', '/');
import { ELEMENTS_PLAYGROUND_BASE_URL, ELEMENTS_REPO_BASE_URL } from '../utils/env.js';
import { escapeAttr, resolvePageMeta, renderJsonLd, BASE_URL } from './metadata.js';

/**
* This renders the base head element with all the common styles and scripts needed for ALL PAGES.
* Page specific resources should not be placed here.
*/
export const renderBaseHead = data => /* html */ `
export const renderBaseHead = data => {
const meta = resolvePageMeta(data);
const ogType = meta.url === '/' ? 'website' : 'article';
return /* html */ `
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="all">
<base href="${BASE_URL}" />
<title data-pagefind-meta="title">${data.title}</title>
<meta name="description" content="Elements - ${data.title}">
<meta property="og:title" content="Elements - ${data.title}" >
<meta property="og:url" content="${data.page?.url ?? ''}">
<meta property="og:description" content="NVIDIA Elements is a flexible, framework-agnostic design system and toolkit that empowers teams to build exceptional user experiences.">
<meta property="og:image" content="/favicon.svg">
<meta property="og:site_name" content="${ELEMENTS_PAGES_BASE_URL}/">
<meta property="og:type" content="website">
<title data-pagefind-meta="title">${escapeAttr(meta.title)}</title>
<meta name="description" content="${escapeAttr(meta.description)}">
<link rel="canonical" href="${meta.canonicalUrl}">
<meta property="og:title" content="${escapeAttr(meta.title)}">
<meta property="og:url" content="${meta.canonicalUrl}">
<meta property="og:description" content="${escapeAttr(meta.description)}">
<meta property="og:image" content="${meta.ogImage}">
Comment on lines +21 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape canonicalUrl and ogImage before injecting into HTML attributes.

Line 21, Line 23, and Line 25 interpolate raw values into href/content. If these strings ever contain quotes, this can break the head markup and create an injection vector.

Proposed hardening
-  <link rel="canonical" href="${meta.canonicalUrl}">
+  <link rel="canonical" href="${escapeAttr(meta.canonicalUrl)}">
   <meta property="og:title" content="${escapeAttr(meta.title)}">
-  <meta property="og:url" content="${meta.canonicalUrl}">
+  <meta property="og:url" content="${escapeAttr(meta.canonicalUrl)}">
   <meta property="og:description" content="${escapeAttr(meta.description)}">
-  <meta property="og:image" content="${meta.ogImage}">
+  <meta property="og:image" content="${escapeAttr(meta.ogImage)}">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/site/src/_11ty/layouts/common.js` around lines 21 - 25, The template
injects raw meta.canonicalUrl and meta.ogImage into attributes; escape them
before rendering to avoid quote/injection issues—use the existing escapeAttr
helper (same as used for meta.title and meta.description) to replace the raw
interpolations for href and og:url/content (i.e., wrap meta.canonicalUrl and
meta.ogImage with escapeAttr in the common.js layout where those symbols are
used).

<meta property="og:site_name" content="NVIDIA Elements">
<meta property="og:type" content="${ogType}">
<link rel="icon" href="/favicon.svg">
${renderJsonLd(data, meta)}
${renderGlobalsScript(data)}
<style>
@import '@nvidia-elements/themes/fonts/inter.css';
Expand Down Expand Up @@ -99,6 +102,7 @@ export const renderBaseHead = data => /* html */ `
globalThis.document.documentElement.removeAttribute('no-js');
</script>
`;
};

export const renderDocsNav = data => /* html */ `
<nve-tree id="docs-nav" data-pagefind-ignore="all" behavior-expand selectable="single">
Expand Down
133 changes: 133 additions & 0 deletions projects/site/src/_11ty/layouts/metadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { join } from 'node:path';
Comment thread
coryrylan marked this conversation as resolved.
import { ELEMENTS_SITE_URL } from '../utils/env.js';
import { siteData } from '../../index.11tydata.js';

export const BASE_URL = join('/', process.env.PAGES_BASE_URL ?? '', '/');

const SITE_ORIGIN = ELEMENTS_SITE_URL.replace(/\/$/, '');
const PATH_PREFIX = BASE_URL.replace(/\/$/, '');

const BREADCRUMB_TERMS = {
api: 'API',
cli: 'CLI',
mcp: 'MCP',
ssr: 'SSR',
i18n: 'i18n',
ui: 'UI',
nextjs: 'Next.js',
solidjs: 'SolidJS',
nuxt: 'Nuxt',
typescript: 'TypeScript',
go: 'Go',
vue: 'Vue',
react: 'React',
svelte: 'Svelte',
preact: 'Preact',
lit: 'Lit',
hugo: 'Hugo',
angular: 'Angular',
monaco: 'Monaco',
jsdoc: 'JSDoc',
json: 'JSON',
html: 'HTML',
css: 'CSS',
nvidia: 'NVIDIA',
npm: 'npm'
};

export function escapeAttr(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
Comment on lines +38 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add JSDoc contracts for metadata helpers and decision branches.

Please document expected inputs/outputs and constraints for escapeAttr, resolvePageMeta, and renderJsonLd (and key decision branches) so build-time behavior is explicit and auditable.

As per coding guidelines, "Document agent capabilities, constraints, and expected inputs/outputs in code comments or docstrings" and "Use structured logging to track agent decision-making processes and state changes for debugging and monitoring".

Also applies to: 60-83, 89-133

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/site/src/_11ty/layouts/metadata.js` around lines 38 - 45, Add JSDoc
blocks for escapeAttr, resolvePageMeta, and renderJsonLd that state expected
parameter types, allowed/required fields, return types, and any constraints
(e.g., null/undefined handling, string encoding expectations); within
resolvePageMeta and renderJsonLd also document the main decision branches (e.g.,
when page.frontmatter takes precedence, when defaults are applied, and when
JSON-LD is omitted) and the conditions that trigger each branch; include notes
about side effects (none) and error behavior (throws vs. safe fallback) and add
brief structured-log call points or log message templates to indicate where
decision outcomes are emitted for observability (reference the exact function
names escapeAttr, resolvePageMeta, renderJsonLd and the key branch conditions in
those functions).


function titleCaseSegment(segment) {
if (BREADCRUMB_TERMS[segment]) return BREADCRUMB_TERMS[segment];
return segment
.split('-')
.map(part => BREADCRUMB_TERMS[part] ?? part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}

function findElementByTag(tag) {
if (!tag) return null;
return siteData.elements.find(e => e.name === tag) ?? null;
}

export function resolvePageMeta(data) {
const url = data.page?.url ?? '/';
const title = data.title ?? 'NVIDIA Elements';
const tag = data.tag ?? data.component?.data?.tag;
const element = findElementByTag(tag);
const componentDescription = element?.manifest?.description?.trim();

let description;
if (data.description) {
description = data.description;
} else if (data.isApiTab && componentDescription) {
description = `${componentDescription} — API reference for <${tag}>.`;
} else if (data.isExamplesTab && componentDescription) {
description = `${componentDescription} — Interactive examples for <${tag}>.`;
} else if (componentDescription) {
description = componentDescription;
} else {
description = `Documentation for ${title} in NVIDIA Elements, the framework-agnostic design system for AI/ML factories.`;
}

const canonicalUrl = `${SITE_ORIGIN}${PATH_PREFIX}${url}`;
const ogImage = `${SITE_ORIGIN}${PATH_PREFIX}/favicon.svg`;
return { title, description, canonicalUrl, ogImage, url };
}

function jsonLdEncode(value) {
return JSON.stringify(value).replace(/<\//g, '<\\/');
}

export function renderJsonLd(data, meta) {
const isDocs = meta.url.startsWith('/docs/');
const articleType = isDocs ? 'TechArticle' : 'WebPage';
const date = data.page?.date instanceof Date ? data.page.date.toISOString() : new Date().toISOString();

const article = {
'@context': 'https://schema.org',
'@type': articleType,
headline: meta.title,
description: meta.description,
url: meta.canonicalUrl,
mainEntityOfPage: meta.canonicalUrl,
inLanguage: 'en',
image: meta.ogImage,
datePublished: date,
dateModified: date,
Comment on lines +92 to +104
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid build-time “now” as JSON-LD publish/modify dates.

Line 92 falls back to new Date(); this makes datePublished/dateModified change on every build and can misrepresent content freshness.

Suggested fix
-  const date = data.page?.date instanceof Date ? data.page.date.toISOString() : new Date().toISOString();
+  const date = data.page?.date instanceof Date ? data.page.date.toISOString() : null;
@@
-    datePublished: date,
-    dateModified: date,
+    ...(date ? { datePublished: date, dateModified: date } : {}),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const date = data.page?.date instanceof Date ? data.page.date.toISOString() : new Date().toISOString();
const article = {
'@context': 'https://schema.org',
'@type': articleType,
headline: meta.title,
description: meta.description,
url: meta.canonicalUrl,
mainEntityOfPage: meta.canonicalUrl,
inLanguage: 'en',
image: meta.ogImage,
datePublished: date,
dateModified: date,
const date = data.page?.date instanceof Date ? data.page.date.toISOString() : null;
const article = {
'@context': 'https://schema.org',
'@type': articleType,
headline: meta.title,
description: meta.description,
url: meta.canonicalUrl,
mainEntityOfPage: meta.canonicalUrl,
inLanguage: 'en',
image: meta.ogImage,
...(date ? { datePublished: date, dateModified: date } : {}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/site/src/_11ty/layouts/metadata.js` around lines 92 - 104, The
current fallback new Date() makes the JSON-LD dates change every build; change
the date assignment so it only uses a source-controlled date (e.g.,
data.page.date when it exists and is a Date or a parsable string) and do not
default to new Date(); update the article object to conditionally include
datePublished and dateModified only when that deterministic date is available
(modify the const date and the article literal where datePublished/dateModified
are set), ensuring you neither inject a build-time "now" nor leave invalid
values in the JSON-LD.

publisher: { '@type': 'Organization', name: 'NVIDIA', url: SITE_ORIGIN },
author: { '@type': 'Organization', name: 'NVIDIA' }
};

const segments = meta.url.split('/').filter(Boolean);
const articleScript = `<script type="application/ld+json">${jsonLdEncode(article)}</script>`;
if (segments.length === 0) {
return articleScript;
}

const itemListElement = [{ '@type': 'ListItem', position: 1, name: 'Home', item: `${SITE_ORIGIN}${PATH_PREFIX}/` }];
let cumulative = '';
segments.forEach((seg, i) => {
cumulative += `/${seg}`;
itemListElement.push({
'@type': 'ListItem',
position: i + 2,
name: titleCaseSegment(seg),
item: `${SITE_ORIGIN}${PATH_PREFIX}${cumulative}/`
});
});

const breadcrumb = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement
};
return `${articleScript}\n <script type="application/ld+json">${jsonLdEncode(breadcrumb)}</script>`;
}
36 changes: 36 additions & 0 deletions projects/site/src/_11ty/plugins/sitemap-xml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { promises as fsp } from 'node:fs';
import { BASE_URL } from '../layouts/metadata.js';
import { ELEMENTS_SITE_URL } from '../utils/env.js';

const SITE_ORIGIN = ELEMENTS_SITE_URL.replace(/\/$/, '');
const PATH_PREFIX = BASE_URL.replace(/\/$/, '');
const EXCLUDED_PREFIXES = ['/docs/changelog/', '/docs/metrics/', '/examples/', '/404'];

function isPublishable(url) {
if (!url) return false;
if (!url.endsWith('/') && !url.endsWith('.html')) return false;
if (EXCLUDED_PREFIXES.some(prefix => url.startsWith(prefix))) return false;
return true;
}

export function sitemapPlugin(eleventyConfig) {
eleventyConfig.on('eleventy.after', async ({ results } = {}) => {
const urls = [...new Set((results ?? []).map(r => r.url).filter(isPublishable))].sort();
const lastmod = new Date().toISOString();
const entries = urls.map(url => {
const loc = `${SITE_ORIGIN}${PATH_PREFIX}${url}`;
return ['<url>', `<loc>${loc}</loc>`, `<lastmod>${lastmod}</lastmod>`, '</url>'].join('\n');
});
Comment on lines +21 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Escape <loc> values before XML serialization.

Line 22 writes raw URL text into XML. If a URL contains XML-sensitive characters, sitemap output becomes invalid.

Proposed hardening
+const escapeXml = value =>
+  value
+    .replaceAll('&', '&amp;')
+    .replaceAll('<', '&lt;')
+    .replaceAll('>', '&gt;')
+    .replaceAll('"', '&quot;')
+    .replaceAll("'", '&apos;');
+
 export function sitemapPlugin(eleventyConfig) {
   eleventyConfig.on('eleventy.after', async ({ results } = {}) => {
@@
     const entries = urls.map(url => {
       const loc = `${SITE_ORIGIN}${PATH_PREFIX}${url}`;
-      return ['<url>', `<loc>${loc}</loc>`, `<lastmod>${lastmod}</lastmod>`, '</url>'].join('\n');
+      return ['<url>', `<loc>${escapeXml(loc)}</loc>`, `<lastmod>${lastmod}</lastmod>`, '</url>'].join('\n');
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const loc = `${SITE_ORIGIN}${PATH_PREFIX}${url}`;
return ['<url>', `<loc>${loc}</loc>`, `<lastmod>${lastmod}</lastmod>`, '</url>'].join('\n');
});
const loc = `${SITE_ORIGIN}${PATH_PREFIX}${url}`;
return ['<url>', `<loc>${escapeXml(loc)}</loc>`, `<lastmod>${lastmod}</lastmod>`, '</url>'].join('\n');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/site/src/_11ty/plugins/sitemap-xml.js` around lines 21 - 23, The
<loc> element is being populated with raw URL text (constructed via SITE_ORIGIN,
PATH_PREFIX, url into the loc variable) which can break XML if the URL contains
characters like &, <, >, ", or '. Fix by HTML/XML-escaping the loc value before
serialization: add a small helper (e.g., escapeXml or encodeXml) that replaces
&, <, >, ", and ' with their XML entities and call it when creating the <loc>
string (use the escaped value instead of loc in the array passed to join).
Ensure the helper is reused wherever sitemap <loc> values are produced.


const xml = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
...entries,
'</urlset>',
''
].join('\n');

await fsp.mkdir('./.11ty-vite/public/', { recursive: true });
await fsp.writeFile('./.11ty-vite/public/sitemap.xml', xml, 'utf-8');
});
}
1 change: 1 addition & 0 deletions projects/site/src/_11ty/utils/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export const ELEMENTS_REGISTRY_URL = process.env.ELEMENTS_REGISTRY_URL ?? '';
export const ELEMENTS_ASSETS_CDN_BASE_URL = process.env.ELEMENTS_ASSETS_CDN_BASE_URL ?? '';
export const ELEMENTS_ESM_CDN_BASE_URL = process.env.ELEMENTS_ESM_CDN_BASE_URL ?? '';
export const ELEMENTS_CDN_BASE_URL = process.env.ELEMENTS_CDN_BASE_URL ?? '';
export const ELEMENTS_SITE_URL = process.env.ELEMENTS_SITE_URL ?? 'https://nvidia.github.io';
1 change: 1 addition & 0 deletions projects/site/src/docs/elements/_tabs/examples.11ty.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export async function render(data) {
data.tag = componentData.tag;
data.title = componentData.title;
data.page.fileSlug = componentData.page.fileSlug;
data.isExamplesTab = true;

return /* html */ `
<style scoped>
Expand Down
1 change: 1 addition & 0 deletions projects/site/src/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
{
title: 'Getting Started',
description: 'Get started with NVIDIA Elements: install Web Components, themes, and starter templates for building framework-agnostic UI for AI/ML factories.',
layout: 'docs.11ty.js'
}
---
Expand Down
74 changes: 0 additions & 74 deletions projects/site/src/robots.txt

This file was deleted.

Loading