@whittakertech/virgil
v0.1.1
Published
Build-phase sigil forge for static site artifacts
Maintainers
Readme
WhittakerTech::Virgil
Build-phase sigil forge for static site artifacts
Virgil generates OG images, sitemaps, robots.txt, and other build artifacts deterministically with content-addressed caching.
Philosophy
- Deterministic: Content-addressed hashing eliminates timestamp lies
- Locked: Only regenerates what actually changed
- CI-first: Designed for GitHub Actions, not local dev servers
- Declarative: Everything in
virgil.spec.json
Installation
npm install @whittakertech/virgilQuick Start
1. Create virgil.spec.json
{
"brand": {
"name": "WhittakerTech",
"logo": "docs/public/logo.svg",
"color": "#5b7cff"
},
"product": {
"name": "MosaicJS",
"logo": "docs/public/mosaic-logo.svg",
"version": "0.3.0"
},
"outputs": [
{
"type": "og-image",
"id": "getting-started",
"page": {
"title": "Getting Started",
"url": "https://mosaicjs.whittakertech.com/getting-started",
"description": "Learn how to use MosaicJS"
}
}
]
}2. Run Virgil
npx virgil build3. Reference in frontmatter
---
og-image: og:getting-started
---Your static site generator resolves og:getting-started via virgil.manifest.json.
How It Works
Execution Flow
- Load spec (
virgil.spec.json) - declarative intent - Load lock (
virgil.lock.json) - truth from last run - Hash inputs - SHA-256 of data + template + generator version
- Compare - skip if hash matches
- Generate - create artifacts only when needed
- Update manifest (
virgil.manifest.json) - stable references - Write lock - record current state
Content-Addressed Hashing
hash = sha256(
JSON.stringify(data) +
templateContents +
generatorVersion
)Changes to data, template, or generator trigger regeneration. Timestamp changes do not.
Lock File (virgil.lock.json)
{
"version": "0.1",
"entries": {
"getting-started": {
"hash": "sha256:9fa4c...",
"generatedAt": "2025-01-15T10:30:00Z",
"generator": "og-image",
"generatorVersion": "0.1.0"
}
}
}Manifest (virgil.manifest.json)
{
"version": "0.1",
"og": {
"getting-started": "/og/getting-started.1735346221.png"
}
}Frontmatter uses stable IDs (og:getting-started), manifest maps to cache-busted filenames.
Output Types
OG Images
{
"type": "og-image",
"id": "home",
"page": {
"title": "Welcome",
"url": "https://example.com",
"description": "Optional description"
}
}Generates 1200×630 PNG using Playwright.
Sitemap
{
"type": "sitemap",
"id": "main",
"baseUrl": "https://example.com",
"pages": [
{
"path": "/",
"lastmod": "2025-01-15",
"changefreq": "weekly",
"priority": 1.0
}
]
}Robots.txt
{
"type": "robots",
"id": "default",
"rules": [
{
"userAgent": "*",
"allow": ["/"],
"disallow": ["/admin"]
}
],
"sitemap": "https://example.com/sitemap.xml"
}CLI
virgil build [options]
Options:
-r, --root <dir> Project root directory (default: cwd)
-o, --output <dir> Output directory (default: public)
-v, --verbose Verbose outputProgrammatic Usage
import { run } from '@whittakertech/virgil';
const result = await run({
rootDir: process.cwd(),
outputDir: 'public',
verbose: true
});
console.log(`Generated: ${result.generated}`);
console.log(`Skipped: ${result.skipped}`);GitHub Actions
- name: Generate artifacts
run: npx virgil build --verboseVirgil skips unchanged artifacts, keeping CI fast.
v0.1 Scope
Supported:
- ✅ OG image generation (PNG)
- ✅ sitemap.xml generation
- ✅ robots.txt generation
- ✅ Content-addressed locking
- ✅ Manifest rewriting
- ✅ No-op if nothing changed
Not Yet:
- ❌ Watch mode
- ❌ Vite/VitePress runtime integration
- ❌ React/Vue rendering
- ❌ Remote data fetching
- ❌ Partial page crawling
License
MIT
Author
Lee Whittaker • WhittakerTech
