Last updated June 23, 2026

Page Features

4 minutes read

Table of contents

Every page with at least one h1 or h2 heading in its body content gets an auto-generated table of contents panel on the right side of the layout, on xl screens and wider. It lists those headings and highlights the one currently in view as you scroll. The auto-rendered page title doesn’t count. Only headings written directly in the MDX file appear in the table of contents.

Headings deeper than h2 are intentionally excluded to keep the ToC scannable.

Heading anchors

Every heading gets a stable URL anchor derived from its text: lowercase, with spaces and underscores replaced by hyphens, and other punctuation stripped. The anchor appears as a # link prepended to the heading when you hover over it. Click it or share it to link directly to that section.

Examples:

Heading textGenerated anchor
# Getting Started#getting-started
## How to set up?#how-to-set-up
### What's next!#whats-next

Reading time

folio.md calculates an estimated reading time for every page at build time and displays it below the page title, for example 3 minute read. The estimate counts words in the page body, excluding code blocks and inline code, at 200 words per minute. The result is always at least 1 minute.

If the page also has an author set in frontmatter, the reading time appears on the same line separated by a dot.

Last updated date

A “Last updated” line appears at the top-right of each page. folio.md reads the date from git log at build time, using the date of the most recent commit that touched that file. This requires a git repository with at least one commit.

folio.md locale-formats the date using the seo.locale value from folio.config.ts, defaulting to en-US. It omits the date when git history is unavailable, for example in a fresh clone with no commits or on a host that checks out without git history.

::: tip When deploying with GitHub Actions, pass fetch-depth: 0 to actions/checkout so the full commit history is available. See Deployment for an example workflow. :::

Link validation

At build time, folio.md checks every href in your MDX files:

Link typeBehaviour
Internal pathError if the target page doesn’t exist. Build fails.
Internal path with anchorError if the target page or heading doesn’t exist. Build fails.
Anchor-only linkError if the heading doesn’t exist on the current page. Build fails.
External URLWarning if the URL is unreachable. Build continues.
mailto: linkSkipped, not checked

This makes it safe to rename or delete pages: you can’t ship a dead link by accident.

External links open in a new tab automatically and include a screen-reader label (opens in new tab).

When a page has alias: in its frontmatter, folio.md also validates the redirect target as an internal link. If the target doesn’t exist, the build fails.

If a redirect stub contains body content below its frontmatter, the link validator emits an ALIAS [non-empty] warning, because that body is never rendered:

text
ALIAS [non-empty] docs/old-page.mdx
      has alias → /new-page but also contains body content (body will never be rendered)

If another page links to a redirect stub rather than to its destination, the link validator emits an ALIAS warning:

text
ALIAS [internal] /old-path → /new-path
      in docs/some-page.mdx (link to the destination directly)

Linking to redirect stubs is an antipattern. Update those links to point to the destination directly.

How redirect stubs work

At build time, folio.md scans every file’s frontmatter for alias: and registers static Astro redirects. The web server or CDN handles the redirect with no JavaScript involved. folio.md excludes redirect stubs from navigation, search, and prev/next pagination.

Copy code button

Every fenced code block gets a Copy button injected at build time. Clicking it copies the raw code to the clipboard without any syntax-highlighting markup and gives brief visual feedback that the copy succeeded.