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.
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 Createdfollows renames, so a moved file keeps its first date- Exports
gitLastModified()andgitCreated()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
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: 0Files 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 Modifiedentry gets that date. That is why it's opt-in per entry.Moving from
type: 'content'collections: theglob()loader gives entries anidinstead of aslug, and you render withrender(entry)fromastro:contentinstead ofentry.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 Modifiedtext, 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 devuntil the file changes or you deletenode_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