`;
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')}.
`;
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(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=``+
+ `
${link(appURL(r,kind),`Open the interactive ${r.name} pinout in Black Wire →`)}
${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.
${[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=>`
${link(base+`wiki/${rootOf(kind)}/${slug(group)}/`,`All ${group} references`)} · ${link(base,'Open the reference guide →')}
`;
+ 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;
}
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){