Git Metadata

Derive last-updated dates and contributor lists from git history with @bdocs/plugin-git-meta.

The @bdocs/plugin-git-meta package ships two plugins that read your repository's history: one for page last-updated dates and one for contributor lists. Both share a single git log scan, so enabling them together costs almost nothing extra.

Concepts & ArchitectureLink

How it worksLink

  1. One scan per build. The package runs a single git log --name-only over the documentation directory and aggregates the result per file.
  2. HEAD-keyed cache. The scan is cached by repository root and HEAD. The second plugin reuses the first plugin's result, and a dev server automatically sees new commits without a restart.
  3. routes:resolved enrichment. Both plugins run through the documented routes:resolved hook, after routes are crawled, normalized, and sorted. The enriched metadata reaches the SSG route table and the client virtual:boltdocs-routes module.
  4. Layout rendering. Routes carry lastUpdated and contributors; the default page footer renders both. Custom themes can read them with useRoutes().

Only non-merge commits are considered. Dates use the committer date (%cI), and contributor identities are grouped by email before the optional name merge.

What each plugin addsLink

PluginRoute fieldSource
lastUpdatedPlugin()route.lastUpdatedLast non-merge commit touching the file
contributorsPlugin()route.contributorsAuthors of every non-merge commit touching the file

InstallationLink

bash
pnpm add @bdocs/plugin-git-meta
boltdocs.config.ts
import { defineConfig } from "boltdocs";
import { contributorsPlugin, lastUpdatedPlugin } from "@bdocs/plugin-git-meta";

export default defineConfig({
  plugins: [lastUpdatedPlugin(), contributorsPlugin()],
});

Both plugins are independent. Enable only the one you need.


ConfigurationLink

lastUpdatedPluginLink

OptionTypeDefaultDescription
fallbackToMtimebooleanfalseUse the file modification time when git has no history for a page (untracked file, shallow clone).
overrideFrontmatterbooleanfalseReplace a string or Date written in lastUpdated frontmatter with the git date.
pathsstring[]—Only process routes whose path starts with one of these prefixes.
excludePathsstring[]—Skip routes whose path starts with one of these prefixes.
ts
lastUpdatedPlugin({
  overrideFrontmatter: true,
  excludePaths: ["/changelog"],
});

When fallbackToMtime is disabled and a file has no git history, the existing route date is left untouched. Explicit frontmatter dates always win unless overrideFrontmatter is enabled.

contributorsPluginLink

OptionTypeDefaultDescription
maxnumber12Maximum contributors stored per page (1–100).
order'commits' | 'recent''commits'Rank by total commits or by the latest contribution.
excludeBotsbooleantrueDrop automation accounts such as dependabot, renovate, and [bot] identities.
excludeAuthorsstring[][]Names or emails to exclude.
minCommitsnumber1Ignore contributors below this commit count.
mergeByNamebooleantrueCombine identities that share a display name.
includeEmailbooleanfalseInclude the author email in route metadata.
autoRenderbooleantrueMount the exported Contributors component in the default page footer.
profilesRecord<string, Profile>{}Per-identity overrides keyed by email or display name.
pathsstring[]—Only process routes whose path starts with one of these prefixes.
excludePathsstring[]—Skip routes whose path starts with one of these prefixes.

Profiles and avatarsLink

GitHub noreply emails (12345+user@users.noreply.github.com) automatically produce an avatar and a profile URL. For authors who commit with a personal email, map them explicitly:

ts
contributorsPlugin({
  max: 8,
  profiles: {
    "jane@example.com": {
      name: "Jane Doe",
      avatar: "https://github.com/janedoe.png?size=64",
      url: "https://github.com/janedoe",
    },
  },
});

Profile keys are matched against the email first, then the display name. Emails are never exposed to the client unless includeEmail is enabled.


Rendering dates and contributorsLink

The UI lives in the plugin, not in the core. contributorsPlugin() mounts the exported Contributors component on the left side of the default page footer through the page:footer:start client slot, next to the framework's last-updated date. Disable the automatic mount when you want to render it yourself:

ts
contributorsPlugin({ autoRender: false });

Custom footersLink

The plugin client entry exports Contributors, LastUpdated, and a ready-made GitMetaFooter. Every component accepts class slots and render props:

theme/my-page-footer.tsx
import { GitMetaFooter } from "@bdocs/plugin-git-meta/client";
import { useRoutes } from "boltdocs/client";

export function MyPageFooter() {
  const { currentRoute } = useRoutes();

  return (
    <GitMetaFooter
      date={currentRoute?.lastUpdated}
      contributors={currentRoute?.contributors}
      maxContributors={5}
      labels={{ contributors: "Autores", lastUpdated: "Actualizado el" }}
      className="mt-16 border-t border-subtle pt-6"
      contributorClassNames={{ avatar: "ring-2 ring-surface" }}
    />
  );
}

Use Contributors alone when the date is rendered elsewhere:

tsx
import { Contributors } from "@bdocs/plugin-git-meta/client";

<Contributors
  contributors={currentRoute?.contributors}
  max={3}
  size="md"
  classNames={{ link: "font-medium" }}
  renderName={(contributor) => contributor.name.split(" ")[0]}
/>;

Component propsLink

ComponentKey props
Contributorscontributors, max, size (sm/md), showAvatars, showNames, label, classNames, className, linkTarget, renderAvatar, renderName, renderContributor
LastUpdateddate, label, formatDate, className
GitMetaFooterdate, contributors, showContributors, showLastUpdated, maxContributors, labels, formatDate, className, contributorsClassName, dateClassName, contributorClassNames, contributorProps

Route data shapeLink

Each contributor is a RouteContributor:

FieldTypeNotes
namestringDisplay name after profiles and name merging.
commitsnumberNumber of commits authored on the page.
lastCommitstringISO date of the contributor's latest commit.
avatarstringOptional avatar URL.
urlstringOptional profile URL.
emailstringOnly present when includeEmail is enabled.

Performance & limitationsLink

  • Shallow clones only expose the commits that were cloned. In CI, use a deeper fetch (for example fetch-depth: 0) if you want complete contributor history.
  • Squash merges collapse a pull request into one commit, so the squashed author receives the credit for the whole change.
  • Renames are not followed. A renamed file starts its history at the rename commit.
  • Merge commits are skipped on purpose: they would attribute every file in a pull request to the person who merged it.
  • No git repository in the build environment? Both plugins are no-ops; lastUpdated keeps the core's file modification time and contributors stays empty.

TroubleshootingLink

SymptomLikely causeFix
No dates or contributorsThe docs directory is not inside a git repositoryRun the build from a checkout, or enable fallbackToMtime.
A new page has no metadataThe file has never been committedCommit it, or use fallbackToMtime for the date.
Dates look older than expectedSquash merges collapse a pull request into one commitFetch full history or accept the squash date.
A contributor is missingBot filtering, minCommits, excludeAuthors, or maxReview the contributors options.
No avatarThe commit email is not a GitHub noreply addressAdd a profiles entry.
Emails appear in route dataincludeEmail: trueDisable it if the metadata is public.
Components are not visibleautoRender: false, or a custom layout without the footerRender Contributors / GitMetaFooter yourself.

See alsoLink

  • Lifecycle Hooks — routes:resolved and the rest of the plugin lifecycle.
  • Collections — lastUpdated and author metadata in collection posts.
  • Frontmatter — manually overriding lastUpdated.
Last updated on October 18, 2026

Was this page helpful?