In this post I will explain how to use the pre-commit Git hook to automate the input of the created (pubDatetime
) and modified (modDatetime
) in the AstroPaper blog theme frontmatter
Table of contents
Open Table of contents
Have them Everywhere
Git hooks are great for automating tasks like adding or checking the branch name to your commit messages or stopping you committing plain text secrets. Their biggest flaw is that client-side hooks are per machine.
You can get around this by having a hooks
directory and manually copy them to the .git/hooks
directory or set up a symlink, but this all requires you to remember to set it up, and that is not something I am good at doing.
As this project uses npm, we are able to make use of a package called Husky (this is already installed in AstroPaper) to automatically install the hooks for us.
The Hook
As we want this hook to run as we commit the code to update the dates and then have that as part of our change we are going to use the pre-commit
hook. This has already been set up by this AstroPaper project, but if it hadn’t, you would run npx husky add .husky/pre-commit 'echo "This is our new pre-commit hook"'
.
Navigating to the hooks/pre-commit
file, we are going to add one or both of the following snippets.
Updating the modified date when a file is edited
# Modified files, update the modDatetime
git diff --cached --name-status | egrep -i "^(M).*\.(md)$" | while read a b; do
cat $b | sed "/---.*/,/---.*/s/^modDatetime:.*$/modDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/" > tmp
mv tmp $b
git add $b
done
git diff --cached --name-status
gets the files from git that have been staged for committing. The output looks like:
A src/content/blog/setting-dates-via-git-hooks.md
The letter at the start denotes what action has been taken, in the above example the file has been added. Modified files have M
We pipe that output into the grep command where we are looking at each line to find that have been modified. The line needs to start with M
(^(M)
), have any number of characters after that (.*
) and end with the .md
file extension (.(md)$
).This is going to filter out the lines that are not modified markdown files egrep -i "^(M).*\.(md)$"
.
Improvement - More Explicit
This could be added to only look for files that we markdown files in the blog
directory, as these are the only ones that will have the right frontmatter
The regex will capture the two parts, the letter and the file path. We are going to pipe this list into a while loop to iterate over the matching lines and assign the letter to a
and the path to b
. We are going to ignore a
for now.
The next part with the sed command is a bit magical to me as I don’t often use it, it was copied from another blog post on doing something similar. In essence, it is looking inside the frontmatter tags (---
) of the file to find the pubDatetime:
key, getting the full line and replacing it with the pubDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/"
same key again and the current datetime formatted correctly.
This replacement is in the context of the whole file so we put that into a temporary file (> tmp
), then we move (mv
) the new file into the location of the old file, overwriting it. This is then added to git ready to be committed as if we made the change ourselves.
NOTE
For the sed
to work the frontmatter needs to already have the modDatetime
key in the frontmatter. There are some other changes you will need to make for the app to build with a blank date, see further down
Adding the Date for new files
Adding the date for a new file is the same process as above, but this time we are looking for lines that have been added (A
) and we are going to replace the pubDatetime
value.
# New files, add/update the pubDatetime
git diff --cached --name-status | egrep -i "^(A).*\.(md)$" | while read a b; do
cat $b | sed "/---.*/,/---.*/s/^pubDatetime:.*$/pubDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/" > tmp
mv tmp $b
git add $b
done
Improvement - Only Loop Once
We could use the a
variable to switch inside the loop and either update the modDatetime
or add the pubDatetime
in one loop.
Populating the frontmatter
If your IDE supports snippets then there is the option to create a custom snippet to populate the frontmatter.In AstroPaper v4 will come with one for VSCode by default.
Empty modDatetime
changes
To allow Astro to compile the markdown and do its thing, it needs to know what is expected in the frontmatter. It does this via the config in src/content/config.ts
To allow the key to be there with no value we need to edit line 10 to add the .nullable()
function.
const blog = defineCollection({
type: "content",
schema: ({ image }) =>
z.object({
author: z.string().default(SITE.author),
pubDatetime: z.date(),
- modDatetime: z.date().optional(),
+ modDatetime: z.date().optional().nullable(),
title: z.string(),
featured: z.boolean().optional(),
draft: z.boolean().optional(),
tags: z.array(z.string()).default(["others"]),
ogImage: image()
.refine(img => img.width >= 1200 && img.height >= 630, {
message: "OpenGraph image must be at least 1200 X 630 pixels!",
})
.or(z.string())
.optional(),
description: z.string(),
canonicalURL: z.string().optional(),
readingTime: z.string().optional(),
}),
});
To stop the IDE complaining in the blog engine files I have also done the following:
- added
| null
to line 15 insrc/layouts/Layout.astro
so that it looks like
export interface Props {
title?: string;
author?: string;
description?: string;
ogImage?: string;
canonicalURL?: string;
pubDatetime?: Date;
modDatetime?: Date | null;
}
- added
| null
to line 5 insrc/components/Datetime.tsx
so that it looks like
interface DatetimesProps {
pubDatetime: string | Date;
modDatetime: string | Date | undefined | null;
}