astro-git-dates

Set dates in Astro content collections from git. Write 'git Last Modified' as a date in the frontmatter and the build swaps in the file's last commit date, like Eleventy does.

npmjs.com Repo TypeScript, Astro

The Eleventy version of this site had date: git Last Modified, and I missed it after the move to Astro. My Code notes get edited for years, so a fixed date in the frontmatter is wrong the moment I touch the note again. This package brings the Eleventy feature back.

You wrap a collection’s loader with gitDates(), and any top-level frontmatter field set to git Last Modified or git Created becomes that git date before the schema sees it. It’s opt-in per entry, so old posts keep their real dates.

import { glob } from 'astro/loaders';
import { gitDates } from 'astro-git-dates';

const notes = defineCollection({
  loader: gitDates(glob({ pattern: '**/*.md', base: './src/content/notes' })),
  schema: z.object({ title: z.string(), date: z.date() }),
});
  • Works with any loader that passes a file path, glob() included
  • git Created follows renames, so a moved file keeps its first date
  • Exports gitLastModified() and gitCreated() helpers for scripts outside a collection
  • Agent skill: npx skills add mrmartineau/astro-git-dates

Your CI needs the full git history (fetch-depth: 0 on GitHub Actions), or every file gets the same date.

Project README

astro-git-dates

npm

Set dates in Astro content collections from git, like Eleventy's git Last Modified.

Write this in an entry's frontmatter:

---
title: Array methods summarised
date: git Last Modified
---

At build time the value becomes the date of the file's last commit. Entries with a normal date are left alone, so you can switch entries over one at a time.

Install

pnpm add astro-git-dates

Needs Astro 5 or later and collections that use a content loader such as glob().

Usage

Wrap the loader of each collection that should support git dates with gitDates(). One call per collection, so you choose which collections use it.

// src/content.config.ts (or src/content/config.ts)
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
import { gitDates } from "astro-git-dates";

const blog = defineCollection({
  loader: gitDates(glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" })),
  schema: z.object({
    title: z.string(),
    date: z.date(),
    modified: z.date().optional(),
  }),
});

const notes = defineCollection({
  loader: gitDates(glob({ pattern: "**/*.md", base: "./src/content/notes" })),
  schema: z.object({
    title: z.string(),
    date: z.date().optional(),
  }),
});

export const collections = { blog, notes };

The schema stays z.date(). The swap happens before validation, so the schema only ever sees a real Date.

Keywords

Frontmatter value Becomes
git Last Modified Date of the last commit that changed the file
git Created Date of the commit that added the file (follows renames)

They work in any top-level field, not just date:

---
title: My post
date: git Created
modified: git Last Modified
---
---
title: A code note I keep updating
date: git Last Modified
---
---
title: An old post with a fixed date
date: 2022-09-21
---

Helpers

The functions behind the keywords are exported too, for files outside a collection:

import { gitCreated, gitLastModified } from "astro-git-dates";

gitLastModified("src/pages/about.mdx"); // Date
gitCreated("src/pages/about.mdx"); // Date

Good to know

  • CI needs the full git history. Most CI clones only the latest commit, so every file would get the same date. On GitHub Actions:

    - uses: actions/checkout@v6
      with:
        fetch-depth: 0
    
  • Files not committed yet get the current time. That is the date they will most likely get when you commit them.

  • Bulk commits count. If one commit touches every file (a reformat, a move), every git Last Modified entry gets that date. That is why it's opt-in per entry.

  • Moving from type: 'content' collections: the glob() loader gives entries an id instead of a slug, and you render with render(entry) from astro:content instead of entry.render(). Files and folders starting with _ are no longer skipped for you; add '!**/_*' and '!**/_*/**' to the glob pattern to keep that.

  • Other tools that read your frontmatter (search indexers, feed scripts) see the raw git Last Modified text, not a date. Use the helpers above in those tools.

  • Dev cache. Astro skips re-parsing files whose content has not changed. A date can stay out of date in astro dev until the file changes or you delete node_modules/.astro. Builds in CI start clean.

How it works

gitDates() wraps the loader's parseData. For each entry that has a filePath, it replaces any top-level field set to a keyword with the date from git log, then hands the data on to the normal schema validation. It runs one git log per keyword, only for entries that use one.

Agent skill

The repo ships a skill (SKILL.md) that teaches coding agents how to set this package up. Install it with npx skills:

npx skills add mrmartineau/astro-git-dates

Development

pnpm install
pnpm run build        # tsdown → dist/
pnpm run test         # bun test
pnpm run check        # vp check --fix (format, lint, types)

Releases run from the NPM Release workflow in the Actions tab. Versions follow conventional commits: fix: patch, feat: minor, feat!: major.

More Astro packages

Other Astro tools I have made:

  • astro-d1-search: site search for Astro backed by Cloudflare D1
  • zed-astro-starter: an opinionated Astro starter, ready to deploy to Cloudflare Workers
  • ZUI: a CSS-first UI library with Astro (and React, Solid, Svelte, Vue) components

License

ISC © Zander Martineau