npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@beforesemicolon/site-builder

v0.40.0

Published

Site builder based on JSON files

Readme

BeforeSemicolon Site Builder

npm version License: BSD-3-Clause Build Status

JSON-first static site builder with TypeScript APIs for parsing templates, widgets, and components.

Installation

npm install @beforesemicolon/site-builder

Quick Start

import { buildTemplates } from '@beforesemicolon/site-builder'

await buildTemplates({
    srcDir: './src',
    publicDir: './public',
    prod: true,
})

Project Structure

src/
  templates/      # *.json templates
  components/     # *.json components
  widgets/        # *.js widget modules
  scripts/        # JS files referenced by templates/widgets
  stylesheets/    # CSS files referenced by templates/widgets
  locales/        # i18n JSON files (flattened at build time)
  assets/         # copied to public/assets
  data/           # copied to public/data
public/           # generated output

Templates

Templates are page/base documents and map to the Template type in src/types.ts.

Template fields (core)

  • id: string
  • name: string
  • type: 'base' | 'page'
  • pathname?: string path used for routing/sitemap URL generation
  • extends?: string inherit from another template id
  • excluded?: boolean skip page generation when true
  • content?: string | TemplateContent

Template example

{
    "id": "home",
    "name": "Home",
    "type": "page",
    "lang": "en",
    "title": "My Site",
    "description": "Home page",
    "domain": "https://example.com",
    "pathname": "/",
    "stylesheets": ["main.css"],
    "scripts": ["main.js"],
    "content": [
        {
            "name": "main",
            "class": "page",
            "children": [
                { "name": "h1", "children": ["{{title}}"] },
                {
                    "name": "component",
                    "id": "hero",
                    "headline": "Ship faster"
                },
                { "name": "widget", "id": "newsletter", "theme": "light" }
            ]
        }
    ],
    "widgetsData": {
        "newsletter-home-1": { "cta": "Subscribe now" }
    }
}

Template inheritance example

{
    "id": "about",
    "name": "About",
    "type": "page",
    "extends": "base",
    "pathname": "/about",
    "title": "About Us",
    "content": [{ "name": "h1", "children": ["About"] }]
}

Components

Components map to the Component type:

interface Component {
    id: string
    stylesheets?: Stylesheet[]
    content: TemplateContent | string
}

They are static reusable blocks rendered inside template/widget content using name: 'component' + id.

Component example

{
    "id": "hero",
    "stylesheets": ["hero.css"],
    "content": [
        {
            "name": "section",
            "class": "hero",
            "children": [
                { "name": "h2", "children": ["{{headline}}"] },
                { "name": "p", "children": ["{{subhead}}"] }
            ]
        }
    ]
}

Using a component in template content

{
    "name": "component",
    "id": "hero",
    "headline": "Build with JSON",
    "subhead": "Typed and composable"
}

Widgets

Widgets map to the Widget type and are loaded via fetchWidget (during build, this comes from src/widgets/<id>.js).

interface Widget {
    id: string
    name: string
    cssSelector?: string
    scripts?: Script[]
    style?: Style | ((props?: Record<string, unknown>) => Style)
    stylesheets?: Stylesheet[]
    inputs?: InputDefinition[]
    render?: (props) => string
    content?: TemplateContent | string
}

Widget props merge order

When rendering, props are merged in this order:

  1. defaults from inputs
  2. template-level widgetsData[data-render-id]
  3. inline attributes on the <widget> node
  4. env object (assetsOrigin, prod, and template data)

Later entries override earlier ones.

Widget example

// src/widgets/newsletter.js
export default {
    id: 'newsletter',
    name: 'Newsletter',
    inputs: [
        { name: 'title', type: 'text', value: 'Join our list' },
        { name: 'cta', type: 'text', value: 'Subscribe' },
    ],
    style: {
        '.newsletter': {
            padding: '1rem',
            border: '1px solid #ddd',
        },
    },
    render: ({ title, cta, $msg }) => {
        return `<section class="newsletter"><h3>${title}</h3><button>${cta}</button><small>${$msg('newsletter.disclaimer')}</small></section>`
    },
    scripts: ['newsletter.js'],
    stylesheets: ['newsletter.css'],
}

Using a widget in template content

{
    "name": "widget",
    "id": "newsletter",
    "title": "Weekly updates"
}

And template-level overrides:

{
    "widgetsData": {
        "newsletter-home-1": {
            "cta": "Get updates"
        }
    }
}

InputDefinitionType

InputDefinitionType defines how widget inputs are declared and converted into runtime props.

Enum values

