# rhedyn a quick and dirty static site generator with intelligent caching. Run this script from a directory containing markdown, templates and styles and a `dist/` directory will be created containing the rendered-out static assets ready for deployment. While the defaults should work for most (of my) use cases, you can configure behaviour using a `rhedyn.config.js` in the directory where you run the tool, or pass a path to a config using `-c /path/to/config.js` or `--config path/to/config.js`. "Rhedyn" is Welsh for "fern", and is pronounced a bit like "read in". ## Installation and usage The default config will look for `.md` files in the `markdown/` directory, `.scss` files in the `styles/` directory, and handlebars (`.hbs`) files in the `templates/` directory. It also processes images, SVG icons, static files, GPX routes, and generates favicons, an RSS feed, and a sitemap. The styles will be compiled using `sass` and output to a matching path in the `dist/` directory with a `.css` file extension. These file paths are made available to the template renderer, for inclusion as whatever tags you see fit. Markdown is compiled using `marked` with the `marked-code-preview` extension enabled, then passed to the template renderer in the `content` property. Images referenced in markdown are automatically processed and converted to responsive srcsets if matching images are found in the images task. Mermaid fences are rendered to `.mermaid` blocks, and `renderMarkdownWithTemplate` injects the Mermaid runtime automatically when a page contains a Mermaid diagram. Raw HTML in markdown is stripped by default; set `opts.markdown.allowHtml` to `true` to allow it. Once the markdown content and template have been rendered out, the resulting `.html` file is minified and output to `dist/`, with the matching path (e.g. `markdown/recipes/soup.md` would be rendered to `dist/recipes/soup.html`). Usual stuff applies - install as a module locally or globally and call the default function: `npx rhedyn` ``` "scripts": { "build": "rhedyn" } ``` You can also build a self-contained executable if you like chunky binaries with `npm run build`. That'll output to the `dist/` directory, and you can then put that binary wherever you like (probably somewhere in your $path). If you want the binary to carry a specific config module and the action handlers it imports, build it with: `npm run build-as-tool -- --config ./rhedyn.config.js` That build path stages a generated module tree before running `nexe`, so the config module and any action code it imports are bundled into the binary and do not need to ship alongside it afterwards. The content files referenced by those tasks are not embedded: markdown, Sass, templates, partials, images, and other source files are still read from disk at runtime. If you want a different binary name without passing a full output path, add `--name`, for example: `npm run build-as-tool -- --config ./rhedyn.config.js --name md2doc` ## Caching Rhedyn includes intelligent caching that tracks both file dependencies and configuration state. Tasks are only re-run when: - Input files have changed (detected via file hashes) - Configuration has changed (detected via state hashes) - Output files are missing (can be skipped by setting `opts.ignoreExisting` to `true`) Cache files are stored in `.cache/` by default and can be disabled by setting `cacheDir: false` in your config. If output file checks are skipped with `ignoreExisting`, only files that have changed inputs will be output. `opts.includeStateValues` controls whether cache files store state values for debugging. It defaults to `true` and can be set to `false` to reduce cache file size. ## Configuration By default, rhedyn will look for `rhedyn.config.js` in the current directory and fall back to the default if not found. You can override this behaviour using the `-c /path/to/config.js` or `--config path/to/config.js` CLI switches. For single-binary builds, `npm run build-as-tool -- --config /path/to/config.js` embeds that config module into the executable. Add `--name my-tool` to write the binary to `dist/my-tool` by default. Because the config executes inside the binary at runtime, expressions like `process.cwd()` still resolve when the tool is run, not when it is built, and the original config file can be deleted after the build completes. A working example of how to configure can be found in `src/defaults.js`, with the actions separated out to `src/actions/` for tidiness. Your `rhedyn.config.js` should export an object with "tasks" and "opts". These are detailed below. If you want to extend the default config, it can be imported as `defaultConfig` from this package. #### opts The `opts` object in your config is for anything related to the project as a whole, rather than individual tasks. That means things like the base output directory, cache directory, site metadata, etc. It'll be passed to the action function as a key on the `meta` object. `opts` can also have an `include` property, an object containing task keys (e.g. "styles") and additional glob patterns to include for that task. The path can be anywhere, not just local to the project - I use it to include basic typography styles that I know I'll want in every project, but it could just as easily be used to produce a sort-of "theme" that could be shared across multiple projects. ```javascript opts: { outDir: 'dist/', runDir: process.cwd(), cacheDir: '.cache', clean: true, ignoreExisting: false, includeStateValues: true, logLevel: 'debug', include: { styles: [{ pattern: '~/.rhedyn/styles/*.scss' }] }, markdown: { allowHtml: false, mermaid: { scriptSrc: "https://cdn.jsdelivr.net/npm/mermaid@11.13.0/dist/mermaid.min.js", initialize: { theme: "default" } } }, site: { name: "My Website", shortName: "My Site", description: "A website generated from files using Rhedyn", author: "Your Name", url: "https://example.com", language: "en-GB", backgroundColor: "#ffffff", themeColor: "#000000" } } ``` #### tasks Tasks can be either individual task objects or arrays of task objects that run in parallel. Tasks in different array elements run sequentially, allowing you to control dependencies between task groups. Example structure: ```javascript tasks: [ [ // These tasks run in parallel { key: "styles", name: "Styles", ... }, { key: "icons", name: "Icons", ... }, { key: "images", name: "Images", ... } ], // This task runs after the above group completes { key: "pages", name: "Pages", ... } ] ``` Each task object should look something like this: ```javascript { key: "styles", name: "Styles", action: compileSass, jobConfig: { inputFiles: [{ pattern: "styles/**/*.scss", ignore: "**/_*.scss" }], stripPaths: ["styles/"], outputDir: "static/styles/", outputFileExtension: ".css" }, actionConfig: {} } ``` `compileSass` enables Sass package imports by default through Sass's official `pkg:` importer. Prefer `@use` rather than deprecated `@import`: ```scss @use "pkg:@fontsource/overlock" as overlock; @use "pkg:@fontsource/atkinson-hyperlegible" as atkinson; @use "pkg:@fontsource/source-code-pro" as code; h1, h2, h3 { font-family: "Overlock"; } body { font-family: "Atkinson Hyperlegible"; } code, pre { font-family: "Source Code Pro"; } ``` When imported stylesheets emit relative `url(...)` references, `compileSass` copies those assets into the output tree and rewrites the CSS to site-rooted paths. The default asset routing sends font files to `static/fonts/`, and other referenced assets to `static/assets/`. You can override that per task: ```javascript actionConfig: { includeCssInDetail: true, assetOutputDirsByExtension: { ".woff2": "static/fonts/", ".woff": "static/fonts/", default: "static/assets/" } } ``` If you need a downstream template to inline styles, set `actionConfig.includeCssInDetail: true`. That adds the compiled CSS to `detail.css` for that stylesheet resource only. The default is `false` so CSS text is not carried in action state or cache files unless you explicitly opt in. **Task Properties:** - `key`: Task identifier used in state/resources (required) - `name`: Human-readable task label used in logs (optional, defaults to `key`) - `action`: Function that processes the files (required) - `jobConfig`: Config for file/state selection, expansion, output paths, and caching (see below) - `actionConfig`: Config passed to the action (plus computed fields like `filePath` and `fileOutputPath`) **Input File Patterns:** ```javascript jobConfig: { inputFiles: [ { pattern: "styles/**/*.scss", ignore: "**/_*.scss" }, { pattern: "images/*.jpg" }, { pattern: "static/*" } ] } ``` **jobConfig options:** - `inputFiles`: Array of glob objects (`{ pattern, ignore?, dot? }`) used for file expansion. - `stateSelectors`: Array of dot-paths (e.g. `resources.markdown`) to expand from `meta` using task `key` values. - `expand`: When `false`, run a single job with aggregated inputs instead of expanding per file/state item. - `stripPaths`: Array of path prefixes removed from inputs when building output paths (supports `~`). - `outputDir`: Output directory relative to `opts.outDir` for generated files. - `outputFileExtension`: New extension appended to output files (e.g. `.html`). - `outputFileName`: Base filename used when building a single output path for state tasks. - `buildFilePath`: When `true`, compute and pass `fileOutputPath` into the action for state tasks. - `buildIndexList`: When `true` (default), generate an `index.html` in the output dir listing all index pages (state expansion only). - `itemsPerPage`: Page size for state expansion pagination (defaults to `opts.itemsPerPage`). - `skipCache`: When `true`, disables cache usage for this job. - `deps`: Extra cache dependencies `{ paths?: string[], state?: string[] }` added to the job fingerprint. - `logLevel`: Override log level for this task only. - `jobId`: Override the job ID used for logging and caching. #### actions An action is a function that receives an object with `config`, `jobConfig`, and `meta` properties and returns an object describing what was processed. An action that returns a `ref` will have its `detail`, `paths`, `ref` and `fromCache` properties made available in `meta.resources`. For expanded tasks (multiple results), the `ref` is used as the key under the task key. For single-result tasks, the result is stored directly under the task key: ```javascript { ...meta, resources: { [task.key]: { [jobResult.ref]: { detail, paths, ref, fromCache } } // or, for single-result tasks: // [task.key]: { detail, paths, ref, fromCache } } } ``` The action function signature: ```javascript async function myAction({ config, jobConfig, meta }) { // config is the actionConfig plus computed properties: // - filePath: path to the input file being processed // - fileOutputPath: calculated output path // - fileOutputDir: directory for the output file // - inputs/pagination/stateKey when expanding from state // - key/name for the current task identity // // jobConfig contains selection/expansion config (inputFiles, stateSelectors, etc.) // meta contains: // - opts: global configuration // - resources: results from previously completed tasks, structured as described above // Process the file... return { detail: {}, // Optional: any metadata about the processed file paths: [outputPath], // Array of output file paths (relative to outDir) deps: { // Optional: dependencies for caching paths: [inputFile1, inputFile2], // File dependencies state: [] // State dependencies (tracked automatically) }, ref: "unique-identifier" // Optional: reference key for this result } } ``` **Built-in Actions:** The following actions are available from `actions`: - `compileSass`: Compiles SCSS files to compressed CSS - `renderMarkdownToHtml`: Parses markdown into HTML and returns it as task detail - `renderMarkdownToMeta`: Parses markdown into frontmatter metadata only - `renderTemplate`: Renders Handlebars templates with the current task state - `renderIndex`: Renders index pages and index list pages - `renderMarkdownWithTemplate`: Renders markdown with Handlebars templates, includes frontmatter support - `optimiseSvg`: Optimizes SVG files using SVGO - `copy`: Copies files without processing - `imageToWebP`: Converts images to WebP with multiple sizes for responsive images - `generateFavicons`: Generates favicon sets and web app manifests - `generateRss`: Generates an RSS feed from state inputs - `generateTaxonomy`: Builds grouped/sorted lists from state inputs - `generateRouteAssets`: Sanitizes GPX files and renders responsive route preview images - `generateSitemap`: Generates `sitemap.xml` from rendered page state entries **Resources and Cross-Task References:** Processed files are made available to subsequent tasks via `meta.resources[taskKey][ref]`. For example, the image action makes processed images available to the markdown renderer for automatic srcset generation. Some examples can be found in `src/actions/`, and you can find utility functions exported from this package as `utils`. The sample actions are also exported from this module as `actions`. ## Default Tasks The default configuration includes: 1. **Parallel processing group:** - `images`: Converts JPG images to WebP with multiple sizes - `routes`: Sanitizes GPX files and renders responsive route previews over OpenStreetMap tiles - `styles`: Compiles SCSS files to CSS - `icons`: Optimizes SVG icons - `static-files`: Copies static files - `favicons`: Generates favicon sets from source images 2. **Sequential tasks:** - `posts-markdown`: Parses post frontmatter metadata (no HTML rendering) - `markdown`: Parses general frontmatter metadata (no HTML rendering) 3. **Parallel processing group:** - `author-taxonomy`: Builds author groupings from post entries - `tag-taxonomy`: Builds tag groupings from post entries - `posts-latest`: Builds a sorted list of recent posts - `includes`: Renders template includes to HTML 4. **Parallel processing group:** - `render-pages`: Renders standard pages from markdown files - `render-post-pages`: Renders post pages from posts markdown files - `render-author-indexes`: Renders author index pages and index list - `render-tag-indexes`: Renders tag index pages and index list - `render-posts-home`: Renders the posts index page 5. **Sequential task:** - `sitemap`: Generates `sitemap.xml` from rendered page state entries 6. **Sequential task:** - `rss`: Generates `rss.xml` from the sorted posts metadata ## Route Blocks In Markdown Route blocks can be embedded in markdown by GPX basename: ```markdown ::route mountain-loop ``` This renders a block containing the route preview image and a link to download the sanitized GPX generated from `gpx/mountain-loop.gpx`. Route blocks render semantic HTML with stable `.route-block*` classes, but no injected CSS or inline styling. The consuming app is responsible for presentation. ## Mermaid In Markdown Mermaid diagrams can be embedded with fenced code blocks: ~~~markdown ```mermaid flowchart LR A[Start] --> B[Finish] ``` ~~~ The shared markdown renderer converts these blocks to `.mermaid` elements. When using `renderMarkdownWithTemplate`, Rhedyn injects the Mermaid runtime automatically only on pages that contain Mermaid diagrams. By default the runtime is loaded from jsDelivr. To self-host it or pin a different build, override `opts.markdown.mermaid.scriptSrc`. Any `opts.markdown.mermaid.initialize` values are passed through to `mermaid.initialize(...)`. ## Logging Rhedyn includes comprehensive logging with configurable levels: - `silent` (1): No output - `error` (2): Errors only - `warn` (3): Warnings and errors - `info` (4): General information (default) - `debug`: Detailed debugging information - `trace` (6): Maximum verbosity Set the log level in your config: ```javascript opts: { logLevel: 'debug' } ```