From 5f187ff02899b8e2a1773bf54f49c552ca5ac6e7 Mon Sep 17 00:00:00 2001 From: Michael Sydney Moore Date: Fri, 11 Sep 2026 16:30:15 +0100 Subject: [PATCH 1/2] Declare each concept a defined term in a glossary set The graph on every page described only where a page sat: WebSite, WebPage and a BreadcrumbList. Nothing said what a page held, so a concept page and any other page looked like the same kind of thing to a crawler. Each concept now adds a DefinedTerm carrying the short definition, pointing at the MDN page for the same term through sameAs, and claiming membership of a DefinedTermSet that the home page declares over all thirty-five concepts. A TechArticle wraps the prose around it, with the proficiency level taken from the existing Fundamentals and Advanced split and the modified date from the same content commit the sitemap already uses. This matters more for assistants answering "what is a closure in JavaScript" than for blue links: a page that names itself the definition of a term is far easier to identify as the source than one that only names itself a page. --- scripts/build.mjs | 24 +++++++++++++++++++++++- test/site.test.mjs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index e215d7e..d5718d4 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -31,6 +31,9 @@ const escapeHTML = (value) => String(value).replace(/[&<>"']/g, (character) => ( const url = (path = '/') => origin + path; const pathFor = (concept) => `/${concept.slug}/`; const labelFor = (concept) => concept.id === 'this' ? 'this keyword' : concept.label; +// schema.org only defines Beginner and Expert for proficiencyLevel, which maps +// onto the two groups the concepts are already sorted into. +const proficiencyFor = (concept) => concept.group === 'Advanced' ? 'Expert' : 'Beginner'; const summaryFor = (concept) => [concept.text, ...(concept.definitionItems || [])].filter(Boolean).join(' '); const searchDescription = (concept) => { const text = `${concept.label} in JavaScript: ${concept.text || concept.explanation}`.replace(/\s+/g, ' ').trim(); @@ -140,15 +143,34 @@ function shareMenu(title, path) { function document({ title, description, path, content, current, noindex = false }) { const pageTitle = `${title} | ${brand}`; + // WebSite/WebPage/BreadcrumbList only say where a page sits. DefinedTerm says + // what it holds: this URL is the definition of one named term, and the set on + // the home page is the glossary those 35 terms belong to. TechArticle carries + // the prose around the definition. const structuredData = { '@context':'https://schema.org', '@graph':[ {'@type':'WebSite','@id':url('/#website'),url:url('/'),name:brand,inLanguage:'en'}, - {'@type':'WebPage','@id':url(path),url:url(path),name:title,description,inLanguage:'en',isPartOf:{'@id':url('/#website')},...(current ? {breadcrumb:{'@id':url(path+'#breadcrumb')}} : {})}, + {'@type':'WebPage','@id':url(path),url:url(path),name:title,description,inLanguage:'en',isPartOf:{'@id':url('/#website')}, + ...(current ? {breadcrumb:{'@id':url(path+'#breadcrumb')},mainEntity:{'@id':url(path+'#article')}} : {}), + ...(!current && !noindex ? {mainEntity:{'@id':url('/#glossary')}} : {})}, ...(current ? [{'@type':'BreadcrumbList','@id':url(path+'#breadcrumb'),itemListElement:[ {'@type':'ListItem',position:1,name:'All concepts',item:url('/')}, {'@type':'ListItem',position:2,name:labelFor(current),item:url(path)}, ]}] : []), + ...(current ? [ + {'@type':'TechArticle','@id':url(path+'#article'),url:url(path),headline:current.heading,description, + inLanguage:'en',isPartOf:{'@id':url('/#website')},about:{'@id':url(path+'#term')}, + proficiencyLevel:proficiencyFor(current),dateModified:lastModified}, + {'@type':'DefinedTerm','@id':url(path+'#term'),url:url(path),name:labelFor(current), + description:summaryFor(current),inDefinedTermSet:{'@id':url('/#glossary')}, + // The MDN page for the same term, so the definition is tied to the + // reference every reader already trusts. + ...(current.reference ? {sameAs:current.reference} : {})}, + ] : []), + ...(!current && !noindex ? [{'@type':'DefinedTermSet','@id':url('/#glossary'),url:url('/'),name:brand, + description,inLanguage:'en', + hasDefinedTerm:definitions.map((item) => ({'@id':url(pathFor(item)+'#term')}))}] : []), ], }; const practiceURL = new URL('https://www.practice-pad.app/'); diff --git a/test/site.test.mjs b/test/site.test.mjs index 1dbbec6..c175555 100644 --- a/test/site.test.mjs +++ b/test/site.test.mjs @@ -103,6 +103,40 @@ test('indexable pages have one heading, unique metadata, and accurate structured assert.match(read('_redirects'),/^https:\/\/javascript-in-30-words\.netlify\.app\/\*/); }); +test('every concept declares itself a defined term inside the glossary the home page sets out',()=>{ + const graphOf = (file)=>JSON.parse(read(file).match(/