enum InputDefinitionType {
    Text = 'text',
    Html = 'html',
    Markdown = 'markdown',
    Number = 'number',
    Code = 'code',
    Image = 'image',
    Video = 'video',
    Audio = 'audio',
    File = 'file',
    Font = 'font',
    Pdf = 'pdf',
    Icon = 'icon',
    Embed = 'embed',
    Favicon = 'favicon',
    Email = 'email',
    Password = 'password',
    Url = 'url',
    Tel = 'tel',
    Color = 'color',
    Date = 'date',
    DateTimeLocal = 'datetime-local',
    Month = 'month',
    Time = 'time',
    Week = 'week',
    Textarea = 'textarea',
    Boolean = 'boolean',
    Options = 'options',
    Group = 'group',
    List = 'list',
}

Input definition shape

interface InputDefinition<T = unknown> {
    type: InputDefinitionType
    readonly?: boolean
    name?: string
    value?: T
    label?: string
    dir?: string
    description?: string
    definitions?: InputDefinition[]
}

How it works in practice

  • Primitive-like types (text, number, boolean, etc.) resolve to name: value.
  • group resolves to a nested object based on definitions.
  • list resolves to an array from definitions.
  • options resolves to:
    • value: selected value
    • options: mapped option objects from definitions

This conversion is performed by inputDefinitionsToObject.

InputDefinitionType example

import {
    InputDefinitionType,
    inputDefinitionsToObject,
} from '@beforesemicolon/site-builder'

const defs = [
    { name: 'title', type: InputDefinitionType.Text, value: 'Welcome' },
    {
        name: 'theme',
        type: InputDefinitionType.Options,
        value: 'dark',
        definitions: [
            {
                type: InputDefinitionType.Group,
                definitions: [
                    {
                        name: 'label',
                        type: InputDefinitionType.Text,
                        value: 'Dark',
                    },
                    {
                        name: 'value',
                        type: InputDefinitionType.Text,
                        value: 'dark',
                    },
                ],
            },
            {
                type: InputDefinitionType.Group,
                definitions: [
                    {
                        name: 'label',
                        type: InputDefinitionType.Text,
                        value: 'Light',
                    },
                    {
                        name: 'value',
                        type: InputDefinitionType.Text,
                        value: 'light',
                    },
                ],
            },
        ],
    },
    {
        name: 'seo',
        type: InputDefinitionType.Group,
        definitions: [
            {
                name: 'description',
                type: InputDefinitionType.Text,
                value: 'Landing page',
            },
            {
                name: 'indexable',
                type: InputDefinitionType.Boolean,
                value: true,
            },
        ],
    },
]

const props = inputDefinitionsToObject(defs)
// {
//   title: 'Welcome',
//   theme: { value: 'dark', options: [{label: 'Dark', value: 'dark'}, {label: 'Light', value: 'light'}] },
//   seo: { description: 'Landing page', indexable: true }
// }

Updating definitions from data

Use mergeDataIntoInputDefinition(data, definition) when you need to merge user/content data back into an existing schema. This keeps group and list structures typed and synchronized.

Parse APIs

parseTemplate(template, options)

Returns a full HTML document string (<!doctype html>...) with metadata, OG tags, scripts, styles, optional CSP, preload links, and structured data.

parseTemplateContent({ content, data, opt })

Parses TemplateContent arrays and recursively resolves HTML nodes, widgets, and components.

parseWidget({ node, data, opt })

Renders a single widget node and collects widget scripts/styles.

parseComponent(component, props)

Renders component content with placeholder replacement.

Parse options

interface parseOptions {
    useCache?: boolean
    prod?: boolean
    assetsOrigin?: string
    components?: Record<string, Component>
    locales?: Record<string, ObjectLiteral>
    fetchWidget?: (id: string) => Widget | null | Promise<Widget | null>
    fetchTemplate?: (id: string) => Template | null | Promise<Template | null>
}

Build Behavior (buildTemplates)

buildTemplates({ srcDir, publicDir, prod }) does the following:

  • clears and recreates publicDir
  • copies src/assets to public/assets
  • copies src/data to public/data
  • loads and flattens src/locales/*.json
  • loads templates/components
  • builds only type: 'page' templates where excluded !== true
  • resolves extends inheritance
  • bundles local scripts with esbuild
  • minifies local stylesheets with clean-css
  • parses templates to HTML and minifies output
  • writes fallback robots.txt and sitemap.xml when missing in src/assets

Development

npm run test
npm run test:watch
npm run lint
npm run build

License

BSD-3-Clause. See LICENSE.