* feat: add initial configuration and social icons for AstroPaper theme * feat: add theme styles and design tokens * chore: configure Astro with sitemap, i18n, fonts, and tooling - Set site URL, i18n (single locale, no prefix), sitemap integration - Add Google Sans Code font via stable fonts API - Configure Shiki dual-theme markdown (min-light / night-owl) - Fix astro-paper.config.ts to use relative import (alias not available when Vite loads root-level config files) - Add @astrojs/sitemap and @typescript-eslint/parser dependencies - Wire @typescript-eslint/parser into ESLint so TypeScript syntax in Astro frontmatter (type Props, etc.) parses correctly * feat: add base styles, dark variant, and typography - global.css: dark variant via [data-theme], base layer for * / a / button selectors, scrollbar tokens, layout utilities (max-w-app, app-layout), active-nav as @utility for Tailwind variant support - html and body classes moved directly onto elements in BaseLayout (Tailwind-native approach) so only truly global selectors remain here - typography.css: prose overrides, Shiki light/dark code block switching, diff/highlight/word-highlight transformer styles * feat: add i18n types, English strings, and useTranslations helper Plain typed object approach — no third-party i18n library needed. Paraglide.js was considered but requires SSR output (hard blocker for a static Astro site). - UIStrings interface with nav, post, pagination, and a11y groups - en.ts with `satisfies UIStrings` for literal-type preservation - useTranslations(locale) helper falls back to English automatically Adding a new language is a one-file addition; full routing stays as a registry add-on. * feat: add client-side theme toggle script - Reads window.__theme.value set by the inline FOUC-prevention script to avoid re-detecting the theme on first load - Toggles data-theme on <html> and persists to localStorage - Fills <meta name="theme-color"> with the computed background colour so Android's browser chrome matches the page - Re-runs setup on astro:after-swap for View Transitions navigation - Carries theme-color across astro:before-swap to prevent navbar flicker on Android during page transitions - Syncs with OS prefers-color-scheme changes at runtime * feat: add BaseLayout and PostLayout BaseLayout (replaces scaffold Layout.astro): - Font, canonical URL via Astro.site, RSS autodiscovery - FOUC-prevention inline script sets data-theme before first paint and exposes window.__theme.value for theme.ts to reuse - html/body classes on elements directly (Tailwind-native) - <slot name="head"> for child layout injection - ClientRouter for View Transitions (respects lightAndDarkMode flag) - googleVerification rendered from config instead of .env — it is a public string, not a secret, and not environment-specific PostLayout (new): - Wraps BaseLayout; injects article-specific head content via slot - Overrides og:type to "article" - Adds article:published_time / article:modified_time conditionally - JSON-LD BlogPosting with conditional datePublished/dateModified (fixes old bug where absent dates produced "undefined" string) - Structured data only on actual post pages (not home/archives/tags) SiteConfig: add optional googleVerification field * chore: replace scaffold index page with BaseLayout placeholder * feat: add Header component * feat: add Footer and Socials component * chore: install dayjs, slugify, zod, @astrojs/mdx and add MDX integration * feat: add resolved config that applies defaults in one place Defaults for posts, features, socials, and shareLinks are merged in src/config.ts so all consuming code gets non-optional types with no scattered ?? fallbacks. * feat: add content schema, blog path, and post utilities Renames collection blog→posts, moves content to src/content/posts/, adds .mdx support, and adds a typed pages collection. Includes getSortedPosts, postFilter, getPath, and slugify utilities. * feat: add Datetime and Card components * refactor: switch layouts and components to resolved config Replaces @/astro-paper.config imports with @/config and removes optional chaining on feature flags now that the resolved config guarantees non-optional values. * feat: add home and footer i18n strings * feat: implement home page with hero, featured, and recent posts Replaces the BaseLayout placeholder with the full index page: hero section with RSS link and social links, featured posts section, recent posts capped at posts.perIndex, and an All Posts link. All section labels come from i18n. * feat: add pagination component * feat: add Breadcrumb component with locale support * feat: add Main component * refactor: update code formatting in Card.astro * feat: add posts page * fix: add missing @astrojs/mdx dependency * refactor: rename meta to pages in i18n * refactor: improve code formatting in Breadcrumb * refactor: destructure props in Main component for clarity * feat: add tag pages * feat: add About page * chore: update i18n configuration to use site language from config * feat: add Archives page * feat: integrate Pagefind search functionality * feat: add dynamic Open Graph image generation for posts * feat: add post detail page * feat: add Shiki transformers * chore: remove unused edit link text * chore: update .gitignore to include generated Pagefind directory * chore: update site URL in configuration to production link * chore: replace favicon and remove unused SVG assets, add new Open Graph image * feat: update site config with default values * feat: add default OG image path resolution * fix: ensure edit post feature checks for enabled status correctly * feat: implement dynamic Open Graph image generation * feat: add RSS feed generation functionality * feat: add 404 Not Found page * feat: add robots.txt generation for SEO optimization * refactor: change getSortedPosts to a named export * refactor: change postFilter to a named export * refactor: rename BaseLayout to Layout * refactor: standardize locale handling across components * refactor: update getPostsByGroupCondition location * chore: remove unused Welcome component * refactor(i18n): update links using getRelativeLocaleUrl * refactor(i18n): add dynamic locale loading * fix: update language attribute to use current locale * fix: correct wording for updatedAt in post translations * fix: update layout titles to use correct translation keys for tags pages * chore: use new colors for better DX and a11y * feat: add underline icon for active icon nav * feat: add toc for markdown contents * refactor: remove unused feature flags in config * refactor(i18n): enhance locale handling and path management * fix: ts unused variable hint * refactor: extract locale stripping logic into a util func * refactor(i18n): improve locale handling in tag pages * refactor: update post URL helper functions * refactor(i18n): replace pathWithBase with getRelativeLocaleUrl for locale handling * refactor: rename fileWithBase to getAssetPath * refactor: use runtime agnostic APIs for file paths * refactor: use transition directive for heading transition * refactor: remove unused title element from tag page * fix: decouple ClientRouter from lightAndDarkMode flag * refactor: rename `postPaths.ts` to `getPostPaths.ts` * refactor: remove unused `minutesRead` string from post translations * refactor: simplify AdjacentPostNav component by passing prevPost and nextPost directly * refactor: remove unused code declaration * docs: add existing blog posts * chore: update astro and @astrojs/check dependencies to latest versions * build: use experimental svg optimizer * chore(repo): restore project meta files and Docker configs * refactor: update code formatting with Prettier * docs: update README and docs * docs: add JSDoc comments to utility functions * docs: add JSDoc comments to resolved config file * fix(og): avoid double slashes when appending index.png * chore(ci): update Node.js version to 22.12.0 in CI workflow * feat: improve og image generation with Astro's getFontFileURL API (#632) * feat: improve og image generation with Astro's getFontFileURL API - remove @resvg/resvg-js and use sharp to convert SVG to PNG - update og image generation to use Astro's getFontFileURL API for better font handling * docs: update README and dynamic OG images documentation - remove outdated content and clarify dynamic OG image generation process in README. - add new image example and update instructions for handling non-Latin characters in dynamic OG images. - adjust copyright year in README to 2026. * fix(header): adjust icon button alignment in mobile * feat(docs): add ResponsiveTable to use it in MDX posts (#634) * feat(docs): add ResponsiveTable to use it in MDX posts Introduce ResponsiveTable with variant-based styling. Rename docs posts to .mdx, wrap tables in the component, and remove redundant table `code` rules from prose typography. * chore(prettier): ignore .mdx files when checking format * chore(ci): update Node.js version to 24 in CI workflow * bump: upgrade Astro and dependencies * fix: remove redundant backUrl script bug * fix(header): adjust button and icon alignment * fix(a11y): improve muted-foreground color contrast * docs: update predefined color schemes * chore: support site verification env as fallback for googleVerification * docs: add astro paper v6 updates in docs * docs: add AstroPaper-v6.png in astro-paper-6 blog post * fix: update back-to-top button DOM placement (#641) Closes #626, #131, #493
8.8 KiB
author, pubDatetime, modDatetime, title, featured, draft, tags, canonicalURL, description
| author | pubDatetime | modDatetime | title | featured | draft | tags | canonicalURL | description | ||
|---|---|---|---|---|---|---|---|---|---|---|
| Simon Smale | 2024-01-03T20:40:08Z | 2024-01-08T18:59:05Z | How to use Git Hooks to set Created and Modified Dates | false | false |
|
https://smale.codes/posts/setting-dates-via-git-hooks/ | How to use Git Hooks to set your Created and Modified Dates on AstroPaper |
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
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.
Update! In AstroPaper v4.3.0, the pre-commit hook has been removed in favor of GitHub Actions. However, you can easily install Husky yourself.
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
UPDATE:
This section has been updated with a new version of the hook that is smarter. It will now not increment the modDatetime until the post is published. On the first publish, set the draft status to first and watch the magic happen.
# Modified files, update the modDatetime
git diff --cached --name-status |
grep -i '^M.*\.md$' |
while read _ file; do
filecontent=$(cat "$file")
frontmatter=$(echo "$filecontent" | awk -v RS='---' 'NR==2{print}')
draft=$(echo "$frontmatter" | awk '/^draft: /{print $2}')
if [ "$draft" = "false" ]; then
echo "$file modDateTime updated"
cat $file | sed "/---.*/,/---.*/s/^modDatetime:.*$/modDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/" > tmp
mv tmp $file
git add $file
fi
if [ "$draft" = "first" ]; then
echo "First release of $file, draft set to false and modDateTime removed"
cat $file | sed "/---.*/,/---.*/s/^modDatetime:.*$/modDatetime:/" | sed "/---.*/,/---.*/s/^draft:.*$/draft: false/" > tmp
mv tmp $file
git add $file
fi
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.
To know the draft status of the file, we need its frontmatter. In the following code we are using cat to get the content of the file, then using awk to split the file on the frontmatter separator (---) and taking the second block (the fonmtmatter, the bit between the ---). From here we are using awk again to find the draft key and print is value.
filecontent=$(cat "$file")
frontmatter=$(echo "$filecontent" | awk -v RS='---' 'NR==2{print}')
draft=$(echo "$frontmatter" | awk '/^draft: /{print $2}')
Now we have the value for draft we are going to do 1 of 3 things, set the modDatetime to now (when draft is false if [ "$draft" = "false" ]; then), clear the modDatetime and set draft to false (when draft is set to first if [ "$draft" = "first" ]; then), or nothing (in any other case).
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(), // [!code --]
modDatetime: z.date().optional().nullable(), // [!code ++]
title: z.string(),
featured: z.boolean().optional(),
draft: z.boolean().optional(),
tags: z.array(z.string()).default(["others"]),
ogImage: image().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
| nullto line 15 insrc/layouts/Layout.astroso that it looks likeexport interface Props { title?: string; author?: string; description?: string; ogImage?: string; canonicalURL?: string; pubDatetime?: Date; modDatetime?: Date | null; } -
added
| nullto line 5 insrc/components/Datetime.tsxso that it looks likeinterface DatetimesProps { pubDatetime: string | Date; modDatetime: string | Date | undefined | null; }