Skip to content

Configuration Reference

Scavold has two configuration surfaces: .vitepress/config.js (build tool integration) and .cratly.config.yaml (site specification). They serve different concerns and intentionally do not overlap.


.vitepress/config.jsaugmentConfig()

js
import { defineConfig } from "vitepress";
import { augmentConfig } from "scavold/config";

export default defineConfig( await augmentConfig( rawConfig, options ) );

augmentConfig(rawConfig, options?) extends a standard VitePress user config with all Scavold features. It reads .cratly.config.yaml at the project root, derives srcDir, vite.publicDir, and other paths from it, and registers the image processing plugin and Markdown container extensions.

Required Vite option

Scavold's theme entry uses import.meta.glob, a Vite-specific transform. Because the package lives in node_modules, you must tell Vite to process it as source rather than as a pre-built external:

js
vite: {
    ssr: {
        noExternal: [ "scavold" ],
    },
},

Include this in the rawConfig object passed to augmentConfig. It is spread through and preserved in the final config.

rawConfig — Scavold-specific keys

These keys are recognised in addition to all standard VitePress config options:

KeyTypeDescription
mediaDirstringPath to the media folder relative to the project root. Overrides media_folder from .cratly.config.yaml. Useful when you want to specify a path in code rather than in the YAML.

All other folder paths (srcDir, vite.publicDir) are derived automatically from .cratly.config.yaml. Do not set them manually unless you need to override the defaults.

Defaults Scavold sets

metaChunk is turned on unless the site says otherwise. VitePress otherwise writes its page-to-hash map into an inline script whose content changes with every build — and under a content security policy naming script hashes, that one hash then has to reach the server with every deploy. When it does not, the browser blocks the script, the router finds no page chunks, and the client side of the site is dead while the pre-rendered HTML still shows. Extracted into a chunk, the only inline scripts left are VitePress's dark-mode and platform checks, whose hashes hold still until VitePress itself changes them.

The option is marked experimental upstream. A site that would rather not can set metaChunk: false in its own config, which wins.

options — integration overrides

The second argument controls how Scavold maps container names to components.

js
await augmentConfig( rawConfig, {
    containers: {
        section: "MySiteSection",   // override the built-in section component
        hero:    "MySiteHero",      // component for a site-specific container
    },
} )

The containers map keys are container names from .cratly.config.yaml (or the seven built-in sectioning names). Values are the Vue component names to use. When absent, Scavold derives the component name as Scavold{Name} automatically.

options.feed — an RSS feed for the site

Set it and Scavold writes an RSS 2.0 document at the end of every build, listing the pages of a section newest first.

js
await augmentConfig( {
    title: "Technical Blog",
    description: "Articles from time to time",
    sitemap: { hostname: "https://blog.example.com" },
}, {
    feed: { from: "de/blog", limit: 20 },
} )
KeyDefaultMeaning
fromthe whole sitePath of the section whose pages are announced, relative to the pages folder.
limit20Maximum number of entries. 0 announces all of them.
path"/feed.xml"Where the feed is written, relative to the site root.
titlethe site's titleTitle of the channel.
descriptionthe site's descriptionOne line describing it.
hostnamesitemap.hostnameOrigin the site is served from.

feed: true accepts every default, and an array declares several — one per language, say:

js
feed: [
    { from: "de/blog", path: "/de/feed.xml", title: "Neues" },
    { from: "en/blog", path: "/en/feed.xml", title: "News" },
]

A feed is read by its absolute links, so a hostname is required — usually the one sitemap.hostname already carries. Without either, no feed is written and the build says so.

What ends up in it

The feed is compiled from the site's page hierarchy, not from anything on a page. It is unrelated to :::pagelist: a page may appear in three lists and in the feed, in the feed alone, or in neither. What decides is:

  • the page lies below from — with no from, the whole site qualifies
  • it carries a date. Anything without one is left out rather than sorted last: a feed is a chronology, and an entry a reader cannot place in it is noise
  • its hide is neither true nor "feed". The value names one surface at a time, so hide: list keeps a page out of page lists and leaves it in the feed; hide: true is what an unfinished post wants — reachable by URL, announced nowhere
  • the section's own index page is not announced as an entry of itself

Entries carry the page's title, its link, its date and its excerpt as the description.

options.verify — reading the build's own result

At the end of every build Scavold reads what the build wrote and checks that the pages find what they name. Two findings end the build:

  • a page the generator listed but never wrote. Two pages whose paths flatten to one build identity overwrite each other, and on a file system that ignores letter case — macOS, Windows — this is the only trace they leave.
  • a missing script or stylesheet. A page in that state renders and then stays dead in the browser, which no build log mentions.

