@kenjura/ursa
v0.95.0
Published
static site generator from MD/wikitext/YML
Downloads
1,410
Maintainers
Readme
Ursa Static Site Generator
A flexible static site generator that converts Markdown, Wikitext, and YAML files into beautiful HTML sites.
There are many like it, but this one's mine.
Installation
As a CLI tool (global installation)
npm install -g @kenjura/ursaAs a library dependency
npm install @kenjura/ursaCLI Usage
After global installation, you can use the ursa command:
# Generate site once
ursa <source-directory>
ursa generate <source-directory>
# Development server with live reloading
ursa serve <source-directory>
# With custom meta and output directories
ursa content --meta=templates --output=dist
ursa serve content --meta=templates --output=dist --port=3000
# Using a whitelist file to filter which files are processed
ursa content --whitelist=my-whitelist.txt
ursa serve content --whitelist=my-whitelist.txt
# Using default meta and output directories (meta/ and output/)
ursa content
ursa serve contentIf not installed, you can run:
node bin/ursa (same args)CLI Commands
ursa [generate] <source>
Generate a static site once and exit.
ursa serve <source>
Start a development server that:
- Generates the site initially
- Starts an HTTP server to serve the output directory
- Watches source and meta directories for changes
- Automatically regenerates the site when files change
CLI Options
<source>- Source directory containing markdown/wikitext files (required)--meta, -m- Meta directory containing templates and styles (default: "meta")--output, -o- Output directory for generated site (default: "output")--port, -p- Port for development server (default: 8080, serve command only)--whitelist, -w- Path to whitelist file containing patterns for files to include--exclude, -e- Folders to exclude: comma-separated paths relative to source, or path to file with one folder per line--clean- Delete the.ursacache folder and clear output directory, forcing full regeneration--json-only, -j- Emit only the.jsondata files (generate command only)
JSON-Only Builds
--json-only builds the data and skips the site:
ursa content --json-only --output dataWhat it emits: every document's <name>.json and every directory's <dir>.json
record list — the same files a normal build writes, byte for byte.
What it skips: HTML, XML, images and their previews, meta/template assets, the
React runtime, per-folder CSS/JS bundles, static file copying (fonts, audio,
video, PDFs), the search and full-text indices, menu-data.json,
recent-activity.json, and auto-generated index pages.
This is for pipelines that consume ursa's JSON as data rather than publishing a
site — ingesting a docs repo into an application at build time, for example.
Everything skipped operates on the assembled page; the JSON's bodyHtml is
the pre-template render, which none of those steps touch. That is why the output
is identical rather than merely similar.
Mixing modes against one source tree is safe. The two share the .ursa hash
cache, but each asks only for the outputs its own mode emits, so a full build
following a JSON-only build still writes the HTML it is missing. A JSON-only
build does not write the dependency graph or seed the watch cache, so it cannot
degrade a later serve.
Whitelist File Format
The whitelist file is a plain text file where each line specifies a pattern for files to include. Patterns can be:
# Comments start with # and are ignored
# Empty lines are also ignored
# Full absolute paths
/full/path/to/file.md
# Relative paths from source root
character/classes/psion.md
character/classes/
# Directory paths (include trailing slash to match directories)
spells/
documentation/
# Just filenames (matches anywhere in the source tree)
index.md
README.md
# Partial path matches
important-document
classes/wizardExclude Option
The --exclude option allows you to skip certain folders during generation. This can be specified as:
- Comma-separated paths directly on the command line:
ursa content --exclude=archive,drafts,old-content
ursa serve content --exclude=test,backup- A file path containing one folder per line:
ursa content --exclude=exclude-list.txtThe exclude file format is similar to the whitelist:
# Comments start with # and are ignored
# Empty lines are also ignored
# Folders to exclude (relative to source)
archive
drafts
old-content/v1
test/fixturesIgnoring a Folder
To keep a folder out of the build permanently — working notes, prompt
scratchpads, raw source material — put a config.json in it:
{
"hidden": true
}The folder and everything beneath it then take no part in the build:
- no HTML is rendered from its documents
- its images, fonts and other static assets are not copied to the output
- it does not appear in the sidebar menu, in any auto-index, or in breadcrumbs
- its text is not added to the search index
ursa servereturns 404 for anything under it, matching whatgenerateproduces
The files stay where they are in the source tree; the site simply behaves as though they were not there.
This differs from --exclude in scope and in lifetime: --exclude is a flag on
one invocation, useful for a one-off or a per-environment build, while
config.json travels with the content and applies to every build and every
person who checks the repo out.
config.json accepts a few other keys, all of which apply to the folder it
sits in:
| Key | Type | Meaning |
| --- | --- | --- |
| hidden | boolean | Ignore this folder and its subtree entirely (above) |
| label | string | Name to show for this folder in menus and indices |
| icon | string | URL of an icon to show beside it in the menu |
| openMenuItems | string[] | Root config.json only: folders to expand by default |
Large Workloads
For sites with many documents (hundreds or thousands), you may need to increase Node.js memory limits:
# Increase heap size to 8GB for large sites
node --max-old-space-size=8192 $(which ursa) serve content
# Or use the npm scripts
npm run serve:large content
npm run generate:large content
# You can also set environment variables to tune batch processing
URSA_BATCH_SIZE=25 ursa serve content # Process fewer files at once (default: 50)Environment Variables for Performance Tuning:
URSA_BATCH_SIZE- Number of files to process concurrently (default: 50). Lower values use less memory but are slower.NODE_OPTIONS="--max-old-space-size=8192"- Increase Node.js heap size for very large sites.
Library Usage
ES Modules (recommended)
import generateSite, { generate, serve } from '@kenjura/ursa';
// One-time generation using the default export
await generateSite({
source: './content',
meta: './meta',
output: './dist',
whitelist: './my-whitelist.txt' // optional
});
// One-time generation using the named export (matches internal API)
await generate({
_source: './content',
_meta: './meta',
_output: './dist',
_whitelist: './my-whitelist.txt' // optional
});
// Development server with live reloading
await serve({
_source: './content',
_meta: './meta',
_output: './dist',
port: 3000 // optional, defaults to 8080
});CommonJS
const generateSite = require('@kenjura/ursa').default;
const { generate, serve } = require('@kenjura/ursa');
// Usage is the same as aboveLibrary Functions
generateSite({ source, meta, output })
Default export. Generates the site once with user-friendly parameter names.
generate({ _source, _meta, _output })
Named export that matches the internal API. Generates the site once.
serve({ _source, _meta, _output, port? })
Starts a development server with live reloading:
- Generates the site initially
- Starts HTTP server on specified port (default: 8080)
- Watches for file changes in source and meta directories
- Automatically regenerates when changes are detected
Project Structure
Your project should have the following structure:
your-project/
├── source/ # Source files (markdown, wikitext, yaml)
│ ├── index.md # Required: main page
│ └── ...
├── meta/ # Templates, styles, and configuration
│ ├── templates/
│ ├── styles/
│ └── ...
└── output/ # Generated site (created automatically)Styling a Site
Drop a style.css (or style-ursa.css, or _style.css) in any source folder and it applies to every document in that folder and below. Every such file from the docroot down to the document's own folder is included, nearest last, so a deeper file overrides a shallower one.
Ursa's own stylesheet is scoped and layered so that your CSS wins without a fight:
- Document content — headings, images, figures, the article box itself — is styled inside
@layer ursa.content. Your stylesheet is unlayered, and an unlayered rule beats a layered one no matter how specific it is, so a plainh1 { position: static }or#main-content { width: 1000px }overrides whatever Ursa sets. Noarticle#main-content h1escalation, no!important. - Chrome — the top bar, menus, widgets, search, footer, breadcrumbs, image hover controls and lightbox — is scoped but not layered, so a broad rule like
a { color: … }in your stylesheet cannot bleed into the navigation. Overriding chrome works as it always did: use a more specific selector than the built-in one. - Nothing built in reaches into content it shouldn't. Chrome rules stop at the article's children; content rules stop at the article's edge.
class="ursa-unstyled"on any element puts it and its descendants outside every one of Ursa's scopes — none of Ursa's CSS applies in there at all, and the lightbox leaves images in there alone.
This uses the CSS @scope and @layer rules: Chrome 118+, Safari 17.4+, Firefox 128+.
Styling one widget at a time
The two widget panels are shared containers — the right-hand one holds the table
of contents, search and profile in turn — so styling .widget-dropdown styles
all of them at once. While a panel is open it carries data-active-widget naming
whichever widget is showing, and the attribute is removed when it closes, so a
site can give each one its own treatment:
/* Only the table of contents; search and profile keep the default panel. */
.widget-dropdown[data-active-widget="toc"] {
background: rgba(20, 24, 28, 0.78);
backdrop-filter: blur(10px);
}Both panels carry it: #widget-dropdown for the right-hand widgets (toc,
search, profile) and #widget-dropdown-left for the left-hand ones
(recent-activity, suggested). The value is the widget's data-widget name.
Ursa uses this hook itself, to lay the TOC out along the bottom of the viewport
on a narrow screen.
Auto-Index Generation
Ursa automatically generates index pages for folders that don't have one. You can also explicitly control auto-index generation in your index documents using frontmatter:
---
title: My Section
generate-auto-index: true
auto-index-depth: 2
auto-index-position: bottom
---
# Welcome to My Section
This is the introduction to my section. The auto-generated file listing will appear below.Auto-Index Frontmatter Options
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| generate-auto-index | boolean | false | When true, generates an auto-index listing for this folder |
| auto-index-depth | number | 1 | Recursion depth: 1 = current folder only, 2 = include subfolders, etc. |
| auto-index-position | 'top' | 'bottom' | 'top' | Where to insert the auto-index relative to document content |
Examples
Basic auto-index (top of page):
---
generate-auto-index: true
---Deep auto-index at bottom:
---
generate-auto-index: true
auto-index-depth: 3
auto-index-position: bottom
---
# Section Overview
Here's some content explaining this section...Developing
For development on ursa itself:
npm run serveWatches source and meta folder; on change, writes HTML to build folder.
Environment Variables
SOURCE: path to the source folder, default${cwd}/sourceMETA: path to the meta folder, default${cwd}/metaBUILD: path to the build folder, default${cwd}/build
Running Locally
npm startGenerates the site once using default directories.
Requirements
SOURCE folder should have at least an index.md in it.
Link logic
Links are allowed to be extensionless. Link resolution works as follows:
- If link has an extension, look for exact match, and 404 if not found
- If link has no extension:
- Look for exact match with .md, .txt, .yml extensions (in that order)
- If not found, assume the path is a folder, and look for:
- index.md, index.txt, _index.md, _index.txt
- home.md, home.txt, _home.md, _home.txt
- (folder name).md, (folder name).txt
- If any of these are found, link to that file's html version
- If still not found, 404
