From f0fde58242ad7593a57a0ccd42a44ca36774627e Mon Sep 17 00:00:00 2001 From: Your Pal Kal Date: Fri, 2 Oct 2026 18:22:54 -0400 Subject: [PATCH 1/3] Allow generated pinout pages in the web release --- scripts/check-web-release.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-web-release.mjs b/scripts/check-web-release.mjs index 835ef15..6d7dddd 100644 --- a/scripts/check-web-release.mjs +++ b/scripts/check-web-release.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import {pathToFileURL} from 'node:url'; -const allowed=new Set(['assets','brand','library','licenses','pdf','wiki','_headers','404.html','catalog.json','index.html','robots.txt','sitemap.xml','theme-init.js','THIRD_PARTY_NOTICES.txt','wiki.css','wiki.js','build-info.json']); +const allowed=new Set(['assets','brand','library','licenses','pdf','wiki','pinout','_headers','404.html','catalog.json','index.html','robots.txt','sitemap.xml','theme-init.js','THIRD_PARTY_NOTICES.txt','wiki.css','wiki.js','build-info.json']); export async function checkWebRelease(root,{maxFiles=20000,maxFileBytes=25*1024*1024}={}){ root=path.resolve(root);let count=0,total=0; async function walk(dir){ From 92e75a47da71695b70621240b8833cbd25fe7e7b Mon Sep 17 00:00:00 2001 From: Your Pal Kal Date: Fri, 2 Oct 2026 18:23:41 -0400 Subject: [PATCH 2/3] Add styles for breadcrumbs and pinout figure --- public/wiki.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/wiki.css b/public/wiki.css index 02e28f3..f3be511 100644 --- a/public/wiki.css +++ b/public/wiki.css @@ -99,3 +99,5 @@ footer nav{display:flex;flex-wrap:wrap;gap:16px} } .wiring-figure{margin:28px 0}.wiring-figure img{display:block;width:100%;height:auto;background:#fff;border-radius:10px}.wiring-figure figcaption{font-size:13px;color:var(--muted);padding:12px 0}details code{overflow-wrap:anywhere} +.breadcrumbs{font-size:13px;color:var(--muted);margin:0 0 18px;display:flex;flex-wrap:wrap;gap:6px;align-items:center}.breadcrumbs a{color:inherit}.breadcrumbs [aria-current]{color:var(--ink)}th[scope=row]{text-align:left;white-space:nowrap;vertical-align:top;width:1%} +.pinout-figure img{width:auto;max-width:min(100%,760px);margin:0 auto;padding:12px} From 159451c87afc94ad02e603d971bbda9b3367c80b Mon Sep 17 00:00:00 2001 From: Your Pal Kal Date: Fri, 2 Oct 2026 18:25:14 -0400 Subject: [PATCH 3/3] Generate an SEO page for every board and module pinout Updated the buildWiki function to include new parameters for page options and improved sitemap generation with image support. Refactored code for better organization and added additional imports for handling assets. --- scripts/build-wiki.mjs | 83 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/scripts/build-wiki.mjs b/scripts/build-wiki.mjs index 6c4c6af..1031dc1 100644 --- a/scripts/build-wiki.mjs +++ b/scripts/build-wiki.mjs @@ -1,9 +1,9 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import {articles, makerIntros} from '../data/wiki.mjs'; -import {hardwareLinks,documentationSummary,availabilityLabel} from '../src/hardware-links.mjs'; +import {hardwareLinks,documentationSummary,availabilityLabel,safeSourceURL} from '../src/hardware-links.mjs'; const appVersion=JSON.parse(await fs.readFile(new URL('../package.json',import.meta.url),'utf8')).version; -import {coverageLabels} from '../src/pin-coverage.mjs'; +import {coverageLabels,orderedAssets} from '../src/pin-coverage.mjs'; import {wiringHTML,wiringMarkdown} from './wiring-wiki.mjs'; const site='https://valleytech-black-wire-guide.pages.dev'; @@ -15,15 +15,18 @@ const list=(items,ordered=false)=>{const tag=ordered?'ol':'ul';return `<${tag}>$ export async function buildWiki(out,base='/') { const catalog=JSON.parse(await fs.readFile('public/catalog.json','utf8')); - const paths=[]; - const page=async(route,name,description,body,type='WebPage')=>{ + const paths=[],sitemapImages=new Map(); + // opts: pageTitle (full ), noindex (kept out of the sitemap), image (social card override), + // images (image-sitemap entries), schema (replaces the default JSON-LD), section (kicker override). + const page=async(route,name,description,body,type='WebPage',opts={})=>{ const url=site+base+route; - const schema={'@context':'https://schema.org','@type':type,name,description,url,isPartOf:{'@type':'WebSite',name:title,url:site+'/'}}; + const schema=opts.schema||{'@context':'https://schema.org','@type':type,name,description,url,isPartOf:{'@type':'WebSite',name:title,url:site+'/'}}; + const pageTitle=opts.pageTitle||`${name} | Black Wire`,ogImage=opts.image||`${site}/brand/social-card.png`; // Kicker label and active navigation item come from the route. - const section=route.startsWith('wiki/wiring/')?'Wiring':route.startsWith('wiki/boards/')?'Boards':route.startsWith('wiki/modules/')?'Modules':route==='wiki/'?'Wiki':'Guide'; + const section=opts.section||(route.startsWith('wiki/wiring/')?'Wiring':route.startsWith('wiki/boards/')?'Boards':route.startsWith('wiki/modules/')?'Modules':route==='wiki/'?'Wiki':'Guide'); const nav=[['wiki/','Wiki',['Wiki','Guide']],['wiki/wiring/','Wiring',['Wiring']],['wiki/boards/','Boards',['Boards']],['wiki/modules/','Modules',['Modules']]].map(([href,label,sections])=>`<a href="${base+href}"${sections.includes(section)?' aria-current="page"':''}>${label}</a>`).join(''); - const html=`<!doctype html><html lang="en" data-theme="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(name)} | Black Wire
Black WireMaker's Technical Reference
${body}
${escape(title)}App ${escape(appVersion)} · Collection ${escape(catalog.editionInfo.snapshot)} · Original guide text by Kal / Valleytech Solutions. Source artwork retains its own rights.
`; - const dest=path.join(out,route,'index.html');await fs.mkdir(path.dirname(dest),{recursive:true});await fs.writeFile(dest,html);paths.push(url); + const html=`${escape(pageTitle)}
Black WireMaker's Technical Reference
${body}
${escape(title)}App ${escape(appVersion)} · Collection ${escape(catalog.editionInfo.snapshot)} · Original guide text by Kal / Valleytech Solutions. Source artwork retains its own rights.
`; + const dest=path.join(out,route,'index.html');await fs.mkdir(path.dirname(dest),{recursive:true});await fs.writeFile(dest,html);if(!opts.noindex){paths.push(url);if(opts.images?.length)sitemapImages.set(url,opts.images);} }; const cards=items=>`
${items.map(({url,title,description})=>`

${escape(title)}

${escape(description)}

`).join('')}
`; await page('wiki/','The maker reference wiki','Find a reference, understand its limits and get back to building. Guides and catalog directories you can read without opening the app.',cards([{url:base+'wiki/wiring/',title:'Wiring & protocols',description:'Connection diagrams for Ethernet, telephone jacks, serial buses, debugging and RFID/NFC.'},...articles.map(a=>({...a,url:base+'wiki/'+a.slug+'/'}))])+`

Browse the catalog

`+cards([{url:base+'wiki/boards/',title:'Board & device directory',description:`${catalog.boards.length.toLocaleString('en-US')} board and device listings, grouped by family, with documentation status.`},{url:base+'wiki/modules/',title:'Displays, sensors & modules',description:`${catalog.makerParts.length} maker records, grouped by purpose, including power and charging.`}])+`

These counts include family records and documentation gaps. They are not a count of fully verified pinouts. ${link(base+'wiki/coverage-and-sources/','Read the coverage policy')}.

`); @@ -33,19 +36,75 @@ export async function buildWiki(out,base='/') { const content=a.sections.map(s=>`

${escape(s.heading)}

${s.text?`

${escape(s.text)}

`:''}${s.items?list(s.items):''}${s.steps?list(s.steps,true):''}${(s.links||[]).map(([label,url])=>``).join('')}
`).join(''); await page('wiki/'+a.slug+'/',a.title,a.description,`
${content}
`); } - const recordTable=(records,kind)=>`

${[...records].sort((a,b)=>(a.brand+' '+a.name).localeCompare(b.brand+' '+b.name)).map(r=>``).join('')}
Model / manufacturerFamily / interfaceReference coverage
${link(base+(kind==='maker'?'?tab=makers&part=':'?board=')+encodeURIComponent(r.id),r.name)}${escape(r.brand)} · ${escape(r.revision||'Revision not identified')}${hardwareLinks(r).length?`
Datasheets & original links${hardwareLinks(r).map(s=>`${link(s.url,s.label)}${s.scope==='component'?' (chip/component)':''}${availabilityLabel(s.availability)?' · '+escape(availabilityLabel(s.availability)):''}`).join('')}
`:''}
${escape(kind==='maker'?(r.interfaces||[]).join(' / ')||r.technology||'See record':r.processor||r.family)}${kind==='maker'&&r.resolution?`${escape(r.resolution)}`:''}${escape(coverageLabels[r.pinoutCoverage?.status]||'Documentation needed')}${r.assets?.length||0} reference files · ${(r.pinoutCoverage?.purposeRows)||0} documented purpose rowsBoard datasheet: ${documentationSummary(r).boardDatasheet?'recorded':'not yet recorded'} · Original website: ${documentationSummary(r).website?'linked':'not yet identified'} · Visual: ${documentationSummary(r).visualCount?'available':'still needed'}
`; + const recordTable=(records,kind)=>`

${[...records].sort((a,b)=>(a.brand+' '+a.name).localeCompare(b.brand+' '+b.name)).map(r=>``).join('')}
Model / manufacturerFamily / interfaceReference coverage
${link(refURL(r),r.name)}${escape(r.brand)} · ${escape(r.revision||'Revision not identified')}${hardwareLinks(r).length?`
Datasheets & original links${hardwareLinks(r).map(s=>`${link(s.url,s.label)}${s.scope==='component'?' (chip/component)':''}${availabilityLabel(s.availability)?' · '+escape(availabilityLabel(s.availability)):''}`).join('')}
`:''}
${escape(kind==='maker'?(r.interfaces||[]).join(' / ')||r.technology||'See record':r.processor||r.family)}${kind==='maker'&&r.resolution?`${escape(r.resolution)}`:''}${escape(coverageLabels[r.pinoutCoverage?.status]||'Documentation needed')}${r.assets?.length||0} reference files · ${(r.pinoutCoverage?.purposeRows)||0} documented purpose rowsBoard datasheet: ${documentationSummary(r).boardDatasheet?'recorded':'not yet recorded'} · Original website: ${documentationSummary(r).website?'linked':'not yet identified'} · Visual: ${documentationSummary(r).visualCount?'available':'still needed'}
`; const boardIntros={CYD:'ESP32 display boards commonly grouped under CYD. Connector and display revisions differ; match the complete model code.',ESP32:'ESP32 and ESP8266 development boards. Match the module, board revision and USB or header layout.',RP2040:'RP2040 development boards and products. A shared microcontroller does not mean a shared header.',RP2350:'RP2350 development boards and products. Compare the exact board and revision.',Teensy:'Teensy boards and reference documents. Use the exact version, including any suffix.','Raspberry Pi SBC':'Raspberry Pi computers. Physical header numbering and GPIO signal numbers are different.','Other SBC':'Single-board computers from additional manufacturers. Connector assignments are model-specific.',Expansion:'I/O carriers, GPIO expanders, adapter and radio boards. Read host compatibility and connector scope.','GPIO device':'Devices exposing pins or GPIO. Match connector orientation and model revision.',FPGA:'FPGA development boards. Match the device, connector and I/O bank voltage before connecting external hardware.',Other:'Additional development boards. Search within this directory by model or manufacturer.','Source collection':'Manufacturer source records awaiting more detailed identification or review. A source record does not mean a complete pinout.','Chip reference':'Chip-package references. These locate contacts on the IC package and must not be treated as development-board header maps.'}; + // One static, crawlable page per board and maker record so searches such as + // " pinout" can land on the exact reference instead of the app shell. + const refSlug=new Map(),usedSlugs=new Set(),refTitles=new Set(); + const allRecords=[...catalog.boards.map(r=>['board',r]),...catalog.makerParts.map(r=>['maker',r])]; + for(const [,r] of allRecords){ + const candidates=[slug(r.name),slug(`${r.brand} ${r.name}`),slug(`${r.name} ${r.revision&&!/not identified/i.test(r.revision)?r.revision:''}`),slug(`${r.name} ${r.id}`)].filter(Boolean); + let s=candidates.find(c=>!usedSlugs.has(c));for(let i=2;!s;i++)if(!usedSlugs.has(`${candidates[0]}-${i}`))s=`${candidates[0]}-${i}`; + usedSlugs.add(s);refSlug.set(r.id,s); + } + const refURL=r=>base+'pinout/'+refSlug.get(r.id)+'/'; + const appURL=(r,kind)=>base+(kind==='maker'?'?tab=makers&part=':'?board=')+encodeURIComponent(r.id); + const groupOf=(r,kind)=>kind==='maker'?r.category:r.family; + const rootOf=kind=>kind==='maker'?'modules':'boards'; + const chipOf=(r,kind)=>kind==='maker'?(r.controllers||[]).join(', '):r.processor||''; + // Index a record only when it carries something a visitor can use; empty + // identification tasks stay reachable but are kept out of search results. + const indexable=(r,kind)=>(r.assets?.length||0)>0||documentationSummary(r).boardDatasheet||(kind==='maker'&&(r.pinLabels||[]).length>=3); + const clip=(s,n)=>s.length<=n?s:s.slice(0,n-1).replace(/\s+\S*$/,'')+'…'; + const dateOf=String(catalog.editionInfo?.snapshot||'').replace(/\./g,'-'); + const absolute=rel=>site+base+rel.replace(/^\//,''); + const refImages=r=>orderedAssets((r.assets||[]).filter(a=>a.thumb||/\.(png|jpe?g|webp|svg)$/i.test(a.file||''))); + const recordsByGroup=new Map(); + for(const [kind,r] of allRecords){const k=kind+'|'+groupOf(r,kind);if(!recordsByGroup.has(k))recordsByGroup.set(k,[]);recordsByGroup.get(k).push(r);} + let referencePages=0,indexedReferencePages=0; + for(const [kind,r] of allRecords){ + const group=groupOf(r,kind),chip=chipOf(r,kind),images=refImages(r),links=hardwareLinks(r),cov=r.pinoutCoverage||{}; + const isIndexed=indexable(r,kind); + const noun=kind==='maker'?'Pinout & Wiring Reference':'Pinout & GPIO Reference'; + let pageTitle=`${r.name} ${noun} | Black Wire`; + if(refTitles.has(pageTitle))pageTitle=`${r.name} (${r.brand}) ${noun} | Black Wire`; + if(refTitles.has(pageTitle))pageTitle=`${r.name} (${r.brand}, ${r.id}) ${noun} | Black Wire`; + refTitles.add(pageTitle); + const what=kind==='maker'?[chip&&`${chip}`,(r.interfaces||[]).length&&(r.interfaces||[]).join('/')+' interface',r.resolution].filter(Boolean).join(', '):[chip&&`${chip}`,group&&group!==chip&&`${group} board`].filter(Boolean).join(' '); + const by=r.brand&&!r.name.toLowerCase().startsWith(r.brand.toLowerCase())?` by ${r.brand}`:''; + const offer=[images.length&&`${images.length===1?'pinout image':images.length+' pinout images'}`,kind==='maker'&&(r.pinLabels||[]).length?'pin labels':'pin functions',links.length&&'datasheets & source links'].filter(Boolean); + const lead=`${r.name} pinout${by}${what?` (${what})`:''}`; + const description=lead.length>120?clip(lead,157):clip(`${lead}: ${offer.join(', ')}.`,158); + const facts=[['Manufacturer',r.brand],['Model',r.name],[kind==='maker'?'Controller / chip':'Processor / chip',chip],[kind==='maker'?'Category':'Board family',group],['Revision',r.revision],['Interfaces',(r.interfaces||[]).join(', ')],['Display',[r.technology,r.diagonalInches?r.diagonalInches+'"':'',r.resolution].filter(Boolean).join(' · ')],['Also known as',(r.aliases||[]).join(', ')],['Pin labels',(r.pinLabels||[]).join(', ')],['Reference coverage',(coverageLabels[cov.status]||'Documentation needed')+(r.coverage?` — ${r.coverage}`:'')]].filter(([,v])=>v&&String(v).trim()); + const crumbs=[['Wiki',base+'wiki/'],[kind==='maker'?'Modules':'Boards',base+`wiki/${rootOf(kind)}/`],[group,base+`wiki/${rootOf(kind)}/${slug(group)}/`],[r.name,refURL(r)]]; + const figures=images.slice(0,10).map(a=>{const alt=`${r.name} ${a.type||'reference'}${a.label&&!a.label.startsWith(r.name)?` – ${a.label}`:''}`;const src=base+'library/'+(a.thumb||a.file);return `
${escape(alt)}
${escape(a.label||a.type||'Reference image')}${a.review?` · ${escape(a.review)}`:''}${(a.sources||[]).filter(safeSourceURL).slice(0,1).map(u=>` · ${link(u,'Original source')} ↗`).join('')}${a.rights?`
${escape(a.rights)}`:''}
`;}).join(''); + const siblings=(recordsByGroup.get(kind+'|'+group)||[]).filter(o=>o.id!==r.id&&indexable(o,kind)).sort((a,b)=>(b.assets?.length||0)-(a.assets?.length||0)||a.name.localeCompare(b.name)).slice(0,12); + const body=``+ + ``+ + `

${escape(r.name)} specifications

${facts.map(([k,v])=>``).join('')}
${escape(k)}${escape(v)}
`+ + (figures?`

${escape(r.name)} pinout diagram${images.length===1?'':'s'}

${figures}${images.length>10?`

${images.length-10} more reference images are available ${link(appURL(r,kind),'in the guide')}.

`:''}`:`

${escape(r.name)} pinout status

A pinout image for this exact model has not been collected yet. ${link(base+'wiki/contributing/','Contribute a source')} or check the datasheets below.

`)+ + (links.length?`

${escape(r.name)} datasheets & original sources

    ${links.map(s=>`
  • ${link(s.url,s.label)}${s.scope==='component'?' (chip/component)':''}${availabilityLabel(s.availability)?` · ${escape(availabilityLabel(s.availability))}`:''}
  • `).join('')}
`:'')+ + `

Before you wire the ${escape(r.name)}

    ${[cov.scope,...(cov.missing||[]),...(r.notes||[]),'Match the exact model, PCB revision and connector orientation before connecting power. Check logic voltage levels against the original datasheet.'].filter(Boolean).filter((v,i,a)=>a.indexOf(v)===i).map(t=>`
  • ${escape(t)}
  • `).join('')}
`+ + (siblings.length?`

More ${escape(group)} pinouts

`+cards(siblings.map(o=>({url:refURL(o),title:o.name+' pinout',description:[o.brand,chipOf(o,kind)].filter(Boolean).join(' · ')||group}))):'')+ + ``; + const imageURLs=images.slice(0,10).map(a=>absolute('library/'+(a.thumb||a.file))); + const schema={'@context':'https://schema.org','@graph':[ + {'@type':'TechArticle','@id':site+refURL(r)+'#article',headline:clip(`${r.name} pinout`,110),name:pageTitle.replace(/ \| Black Wire$/,''),description,url:site+refURL(r),inLanguage:'en',...(dateOf?{dateModified:dateOf}:{}),...(imageURLs.length?{image:imageURLs}:{}),keywords:[`${r.name} pinout`,chip&&`${chip} pinout`,`${r.name} GPIO`,`${r.name} datasheet`,...(r.aliases||[]).slice(0,5)].filter(Boolean).join(', '),about:{'@type':'Product',name:r.name,...(r.brand?{brand:{'@type':'Brand',name:r.brand}}:{}),...(chip?{model:chip}:{})},author:{'@type':'Person',name:'Kal',url:'https://www.youtube.com/@valleytechsolutions'},publisher:{'@type':'Organization',name:'Valleytech Solutions',url:'https://valleytechsolutions.tech',logo:{'@type':'ImageObject',url:site+'/brand/black-wire-red.png'}},isPartOf:{'@type':'WebSite',name:title,url:site+'/'}}, + {'@type':'BreadcrumbList',itemListElement:crumbs.map(([t,u],i)=>({'@type':'ListItem',position:i+1,name:t,item:site+u}))}]}; + await page('pinout/'+refSlug.get(r.id)+'/',`${r.name} pinout`,description,body,'TechArticle',{pageTitle,noindex:!isIndexed,image:imageURLs[0],images:images.slice(0,10).map((a,i)=>({loc:imageURLs[i],title:`${r.name} ${a.type||'pinout'}`})),schema,section:kind==='maker'?'Modules':'Boards'}); + referencePages++;if(isIndexed)indexedReferencePages++; + } const makerCategories=new Set(catalog.makerParts.map(r=>r.category)),sharedGroups=new Set(catalog.boards.map(r=>r.family).filter(g=>makerCategories.has(g))); for(const [kind,records,field,root,label,intros,noun] of [['board',catalog.boards,'family','boards','Board & device directory',boardIntros,'board'],['maker',catalog.makerParts,'category','modules','Displays, sensors & modules',makerIntros,'module']]){ const groups=[...new Set(records.map(r=>r[field]))].sort(); await page(`wiki/${root}/`,label,`Browse ${records.length.toLocaleString('en-US')} catalog listings with source coverage and direct links to their references.`,cards(groups.map(g=>({url:base+`wiki/${root}/${slug(g)}/`,title:g,description:`${records.filter(r=>r[field]===g).length} records. ${intros[g]||'Compare the exact model and revision.'}`})))+`

A listing may be an exact model, a family, or an identification task. Coverage is shown for each record; no all-device completeness is implied.

`,'CollectionPage'); - for(const g of groups)await page(`wiki/${root}/${slug(g)}/`,g+(sharedGroups.has(g)?` ${noun}`:'')+' references',intros[g]||`Find ${g} references in the Black Wire guide.`,recordTable(records.filter(r=>r[field]===g),kind)+`

Physical source availability does not establish all-pin completeness or electrical compatibility. ${link(base+'wiki/coverage-and-sources/','Read the coverage labels')}.

`,'CollectionPage'); + for(const g of groups)await page(`wiki/${root}/${slug(g)}/`,g+(sharedGroups.has(g)?` ${noun}`:'')+' pinouts & references',intros[g]||`Find ${g} pinouts and references in the Black Wire guide.`,recordTable(records.filter(r=>r[field]===g),kind)+`

Physical source availability does not establish all-pin completeness or electrical compatibility. ${link(base+'wiki/coverage-and-sources/','Read the coverage labels')}.

`,'CollectionPage'); } - await fs.writeFile(path.join(out,'sitemap.xml'),`${[site+base,...paths].map(url=>`${escape(url)}`).join('')}`); + await fs.writeFile(path.join(out,'sitemap.xml'),`${[site+base,...paths].map(url=>`${escape(url)}${dateOf?`${dateOf}`:''}${(sitemapImages.get(url)||[]).map(i=>`${escape(i.loc)}`).join('')}`).join('')}`); await fs.writeFile(path.join(out,'robots.txt'),`User-agent: *\nAllow: /\nSitemap: ${site}${base}sitemap.xml\n`); const index=path.join(out,'index.html'); if(base!=='/')await fs.writeFile(index,(await fs.readFile(index,'utf8')).replaceAll('href="/wiki/',`href="${base}wiki/`).replaceAll('src="/theme-init.js"',`src="${base}theme-init.js"`).replaceAll(site+'/',site+base)); - console.log(`Built ${paths.length} static wiki and directory pages with sitemap.`); + console.log(`Built ${paths.length} indexed static pages (${referencePages} pinout pages, ${indexedReferencePages} indexed) with sitemap.`); return paths; }