@beforesemicolon/builder
v1.8.25
Published
Utilities to build npm packages and documentation website
Readme
@beforesemicolon/builder
Utilities to build npm packages and static documentation websites for Before Semicolon projects.
This package provides three public helpers:
buildModules()builds TypeScript sources intodist/esmanddist/cjs.buildBrowser()builds a browser bundle, usually for demos or docs.buildDocs()renders a static Markdown documentation site.
The docs builder supports reusable templates, Markdown layout blocks, source-level template extension, generated SEO/AI files, theme variables, page scripts, assets, stylesheets, and custom marked options.
Requirements
- Node.js
>=18.16.0 - ESM projects are supported directly.
- CommonJS consumers can use the package
requireexport.
Installation
npm install --save-dev @beforesemicolon/builderQuick Start
import { buildModules, buildBrowser, buildDocs } from '@beforesemicolon/builder'
await buildModules()
await buildBrowser()
await buildDocs()Common project script:
import { buildModules, buildBrowser, buildDocs } from '@beforesemicolon/builder'
const docsOptions = {
template: 'fading-citrus',
siteUrl: 'https://example.com',
generatedFiles: {
netlify: true,
},
}
const run = async () => {
await Promise.all([buildModules(), buildBrowser(), buildDocs(docsOptions)])
}
run()API
buildModules(options?)
buildModules(options?: {
directoryPath?: string
}): Promise<void>Builds source files into server-friendly ESM and CommonJS output.
Defaults:
directoryPath:process.cwd()/src- ESM output:
dist/esm - CommonJS output:
dist/cjs
Behavior:
- Recursively scans the source directory.
- Skips files ending in
.spec.ts. - Skips
/client.tsfrom module builds. - Uses
esbuild. - Minifies output.
- Keeps symbol names for better stack traces.
buildBrowser(options?)
buildBrowser(options?: {
entry?: string
out?: string
}): Promise<void>Builds a single browser bundle.
Defaults:
entry:src/clientout:dist/client.js
Behavior:
- Uses
esbuild. - Generates sourcemaps.
- Minifies output.
- Includes a small internal plugin that removes the
Docexport from@beforesemicolon/html-parserwhen bundling.
buildDocs(options?)
buildDocs(options?: {
srcDir?: string
publicDir?: string
markedOptions?: MarkedExtension
template?: string
siteUrl?: string
generatedFiles?:
| boolean
| {
sitemap?: boolean
robots?: boolean
llms?: boolean
llmsFull?: boolean
netlify?: boolean
}
}): Promise<void>Builds a static documentation site from Markdown.
Defaults:
srcDir:process.cwd()/docspublicDir:process.cwd()/websitetemplate: no named template, uses the built-indefaultlayoutgeneratedFiles: enabled forsitemap,robots,llms, andllmsFullgeneratedFiles.netlify:false
Example:
await buildDocs({
template: 'fading-citrus',
siteUrl: 'https://docs.example.com',
generatedFiles: {
netlify: true,
},
})Docs Directory Structure
The default source directory is docs/.
docs/
index.md
guide/
getting-started.md
assets/
stylesheets/
scripts/
_layouts/
_template/
template.config.js
assets/
stylesheets/
scripts/
layouts/
robots.txt
sitemap.xml
llms.txt
llms-full.txt
_redirects
netlify.tomlSupported folders:
assets/: copied to the same relative location in the output directory.stylesheets/: CSS files are minified and copied to output.scripts/: JS files are minified and copied to output._layouts/: page layout modules. Each file default-exports a page layout function._template/: source-level extension for the selected template._template/assets/: copied intopublicDir/assets, overriding or extending template assets._template/stylesheets/: copied intopublicDir/stylesheets, overriding or extending template styles._template/scripts/: copied intopublicDir/scripts, overriding or extending template scripts._template/layouts/: custom page layouts that can override or extend selected template layouts._template/template.config.js: source-level template config merged with the selected template config.
Files and folders starting with . or _ are skipped during Markdown page discovery. _template and _layouts are used explicitly by the docs builder.
Page Front Matter
Each Markdown page can include front matter:
---
name: Get Started
title: Get Started with Example
description: Learn how to install and use Example.
order: 1
layout: document
---
# Get StartedCommon fields:
name: label used in the generated site map.title: HTML title and generated metadata title.description: meta description and generated metadata description.order: numeric sort order for site map and generated files.layout: page layout name. Defaults todefault.
The final page props include:
interface PageProps {
name?: string
path?: string
order?: number
title?: string
description?: string
content?: string
siteMap?: SiteMap
tableOfContent?: Array<{
path: string
label: string
level: string
}>
projectMeta?: {
name: string
version: string
[key: string]: unknown
}
renderMarkdown?: (markdown: string) => string
scripts?: string[]
themeStylesheet?: string
}Page Layouts
Page layouts render complete HTML documents. A layout file must default-export a function that receives PageProps and returns an HTML string.
Example docs/_layouts/document.js:
export default ({
title,
description,
content,
scripts = [],
}) => `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="${description || ''}">
<title>${title || ''}</title>
</head>
<body>
${content || ''}
${scripts.join('')}
</body>
</html>`Layout lookup order:
- Built-in layouts.
- Selected template layouts.
docs/_layouts.docs/_template/layouts.
Later layout files with the same basename override earlier ones.
Templates
Named templates are loaded from:
src/docs/templates/<template-name>/Available templates:
fading-citrus: a complete landing and documentation template with Markdown layout handlers, theme variables, assets, and page scripts. See fading-citrus template README.
A template can provide:
template.config.js
assets/
stylesheets/
scripts/
layouts/The selected template is a complete out-of-the-box docs site shell. A docs source can extend it through docs/_template. Template-specific layouts, assets, options, and assumptions should be documented by each template.
Template Config
A template config exports an object:
export default {
meta: {},
site: {},
markedOptions: {},
markdownLayouts: {},
headScripts: {},
scripts: {},
theme: {
light: {},
dark: {},
},
}Config from docs/_template/template.config.js is merged into the selected template config.
Merge behavior:
markdownLayoutsare shallow-merged by layout name.headScriptsandscriptsare shallow-merged by script name.metais shallow-merged by metadata field.siteis shallow-merged by site field.theme.lightandtheme.darkare shallow-merged by CSS variable name.- Other top-level config values use the docs source config value when provided.
Example docs source extension:
import pricingCards from './layouts/pricing-cards.js'
export default {
meta: {
siteName: 'Example',
title: 'Example Docs',
description: 'Documentation for Example.',
image: '/assets/site-image.jpg',
},
site: {
name: 'Example',
packageName: '@example/docs',
repositoryUrl: 'https://github.com/example/docs',
repositoryLabel: 'Example GitHub repository',
docsEditUrl: 'https://github.com/example/docs/tree/main/docs',
footerDescription: 'Documentation for Example.',
footerGroups: [
{
title: 'Learning Resources',
links: [{ label: 'Documentation', href: '/documentation' }],
},
],
},
markdownLayouts: {
'pricing-cards': pricingCards,
},
theme: {
light: {
'--primary': 'oklch(0.62 0.18 250)',
},
dark: {
'--primary': 'oklch(0.76 0.16 250)',
},
},
}Common site fields:
name: display name used by shared layouts.packageName: package name used by templates that render install commands.repositoryUrlandrepositoryLabel: repository link and accessible label.docsEditUrl: base URL for edit links, usually a repositorydocsfolder URL.navLinksandactionLinks: landing header links.footerDescription,footerGroups,socialLinks, andcopyright: footer content.landingHeroVersionHrefandlandingHeroSecondaryLabel: optional landing hero overrides.
Markdown Layout Syntax
The docs renderer extends marked with a custom block syntax:
::: layout <type> [options]
=== <name> [options]
Markdown content for this part.
=== <name> [options]
More Markdown content.
:::Example:
::: layout grid columns=3 gap=lg
=== card span=2
## First card
Markdown content.
=== card sticky
## Second card
More Markdown content.
===
Unnamed item.
:::Header parsing:
::: layout grid columns=3 gap=lgProduces:
{
type: 'grid',
options: {
columns: 3,
gap: 'lg',
},
}Item header parsing:
=== hero span=2 stickyProduces:
{
name: 'hero',
options: {
span: 2,
sticky: true,
},
}Unnamed items are supported:
===
ContentProduces:
{
name: null,
options: {},
}Option parsing rules:
key=valuebecomes a keyed option.- Bare words become boolean
true. - Numeric values become numbers.
trueandfalsebecome booleans.- Quoted values are supported.
Examples:
columns=3
gap=lg
sticky
label="Get Started"
enabled=falseNested layout blocks are supported. Nested blocks are preserved inside the parent item body and rendered through the same Markdown renderer.
Markdown Layout Handlers
Markdown layout handlers are registered through template.config.js:
import pricingCards from './layouts/pricing-cards.js'
export default {
markdownLayouts: {
'pricing-cards': pricingCards,
},
}A handler receives parsed layout data and a rendering context:
type MarkdownLayoutHandler = (
layout: {
type: string
options: Record<string, string | number | boolean>
parts: Array<{
name: string | null
options: Record<string, string | number | boolean>
body: string
html: string
}>
raw: string
},
context: {
renderMarkdown(markdown: string): string
renderDefault(node): string
renderParts(node): Array<{ html: string }>
}
) => stringEach part body is rendered from Markdown to HTML before the handler receives it. Use part.html when injecting content.
Example handler:
export default ({ parts, options }) => {
const tierClass = options.featured ? ' pricing-cards-featured' : ''
return `<div class="pricing-cards${tierClass}">
${parts
.map(
(
part,
index
) => `<section class="pricing-card option-${index + 1}">
${part.html}
</section>`
)
.join('')}
</div>`
}Markdown:
::: layout pricing-cards featured
===
## Starter
$10/month
===
## Pro
$30/month
:::Generated HTML is entirely controlled by the handler.
Default Markdown Layout Rendering
If a layout type has no custom handler, builder renders a generic structure:
<div
class="bfs-layout bfs-layout-grid"
data-layout="grid"
style="--columns: 3; --gap: lg;"
>
<section class="bfs-layout-item" data-name="card" style="--span: 2;">
...
</section>
</div>Boolean options are omitted from inline styles. Non-boolean options are converted to CSS custom properties.
marked Options
The docs builder uses marked, marked-highlight, and a custom renderer for headings, code, and links.
You can extend marked globally for docs generation:
await buildDocs({
markedOptions: {
renderer: {
codespan({ text }) {
return `<code data-inline>${text}</code>`
},
},
},
})Templates can also provide markedOptions through template.config.js.
Page Scripts
Template scripts are declared in template.config.js.
import { renderCodeCopyScript } from './layouts/_code-snippet.js'
export default {
scripts: {
'code-copy': {
match: 'code-copy-btn',
render: renderCodeCopyScript,
},
},
}Use headScripts for scripts that must render in <head>, such as analytics bootstrap tags. Use scripts for scripts that can render near </body>, such as interaction handlers.
Script definitions:
type DocsScriptMatcher =
| string
| string[]
| RegExp
| ((html: string) => boolean)
interface DocsScriptDefinition {
match?: DocsScriptMatcher
render: () => string
}
type DocsScriptRegistry = Record<
string,
false | DocsScriptDefinition | (() => string)
>Behavior:
- Scripts are rendered per page after Markdown has been rendered.
- If
matchis omitted, the script is included on every page. - A string matcher checks
html.includes(match). - An array matcher checks whether any string is present.
- A RegExp matcher tests the rendered page HTML.
- A function matcher receives the rendered page HTML and returns a boolean.
- A script can be disabled by setting its registry value to
falsein an extending config.
Layouts receive scripts through props.headScripts and props.scripts. Template layouts must insert them where appropriate.
export default (props) => `
<!doctype html>
<html>
<head>
${props.headScripts?.join('') || ''}
</head>
<body>
${props.content}
${props.scripts?.join('') || ''}
</body>
</html>`Theme Variables
Templates can define theme variables in template.config.js:
export default {
theme: {
light: {
'--background': 'oklch(0.98 0.006 250)',
'--foreground': 'oklch(0.18 0.015 250)',
'--primary': 'oklch(0.66 0.18 45)',
},
dark: {
'--background': 'oklch(0.18 0.015 250)',
'--foreground': 'oklch(0.96 0.005 250)',
'--primary': 'oklch(0.74 0.18 45)',
},
},
}Builder converts theme variables into:
website/stylesheets/theme.cssThe generated file includes:
:rootvariables.@media (prefers-color-scheme: dark)variables.[data-theme="light"]variables.[data-theme="dark"]variables.
A theme mode can be disabled with false:
export default {
theme: {
light: false,
},
}When one mode is disabled, the remaining mode is emitted as :root. For example, light: false makes the dark theme the default theme and skips light-mode selectors and prefers-color-scheme switching.
Page layouts receive:
themeStylesheet?: stringTemplates should include it in <head>:
${props.themeStylesheet ? `<link rel="stylesheet" href="${props.themeStylesheet}">` : ''}Docs sources can override only variable values by adding docs/_template/template.config.js.
Generated Files
buildDocs() can generate common root-level files into publicDir.
Defaults:
generatedFiles: {
sitemap: true,
robots: true,
llms: true,
llmsFull: true,
netlify: false,
}Disable all generated files:
await buildDocs({
generatedFiles: false,
})Enable Netlify files:
await buildDocs({
siteUrl: 'https://docs.example.com',
generatedFiles: {
netlify: true,
},
})Generated files:
sitemap.xml: generated from discovered Markdown pages. RequiressiteUrl.robots.txt: generated withAllow: /and a sitemap URL whensiteUrlis provided.llms.txt: generated page index for AI tools.llms-full.txt: generated expanded page index with source paths, descriptions, and summaries._redirects: generated only whengeneratedFiles.netlifyistrue.netlify.toml: generated only whengeneratedFiles.netlifyistrue.
Source-first behavior:
- If
docs/sitemap.xmlexists, it is copied topublicDir/sitemap.xmlinstead of generated. - If
docs/robots.txtexists, it is copied topublicDir/robots.txtinstead of generated. - If
docs/llms.txtexists, it is copied topublicDir/llms.txtinstead of generated. - If
docs/llms-full.txtexists, it is copied topublicDir/llms-full.txtinstead of generated. - If
generatedFiles.netlifyistrueanddocs/_redirectsexists, it is copied topublicDir/_redirectsinstead of generated. - If
generatedFiles.netlifyistrueanddocs/netlify.tomlexists, it is copied topublicDir/netlify.tomlinstead of generated.
Netlify notes:
_redirectsis treated as Netlify-specific.netlify.tomlis written topublicDir, not the project root.- Generated
netlify.tomldefaults tocommand = "node build-docs.js"andpublish = "website"unlesspublicDirhas a different basename.
llms-full.txt Content
The generated llms-full.txt is derived from discovered Markdown pages.
For each page it uses:
title: front mattertitle, fallback toname, fallback toDocumentation.description: front matterdescription, fallback to stripped Markdown body text.URL: file-derived page URL joined withsiteUrl.Source: relative Markdown source path.summary: first 320 characters of stripped Markdown body text.
The body summary is not the final rendered HTML. It is a lightweight Markdown text extraction used for AI-facing page discovery.
Extension Example
Project docs:
docs/
index.md
_template/
template.config.js
assets/
logo.svg
layouts/
pricing-cards.jsdocs/_template/template.config.js:
import pricingCards from './layouts/pricing-cards.js'
export default {
markdownLayouts: {
'pricing-cards': pricingCards,
},
headScripts: {
analytics: () =>
`<script async src="https://example.com/analytics.js"></script>`,
},
scripts: {
pageView: {
match: '<main',
render: () => `<script>console.log('page viewed')</script>`,
},
},
theme: {
light: {
'--primary': 'oklch(0.62 0.18 250)',
},
dark: {
'--primary': 'oklch(0.78 0.16 250)',
},
},
}docs/index.md:
---
title: Example
description: Example documentation.
layout: landing
---
::: layout pricing-cards featured
===
## Starter
For small teams.
===
## Pro
For growing teams.
:::Build Output
Given default options, output is written to:
website/
index.html
guide/
getting-started.html
assets/
stylesheets/
scripts/
robots.txt
sitemap.xml
llms.txt
llms-full.txtIf generatedFiles.netlify is true, output also includes:
website/
_redirects
netlify.tomlPackage Scripts
This repo provides:
npm run build: removesdist, emits TypeScript declarations, then builds package outputs.npm run lint: runs ESLint and Prettier checks.npm run format: runs ESLint autofix and Prettier write.npm test: runs the Markdown layout parser/renderer tests.
Development
Install dependencies:
npm installRun checks:
npm run lint
npm test
npm run buildPack locally:
npm packInstall the packed artifact into a sibling project:
npm install ../builder/beforesemicolon-builder-<version>.tgzImplementation Notes
- Markdown rendering uses
marked. - Syntax highlighting uses
marked-highlightandhighlight.js. - Front matter parsing uses
front-matter. - HTML is sanitized with
isomorphic-dompurify. - HTML output is minified with
html-minifier. - CSS output is minified with
clean-css. - JS output copied from docs script folders is minified with
@putout/minify. - Static module and browser builds use
esbuild.
License
BSD-3-Clause. See package.json.
