@beforesemicolon/builder 1.8.23 → 1.8.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +26 -0
  2. package/dist/cjs/docs/markdown-layout/marked-extension.js +1 -1
  3. package/dist/cjs/docs/markdown-layout/types.js +1 -1
  4. package/dist/cjs/docs/run.js +2 -2
  5. package/dist/cjs/docs/templates/fading-citrus/README.md +56 -31
  6. package/dist/cjs/docs/templates/fading-citrus/assets/favicon/site.webmanifest +2 -2
  7. package/dist/cjs/docs/templates/fading-citrus/layouts/_footer.js +53 -60
  8. package/dist/cjs/docs/templates/fading-citrus/layouts/_header.js +65 -20
  9. package/dist/cjs/docs/templates/fading-citrus/layouts/_layout-utils.js +20 -0
  10. package/dist/cjs/docs/templates/fading-citrus/layouts/document.js +7 -4
  11. package/dist/cjs/docs/templates/fading-citrus/layouts/landing-hero.js +11 -3
  12. package/dist/cjs/docs/templates/fading-citrus/template.config.js +26 -4
  13. package/dist/cjs/docs/types.js +1 -1
  14. package/dist/esm/docs/markdown-layout/marked-extension.js +1 -1
  15. package/dist/esm/docs/run.js +4 -4
  16. package/dist/esm/docs/templates/fading-citrus/README.md +56 -31
  17. package/dist/esm/docs/templates/fading-citrus/assets/favicon/site.webmanifest +2 -2
  18. package/dist/esm/docs/templates/fading-citrus/layouts/_footer.js +53 -60
  19. package/dist/esm/docs/templates/fading-citrus/layouts/_header.js +65 -20
  20. package/dist/esm/docs/templates/fading-citrus/layouts/_layout-utils.js +20 -0
  21. package/dist/esm/docs/templates/fading-citrus/layouts/document.js +7 -4
  22. package/dist/esm/docs/templates/fading-citrus/layouts/landing-hero.js +11 -3
  23. package/dist/esm/docs/templates/fading-citrus/template.config.js +26 -4
  24. package/dist/types/docs/markdown-layout/marked-extension.d.ts +2 -1
  25. package/dist/types/docs/markdown-layout/types.d.ts +1 -0
  26. package/dist/types/docs/types.d.ts +32 -0
  27. package/package.json +1 -1
  28. package/dist/cjs/docs/templates/fading-citrus/assets/client-server.svg +0 -4
  29. package/dist/cjs/docs/templates/fading-citrus/assets/fast.svg +0 -4
  30. package/dist/cjs/docs/templates/fading-citrus/assets/independent.svg +0 -23
  31. package/dist/cjs/docs/templates/fading-citrus/assets/markup-banner.jpg +0 -0
  32. package/dist/cjs/docs/templates/fading-citrus/assets/markup-essentials-training.jpg +0 -0
  33. package/dist/cjs/docs/templates/fading-citrus/assets/markup-favicon.jpg +0 -0
  34. package/dist/cjs/docs/templates/fading-citrus/assets/markup-favicon.png +0 -0
  35. package/dist/cjs/docs/templates/fading-citrus/assets/markup-logo-name.svg +0 -10
  36. package/dist/cjs/docs/templates/fading-citrus/assets/markup-logo-name@2x.png +0 -0
  37. package/dist/cjs/docs/templates/fading-citrus/assets/markup-logo.svg +0 -13
  38. package/dist/cjs/docs/templates/fading-citrus/assets/markup-logo@2x.png +0 -0
  39. package/dist/cjs/docs/templates/fading-citrus/assets/reactive.svg +0 -3
  40. package/dist/cjs/docs/templates/fading-citrus/assets/simple.svg +0 -8
  41. package/dist/cjs/docs/templates/fading-citrus/assets/small.svg +0 -8
  42. package/dist/esm/docs/templates/fading-citrus/assets/client-server.svg +0 -4
  43. package/dist/esm/docs/templates/fading-citrus/assets/fast.svg +0 -4
  44. package/dist/esm/docs/templates/fading-citrus/assets/independent.svg +0 -23
  45. package/dist/esm/docs/templates/fading-citrus/assets/markup-banner.jpg +0 -0
  46. package/dist/esm/docs/templates/fading-citrus/assets/markup-essentials-training.jpg +0 -0
  47. package/dist/esm/docs/templates/fading-citrus/assets/markup-favicon.jpg +0 -0
  48. package/dist/esm/docs/templates/fading-citrus/assets/markup-favicon.png +0 -0
  49. package/dist/esm/docs/templates/fading-citrus/assets/markup-logo-name.svg +0 -10
  50. package/dist/esm/docs/templates/fading-citrus/assets/markup-logo-name@2x.png +0 -0
  51. package/dist/esm/docs/templates/fading-citrus/assets/markup-logo.svg +0 -13
  52. package/dist/esm/docs/templates/fading-citrus/assets/markup-logo@2x.png +0 -0
  53. package/dist/esm/docs/templates/fading-citrus/assets/reactive.svg +0 -3
  54. package/dist/esm/docs/templates/fading-citrus/assets/simple.svg +0 -8
  55. package/dist/esm/docs/templates/fading-citrus/assets/small.svg +0 -8
package/README.md CHANGED
@@ -304,6 +304,7 @@ A template config exports an object:
304
304
  ```js
305
305
  export default {
306
306
  meta: {},
307
+ site: {},
307
308
  markedOptions: {},
308
309
  markdownLayouts: {},
309
310
  headScripts: {},
@@ -322,6 +323,7 @@ Merge behavior:
322
323
  - `markdownLayouts` are shallow-merged by layout name.
323
324
  - `headScripts` and `scripts` are shallow-merged by script name.
324
325
  - `meta` is shallow-merged by metadata field.
326
+ - `site` is shallow-merged by site field.
325
327
  - `theme.light` and `theme.dark` are shallow-merged by CSS variable name.
326
328
  - Other top-level config values use the docs source config value when provided.
327
329
 
@@ -337,6 +339,20 @@ export default {
337
339
  description: 'Documentation for Example.',
338
340
  image: '/assets/site-image.jpg',
339
341
  },
342
+ site: {
343
+ name: 'Example',
344
+ packageName: '@example/docs',
345
+ repositoryUrl: 'https://github.com/example/docs',
346
+ repositoryLabel: 'Example GitHub repository',
347
+ docsEditUrl: 'https://github.com/example/docs/tree/main/docs',
348
+ footerDescription: 'Documentation for Example.',
349
+ footerGroups: [
350
+ {
351
+ title: 'Learning Resources',
352
+ links: [{ label: 'Documentation', href: '/documentation' }],
353
+ },
354
+ ],
355
+ },
340
356
  markdownLayouts: {
341
357
  'pricing-cards': pricingCards,
342
358
  },
@@ -351,6 +367,16 @@ export default {
351
367
  }
352
368
  ```
353
369
 
370
+ Common `site` fields:
371
+
372
+ - `name`: display name used by shared layouts.
373
+ - `packageName`: package name used by templates that render install commands.
374
+ - `repositoryUrl` and `repositoryLabel`: repository link and accessible label.
375
+ - `docsEditUrl`: base URL for edit links, usually a repository `docs` folder URL.
376
+ - `navLinks` and `actionLinks`: landing header links.
377
+ - `footerDescription`, `footerGroups`, `socialLinks`, and `copyright`: footer content.
378
+ - `landingHeroVersionHref` and `landingHeroSecondaryLabel`: optional landing hero overrides.
379
+
354
380
  ## Markdown Layout Syntax
355
381
 
356
382
  The docs renderer extends `marked` with a custom block syntax:
@@ -1 +1 @@
1
- "use strict";var d=Object.defineProperty;var M=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var L=Object.prototype.hasOwnProperty;var y=(o,r)=>d(o,"name",{value:r,configurable:!0});var m=(o,r)=>{for(var e in r)d(o,e,{get:r[e],enumerable:!0})},l=(o,r,e,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let n of w(r))!L.call(o,n)&&n!==e&&d(o,n,{get:()=>r[n],enumerable:!(t=M(r,n))||t.enumerable});return o};var c=o=>l(d({},"__esModule",{value:!0}),o);var x={};m(x,{createMarkdownLayoutExtension:()=>p});module.exports=c(x);var k=require("./parser.js"),u=require("./renderer.js");const p=y(({marked:o,handlers:r={},renderMarkdown:e})=>({extensions:[{name:"markdownLayout",level:"block",start(t){return t.match(/^::: layout /m)?.index},tokenizer(t){const n=(0,k.readLayoutBlock)(t);if(n)return{...(0,k.parseLayoutBlock)(n),type:"markdownLayout",raw:n}},renderer(t){const n=t,s={marked:o,renderMarkdown(a){return e?e(a):o.parse(a)},renderDefault(a){return(0,u.renderDefaultMarkdownLayout)(a,s)},renderParts(a){return a.items.map(i=>({...i,html:s.renderMarkdown(i.body)}))}};return(0,u.renderMarkdownLayout)(n,s,r)}}]}),"createMarkdownLayoutExtension");0&&(module.exports={createMarkdownLayoutExtension});
1
+ "use strict";var k=Object.defineProperty;var M=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var m=Object.prototype.hasOwnProperty;var w=(o,r)=>k(o,"name",{value:r,configurable:!0});var l=(o,r)=>{for(var t in r)k(o,t,{get:r[t],enumerable:!0})},c=(o,r,t,d)=>{if(r&&typeof r=="object"||typeof r=="function")for(let n of L(r))!m.call(o,n)&&n!==t&&k(o,n,{get:()=>r[n],enumerable:!(d=M(r,n))||d.enumerable});return o};var p=o=>c(k({},"__esModule",{value:!0}),o);var f={};l(f,{createMarkdownLayoutExtension:()=>x});module.exports=p(f);var u=require("./parser.js"),s=require("./renderer.js");const x=w(({marked:o,handlers:r={},renderMarkdown:t,templateConfig:d})=>({extensions:[{name:"markdownLayout",level:"block",start(n){return n.match(/^::: layout /m)?.index},tokenizer(n){const a=(0,u.readLayoutBlock)(n);if(a)return{...(0,u.parseLayoutBlock)(a),type:"markdownLayout",raw:a}},renderer(n){const a=n,i={marked:o,templateConfig:d,renderMarkdown(e){return t?t(e):o.parse(e)},renderDefault(e){return(0,s.renderDefaultMarkdownLayout)(e,i)},renderParts(e){return e.items.map(y=>({...y,html:i.renderMarkdown(y.body)}))}};return(0,s.renderMarkdownLayout)(a,i,r)}}]}),"createMarkdownLayoutExtension");0&&(module.exports={createMarkdownLayoutExtension});
@@ -1 +1 @@
1
- "use strict";var n=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var u=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var i=(t,o,a,r)=>{if(o&&typeof o=="object"||typeof o=="function")for(let e of u(o))!y.call(t,e)&&e!==a&&n(t,e,{get:()=>o[e],enumerable:!(r=d(o,e))||r.enumerable});return t};var k=t=>i(n({},"__esModule",{value:!0}),t);var p={};module.exports=k(p);
1
+ "use strict";var r=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var i=Object.getOwnPropertyNames;var u=Object.prototype.hasOwnProperty;var y=(t,o,a,n)=>{if(o&&typeof o=="object"||typeof o=="function")for(let e of i(o))!u.call(t,e)&&e!==a&&r(t,e,{get:()=>o[e],enumerable:!(n=d(o,e))||n.enumerable});return t};var k=t=>y(r({},"__esModule",{value:!0}),t);var p={};module.exports=k(p);
@@ -1,4 +1,4 @@
1
- "use strict";var ge=Object.create;var C=Object.defineProperty;var ye=Object.getOwnPropertyDescriptor;var we=Object.getOwnPropertyNames;var ve=Object.getPrototypeOf,Se=Object.prototype.hasOwnProperty;var m=(e,t)=>C(e,"name",{value:t,configurable:!0});var De=(e,t)=>{for(var r in t)C(e,r,{get:t[r],enumerable:!0})},Q=(e,t,r,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of we(t))!Se.call(e,n)&&n!==r&&C(e,n,{get:()=>t[n],enumerable:!(a=ye(t,n))||a.enumerable});return e};var v=(e,t,r)=>(r=e!=null?ge(ve(e)):{},Q(t||!e||!e.__esModule?C(r,"default",{value:e,enumerable:!0}):r,e)),be=e=>Q(C({},"__esModule",{value:!0}),e);var Ue={};De(Ue,{buildDocs:()=>Ne});module.exports=be(Ue);const ze={};var re=require("marked"),c=require("fs/promises"),o=v(require("path"),1),oe=v(require("front-matter"),1),ae=v(require("isomorphic-dompurify"),1),ne=require("marked-highlight"),_=v(require("highlight.js"),1),ie=v(require("./templates/default.template.js"),1),le=v(require("./renderer/index.js"),1),ce=require("./markdown-layout/index.js"),me=v(require("clean-css"),1),ue=v(require("html-minifier"),1),X=v(require("@putout/minify"),1),P=require("url"),f=v(require("fs"),1);let V,A="";const Y=new me.default({inline:["none"]}),R=new Map;R.set("default",ie.default);let W;const Z=m((e,t)=>t===!1?!1:t===void 0?e:e===!1||e===void 0?t:{...e,...t},"mergeThemeMode"),xe=m((e,t)=>{if(!(!e&&!t))return{light:Z(e?.light,t?.light),dark:Z(e?.dark,t?.dark)}},"mergeTheme"),$e=m((e,t)=>({...e,...t,markdownLayouts:{...e.markdownLayouts,...t.markdownLayouts},headScripts:{...e.headScripts,...t.headScripts},scripts:{...e.scripts,...t.scripts},theme:xe(e.theme,t.theme),meta:{...e.meta,...t.meta}}),"mergeTemplateConfig"),Pe=m((e,t)=>{if(typeof e=="function")return!0;const{match:r}=e;return r?typeof r=="string"?t.includes(r):Array.isArray(r)?r.some(a=>t.includes(a)):r instanceof RegExp?r.test(t):r(t):!0},"scriptMatches"),ee=m((e,t)=>e?Object.values(e).filter(r=>r!==!1).filter(r=>Pe(r,t)).map(r=>typeof r=="function"?r():r.render()):[],"renderPageScripts"),G=m((e,t)=>{const r=Object.entries(t).filter(([a])=>a.startsWith("--")).map(([a,n])=>` ${a}: ${n};`).join(`
1
+ "use strict";var ge=Object.create;var C=Object.defineProperty;var ye=Object.getOwnPropertyDescriptor;var we=Object.getOwnPropertyNames;var ve=Object.getPrototypeOf,Se=Object.prototype.hasOwnProperty;var m=(e,t)=>C(e,"name",{value:t,configurable:!0});var De=(e,t)=>{for(var r in t)C(e,r,{get:t[r],enumerable:!0})},Q=(e,t,r,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of we(t))!Se.call(e,n)&&n!==r&&C(e,n,{get:()=>t[n],enumerable:!(a=ye(t,n))||a.enumerable});return e};var v=(e,t,r)=>(r=e!=null?ge(ve(e)):{},Q(t||!e||!e.__esModule?C(r,"default",{value:e,enumerable:!0}):r,e)),be=e=>Q(C({},"__esModule",{value:!0}),e);var Ue={};De(Ue,{buildDocs:()=>Ne});module.exports=be(Ue);const ze={};var re=require("marked"),c=require("fs/promises"),o=v(require("path"),1),oe=v(require("front-matter"),1),ae=v(require("isomorphic-dompurify"),1),ne=require("marked-highlight"),_=v(require("highlight.js"),1),ie=v(require("./templates/default.template.js"),1),le=v(require("./renderer/index.js"),1),ce=require("./markdown-layout/index.js"),me=v(require("clean-css"),1),ue=v(require("html-minifier"),1),X=v(require("@putout/minify"),1),P=require("url"),f=v(require("fs"),1);let V,A="";const Y=new me.default({inline:["none"]}),R=new Map;R.set("default",ie.default);let W;const Z=m((e,t)=>t===!1?!1:t===void 0?e:e===!1||e===void 0?t:{...e,...t},"mergeThemeMode"),$e=m((e,t)=>{if(!(!e&&!t))return{light:Z(e?.light,t?.light),dark:Z(e?.dark,t?.dark)}},"mergeTheme"),xe=m((e,t)=>({...e,...t,markdownLayouts:{...e.markdownLayouts,...t.markdownLayouts},headScripts:{...e.headScripts,...t.headScripts},scripts:{...e.scripts,...t.scripts},theme:$e(e.theme,t.theme),meta:{...e.meta,...t.meta},site:{...e.site,...t.site}}),"mergeTemplateConfig"),Pe=m((e,t)=>{if(typeof e=="function")return!0;const{match:r}=e;return r?typeof r=="string"?t.includes(r):Array.isArray(r)?r.some(a=>t.includes(a)):r instanceof RegExp?r.test(t):r(t):!0},"scriptMatches"),ee=m((e,t)=>e?Object.values(e).filter(r=>r!==!1).filter(r=>Pe(r,t)).map(r=>typeof r=="function"?r():r.render()):[],"renderPageScripts"),G=m((e,t)=>{const r=Object.entries(t).filter(([a])=>a.startsWith("--")).map(([a,n])=>` ${a}: ${n};`).join(`
2
2
  `);return r?`${e} {
3
3
  ${r}
4
4
  }`:""},"renderThemeBlock"),ke=m(e=>{if(!e)return"";const t=e.light===!1,r=e.dark===!1,a=e.light===!1?void 0:e.light,n=r?void 0:e.dark,u=a||n;return u?[G(":root",u),n&&!t?`@media (prefers-color-scheme: dark) {
@@ -75,4 +75,4 @@ for = "/robots.txt"
75
75
  [headers.values]
76
76
  Content-Type = "text/plain; charset=utf-8"
77
77
  Cache-Control = "public, max-age=0, must-revalidate"
78
- `,"generateNetlifyToml"),$=m(async(e,t,r,a)=>{if(await Ce(e,t))return;const n=r();if(!n){a&&console.warn(a);return}await(0,c.mkdir)(o.default.dirname(t),{recursive:!0}),await(0,c.writeFile)(t,n)},"writeGeneratedFile"),_e=m(async({docsDir:e,docsSiteDir:t,pages:r,siteUrl:a,publicDir:n,generatedFiles:u})=>{u.sitemap&&await $(o.default.resolve(e,"sitemap.xml"),o.default.resolve(t,"sitemap.xml"),()=>Le(r,a),"Skipping generated sitemap.xml because buildDocs({ siteUrl }) is not set."),u.robots&&await $(o.default.resolve(e,"robots.txt"),o.default.resolve(t,"robots.txt"),()=>Ee(a)),u.llms&&await $(o.default.resolve(e,"llms.txt"),o.default.resolve(t,"llms.txt"),()=>Oe(r)),u.llmsFull&&await $(o.default.resolve(e,"llms-full.txt"),o.default.resolve(t,"llms-full.txt"),()=>Ae(r,a)),u.netlify&&(await $(o.default.resolve(e,"_redirects"),o.default.resolve(t,"_redirects"),()=>Re(r,u)),await $(o.default.resolve(e,"netlify.toml"),o.default.resolve(t,"netlify.toml"),()=>We(n&&o.default.basename(n))))},"writeGeneratedDocsFiles"),se=m((e={},t,r)=>{const a=new re.Marked((0,ne.markedHighlight)({langPrefix:"hljs language-",highlight(n,u){const h=_.default.getLanguage(u)?u:"plaintext";return _.default.highlight(n,{language:h}).value}}));return a.use({renderer:le.default}),a.use((0,ce.createMarkdownLayoutExtension)({marked:a,handlers:e.markdownLayouts,renderMarkdown:r})),e.markedOptions&&a.use(e.markedOptions),t&&a.use(t),a},"createBaseMarked"),Be=m((e={},t,r)=>{const a=se(e,t),n=m(h=>a.parse(h),"renderMarkdown"),u=se(e,t,n);return u.use({hooks:{postprocess(h){const{layout:w="default",...k}=W.attributes,T=ee(e.headScripts,h),M=ee(e.scripts,h);h=ae.default.sanitize(h);const b=[...h.matchAll(/<h([0-6])\sid="[^"]+".*?>.*?<a\s+href="([^"]+)".*?>([\s\S]*?)<\/a><\/h\1>/gm)].map(p=>({path:p[2],label:p[3].replace(/<[^>]+>/g,"").replace(/\s+/g," ").trim(),level:p[1]}));return R.get(w)?.({...k,content:h,siteMap:W.siteMap,tableOfContent:b,projectMeta:V,renderMarkdown:n,headScripts:T,scripts:M,themeStylesheet:e.theme?`/stylesheets/theme.css?v=${A}`:void 0,cacheKey:A,siteUrl:r,templateConfig:e})||h}}}),u},"createMarked"),S=m(async e=>{const t=await(0,c.readdir)(e),r=[];for(const a of t){const n=o.default.extname(a);/^[._]/.test(a)||(n?r.push(o.default.join(e,a)):/(stylesheets|assets|scripts)/.test(a)||r.push(...await S(o.default.join(e,a))))}return r},"traverseDirectory"),Ne=m(async({srcDir:e,publicDir:t,markedOptions:r,template:a,siteUrl:n,generatedFiles:u}={})=>{A=Date.now().toString(36);try{const s=o.default.resolve(process.cwd(),"package.json");f.default.existsSync(s)&&(V=JSON.parse(await(0,c.readFile)(s,"utf-8")))}catch(s){console.warn("Failed to load project package.json metadata",s)}const h=m(()=>{try{return __dirname}catch{return o.default.dirname((0,P.fileURLToPath)(ze.url))}},"getDirname"),w=a?o.default.resolve(h(),"templates",a):null,k=w?o.default.resolve(w,"assets"):null,T=w?o.default.resolve(w,"stylesheets"):null,M=w?o.default.resolve(w,"layouts"):null;let b={};if(w)try{let s=o.default.resolve(w,"template.config.js");if(f.default.existsSync(s)||(s=s.replace(/\.js$/,".ts")),f.default.existsSync(s)){const i=await import((0,P.pathToFileURL)(s).href);b=i.default||i}}catch(s){console.error(`Failed to load template config for ${a}`,s)}const p=e??o.default.resolve(process.cwd(),"docs"),d=t??o.default.resolve(process.cwd(),"website"),pe=Ge(u),D=o.default.resolve(p,"_template"),B=o.default.resolve(D,"assets"),N=o.default.resolve(D,"stylesheets"),U=o.default.resolve(D,"scripts"),z=o.default.resolve(D,"layouts"),q=o.default.resolve(p,"_layouts"),F=o.default.resolve(p,"assets"),H=o.default.resolve(p,"stylesheets"),L=o.default.resolve(p,"scripts");try{let s=o.default.resolve(D,"template.config.js");if(f.default.existsSync(s)||(s=s.replace(/\.js$/,".ts")),f.default.existsSync(s)){const i=await import((0,P.pathToFileURL)(s).href),l=i.default||i;b=$e(b,l)}}catch(s){console.error("Failed to load docs _template config",s)}const fe=Be(b,r,n);if(await(0,c.mkdir)(d,{recursive:!0}),k&&f.default.existsSync(k))try{await(0,c.cp)(k,F.replace(p,d),{recursive:!0})}catch(s){console.warn(s)}if(f.default.existsSync(F))try{await(0,c.cp)(F,F.replace(p,d),{recursive:!0})}catch(s){console.warn(s)}if(f.default.existsSync(B))try{await(0,c.cp)(B,o.default.resolve(d,"assets"),{recursive:!0})}catch(s){console.warn(s)}await Fe(o.default.resolve(d,"assets"));try{const s=new Set;if(T&&f.default.existsSync(T)&&(await S(T)).forEach(l=>s.add(l)),f.default.existsSync(H)&&(await S(H)).forEach(l=>s.add(l)),f.default.existsSync(N)&&(await S(N)).forEach(l=>s.add(l)),s.size){await(0,c.mkdir)(o.default.join(d,"stylesheets"),{recursive:!0});for(const l of s){const g=await(0,c.readFile)(l,"utf-8"),y=Y.minify(g);y.errors.length&&y.errors.forEach(console.error);const j=l.startsWith(D)?l.replace(D,d):l.startsWith(p)?l.replace(p,d):o.default.join(d,"stylesheets",o.default.basename(l));await(0,c.writeFile)(j,y.styles??g)}}const i=ke(b.theme);if(i){const l=Y.minify(i);l.errors.length&&l.errors.forEach(console.error),await(0,c.mkdir)(o.default.join(d,"stylesheets"),{recursive:!0}),await(0,c.writeFile)(o.default.join(d,"stylesheets","theme.css"),l.styles??i)}}catch(s){console.error(s)}if(f.default.existsSync(L))try{const s=await S(L);s.length&&await(0,c.mkdir)(L.replace(p,d),{recursive:!0});for(const i of s)try{const l=await(0,c.readFile)(i,"utf-8");await(0,c.writeFile)(i.replace(p,d),X.minify(l))}catch(l){console.error(l)}}catch(s){console.warn(s)}if(f.default.existsSync(U))try{const s=await S(U);s.length&&await(0,c.mkdir)(o.default.join(d,"scripts"),{recursive:!0});for(const i of s)try{const l=await(0,c.readFile)(i,"utf-8");await(0,c.writeFile)(i.replace(D,d),X.minify(l))}catch(l){console.error(l)}}catch(s){console.warn(s)}try{const s=new Map;M&&f.default.existsSync(M)&&(await S(M)).forEach(i=>{/\.(j|t)s$/.test(i)&&s.set(o.default.basename(i).replace(/\.(j|t)s$/,""),i)}),f.default.existsSync(q)&&(await S(q)).forEach(i=>{/\.(j|t)s$/.test(i)&&s.set(o.default.basename(i).replace(/\.(j|t)s$/,""),i)}),f.default.existsSync(z)&&(await S(z)).forEach(i=>{/\.(j|t)s$/.test(i)&&s.set(o.default.basename(i).replace(/\.(j|t)s$/,""),i)});for(const[i,l]of s.entries()){const g=await import((0,P.pathToFileURL)(l).href);R.set(i,g.default)}}catch(s){console.error("Template import failed",s)}const de=await S(p),I=new Map,J=(await Promise.all(de.filter(s=>s.endsWith(".md")).map(async s=>{const i=await(0,c.readFile)(s,"utf-8");return{filePath:s,...(0,oe.default)(i)}}))).sort((s,i)=>(s.attributes.order??0)-(i.attributes.order??0)).map(({attributes:s,body:i,filePath:l})=>{const g=l.replace(p,"").replace(/\.md/,".html"),y=l.replace(p,d).replace(".md",".html"),j=y.replace(o.default.basename(y),""),E=o.default.basename(g)||"/",he=g.replace(E,"").replace(/\/$/,"");let x=I;he.split("/").filter(Boolean).forEach(O=>{x.has(O)||x.set(O,new Map),x=x.get(O)});const K={...s,path:g};return x.has(E)||x.set(E,K),{attributes:K,fileWebsitePath:y,fileDirWebsitePath:j,body:i,sourcePath:o.default.relative(p,l),siteMap:I}});for(const{attributes:s,body:i,fileDirWebsitePath:l,fileWebsitePath:g,siteMap:y}of J){W={attributes:s,siteMap:y};const j=await fe.parse(i);await(0,c.mkdir)(l,{recursive:!0}),await(0,c.writeFile)(g,ue.default.minify(j,{includeAutoGeneratedTags:!0,removeAttributeQuotes:!0,removeComments:!0,removeRedundantAttributes:!0,removeScriptTypeAttributes:!0,removeStyleLinkTypeAttributes:!0,sortClassName:!0,useShortDoctype:!0,collapseWhitespace:!0,minifyJS:!0,minifyCSS:!0}))}await _e({docsDir:p,docsSiteDir:d,pages:J.map(({attributes:s,body:i,sourcePath:l})=>{const g=je(String(s.path)),y=Me(i);return{sourcePath:l,urlPath:g,title:String(s.title||s.name||"").trim()||"Documentation",name:String(s.name||s.title||"").trim()||"Documentation",description:String(s.description||"").trim()||y.slice(0,156)||"Documentation page.",order:Number(s.order??999),summary:y.slice(0,320)}}),siteUrl:n,publicDir:d,generatedFiles:pe})},"buildDocs");0&&(module.exports={buildDocs});
78
+ `,"generateNetlifyToml"),x=m(async(e,t,r,a)=>{if(await Ce(e,t))return;const n=r();if(!n){a&&console.warn(a);return}await(0,c.mkdir)(o.default.dirname(t),{recursive:!0}),await(0,c.writeFile)(t,n)},"writeGeneratedFile"),_e=m(async({docsDir:e,docsSiteDir:t,pages:r,siteUrl:a,publicDir:n,generatedFiles:u})=>{u.sitemap&&await x(o.default.resolve(e,"sitemap.xml"),o.default.resolve(t,"sitemap.xml"),()=>Le(r,a),"Skipping generated sitemap.xml because buildDocs({ siteUrl }) is not set."),u.robots&&await x(o.default.resolve(e,"robots.txt"),o.default.resolve(t,"robots.txt"),()=>Ee(a)),u.llms&&await x(o.default.resolve(e,"llms.txt"),o.default.resolve(t,"llms.txt"),()=>Oe(r)),u.llmsFull&&await x(o.default.resolve(e,"llms-full.txt"),o.default.resolve(t,"llms-full.txt"),()=>Ae(r,a)),u.netlify&&(await x(o.default.resolve(e,"_redirects"),o.default.resolve(t,"_redirects"),()=>Re(r,u)),await x(o.default.resolve(e,"netlify.toml"),o.default.resolve(t,"netlify.toml"),()=>We(n&&o.default.basename(n))))},"writeGeneratedDocsFiles"),se=m((e={},t,r)=>{const a=new re.Marked((0,ne.markedHighlight)({langPrefix:"hljs language-",highlight(n,u){const h=_.default.getLanguage(u)?u:"plaintext";return _.default.highlight(n,{language:h}).value}}));return a.use({renderer:le.default}),a.use((0,ce.createMarkdownLayoutExtension)({marked:a,handlers:e.markdownLayouts,renderMarkdown:r,templateConfig:e})),e.markedOptions&&a.use(e.markedOptions),t&&a.use(t),a},"createBaseMarked"),Be=m((e={},t,r)=>{const a=se(e,t),n=m(h=>a.parse(h),"renderMarkdown"),u=se(e,t,n);return u.use({hooks:{postprocess(h){const{layout:w="default",...k}=W.attributes,T=ee(e.headScripts,h),M=ee(e.scripts,h);h=ae.default.sanitize(h);const b=[...h.matchAll(/<h([0-6])\sid="[^"]+".*?>.*?<a\s+href="([^"]+)".*?>([\s\S]*?)<\/a><\/h\1>/gm)].map(p=>({path:p[2],label:p[3].replace(/<[^>]+>/g,"").replace(/\s+/g," ").trim(),level:p[1]}));return R.get(w)?.({...k,content:h,siteMap:W.siteMap,tableOfContent:b,projectMeta:V,renderMarkdown:n,headScripts:T,scripts:M,themeStylesheet:e.theme?`/stylesheets/theme.css?v=${A}`:void 0,cacheKey:A,siteUrl:r,templateConfig:e})||h}}}),u},"createMarked"),S=m(async e=>{const t=await(0,c.readdir)(e),r=[];for(const a of t){const n=o.default.extname(a);/^[._]/.test(a)||(n?r.push(o.default.join(e,a)):/(stylesheets|assets|scripts)/.test(a)||r.push(...await S(o.default.join(e,a))))}return r},"traverseDirectory"),Ne=m(async({srcDir:e,publicDir:t,markedOptions:r,template:a,siteUrl:n,generatedFiles:u}={})=>{A=Date.now().toString(36);try{const s=o.default.resolve(process.cwd(),"package.json");f.default.existsSync(s)&&(V=JSON.parse(await(0,c.readFile)(s,"utf-8")))}catch(s){console.warn("Failed to load project package.json metadata",s)}const h=m(()=>{try{return __dirname}catch{return o.default.dirname((0,P.fileURLToPath)(ze.url))}},"getDirname"),w=a?o.default.resolve(h(),"templates",a):null,k=w?o.default.resolve(w,"assets"):null,T=w?o.default.resolve(w,"stylesheets"):null,M=w?o.default.resolve(w,"layouts"):null;let b={};if(w)try{let s=o.default.resolve(w,"template.config.js");if(f.default.existsSync(s)||(s=s.replace(/\.js$/,".ts")),f.default.existsSync(s)){const i=await import((0,P.pathToFileURL)(s).href);b=i.default||i}}catch(s){console.error(`Failed to load template config for ${a}`,s)}const p=e??o.default.resolve(process.cwd(),"docs"),d=t??o.default.resolve(process.cwd(),"website"),pe=Ge(u),D=o.default.resolve(p,"_template"),B=o.default.resolve(D,"assets"),N=o.default.resolve(D,"stylesheets"),U=o.default.resolve(D,"scripts"),z=o.default.resolve(D,"layouts"),q=o.default.resolve(p,"_layouts"),F=o.default.resolve(p,"assets"),H=o.default.resolve(p,"stylesheets"),L=o.default.resolve(p,"scripts");try{let s=o.default.resolve(D,"template.config.js");if(f.default.existsSync(s)||(s=s.replace(/\.js$/,".ts")),f.default.existsSync(s)){const i=await import((0,P.pathToFileURL)(s).href),l=i.default||i;b=xe(b,l)}}catch(s){console.error("Failed to load docs _template config",s)}const fe=Be(b,r,n);if(await(0,c.mkdir)(d,{recursive:!0}),k&&f.default.existsSync(k))try{await(0,c.cp)(k,F.replace(p,d),{recursive:!0})}catch(s){console.warn(s)}if(f.default.existsSync(F))try{await(0,c.cp)(F,F.replace(p,d),{recursive:!0})}catch(s){console.warn(s)}if(f.default.existsSync(B))try{await(0,c.cp)(B,o.default.resolve(d,"assets"),{recursive:!0})}catch(s){console.warn(s)}await Fe(o.default.resolve(d,"assets"));try{const s=new Set;if(T&&f.default.existsSync(T)&&(await S(T)).forEach(l=>s.add(l)),f.default.existsSync(H)&&(await S(H)).forEach(l=>s.add(l)),f.default.existsSync(N)&&(await S(N)).forEach(l=>s.add(l)),s.size){await(0,c.mkdir)(o.default.join(d,"stylesheets"),{recursive:!0});for(const l of s){const g=await(0,c.readFile)(l,"utf-8"),y=Y.minify(g);y.errors.length&&y.errors.forEach(console.error);const j=l.startsWith(D)?l.replace(D,d):l.startsWith(p)?l.replace(p,d):o.default.join(d,"stylesheets",o.default.basename(l));await(0,c.writeFile)(j,y.styles??g)}}const i=ke(b.theme);if(i){const l=Y.minify(i);l.errors.length&&l.errors.forEach(console.error),await(0,c.mkdir)(o.default.join(d,"stylesheets"),{recursive:!0}),await(0,c.writeFile)(o.default.join(d,"stylesheets","theme.css"),l.styles??i)}}catch(s){console.error(s)}if(f.default.existsSync(L))try{const s=await S(L);s.length&&await(0,c.mkdir)(L.replace(p,d),{recursive:!0});for(const i of s)try{const l=await(0,c.readFile)(i,"utf-8");await(0,c.writeFile)(i.replace(p,d),X.minify(l))}catch(l){console.error(l)}}catch(s){console.warn(s)}if(f.default.existsSync(U))try{const s=await S(U);s.length&&await(0,c.mkdir)(o.default.join(d,"scripts"),{recursive:!0});for(const i of s)try{const l=await(0,c.readFile)(i,"utf-8");await(0,c.writeFile)(i.replace(D,d),X.minify(l))}catch(l){console.error(l)}}catch(s){console.warn(s)}try{const s=new Map;M&&f.default.existsSync(M)&&(await S(M)).forEach(i=>{/\.(j|t)s$/.test(i)&&s.set(o.default.basename(i).replace(/\.(j|t)s$/,""),i)}),f.default.existsSync(q)&&(await S(q)).forEach(i=>{/\.(j|t)s$/.test(i)&&s.set(o.default.basename(i).replace(/\.(j|t)s$/,""),i)}),f.default.existsSync(z)&&(await S(z)).forEach(i=>{/\.(j|t)s$/.test(i)&&s.set(o.default.basename(i).replace(/\.(j|t)s$/,""),i)});for(const[i,l]of s.entries()){const g=await import((0,P.pathToFileURL)(l).href);R.set(i,g.default)}}catch(s){console.error("Template import failed",s)}const de=await S(p),I=new Map,J=(await Promise.all(de.filter(s=>s.endsWith(".md")).map(async s=>{const i=await(0,c.readFile)(s,"utf-8");return{filePath:s,...(0,oe.default)(i)}}))).sort((s,i)=>(s.attributes.order??0)-(i.attributes.order??0)).map(({attributes:s,body:i,filePath:l})=>{const g=l.replace(p,"").replace(/\.md/,".html"),y=l.replace(p,d).replace(".md",".html"),j=y.replace(o.default.basename(y),""),E=o.default.basename(g)||"/",he=g.replace(E,"").replace(/\/$/,"");let $=I;he.split("/").filter(Boolean).forEach(O=>{$.has(O)||$.set(O,new Map),$=$.get(O)});const K={...s,path:g};return $.has(E)||$.set(E,K),{attributes:K,fileWebsitePath:y,fileDirWebsitePath:j,body:i,sourcePath:o.default.relative(p,l),siteMap:I}});for(const{attributes:s,body:i,fileDirWebsitePath:l,fileWebsitePath:g,siteMap:y}of J){W={attributes:s,siteMap:y};const j=await fe.parse(i);await(0,c.mkdir)(l,{recursive:!0}),await(0,c.writeFile)(g,ue.default.minify(j,{includeAutoGeneratedTags:!0,removeAttributeQuotes:!0,removeComments:!0,removeRedundantAttributes:!0,removeScriptTypeAttributes:!0,removeStyleLinkTypeAttributes:!0,sortClassName:!0,useShortDoctype:!0,collapseWhitespace:!0,minifyJS:!0,minifyCSS:!0}))}await _e({docsDir:p,docsSiteDir:d,pages:J.map(({attributes:s,body:i,sourcePath:l})=>{const g=je(String(s.path)),y=Me(i);return{sourcePath:l,urlPath:g,title:String(s.title||s.name||"").trim()||"Documentation",name:String(s.name||s.title||"").trim()||"Documentation",description:String(s.description||"").trim()||y.slice(0,156)||"Documentation page.",order:Number(s.order??999),summary:y.slice(0,320)}}),siteUrl:n,publicDir:d,generatedFiles:pe})},"buildDocs");0&&(module.exports={buildDocs});
@@ -121,7 +121,7 @@ docs/_template/assets/logo.svg
121
121
 
122
122
  The footer also uses social icons from `/assets/*.svg`.
123
123
 
124
- The current header and footer still include Before Semicolon and Markup-oriented links and copy. If a site needs fully different navigation, footer link groups, or legal copy, override `_header.js`, `_footer.js`, or the full page layouts from `docs/_template/layouts`.
124
+ Header and footer links are driven by `site` config. Override `_header.js`, `_footer.js`, or the full page layouts only when the HTML structure itself needs to change.
125
125
 
126
126
  ## Metadata
127
127
 
@@ -167,6 +167,57 @@ export default {
167
167
 
168
168
  Override `_head-meta.js` in `docs/_template/layouts` only when the generated metadata shape itself needs to change.
169
169
 
170
+ ## Site Identity And Links
171
+
172
+ The template is generic. Product-specific labels, repository links, footer links, and edit links should live in `docs/_template/template.config.js` under `site`.
173
+
174
+ ```js
175
+ // docs/_template/template.config.js
176
+ export default {
177
+ site: {
178
+ name: 'Example',
179
+ packageName: '@example/project',
180
+ repositoryUrl: 'https://github.com/example/project',
181
+ repositoryLabel: 'Example GitHub repository',
182
+ docsEditUrl: 'https://github.com/example/project/tree/main/docs',
183
+ navLinks: [
184
+ { label: 'Features', href: '/#features' },
185
+ { label: 'Code', href: '/#code' },
186
+ { label: 'Install', href: '/#install' },
187
+ ],
188
+ actionLinks: [
189
+ {
190
+ label: 'Documentation',
191
+ href: '/documentation',
192
+ className: 'btn-primary',
193
+ },
194
+ ],
195
+ footerDescription: 'Documentation for Example.',
196
+ footerGroups: [
197
+ {
198
+ title: 'Learning Resources',
199
+ links: [{ label: 'Documentation', href: '/documentation' }],
200
+ },
201
+ ],
202
+ socialLinks: [
203
+ {
204
+ name: 'GitHub',
205
+ href: 'https://github.com/example',
206
+ icon: '/assets/github.svg',
207
+ },
208
+ ],
209
+ copyright: `Copyright &copy; ${new Date().getFullYear()} Example.`,
210
+ },
211
+ }
212
+ ```
213
+
214
+ Relevant fields:
215
+
216
+ - `repositoryUrl` powers the header GitHub links and the docs version badge.
217
+ - `docsEditUrl` powers the “edit this doc” link. If omitted, that link is hidden.
218
+ - `packageName` powers the default landing hero install command.
219
+ - `navLinks`, `actionLinks`, `footerGroups`, and `socialLinks` replace the template defaults.
220
+
170
221
  ## Markdown Layout Handlers
171
222
 
172
223
  `template.config.js` registers these Markdown layout handlers:
@@ -227,7 +278,8 @@ Supported layout options:
227
278
  - `title2`: second headline line.
228
279
  - `primaryHref`: primary CTA URL. Defaults to `/documentation/get-started`.
229
280
  - `primaryLabel`: primary CTA label. Defaults to `Get Started`.
230
- - `secondaryLabel`: secondary CTA label. Defaults to `npm i @beforesemicolon/markup`.
281
+ - `secondaryLabel`: secondary CTA label. Defaults to `site.landingHeroSecondaryLabel`, then `npm i ${site.packageName}`, then `Install`.
282
+ - `versionHref`: version badge URL. Defaults to `site.landingHeroVersionHref`, then `site.repositoryUrl`.
231
283
 
232
284
  Supported parts:
233
285
 
@@ -238,7 +290,7 @@ Supported parts:
238
290
  Notes:
239
291
 
240
292
  - The secondary CTA currently points to `#install`.
241
- - The version badge currently links to the Markup GitHub repository. Override `landing-hero.js` if that needs to be generic.
293
+ - The version badge links to `versionHref`, `site.landingHeroVersionHref`, or `site.repositoryUrl`.
242
294
 
243
295
  ## `landing-ecosystem`
244
296
 
@@ -579,9 +631,7 @@ docs/_template/template.config.js
579
631
 
580
632
  ## Assets
581
633
 
582
- The template includes a default asset set. Not all assets are generic; several exist because the template originally shipped with the Markup site.
583
-
584
- Generic or shared assets:
634
+ The template includes only generic/shared assets. Product-specific logos, banners, screenshots, and icons should live in the docs source under `docs/_template/assets`.
585
635
 
586
636
  - `logo.svg`: compatibility fallback for the main site logo.
587
637
  - `logo.light.svg`: preferred logo on light backgrounds.
@@ -589,19 +639,6 @@ Generic or shared assets:
589
639
  - `favicon/*`: default favicons.
590
640
  - social icons: `medium2.svg`, `facebook.svg`, `instagram.svg`, `reddit.svg`, `twitter.svg`, `youtube.svg`.
591
641
 
592
- Markup-specific assets still present:
593
-
594
- - `markup-banner.jpg`
595
- - `markup-essentials-training.jpg`
596
- - `markup-favicon.*`
597
- - `markup-logo*`
598
- - `client-server.svg`
599
- - `fast.svg`
600
- - `independent.svg`
601
- - `reactive.svg`
602
- - `simple.svg`
603
- - `small.svg`
604
-
605
642
  Override assets from a docs project by adding files at the same target path:
606
643
 
607
644
  ```txt
@@ -732,18 +769,6 @@ export default {
732
769
  }
733
770
  ```
734
771
 
735
- ## Known Template-Specific Assumptions
736
-
737
- Some template internals are still Markup or Before Semicolon oriented:
738
-
739
- - `_header.js` has Markup-oriented GitHub and documentation links.
740
- - `_footer.js` has Before Semicolon social links, footer copy, and learning links.
741
- - `document.js` builds edit links against the Markup GitHub docs path.
742
- - Some landing layout handlers contain default labels or links aimed at Markup docs.
743
- - Several assets are Markup-specific.
744
-
745
- These are template defaults, not builder requirements. Override them from `docs/_template` for another documentation site.
746
-
747
772
  ## Minimal Landing Page
748
773
 
749
774
  ````md
@@ -1,6 +1,6 @@
1
1
  {
2
- "name": "Markup",
3
- "short_name": "Markup",
2
+ "name": "Documentation",
3
+ "short_name": "Docs",
4
4
  "icons": [
5
5
  {
6
6
  "src": "/android-chrome-192x192.png",
@@ -1,77 +1,70 @@
1
- const social = [
2
- {
3
- link: 'https://medium.com/before-semicolon',
4
- icon: 'assets/medium2.svg',
5
- name: 'Medium blog',
6
- },
7
- {
8
- link: 'https://www.facebook.com/beforesemicolon/',
9
- icon: 'assets/facebook.svg',
10
- name: 'Facebook',
11
- },
12
- {
13
- link: 'https://www.instagram.com/before_semicolon_/',
14
- icon: 'assets/instagram.svg',
15
- name: 'Instagram',
16
- },
17
- {
18
- link: 'https://www.reddit.com/r/beforesemicolon/',
19
- icon: 'assets/reddit.svg',
20
- name: 'Reddit',
21
- },
22
- {
23
- link: 'https://twitter.com/BeforeSemicolon',
24
- icon: 'assets/twitter.svg',
25
- name: 'Twitter',
26
- },
1
+ import {
2
+ escapeAttribute,
3
+ escapeHTML,
4
+ linkAttrs,
5
+ siteConfig,
6
+ siteName,
7
+ } from './_layout-utils.js'
8
+
9
+ const defaultFooterGroups = [
27
10
  {
28
- link: 'https://www.youtube.com/channel/UCrU33aw1k9BqTIq2yKXrmBw',
29
- icon: 'assets/youtube.svg',
30
- name: 'YouTube',
11
+ title: 'Documentation',
12
+ links: [{ label: 'Docs', href: '/documentation' }],
31
13
  },
32
14
  ]
33
15
 
34
- export default (props = {}) => {
35
- const siteName =
36
- props.projectMeta?.name || props.name || props.title || 'Documentation'
37
- const logo = `
16
+ const renderLogo = (name) => `
38
17
  <span class="site-logo-pair">
39
- <img class="site-logo site-logo-dark" src="/assets/logo.dark.svg" alt="${siteName} logo" width="100" height="30" />
18
+ <img class="site-logo site-logo-dark" src="/assets/logo.dark.svg" alt="${escapeAttribute(name)} logo" width="100" height="30" />
40
19
  <img class="site-logo site-logo-light" src="/assets/logo.light.svg" alt="" width="100" height="30" aria-hidden="true" />
41
20
  </span>`
42
21
 
43
- return `
44
- <footer class="site-footer">
45
- <div class="footer-inner wrapper">
46
- <div class="footer-brand">
47
- ${logo}
48
- <p>A reactive HTML templating system for building dynamic UIs with pure JavaScript. No build step. No virtual DOM.</p>
49
- </div>
22
+ const renderFooterGroup = (group) => `
50
23
  <ul class="footer-links">
51
- <li><h4>Learning Resources</h4></li>
52
- <li><a href="/documentation">Documentation</a></li>
53
- <li><a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=mIr2XglV5nQ&list=PLpWvGP6yhJUgWNiz25vj__CArY9Z0O6ke&index=1">Essentials Training</a></li>
54
- </ul>
55
- <ul class="footer-links">
56
- <li><h4>About <em>Before Semicolon</em></h4></li>
57
- <li><a href="https://github.com/beforesemicolon" rel="noopener" target="_blank">Open Source</a></li>
58
- <li><a href="https://beforesemicolon.com/" rel="noopener" target="_blank">Website</a></li>
59
- <li><a href="https://medium.com/before-semicolon" rel="noopener" target="_blank">Blog</a></li>
60
- <li><a href="https://www.youtube.com/channel/UCrU33aw1k9BqTIq2yKXrmBw" rel="noopener" target="_blank">YouTube Channel</a></li>
61
- </ul>
62
- <div class="footer-social" aria-label="social-media">
63
- ${social
24
+ <li><h4>${escapeHTML(group.title)}</h4></li>
25
+ ${(group.links || [])
64
26
  .map(
65
- (s) => `
66
- <a href="${s.link}" rel="noopener" target="_blank" aria-label="${s.name}">
67
- <span class="footer-social-icon" style="--icon: url('/${s.icon}')" aria-hidden="true"></span>
68
- </a>
69
- `
27
+ (link) =>
28
+ `<li><a href="${escapeAttribute(link.href)}"${linkAttrs(link)}>${escapeHTML(link.label)}</a></li>`
70
29
  )
71
30
  .join('')}
31
+ </ul>`
32
+
33
+ const renderSocialLink = (link) => `
34
+ <a href="${escapeAttribute(link.href)}"${linkAttrs(link)} aria-label="${escapeAttribute(link.name)}">
35
+ <span class="footer-social-icon" style="--icon: url('${escapeAttribute(link.icon)}')" aria-hidden="true"></span>
36
+ </a>`
37
+
38
+ export default (props = {}) => {
39
+ const site = siteConfig(props)
40
+ const name = siteName(props)
41
+ const description =
42
+ site.footerDescription ||
43
+ props.description ||
44
+ 'Documentation, guides, and references for this project.'
45
+ const footerGroups = site.footerGroups || defaultFooterGroups
46
+ const socialLinks = site.socialLinks || []
47
+ const copyright =
48
+ site.copyright ||
49
+ `Copyright &copy; ${new Date().getFullYear()} ${escapeHTML(name)}. All rights reserved.`
50
+
51
+ return `
52
+ <footer class="site-footer">
53
+ <div class="footer-inner wrapper">
54
+ <div class="footer-brand">
55
+ ${renderLogo(name)}
56
+ <p>${escapeHTML(description)}</p>
72
57
  </div>
58
+ ${footerGroups.map(renderFooterGroup).join('')}
59
+ ${
60
+ socialLinks.length
61
+ ? `<div class="footer-social" aria-label="social-media">
62
+ ${socialLinks.map(renderSocialLink).join('')}
63
+ </div>`
64
+ : ''
65
+ }
73
66
  <div class="footer-credit">
74
- <span>Copyright &copy; ${new Date().getFullYear()} Before Semicolon. All rights reserved.</span>
67
+ <span>${copyright}</span>
75
68
  </div>
76
69
  </div>
77
70
  </footer>
@@ -1,42 +1,87 @@
1
- export default (props = {}) => {
2
- const isDocs = props.path?.startsWith('/documentation')
3
- const siteName =
4
- props.projectMeta?.name || props.name || props.title || 'Documentation'
5
- const logo = `
1
+ import {
2
+ escapeAttribute,
3
+ escapeHTML,
4
+ linkAttrs,
5
+ siteConfig,
6
+ siteName,
7
+ } from './_layout-utils.js'
8
+
9
+ const githubIcon =
10
+ '<svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>'
11
+
12
+ const renderLogo = (name, width = 90, height = 27) => `
6
13
  <span class="site-logo-pair">
7
- <img class="site-logo site-logo-dark" src="/assets/logo.dark.svg" alt="${siteName} logo" width="90" height="27" />
8
- <img class="site-logo site-logo-light" src="/assets/logo.light.svg" alt="" width="90" height="27" aria-hidden="true" />
14
+ <img class="site-logo site-logo-dark" src="/assets/logo.dark.svg" alt="${escapeAttribute(name)} logo" width="${width}" height="${height}" />
15
+ <img class="site-logo site-logo-light" src="/assets/logo.light.svg" alt="" width="${width}" height="${height}" aria-hidden="true" />
9
16
  </span>`
10
17
 
18
+ const defaultNavLinks = [
19
+ { label: 'Features', href: '/#features' },
20
+ { label: 'Code', href: '/#code' },
21
+ { label: 'Install', href: '/#install' },
22
+ ]
23
+
24
+ const defaultActionLinks = [
25
+ {
26
+ label: 'Documentation',
27
+ href: '/documentation',
28
+ className: 'btn-primary',
29
+ },
30
+ ]
31
+
32
+ const renderTextLink = (link) => `
33
+ <a href="${escapeAttribute(link.href)}"${linkAttrs(link)}${link.className ? ` class="${escapeAttribute(link.className)}"` : ''}${link.ariaLabel ? ` aria-label="${escapeAttribute(link.ariaLabel)}"` : ''}>${escapeHTML(link.label)}</a>`
34
+
35
+ const renderRepositoryLink = (props, docs = false) => {
36
+ const site = siteConfig(props)
37
+
38
+ if (!site.repositoryUrl) return ''
39
+
40
+ const label = site.repositoryLabel || `${siteName(props)} repository`
41
+
42
+ if (docs) {
43
+ return `
44
+ <a href="${escapeAttribute(site.repositoryUrl)}" target="_blank" rel="noopener" aria-label="${escapeAttribute(label)}" class="github-link docs-github-link">
45
+ <span>v${escapeHTML(props.projectMeta?.version || '')}</span>
46
+ ${githubIcon}
47
+ </a>`
48
+ }
49
+
50
+ return `
51
+ <a href="${escapeAttribute(site.repositoryUrl)}" target="_blank" rel="noopener" aria-label="${escapeAttribute(label)}" class="github-link">
52
+ ${githubIcon}
53
+ </a>`
54
+ }
55
+
56
+ export default (props = {}) => {
57
+ const isDocs = props.path?.startsWith('/documentation')
58
+ const name = siteName(props)
59
+ const site = siteConfig(props)
60
+ const navLinks = site.navLinks || defaultNavLinks
61
+ const actionLinks = site.actionLinks || defaultActionLinks
62
+
11
63
  return `
12
64
  <header class="site-header">
13
65
  <div class="header-inner wrapper">
14
66
  <h1>
15
- <a href="/" aria-label="${siteName} home">
16
- ${logo}
67
+ <a href="/" aria-label="${escapeAttribute(name)} home">
68
+ ${renderLogo(name)}
17
69
  </a>
18
70
  </h1>
19
71
  ${
20
72
  isDocs
21
73
  ? `
22
74
  <nav class="nav-actions docs-actions">
23
- <a href="https://github.com/beforesemicolon/markup" target="_blank" rel="noopener" aria-label="Markup GitHub repository" class="github-link docs-github-link">
24
- <span>v${props.projectMeta?.version || ''}</span>
25
- <svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
26
- </a>
75
+ ${renderRepositoryLink(props, true)}
27
76
  </nav>
28
77
  `
29
78
  : `
30
79
  <nav class="nav-links">
31
- <a href="/#features">Features</a>
32
- <a href="/#code">Code</a>
33
- <a href="/#install">Install</a>
80
+ ${navLinks.map(renderTextLink).join('')}
34
81
  </nav>
35
82
  <nav class="nav-actions">
36
- <a href="https://github.com/beforesemicolon/markup" target="_blank" rel="noopener" aria-label="GitHub" class="github-link">
37
- <svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
38
- </a>
39
- <a href="/documentation" class="btn-primary">Documentation</a>
83
+ ${renderRepositoryLink(props)}
84
+ ${actionLinks.map(renderTextLink).join('')}
40
85
  </nav>
41
86
  `
42
87
  }
@@ -6,6 +6,26 @@ export const escapeHTML = (value = '') =>
6
6
  .replace(/"/g, '&quot;')
7
7
  .replace(/'/g, '&#39;')
8
8
 
9
+ export const escapeAttribute = escapeHTML
10
+
11
+ export const isExternalHref = (href = '') => /^(https?:)?\/\//.test(href)
12
+
13
+ export const linkAttrs = (link = {}) => {
14
+ const href = link.href || '#'
15
+ const external = link.external ?? isExternalHref(href)
16
+
17
+ return external ? ' target="_blank" rel="noopener"' : ''
18
+ }
19
+
20
+ export const siteConfig = (props = {}) => props.templateConfig?.site || {}
21
+
22
+ export const siteName = (props = {}) =>
23
+ siteConfig(props).name ||
24
+ props.projectMeta?.name ||
25
+ props.name ||
26
+ props.title ||
27
+ 'Documentation'
28
+
9
29
  export const partByName = (parts, name) =>
10
30
  parts.find((part) => part.name === name)
11
31
 
@@ -1,9 +1,7 @@
1
1
  import meta from './_head-meta.js'
2
2
  import header from './_header.js'
3
3
  import footer from './_footer.js'
4
-
5
- const githubDocsPath =
6
- 'https://github.com/beforesemicolon/markup/tree/main/docs'
4
+ import { escapeAttribute, siteConfig } from './_layout-utils.js'
7
5
 
8
6
  const navCategoryToHTML = (docs, currentPath) =>
9
7
  Array.from(docs.entries())
@@ -27,6 +25,7 @@ export default (props) => {
27
25
  const docs = props.siteMap.get('documentation')
28
26
  const isDocsIndex = props.path === '/documentation/index.html'
29
27
  const cacheKey = props.cacheKey ? `?v=${props.cacheKey}` : ''
28
+ const docsEditUrl = siteConfig(props).docsEditUrl?.replace(/\/$/, '')
30
29
  const docsMenu = `<ul>${navCategoryToHTML(docs, props.path)}</ul>`
31
30
  const pages = [...docs.values()].flatMap((page) =>
32
31
  page instanceof Map ? [...page.values()] : [page]
@@ -74,7 +73,11 @@ export default (props) => {
74
73
  ${nextPage ? `<a href="${nextPage.path.replace('.html', '')}" id="next-doc">${nextPage.name} <span aria-hidden="true">&gt;&gt;</span></a>` : ''}
75
74
  </div>
76
75
 
77
- <a href="${`${githubDocsPath}${props.path.replace('.html', '')}.md`}" id="edit-doc">edit this doc</a>
76
+ ${
77
+ docsEditUrl
78
+ ? `<a href="${escapeAttribute(`${docsEditUrl}${props.path.replace('.html', '')}.md`)}" id="edit-doc" target="_blank" rel="noopener">edit this doc</a>`
79
+ : ''
80
+ }
78
81
  </article>
79
82
  <aside id="table-of-content">
80
83
  <h4>Content</h4>
@@ -1,14 +1,17 @@
1
1
  import { renderCodeBlock } from './_code-snippet.js'
2
2
  import {
3
3
  codeFromPart,
4
+ escapeAttribute,
4
5
  icon,
5
6
  option,
6
7
  partByName,
8
+ siteConfig,
7
9
  statContent,
8
10
  withFirstParagraphClass,
9
11
  } from './_layout-utils.js'
10
12
 
11
- export default ({ parts, options }) => {
13
+ export default ({ parts, options }, context = {}) => {
14
+ const site = siteConfig(context)
12
15
  const copy = partByName(parts, 'copy') || parts[0]
13
16
  const code = partByName(parts, 'code') || parts[1]
14
17
  const stats = parts.filter((part) => part.name === 'stat')
@@ -22,8 +25,13 @@ export default ({ parts, options }) => {
22
25
  const primaryLabel = option(options.primaryLabel, 'Get Started')
23
26
  const secondaryLabel = option(
24
27
  options.secondaryLabel,
25
- 'npm i @beforesemicolon/markup'
28
+ site.landingHeroSecondaryLabel ||
29
+ (site.packageName ? `npm i ${site.packageName}` : 'Install')
26
30
  )
31
+ const versionHref =
32
+ option(options.versionHref) ||
33
+ site.landingHeroVersionHref ||
34
+ site.repositoryUrl
27
35
 
28
36
  return `
29
37
  <section class="hero-section relative overflow-hidden">
@@ -33,7 +41,7 @@ export default ({ parts, options }) => {
33
41
  <div class="hero-content">
34
42
  ${
35
43
  version
36
- ? `<a href="https://github.com/beforesemicolon/markup" target="_blank" rel="noopener" class="version-badge">
44
+ ? `<a href="${escapeAttribute(versionHref || '#')}"${versionHref ? ' target="_blank" rel="noopener"' : ''} class="version-badge">
37
45
  ${icon('sparkles')}
38
46
  <span>${version}</span>
39
47
  ${icon('arrowRight')}