Skip to content

Add frontmatter support to ConvertFrom-Markdown and ConvertTo-Markdown #21

Description

Markdown documents commonly include YAML frontmatter — a block of metadata delimited by --- at the top of the file. Tools like Jekyll, Hugo, MkDocs, and GitHub Pages all rely on frontmatter for page titles, tags, dates, layout settings, and other metadata. When automating documentation pipelines or generating static-site content from PowerShell, there is currently no way to read or write frontmatter through the Markdown module.

Request

Desired capability

When ConvertFrom-Markdown parses a markdown string that begins with YAML frontmatter (--- delimiters), the resulting MarkdownDocument object should populate its FrontMatter property with the parsed metadata. When ConvertTo-Markdown serializes a MarkdownDocument back to a string, it should re-emit that property as a valid YAML frontmatter block at the top of the document.

The MarkdownFrontMatter type and the MarkdownDocument.FrontMatter property are defined by #8 in 1.3 but left permanently $null there. This issue fills them in, targeting 1.4.

Because the property is reserved up front, this is a minor bump rather than a major one: ConvertFrom-Markdown already returns a MarkdownDocument, the property already exists and is already typed, and no index in Children shifts. The only observable change is that a property which was always $null is now sometimes populated.

This enables round-tripping documents that contain frontmatter without losing or corrupting the metadata.

Example input:

---
title: Getting Started
date: 2026-01-15
tags:
  - powershell
  - markdown
layout: docs
---

# Getting Started

This is the introduction.

Expected behavior with ConvertFrom-Markdown:

$doc = Get-Content -Raw 'article.md' | ConvertFrom-Markdown
$doc.FrontMatter.Data
# Returns: @{ title = 'Getting Started'; date = '2026-01-15'; tags = @('powershell','markdown'); layout = 'docs' }

$doc.FrontMatter.Format
# Returns: Yaml

$doc.FrontMatter.Raw
# Returns: the verbatim text between the delimiters

$doc.Children
# Returns: the parsed markdown block nodes (headings, paragraphs, etc.) — without the frontmatter block

Expected behavior with ConvertTo-Markdown:

$doc.FrontMatter.Data['draft'] = $true
$doc | ConvertTo-Markdown
# Returns the markdown string with updated YAML frontmatter at the top, followed by the document content

Acceptance criteria

  • ConvertFrom-Markdown detects YAML frontmatter (delimited by --- on the first line and a closing ---) and populates MarkdownDocument.FrontMatter with a MarkdownFrontMatter instance
  • MarkdownFrontMatter.Data holds the deserialized metadata, MarkdownFrontMatter.Raw holds the verbatim text between the delimiters, and MarkdownFrontMatter.Format is Yaml
  • The frontmatter block is excluded from the parsed content nodes — it is metadata, not document content
  • ConvertTo-Markdown re-emits the frontmatter block at the top of the output string when FrontMatter is non-null
  • Documents without frontmatter produce a $null FrontMatter property and round-trip without adding spurious --- delimiters
  • YAML parsing and serialization is delegated to the PSModule/YAML module (ConvertFrom-Yaml / ConvertTo-Yaml)
  • Nested YAML structures (arrays, nested objects) are preserved through round-tripping

Dependencies

This feature depends on:

  • PSModule/Markdown#8 — 1.3 delivers the object hierarchy, ConvertFrom-Markdown, and ConvertTo-Markdown, and defines the MarkdownFrontMatter type this issue populates
  • PSModule/YAML#3ConvertFrom-Yaml for parsing the frontmatter block
  • PSModule/YAML#2ConvertTo-Yaml for serializing the metadata back to YAML

Technical decisions

Property name and type: [MarkdownFrontMatter] $FrontMatter on MarkdownDocument, as defined in #8. This supersedes the earlier decision to use a bare [hashtable] $Metadata. A dedicated type carries three things a hashtable cannot: Format (which metadata dialect the block uses), Raw (the verbatim source text, so a document whose metadata is never touched round-trips losslessly), and Data (the deserialized value). It also leaves room for TOML (+++) and JSON frontmatter without another breaking property change.

Data type: [hashtable] for MarkdownFrontMatter.Data. This matches the output of ConvertFrom-Yaml -AsHashtable and is the most natural shape for key-value metadata that scripts modify programmatically. A [PSCustomObject] alternative was considered and rejected on ergonomics.

