Skip to content

Page metadata and share images

Ledger and Crosswave provide page titles, descriptions, and one static 1200×630 share image per site. Plain defaults to a minimal directory title without metadata or generated assets. Set metadata explicitly to enable text metadata in Plain; supply metadata.image explicitly if you also want an image. Each image uses a fixed theme palette and bundled Source font family, with repository name and theme name above the site title.

import { createDefaultTheme, defineConfig } from "@vp-tw/dirwell";
export default defineConfig({
root: "files",
theme: createDefaultTheme({
project: { name: "Downloads", repositoryUrl: "https://github.com/you/downloads" },
}),
metadata: { siteUrl: "https://example.com/downloads/" },
});

Save this as dirwell.config.ts after installing Dirwell, then run npx dirwell build files -o dist. Replace the sample repository and deployment URLs with your own. The CLI source argument remains explicit.

The homepage title is Downloads; a child page is docs · Downloads. The default root description explains the archive, such as Browse files in Downloads. Archive totals: 3 folders · 6 files. Child descriptions name the destination and use direct folder counts, such as Browse docs in Downloads. This folder: 2 files. The share image stays the same while browsing folders, and its default count line is labeled Archive totals:. Rebuilding refreshes its totals.

The example sets the display name once in project.name. If omitted, page and image names come from the built-in theme’s project settings. metadata.siteName controls page/image naming; project.name still controls the theme header or attribution. Set both when you want them to match. Theme names appear in the image’s upper-right corner rather than page titles.

Set siteUrl to the actual deployed explorer root, including its deployment base. Dirwell emits absolute og:image, og:url, and canonical URLs when it is provided. Without it, image links remain portable relative URLs and canonical/og:url are omitted; set it before relying on social preview crawlers. A repository URL is not a deployment URL. Image-enabled pages also emit twitter:card=summary_large_image and explicit Twitter title, description, image, and image-alt tags. Disabling the image omits these image-card tags. Social platforms may crop images, omit descriptions, and cache previews independently of your build; these tags do not guarantee a particular platform layout.

The image presets use Ledger’s light paper, Plain’s white background, and Crosswave’s Azure background. Visitor appearance settings and Crosswave’s configured color do not recolor these static presets. Supply an image or image callback for a different image design.

The order is folders, files, links. Zero counts are omitted; all zero becomes Empty folder.

  • site counts the selected source tree recursively, after include/exclude rules. The root itself and generated HTML, fonts, search assets, and images are excluded. Preserved source HTML remains a source file.
  • directory counts immediate displayed entries, without descendants or the parent-navigation row.
  • A symbolic link counts once as a link, regardless of target type or availability. Following it does not count its target again in site totals. Broken links still count as links; the file list displays their status.
  • Hard-linked source files count by listed path, as ordinary files. Special filesystem entries do not contribute to these three counts.
import { defineConfig } from "@vp-tw/dirwell";
export default defineConfig({
root: "files",
metadata: {
title: "Design kit",
description: "Brand assets and reference documents",
image: { source: "./cover.png", outputPath: "og/cover.png", alt: "Design kit cover" },
},
});

Place cover.png beside the config for this example.

A fixed title is used on every page. Fixed title/description values also feed the default generated image when you leave image unset. Text callbacks affect page metadata; the default image remains a whole-site image with qualified archive totals. Default image alternative text includes its displayed title and description. To create per-folder images, provide an image callback.

image accepts a Node File, a local path string, a file: URL, or { source, outputPath?, alt? }. Local path strings resolve from the config/project directory; programmatic generation can set metadataBaseDirectory explicitly, otherwise it uses the working directory. Remote image downloads are not performed. Supported image filename extensions are PNG, JPEG, WebP, and GIF.

File.name is a filename hint, not a source path or published output path. Without outputPath, Dirwell uses a content hash under __dirwell/metadata/ and deduplicates identical images. Explicit paths are relative to the generated output root and must not overlap source entries, theme output, or reserved internal assets. Different bytes at the same explicit path fail the build. Use a separate og/ directory. Set image: false to disable image generation while retaining text metadata.