A third is reported and left alone: a link or image pointing at a file the build does not contain. That is a content mistake, and a site may make it deliberately by linking to a path its web server provides rather than its build.

Every reference is read the way a browser reads it, which for a relative one means against the address of the page holding it. [Website](./www.example.com) — a link that forgot its protocol — becomes a sibling page nobody wrote, and is reported as such.

A script or stylesheet is judged the same way, and there a relative reference is a mistake in itself: head: [[ "link", { rel: "stylesheet", href: "theme/extra.css" } ]] asks for /theme/extra.css on a top-level page and /de/theme/extra.css one level down, so at most one page depth can ever find it. Write such a reference site-absolute (/theme/extra.css), and put the file where the build carries it along — public/.

js
await augmentConfig( rawConfig, {
    verify: false,                 // check nothing
    verify: { content: false },    // check scripts and pages, say nothing about links
} )

All three classes are also written to .cratly/build-report.json, before the build is ended rather than after, so a failure does not take the reason with it. Each entry names the source file an author would open, not only the page the build wrote. Because Scavold finds a dead link there, ignoreDeadLinks defaults to true — see where a content mistake stops the pipeline for the report's shape and for deciding which branch a wrong link should block.

Neither half is on a timer or a sample: the check reads every built page. On a site of a few hundred pages it costs well under a second.

URLs

Every link a Scavold site generates — menus, breadcrumbs, page lists, the feed, redirect targets — names the file the page is built into:

PageURL
pages/blog/post.md/blog/post.html
pages/blog/index.md/blog/index.html
pages/index.md/index.html

A server would answer /blog/ and /blog/post too, but only after looking for something that is not there first — cratly's own s3-http tries the requested path, then an index.html inside it, then the path plus .html. Naming the file is a hit on the first attempt, and it is the spelling readers are used to.

The sitemap is rewritten to the same URLs, so a page is announced under the one spelling it is linked by. VitePress's router normalises the address bar to / and /blog/ while navigating, so readers still see the short form.


.cratly.config.yaml

The .cratly.config.yaml file at the project root is a framework-agnostic site specification. It describes the site — where content lives, how images are processed, what frontmatter fields exist — not how to build it. Scavold reads it at build time; the cratly online editor reads it to configure its UI.

Top-level structure

yaml
version: 0

pages_folder: pages
media_folder: media
static_folder: public

image_widths: [320, 640, 960, 1280, 1920]
image_sizes: "100vw"

excerpt_length: 200

containers:
  hero:
    label: "Hero section"

frontmatter_fields:
  image:
    type: media-file
    inherits: true

version

Type: number — currently 0

Tracks the version of this specification. Set this to the latest version available, which is currently 0. Version 0 is pre-stable; breaking changes may occur.

pages_folder

Type: string

Path to the folder containing all Markdown page files, relative to the project root. Scavold sets VitePress's srcDir to this value.

media_folder

Type: string

Path to the folder containing all author-managed media, relative to the project root. Every reference resolves against it — the path the cratly editor writes is relative to this folder, never to the page.

How a file is published depends on its type:

TypePublished as
Images (jpg, png, webp, avif, gif, tiff)Scaled variants plus WebP versions, referenced through <picture>
Everything else (PDFs, videos, archives, …)Copied verbatim into the static folder, keeping its path

References are rewritten to the published URL in three places: Markdown image syntax, Markdown links whose target is a media file, and container arguments declared as media-file — a video's src and poster, for instance. A link that does not point at a media file is left untouched, so ordinary page links, anchors and external URLs are unaffected.

Where a single URL is required rather than a set of variants — a video's poster, or a link to an image — the reference resolves to the largest generated variant.

static_folder

Type: string

Path to the folder served as the VitePress public directory (equivalent to vite.publicDir). Generated responsive image variants are written to {static_folder}/media/.

image_widths

Type: number[] — default [320, 640, 960, 1280, 1920]

Pixel widths for which Scavold generates responsive image variants. Sharp produces one WebP and one original-format file per width per source image.

image_sizes

Type: string — default "100vw"

Default CSS sizes descriptor applied to all responsive images that have no per-image override. Set this to the most common layout width on your site (e.g. the prose column width for a text-heavy site).

Per-image overrides are set via the Markdown image title field:

markdown
![Alt text](/photo.jpg "sizes=(min-width: 60rem) 50vw, 100vw")

excerpt_length

Type: number — default 200

Number of characters an auto-derived page excerpt is trimmed to, at a word boundary. Excerpts are what <ScavoldPageList> shows per entry when its teaser flag is set; a page that declares excerpt in its front matter is unaffected by this value.