Frontmatter is a property, never a child node: mdast models frontmatter as the first child of the root. That approach is rejected here because it shifts every index in Children for documents that have frontmatter, and forces every traversal to skip a node that is not markdown. As a property it is invisible to anything walking the tree, which is what keeps this release additive.

Emission uses Raw when unchanged: When Data has not been modified, ConvertTo-Markdown re-emits Raw verbatim so key order, comments, and formatting survive. When Data has been modified, the block is re-serialized with ConvertTo-Yaml and Raw is refreshed.

Release shape: A minor bump — 1.4. Nothing changes shape; a reserved property starts being populated.

YAML dependency: Frontmatter parsing and serialization is delegated entirely to the PSModule/YAML module. The Markdown module declares a module dependency on YAML. This avoids reimplementing YAML parsing and ensures consistency with the rest of the PSModule ecosystem.

Frontmatter detection: The parser detects frontmatter only when the document starts with --- on the very first line (optionally preceded by a UTF-8 BOM). The closing --- delimiter ends the frontmatter block. Content before the first --- or documents that do not start with --- are treated as having no frontmatter. This matches the Jekyll frontmatter specification.

Separation of concerns: Frontmatter extraction happens as a preprocessing step in ConvertFrom-Markdown before the block parser runs. The raw text between the delimiters is passed to ConvertFrom-Yaml, and the remainder of the document (after the closing ---) is passed to the block parser. This keeps the frontmatter logic isolated from the structural parser.

ConvertTo-Markdown emission: When FrontMatter is non-null, ConvertTo-Markdown renders the block, wraps it in --- delimiters, and prepends it to the rendered document content. When FrontMatter is null, no frontmatter block is emitted.

File placement: The frontmatter logic is added to the existing ConvertFrom-Markdown.ps1 and ConvertTo-Markdown.ps1 functions and their private helpers (from #8). The MarkdownFrontMatter class already exists from #8 and only gains behavior here. No separate functions are needed — frontmatter is an integral part of document parsing, not a standalone operation.

Test approach: Pester tests in the existing tests/Markdown.Tests.ps1. Separate Context blocks under the existing Describe blocks for ConvertFrom-Markdown and ConvertTo-Markdown, plus a frontmatter-specific round-trip test.


Implementation plan

Class changes

  • Implement MarkdownFrontMatter.ToString() to render the frontmatter block including its --- delimiters
  • Add a dirty-tracking mechanism so Data modifications invalidate Raw

ConvertFrom-Markdown changes

  • Add frontmatter detection at the start of ConvertFrom-Markdown — check if input begins with ---
  • Extract the YAML content between the opening and closing --- delimiters and store it as Raw
  • Pass the extracted YAML string to ConvertFrom-Yaml -AsHashtable to produce Data
  • Assign the resulting MarkdownFrontMatter to $document.FrontMatter
  • Pass only the content after the closing --- to the block parser
  • Handle edge cases: no frontmatter, empty frontmatter (---\n---), frontmatter with only whitespace

ConvertTo-Markdown changes

  • Check whether $document.FrontMatter is non-null at the start of ConvertTo-Markdown
  • Emit Raw verbatim when Data is unmodified; otherwise call ConvertTo-Yaml on Data
  • Prepend ---, the serialized output, and a closing --- followed by a blank line before the document content
  • If FrontMatter is null, emit no frontmatter block

Module dependency

  • Add YAML as a required module dependency in the module manifest or loader

Tests

  • Add Context 'Frontmatter' under Describe 'ConvertFrom-Markdown' — test parsing of simple key-value, nested objects, arrays, and date values
  • Add Context 'Frontmatter' under Describe 'ConvertTo-Markdown' — test emission of the frontmatter block, and no emission when FrontMatter is null
  • Add Context 'Frontmatter round-trip' — verify ConvertFrom-Markdown | ConvertTo-Markdown preserves metadata through a full cycle
  • Test that an untouched document re-emits Raw verbatim, preserving key order and comments
  • Test document with no frontmatter produces a null FrontMatter and no --- on re-emission
  • Test empty frontmatter block (---\n---) produces empty Data

Documentation

  • Add frontmatter usage examples to ConvertFrom-Markdown help
  • Add frontmatter usage examples to ConvertTo-Markdown help
  • Update README.md with a frontmatter example

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions