Skip to content
Packages

@docvia/schema

Frontmatter handling: YAML extraction, Zod-based validation, and TypeScript interface codegen.

@docvia/schema owns everything related to frontmatter in docvia. It splits the YAML frontmatter block from a markdown file, validates it against a Zod schema (extensible per project), and generates TypeScript type strings so the compiler can emit a precisely typed Frontmatter interface.

The package depends on @docvia/ir (for the FrontmatterData type and the docviaError class), yaml for parsing, and zod. It imports Zod through the zod/v3 compatibility entry so its behavior is pinned to the Zod v3 API surface regardless of the installed major version.

Installation

pnpm add @docvia/schema

Requires Node.js >=20.0.0. ESM only.

Exports

@docvia/schema exposes a single entry point.

SubpathModuleContents
../dist/index.mjsextractFrontmatter, validateFrontmatter, zodSchemaToFrontmatterTs, DocPageSchema, and the ExtractedFrontmatter type.
import {
  extractFrontmatter,
  validateFrontmatter,
  zodSchemaToFrontmatterTs,
  DocPageSchema,
} from "@docvia/schema";
import type { ExtractedFrontmatter } from "@docvia/schema";

Frontmatter extraction

ExtractedFrontmatter

interface ExtractedFrontmatter {
  readonly data: Record<string, unknown>;
  readonly content: string;
  readonly bodyOffset: number;
}
FieldTypeDescription
dataRecord<string, unknown>The parsed YAML object. Empty when the file has no frontmatter.
contentstringThe markdown body with the frontmatter block removed.
bodyOffsetnumberThe 1-based line number where the body begins, for accurate error reporting.

extractFrontmatter

function extractFrontmatter(raw: string): ExtractedFrontmatter

Splits a raw file string into its frontmatter object and markdown body. The algorithm:

  1. Splits the input into lines, tolerating both \n and \r\n line endings.
  2. If the first non-empty line is not exactly ---, the file is treated as having no frontmatter: data is {}, content is the whole input, and bodyOffset is 1.
  3. Otherwise it scans for the closing --- delimiter.
  4. The YAML between the delimiters is parsed with the yaml package.
  5. The body is everything after the closing delimiter; bodyOffset is set to the line where it starts.

Edge cases:

  • An empty or whitespace-only frontmatter block yields data: {} and the body that follows.
  • If the parsed YAML is not an object (for example, a bare scalar), data falls back to {}.

Failure modes, both of which throw a docviaError with code SCHEMA_ERROR:

ConditionError messageLocation
Opening --- with no closing ---Unclosed frontmatter: missing closing ---line 1, column 1
Malformed YAML between the delimitersInvalid YAML in frontmatter: <reason>line 2, column 1

The YAML-parse error also chains the underlying parser error through docviaError.cause.

Schema and validation

DocPageSchema

The base Zod schema every docvia page is validated against. It is a .passthrough() object, so fields beyond the known ones are preserved rather than stripped.

const DocPageSchema = z
  .object({
    title: z.string().min(1, "Title is required"),
    description: z.string().default(""),
    slug: z.string().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
    order: z.number().optional(),
  })
  .passthrough();
FieldZod ruleResulting behavior
titlez.string().min(1)Required; empty strings rejected.
descriptionz.string().default("")Optional input; defaults to "".
slugz.string().optional()Optional.
tagsz.array(z.string()).default([])Optional input; defaults to [].
draftz.boolean().default(false)Optional input; defaults to false.
orderz.number().optional()Optional.

validateFrontmatter

function validateFrontmatter(
  raw: Record<string, unknown>,
  filePath?: string,
  extensionSchema?: z.ZodObject<z.ZodRawShape>,
): FrontmatterData

Validates a raw frontmatter object and returns a typed FrontmatterData.

ParameterTypeDescription
rawRecord<string, unknown>The object returned by extractFrontmatter as data.
filePathstring | undefinedFile path attached to any thrown error for context.
extensionSchemaz.ZodObject | undefinedAn optional project schema merged into DocPageSchema before validation.

When extensionSchema is supplied, it is merged into the base schema with DocPageSchema.merge(extensionSchema), so project-defined fields are validated alongside the built-in ones. Validation runs through Zod's safeParse. On failure, a docviaError with code SCHEMA_ERROR is thrown; its message lists every issue as an indented path: message line, and the error carries filePath plus a { line: 1, column: 1 } location. On success the validated, defaulted data is returned as FrontmatterData.

TypeScript codegen

zodSchemaToFrontmatterTs

function zodSchemaToFrontmatterTs(
  extensionSchema: z.ZodObject<z.ZodRawShape>,
): string

Converts a project's Zod extension schema into a TypeScript object-type literal string. The compiler emits this into types.d.ts so consumers get a precisely typed Frontmatter interface instead of the default union-of-literal-values inference.

The function first merges the extension schema with DocPageSchema, then walks every field of the merged shape and maps each Zod type to its TypeScript equivalent:

Zod typeEmitted TypeScriptOptionality
ZodOptionalinner typefield becomes optional (?)
ZodDefaultinner typefield stays required (a default always produces a value)
ZodNullableT | nullinherits inner optionality
ZodStringstringrequired
ZodNumbernumberrequired
ZodBooleanbooleanrequired
ZodLiteralthe literal value (JSON-encoded)required
ZodEnumunion of quoted membersrequired
ZodArrayArray<T>required
ZodUnionunion of member typesrequired
anything elseunknownrequired

The output also appends an [key: string]: unknown; index signature, mirroring the open-ended shape of FrontmatterData. The result is a brace-delimited type literal such as:

{
  title: string;
  description: string;
  slug?: string;
  tags: Array<string>;
  draft: boolean;
  order?: number;
  author: string;
  category?: "guide" | "reference";
  [key: string]: unknown;
}

Usage example

A complete extract-then-validate flow, including a project extension schema:

import { z } from "zod/v3";
import {
  extractFrontmatter,
  validateFrontmatter,
  zodSchemaToFrontmatterTs,
} from "@docvia/schema";
import { docviaError } from "@docvia/ir";

// Project-defined extra frontmatter fields.
const extensionSchema = z.object({
  author: z.string(),
  category: z.enum(["guide", "reference"]).optional(),
});

function processFile(rawSource: string, filePath: string) {
  try {
    const { data, content } = extractFrontmatter(rawSource);
    const frontmatter = validateFrontmatter(data, filePath, extensionSchema);
    return { frontmatter, content };
  } catch (err) {
    if (err instanceof docviaError && err.code === "SCHEMA_ERROR") {
      console.error(`Frontmatter problem in ${filePath}:\n${err.message}`);
    }
    throw err;
  }
}

// Generate the typed Frontmatter interface body for codegen.
const tsType = zodSchemaToFrontmatterTs(extensionSchema);