import {
createCrosswaveTheme,
createShareImage,
defineConfig,
describeContent,
} from "@vp-tw/dirwell";
export default defineConfig({
root: "files",
theme: createCrosswaveTheme({ project: { name: "Design kit" } }),
metadata: {
description: ({ directory }) => describeContent(directory),
image: async ({ directory, site, description }) => ({
source: await createShareImage({
theme: "crosswave",
repositoryName: site.repositoryName,
title: directory.relativePath || site.name,
description,
directoryPath: directory.relativePath,
}),
outputPath: `og/${directory.relativePath || "root"}.png`,
}),
},
});

Open the dynamic example or inspect its complete config. The small tree shows nested-folder results without a large demo library.

Title resolves first, then description, then the image callback receives both resolved strings. Callbacks may return promises and run at build time for each generated page. They receive public metadata, not absolute filesystem paths or owner IDs. A callback/image error stops generation before replacing the existing output.

Ledger uses Source Sans 3 Regular, with Source Code Pro Regular for symlink target paths. Plain uses the browser’s system serif font without font files. The optional createShareImage({ theme: "plain", ... }) renderer still uses bundled Source Serif 4 Regular. Crosswave uses Source Sans 3 Regular with Light for its brand title. WOFF2 files are served with Ledger and Crosswave; PNG rendering uses pinned OTF sources with system-font discovery disabled. Normal builds need no font download. The source versions and SIL-OFL licenses ship in the package. Themes that emit fonts include a source-fonts-NOTICE.txt asset alongside them; minimal Plain output has neither.

The bundled fonts support their upstream character repertoire; built-in text is English and no CJK font bundle or localization system is included. Use a custom image renderer/font set for languages outside that repertoire. Browser rasterization can differ across environments; pinning font files does not guarantee identical pixels on every device.

Theme packages can supply metadataDefaults.image as a fixed source or an async callback, with the same input/output types as caller metadata.image. Caller settings take precedence, including false, without running the theme callback. imageTheme is an optional built-in fallback preset; omit both defaults for text-only metadata. Set metadataDefaults.enabledByDefault: false to resolve these defaults only when the consumer explicitly supplies metadata; an omitted field preserves the existing default-on behavior. Theme-local files should use new URL("./cover.png", import.meta.url) or a File; a relative path string still resolves from the consumer config directory. See the separately packed theme example.

Assign the following object to your theme factory’s metadataDefaults. This is theme-author code; consumer configuration still uses metadata.image. The callback below creates per-page images; the separately packed theme example instead uses whole-site totals.

import { createShareImage, type ThemeMetadataDefaults } from "@vp-tw/dirwell";
export const imageDefaults: ThemeMetadataDefaults = {
siteName: "Downloads",
repositoryName: "you/downloads",
// Reuse a preset here, or return a File from your own image renderer.
image: ({ site, title, description }) =>
createShareImage({ theme: "plain", title, description, repositoryName: site.repositoryName }),
};

Custom themes can render context.metadata?.head inside their head element, or use its resolved title/description. Metadata does not inject HTML into arbitrary custom themes automatically. Ledger PageShell overrides receive metadata too. Crosswave updates managed metadata tags during persistent folder navigation, including browser history restoration.

Plain no longer emits fonts, a share image, social metadata, or a repository footer by default. Its document title is Index of / (or the current folder path), and native colors follow the system appearance. Directory links, breadcrumbs, file sizes, UTC timestamps, and safe symlink handling are unchanged.

Provide project to createPlainTheme() to opt into the repository footer; a configured project name also supplies the fallback title. Provide metadata to opt into metadata using that project name. An empty metadata: {} enables text only. To restore an image, provide a local image or an explicit createShareImage({ theme: "plain", ... }) callback. This adds the requested image asset and social-card tags without adding browser fonts or JavaScript.

These changes reduce generated output and browser requests; installing Dirwell still includes the dependencies and assets needed by the other themes and optional image renderer.