Excerpts are derived for every page and travel with the page hierarchy into each built page, so a site with hundreds of pages pays for the value in payload size. Setting it to 0 switches the derivation off entirely, leaving only the excerpts pages declare themselves.

retired_urls

Addresses of the previous website that are not coming back — a campaign page, a product that no longer exists. They end up in the redirect manifest with status 410, which tells a search engine to drop the entry rather than keep asking.

yaml
retired_urls:
  - /aktion-2019.php
  - /produkte/alte-serie.html

An address is either retired or inherited by a page's aliases, never both; declaring it twice stops the build.


cratly-redirects.json

What the two declarations amount to, written into the build output at the end of every build — and omitted when a site declares neither:

json
{
	"$schema": "https://cratly.io/schema/redirects/v0.json",
	"specVersion": 0,
	"adapter": { "name": "scavold", "version": "0.2.0" },
	"rules": [
		{ "from": "/kontakt.html", "to": "/de/kontakt.html", "status": 301 },
		{ "from": "/aktion-2019.php", "status": 410 }
	]
}

The format is cratly's, not Scavold's — its grammar is published at https://cratly.io/schema/redirects/v0.json, and it names no server. Only a server-side redirect passes a ranking on, and which server that is cratly does not assume: the deploy step reads this file and writes what its target understands — a Caddy or nginx rule set, an .s3-http.config.yaml, a bucket's routing rules. The rules are sorted by address, so the file only changes when the declarations do.


containers

Type: object

Declares named Markdown container blocks beyond the seven built-in sectioning elements (section, aside, article, header, footer, nav, main).

Each entry can be a shorthand string (component name) or an object with editor metadata:

yaml
containers:

  # shorthand: maps :::hero to <MySiteHero>
  hero: MySiteHero

  # full declaration: maps :::callout to <ScavoldCallout> (default naming)
  callout:
    label: "Callout box"
    flags:
      - warning
      - info

  # with key–value arguments
  teaser:
    label: "Teaser card"
    kv:
      - background    # mapped as data-background attribute
      - link          # mapped as data-link attribute

The component name defaults to Scavold{Name} when not specified in the shorthand or via a component key in the object form.

Declare typed, editor-customizable properties under props — each becomes a control (text input, checkbox, number, media picker, page picker, select) in the cratly editor:

yaml
containers:
  hero:
    label: "Hero section"
    props:
      image:   { type: media-file, label: "Background image" }
      variant: { type: enum, values: [ plain, boxed ], default: plain }
      dark:    { type: boolean, label: "Dark mode" }

flags is shorthand for a set of boolean props; key–value pairs listed under kv become untyped text props. All three are consumed at runtime via useContainer() as class names / data-* attributes.

Scavold merges these declarations with its built-in section definitions and emits the result to .cratly/sections.json (the editor's section-type manifest) at build time. The full, framework-agnostic syntax and type vocabulary is specified in the cratly.io section-type docs.

Container arguments in Markdown follow the pattern key=value (quoted values supported) or plain words for flags:

markdown
::: teaser background=/media/hero.jpg link=/products/
Content here.
:::

frontmatter_fields

Type: object

Declares custom frontmatter fields so that editors know how to present them and adapters know how to process them.

yaml
frontmatter_fields:
  image:
    type: media-file
    inherits: true
    label: "Page image"
    hint: "Used at the top of the page and in social previews."

Fields not declared here are valid frontmatter — they are passed through without any special handling.

Entry properties

KeyRequiredTypeDescription
typeyesstringDetermines editor widget and adapter processing. See types below.
inheritsnobooleanWhen true, adapters walk the page hierarchy upward to find a value when the field is absent on the current page.
labelnostringHuman-readable label shown in editors.
hintnostringShort explanatory text shown below the input in editors.

Field types

ValueEditor widgetAdapter behaviour
textSingle-line text inputPassed through as-is
textareaMulti-line text inputPassed through as-is
booleanToggle / checkboxCoerced to boolean
numberNumeric inputCoerced to number
media-fileMedia folder file pickerPath resolved against media_folder
page-refPage tree pickerPath resolved against pages_folder
dateDate pickerISO 8601 date, YYYY-MM-DD
datetimeDate and time pickerISO 8601 timestamp with an offset

The same types are available for the props of a container, plus enum.

Extensibility

Tool-specific extension keys should be namespaced to avoid clashes:

yaml
_scavold:
  some_option: value

Compliant tools must ignore keys they do not recognise. The version key will be incremented when breaking changes are introduced.