Using Git Hooks for Displaying Last Modified Dates

Part of the Astro series

Not so very long ago, I wrote about using remark and a script called remark-modified-time.mjs to update a page’s front matter Date value for Auto-Generated Last Modified Dates in Astro. This approach worked pretty well until I moved the content for my /Uses page out of a markdown file and into a json file. I didn’t want to have to keep modifying essentially an associated empty markdown file to get the last modified date to change.

What I wanted was a way to see if the json file itself had changed, and then enter THAT date into the /Uses markdown file front matter. In essence, change one file, and another one gets its updated timestamp. This would give me a self-updating last modified date value for everything and I wouldn’t have to remember to manually change anything to make it happen.

Enter Git Hooks.

Git hooks let you run a script when some git action occurs. One git related action that occurs all the time is the git commit. Git commit turns out to be a very good time to look at when files were last modified, since git’s entire job is paying attention to when and how files are modified. The git hook to use if you want it to happen when you perform a git commit is called pre-commit.

Although it’s called pre-commit, it happens during a commit but before you enter the commit message. It can be used to verify commits before allowing them to happen, but instead I use it to modify files and commit those new changes along with the existing ones.

Implementing a pre-commit git hook is simple. Go into your project’s .git/hooks directory and create a file called pre-commit, with no file extension. There should already be a file in there called pre-commit.sample – you can either rename it without the extension and edit it, or just create a new one with the correct name.

Here are the contents of my .git/hooks/pre-commit file:

#!/bin/sh
git diff --cached --name-status |
grep -iE '^M.*src/(content/links/links\.md|content/now/now\.md|content/pins/pins\.md|data/uses\.json|data/spotlight\.json)$' |
while read _ file; do
if [[ "$file" == "src/data/uses.json" ]]; then
file="src/content/uses/uses.md"
elif [[ "$file" == "src/data/spotlight.json" ]]; then
file="src/content/links/links.md"
fi
filecontent=$(cat "$file")
frontmatter=$(echo "$filecontent" | awk -v RS='---' 'NR==2{print}')
cat $file | sed "/---.*/,/---.*/s/^date:.*$/date: \"$(TZ='America/Los_Angeles' date "+%Y-%m-%dT%H:%M:%S-08:00")\"/" > tmp
mv tmp $file
git add $file
done

Two points if you noticed that it’s just a bash script and that it clearly is running a git command to see what files have changed since the last commit!

The output of that command is piped to grep to look to see if any of the modified files are src/content/links/links.md, src/content/now/now.md, src/content/pins/pins.md, src/data/uses.json, or src/data/spotlight.json.1

Once it finds any modified files that match my list, it uses awk and sed to find the front matter and change the date value to git’s modified timestamp. I leave learning all about awk and sed as an exercise for the reader – see you in about 10 years.

For the most part, the files that are modified are the files that get their front matter date value updated, except in the case of the two json files. If src/data/spotlight.json is modified, src/content/links/links.md gets its timestamp instead. And if src/data/uses.json is changed, src/content/uses/uses.md gets that timestamp in its front matter date field.

Another way to look at it is that if any of the markdown files I’m looking for are updated, they get their timestamp set accordingly. In addition, links.md also gets an updated timestamp if src/data/spotlight.json is modified. This is because both links.md and spotlight.json contain data that shows up on Links.

But in the case of my /Uses page, I never look to see if src/content/uses/uses.md gets updated. That’s because I’m really only using it for its front matter at this point. All of the data displayed on the page itself comes from src/data/uses.json. So I only look to see if uses.json was modified, and if so, I update the timestamp in uses.md. Then I can use that value to display on the compiled page as the last modified date and time.

That’s it. It really is that simple to implement git hooks. They’re sitting inside .git/hooks in your repo just waiting to be used for exciting things like telling people when you added your mouse to the list of computer hardware that you gaze at lovingly on a daily basis.

Footnotes

  1. Spotlight.json is used for the Cool Site Spotlight on my Links page.

Astro Templates for JSON Data

Part of the Astro series

Web frameworks like Astro often allow the use of Markdown for blog post “content”, that is, the actual blog post, and then the page design and programming is in a page template which takes the Markdown and renders its contents in the appropriate place for a fully rendered HTML page. This is convenient and allows excellent separation of written words (“content”) and site rendering mechanisms.

Sometimes, though, what you want to put on the page is more complicated than you can easily do in Markdown. Such is the case with my /uses page on this website.

Contents

Background

When I first created my /uses page, I thought I was going to do the same thing I do for my /links and /now pages… namely, use Markdown for the “data” (please don’t make me say content again) which would go into the Astro <Content /> component to render the Markdown into HTML.

But…

As Jason from Hemispheric Views stated rather enthusiastically during a recent episode, Markdown isn’t always the right tool for the job. And it’s NOT the right tool for the /uses job, because I want a format and flexibility for this page that Markdown makes difficult, if not impossible.

The result is that I created the /now page as an Astro page template with all the “data” (no, I will NOT) thrown in amongst the html tags, just like 1990 calling for its beautiful, manually handwritten pages back. But that’s gross. It’s gross because it’s harder to work with from an adding and editing perspective, and it just seems dumb.

The solution is very simple, and it’s what I do on Friends with Beer for the drinks. I use a JSON data file and render it in different Astro page templates.

The Starting Point

My original uses.astro file looks like this:

uses.astro
<Base title={title} description={description}>
<article>
<h1>{title}</h1>
<div class="use-header">
<a href={bigmac}
><Image src={mac} width="300" densities={[2, 3]} alt="My Computer" /></a
>
<p class="uses">{description}</p>
</div>
<div class="time">
<Icon name="bi:calendar2-week-fill" />
<time datetime={uses.data.date}>
<a href={`/${uses.slug}`}
>Last updated {modifieddate(remarkPluginFrontmatter.lastModified)}</a
>
</time>
</div>
<div class="uses">
<h2>Computer</h2>
<div class="item">
<div class="the-thing">
<a href={bigmbp}
><Image
src={mbp}
width="300"
densities={[2, 3]}
alt="2021 M1 Pro Macbook Pro"
/></a
>
<div>
<h3>2021 14" MacBook Pro M1 Pro</h3>
<ul>
<li>M1 Pro 10-core CPU, 16-core GPU</li>
<li>16GB RAM</li>
<li>1TB SSD</li>
</ul>
</div>
</div>
<p class="uses">
It might not be THE most amazing computer, spec-wise, but Apple
Silicon Macs are game changers. It's quiet, it's cool, it's fast, and
it's instant-on. I've traditionally hated laptops because of their
massive compromises, and this removes them all.
</p>
</div>
... a bunch more items ...
</div>
</article>
</Base>

The text that is the whole point of the page, namely the items that I use that I wish to present, are just more stuff in a sea of rendering implementation.

You can see for yourself how un-fun it would be to add new items to or edit existing ones. It requires carefully trawling through everything and make sure I’m not messing up nested divs and all kinds of junk. Even with VSCode’s code folding and syntax highlighting, it’s still ugly and nightmarish.

Let’s fix this mess by breaking out the data from the drawing, so to speak.

The Modifications

A JSON Is Born

Step 1 is creating a data file for the stuff I use. I’m calling it uses.json, and I’m putting it in my data folder at src/data/uses.json.

Step 2 is figuring out the data structure. I break my stuff up into categories or sections, like “Computer” and “Podcast Recording”.1

You can see that in each category, there are multiple items. Each item consists of:

  • The name of the item,
  • Some specs,
  • An optional link to a website for the item,
  • My thoughts about it, aka a description,
  • An image of the item.

Based on that, here’s how I’ll structure my JSON data file.

uses.json
[
{
"Category": "Computer",
"Items": [
{
"Name": "Keychron Q1 Keyboard",
"Specs": [
"84 key (75%)",
"BT and wired",
"QMK for greater customization",
"Gateron Pro Brown switches",
"6063 aluminum CNC machined body"
],
"Link": "https://www.keychron.com/products/keychron-q1",
"Description": "Shhh... no one tell my K2 (see below), but the Keychron Q1 is in a whole other leaque. Wow, built like a TANK, as Jason from Hemispheric Views says. I love the feel of the switches, the sound, the weight, the look, the feel, the everything. I'm not sure I'll ever use another keyboard again. Also... come on. Look at those shades of blue. It is available in other colors, but why??",
"Image": "Keychron-Q1-encoder-D633B1F6-73A7-472F-90C9-680982767284"
}
]
}
]

This lets me have multiple categories, each with multiple items, each with the item data listed above.

Then I just have to modify my uses.astro page template to read it in and map it out to the template I’m using for the “data” section.

An Astro Page Template Gets Smarter

I lied just a little bit. I am going to modify uses.astro to parse the JSON data from uses.json, but I’m also going to create an Astro component called UsesItem.astro that will be used to render the individual items. So uses.astro is going to load the JSON file, map through the categories, and every time it hits a list of items, it’s going to hand the rendering for them over to the UsesItem.astro component.

This will break it things up, make re-use easier if I ever want to render any of these items elsewhere, and generally make me happier. And I like being happier.

Here’s what the /uses page template, uses.astro, looks like now with this approach:

uses.astro
---
import { getEntry } from "astro:content";
import { Icon } from "astro-icon/components";
import { Image } from "astro:assets";
import UsesItem from "../components/UsesItem.astro";
import Base from "../layouts/Base.astro";
import { modifieddate } from "../components/utilities/DateFormat.js";
import usesdata from "../data/uses.json";
import mac from "../assets/images/posts/macsetup-7780B721-09D1-44CC-82B1-E083D8F4A7C9.png";
const bigmac =
"/images/posts/macsetup-7780B721-09D1-44CC-82B1-E083D8F4A7C9.png";
const uses = await getEntry("uses", "uses");
let title = uses.data.title;
let description = uses.data.description;
---
<Base title={title} description={description}>
<article>
<h1>{title}</h1>
<div class="use-header">
<a href={bigmac}
><Image src={mac} width="300" densities={[2, 3]} alt="My Computer" /></a
>
<p class="uses">{description}</p>
</div>
<div class="time">
<Icon name="bi:calendar2-week-fill" />
<time datetime={uses.data.date}>
<a href={`/${uses.slug}`}>Last updated {modifieddate(uses.data.date)}</a
>
</time>
</div>
{
usesdata.map((use) => {
return (
<div class="uses">
<h2>{use.Category}</h2>
{use.Items.map((item) => (
<UsesItem item={item} />
))}
</div>
);
})
}
</article>
</Base>
<style>
div.use-header {
display: flex;
flex-direction: row;
column-gap: 3rem;
justify-content: center;
align-items: center;
margin: 0 auto;
padding: 0.5rem 2rem;
border-radius: 10px;
font-weight: 400;
font-size: 1.2em;
font-style: italic;
}
div.use-header {
background-color: var(--surface-menu);
}
div.use-header img {
min-width: 225px;
}
div.uses {
background-color: var(--surface-menu);
padding: 0.1em 2rem;
margin: 2.25em 0;
border-radius: 10px;
}
div.item {
margin-bottom: 5rem;
}
@media only screen and (max-width: 699px) {
div.use-header {
flex-direction: column;
}
div.uses {
padding: 0.1em 0.5rem;
}
}
div.time,
div.time a {
font-size: 0.8em;
}
div.time {
margin-top: 2.5rem;
}
[data-icon="bi:calendar2-week-fill"] {
font-size: 0.75rem;
margin-bottom: -0.05rem;
}
</style>

All the information, or data, or content, about the items is GONE from the html template. It’s all in the JSON file, and uses.astro is just a set of display instructions.

Now the only part that’s hard-coded into the page template is the page introduction area indicated by the rectangle marker below, which is right at the beginning of the <article> section.

Uses page header section

Everything following it comes from the JSON file and is mapped out in this section:

{
usesdata.map((use) => {
return (
<div class="uses">
<h2>{use.Category}</h2>
{use.Items.map((item) => (
<UsesItem item={item} />
))}
</div>
);
})
}

Very little of the work of rendering the items is done in uses.astro. Instead, it relies on the UsesItem component which it imports from UsesItem.astro. The current item in the map loop is passed as a prop to the component, which then renders the item detail view. Here’s my UsesItem.astro component in its entirety, and half of it is CSS:

UsesItem.astro
---
import { Icon } from "astro-icon/components";
import { Image } from "astro:assets";
const { item } = Astro.props;
---
<div class="item">
<div class="the-thing">
<a href={`/images/posts/${item.Image}.jpeg`}>
<Image
src={import(`../assets/images/posts/${item.Image}.jpeg`)}
width="300"
densities={[2, 3]}
alt={item.Name}
/>
</a>
<div>
<h3>{item.Name}</h3>
<ul>
{item.Specs.map((spec, index) => <li key={index}>{spec}</li>)}
</ul>
</div>
</div>
<p class="uses">
{item.Description}
</p>
{
item.Link && (
<p class="link">
<a href={item.Link}>
<Icon name="fluent:globe-star-20-regular" />
</a>
<a href={item.Link}>{item.Link}</a>
</p>
)
}
</div>
<style>
div.the-thing {
display: flex;
flex-direction: row;
column-gap: 3rem;
border-radius: 10px;
justify-content: flex-start;
align-items: flex-start;
font-size: 1em;
font-style: normal;
font-weight: 400;
column-gap: 2rem;
margin: 0 auto;
padding: 0;
}
p.uses {
font-style: italic;
}
p.link {
margin-top: 2em;
font-size: 0.8em;
}
p.link [data-icon] {
margin-bottom: -0.15em;
}
div.item {
margin-bottom: 5rem;
}
@media only screen and (max-width: 699px) {
div.the-thing {
flex-direction: column;
padding: 0 0.5rem;
}
}
</style>

This uses the astro:assets Image component to optimize the item’s image as specified in uses.json. It puts this next to the item name and a list of item features of specs. Under that is my description of what it is or why I like it, followed by an optional link to a website for the item.

You can see that item.Specs is an array and so I use item.Specs.map to create the list of specs. Also notice where the item URL link is rendered starts with item.Link && which means the following href rendering only happens if item.Link indeed exists.

The end result that is rendered is something like the following:

Keychron Q1 item view

Summarium

You can definitely use Markdown for site content when appropriate, such as for a standard blog post. For anything more complicated, though, you’ll want to use a data source, like a JSON file, and a page template to parse it and map it. This way your data, or “content”, is separate from the rendering mechanics, but can be laid out on the page more intricately than is possible with a straight Markdown to HTML rendering.

The good news is that Astro page templates and components are super flexible in terms of easily mixing HTML, CSS, and JavaScript, and they’re the right tool to use for a one-off page with a custom layout.

Footnotes

  1. That’s all I have right now because instead of adding things I use, I’m busy rewriting how the page is created.

Mac 40

I don’t know if you’ve heard, but the Mac turned 40. And what a glorious 40 it is – with Apple Silicon and the return to some amount of function over form in the laptops and (most of the) desktops, the Mac is better than ever.

Personally, I started early on the Mac. My dad bought us matching 128K Macs the first year they came out. My Mac was upgraded to Fat Mac and then Mac Plus, and it served me far longer than it had any right to. By the time I got a current at the time PC at a discount from my employer, my Mac was way past its useful lifespan.

People have been telling a lot of fun stories about the Mac for its 40th anniversary. I highly encourage you listen to Upgrade #496: 40th Anniversary of the Mac Draft even if you’re not an Upgrade listener. Yes, it’s a draft, yes, John Siracusa actually drafts the current Mac Pro as the worst Mac ever, and yes, it is glorious. There’s also this peak Siracusa moment

Here’s a list of some articles, podcast episodes, and videos I’ve enjoyed this week.

Apple Shares the Secret of Why the 40-Year-Old Mac Still Rules | WIRED

Greg Joswiak on the Mac’s enduring appeal – Six Colors

Happy 40th Birthday, Macintosh

Lost Apple History… FOUND on a Floppy Disk! 💾 Macintosh 40th Birthday Surprise (1984-2024) - YouTube

Mac at 40: The eras tour – Six Colors

The Mac that Steve Jobs unveiled in 1984 is 40, and as amazing as ever

The Mac turns 40 — and keeps on moving - The Verge

Wild Apples: The 12 weirdest and rarest Macs ever made | Ars Technica

Upgrade #496: 40th Anniversary of the Mac Draft - Relay FM

Upgrade 496: 40th Anniversary of the Mac Draft - YouTube

Links and Pins

Not long ago, I created a Links page which I used to hold links I wanted to keep temporarily, like a little pinboard. But then I realized “HEY!! THIS IS BAD!!” People expect a links page to be things like blogrolls and stuff they might like, not the stupid stuff Scott Willsey wants to remember for a day or month or year or three.

So as of today, I now have Links, which is what people expect of a links page, and Pins, which is what I apparently expect of a links page.

Links is now, just to clarify, a permanent-ish list of links to things that YOU, the reader, might also find interesting.

Pins is, just to also clarify, where I’ll stick ephemeral stuff that I want to get back to but doesn’t make sense to stick in a read later app.

Astro Icon 1.x Upgrade

Part of the Astro series

If you started using Nate More’s Astro Icon with Astro early on and are using a version lower than 1.0, you will be in for a surprise when you upgrade to the latest version of Astro Icon and suddenly find a lot of breaking errors related to it.

Migrating from 0.x to 1.x is simple but can be a little tedious depending on how many icons you’re using. A couple key things to understand are that you have to import packages for each icon set that you use now, and that this is now an Astro integration. Don’t worry if you’re not sure how that affects you. You’ll handle adjusting to those changes in the upgrade steps below.

By the way, Nate has a great upgrade guide already at Upgrade to Astro Icon v1 | Astro Icon. I just thought I’d write about it too, since I’ve done this on a couple different sites now and I still have a couple to go.

Contents

Upgrading to Astro Icon 1.x from 0.x

  1. Upgrade to the latest version packages of astro and astro-icon (astro-icon >= 1.0)
  2. Import and integrate astro-icon in your astro.config.mjs
  3. Install @iconify-json/icon-family for every icon package you use
  4. On every page you import astro-icons, change import { Icon } from "astro-icon"; to import { Icon } from "astro-icon/components";
  5. Find and replace [astro-icon] tags with [data-icon] in your css
  6. Find and replace any CSS width or height properties on [data-icon] entries and replace with font-size
  7. Now go through every single instance of [data-icon] in your CSS and tweak the font-size that you just added.

1. Upgrade to the latest Astro and Astro Icon versions

Terminal window
scott@Songoku:friendswithbrews astro-icon ✗ 4h31m △ ⍉ ➜
ncu
Checking /Users/scott/Sites/friendswithbrews/package.json
[====================] 21/21 100%
@astrojs/rss ^4.0.1 → ^4.0.2
@astrojs/sitemap ^3.0.3 → ^3.0.4
astro ^4.0.7 → ^4.1.2
astro-eslint-parser ^0.16.0 → ^0.16.1
astro-icon ^0.8.2 → ^1.0.2
astro-pagefind ^1.3.0 → ^1.4.0
date-fns ^3.0.6 → ^3.2.0
prettier ^3.1.1 → ^3.2.2
prettier-plugin-astro ^0.12.2 → ^0.12.3
sharp ^0.33.1 → ^0.33.2
Run ncu -u to upgrade package.json

I use npm-check-updates to update npm packages, but you can do it the standard way. The key thing to note is that in this instance I’m upgrading from astro-icon 0.8.2 to 1.0.2.

2. Import and integrate astro-icon in your astro.config.mjs

No explanation needed here – import astro-icon and add to the list of integrations.

astro.config.mjs
import { defineConfig } from "astro/config";
import icon from "astro-icon";
import pagefind from "astro-pagefind";
import sitemap from "@astrojs/sitemap";
// https://astro.build/config
export default defineConfig({
site: "https://friendswithbeer.com/",
integrations: [ icon(), pagefind(), sitemap(),],
});

3. Install @iconify-json/icon-family for every icon package you use

This one requires you to scour through every place you specify an icon package name. Search for [astro-icon in your project and look for instances of things like <Icon name="simple-icons:mastodon" /> or CSS instances like [astro-icon="bi:hand-thumbs-up-fill"] and take note of the package names (in these examples, simple-icons and bi).

Now install these by installing @iconify-json/package-name for each. E.g,

Terminal window
npm install @iconify-json/simple-icons @iconify-json/bi

That will install the two packages in my example above.

4. Change your astro-icon import statements on Astro pages

The old way to import astro-icons in your .astro files is

import { Icon } from "astro-icon";

Change all instances of this in your astro files to

import { Icon } from "astro-icon/components";

5. Change all references to [astro-icon] in your CSS to [data-icon]

Another search and replace activity – [astro-icon] references have been replaced with [data-icon] references.

Old:

<style>
[astro-icon="simple-icons:mastodon"] {
color: var(--color-five);
width: 2.2rem;
}
</style>

New:

<style>
[data-icon="simple-icons:mastodon"] {
color: var(--color-five);
font-size: 2.2rem;
}
</style>

6. Use font-size instead of width or height to set the icon sizes

You may have noticed in the previous section that I not only changed [astro-icon] to [data-icon] in my CSS reference to it, but I also removed the width property and replaced it with a font-size property instead.

After performing the first 5 steps, you’ll see wildly sized icons all over the place until you do this.

7. Tweak font-size on all CSS for your icons

Step 6 will get you in the ballpark, but you’ll now find that some of your sizes, while closer, are still off. Just play with font-size values until you get what you like. CSS specificity is key here – be specific about which icons you’re setting your CSS for and use scoped CSS by putting them in <style> tags in specific astro pages.

The first example below affects all icons, the second one affects the bi:hand-thumbs-up-fill icon specifically.

[data-icon] {
font-size: 1em;
}
[data-icon="bi:hand-thumbs-up-fill"] {
margin-bottom: 0.1rem;
}

Summarium

That’s pretty much it. Once change you will notice when running the dev server is that the first time you try to load the site in the browser, Astro Icon has to load all the icon sets and it takes a little bit. Don’t worry. It will work. 🙂

Grin and Bear It

Back when the iPad Pro was my main mobile “work” device,1 Drafts played a huge role in my writing and blogging. For one thing, it has extensive automation and scripting capabilities, and those came in super handy on iPadOS. I created automations to let me choose photos from my photos library and add them to blog posts, including creating proper links and adding them to my site repo.

But now I have an Apple Silicon MacBook Pro and I’ve long since given up trying to fight my way to success through all the limitations of iPadOS. It’s hard to overstate how much faster and easier so many things are on the Mac, and given the current state of Apple’s Shortcuts, having tons of other reliable methods of automating things, as you do on the Mac, is incredibly nice.

All that to say, I realized today I haven’t really done anything in Drafts except type out blog posts and then paste them into VSCode when it was time to publish them. I’m no longer making use of all the great Drafts automations and integrations that I used to. And given THAT fact, I may as well do my writing in something that looks nicer and feels more modern as a writing app.

I own iA Writer but it bugs me in certain ways. I don’t like the non-standard footnote default that makes you manually type the footnote reference at the bottom yourself if you don’t want an inline footnote, and it’s also very limited on font choices.

Ulysses is also weird. It shows you some of the Markdown but tries to hide other Markdown, like URLs, in very inconvenient ways. It’s been a long time since I thought I was a fan of Ulysses.

Before Ulysses, it was Byword for me. To say it looks a little basic now is a bit of an understatement.

That leaves Bear as the only realistic Markdown writing option for me, and I have to say, I like how the editor looks. That’s important to me. If it wasn’t, I’d still be using Drafts because it’s a great app and I do really like Greg Pierce and the work he does on it. It’s just not as important to me on the Mac now as it was on iPadOS.

I’m a person who has a lot of idiosyncrasies, and one of them is that I need a nice looking editor to be able to enjoy the writing process. Bear definitely looks nice. The defaults are nice, customizing it is simple, and things like images and links look really nice in it.

Below is an image showing both the light and dark themes I’m currently using in Bear.

My Bear dark and light themes

There’s not a lot I need to do in order to incorporate Bear into my writing workflow. I already have a script that names my images and puts copies of them in the right locations to both be optimized for the blog and to be able to link to the original. I’ll need to put an automation somewhere in the publish chain that updates the post’s Markdown file with those image paths and creates the links to the larger, original images. But that was also something I needed to do for my Drafts workflow, and hadn’t yet.

Also I think playing with Bear has given me some ideas for improving my site CSS a bit… 🤔

Footnotes

  1. Not my WORK work device, but my personal project work device

Auto-Generated Last Modified Date in Astro

Part of the Astro series

I’m trying to figure out how to use remark plugins in Astro to modify a couple things in posts for me automatically, and along the way I’ve used remark to add a couple quality of life improvements. The first is an auto-generated table of contents for longer posts that I feel need one, and the second is an auto-generated last modified date for pages based on git commit timestamps.

The benefit of an automatically generated last modified date is that I don’t have to remember to update it when I make changes to a page that displays it, like my now page or my links page. I can just commit my changes and the last modified date will update automatically. It’s simple, but it’s kind of beautiful.

I basically did exactly what Astro’s documentation says to do in a helpful recipe called Add last modified time. I created a file called remark-modified-time.mjs and put it in my src/components/utilities folder.1

remark-modified-time.mjs
import { execSync } from "child_process";
export function remarkModifiedTime() {
return function (tree, file) {
const filepath = file.history[0];
const result = execSync(`git log -1 --pretty="format:%cI" "${filepath}"`);
file.data.astro.frontmatter.lastModified = result.toString();
};
}

Then I updated my astro.config.mjs file to reference this as a remark plugin, and to use it in my markdown processing.

astro.config.mjs
import { defineConfig } from "astro/config";
import expressiveCode from "astro-expressive-code";
import icon from "astro-icon";
import pagefind from "astro-pagefind";
import { remarkModifiedTime } from './src/components/utilities/remark-modified-time.mjs';
import remarkToc from 'remark-toc';
/** @type {import('astro-expressive-code').AstroExpressiveCodeOptions} */
const astroExpressiveCodeOptions = {
// Example: Change the themes
themes: ["material-theme-ocean", "light-plus", "github-dark-dimmed"],
themeCssSelector: (theme) => `[data-theme='${theme.name}']`,
}
// https://astro.build/config
export default defineConfig({
site: "https://scottwillsey.com/",
integrations: [expressiveCode(astroExpressiveCodeOptions), icon(), pagefind()],
markdown: {
remarkPlugins: [ remarkModifiedTime, [remarkToc, { heading: "contents" } ] ],
},
});

Once those are set up, it’s just a matter of referencing remarkPluginFrontMatter.lastModified in the Astro template.

now.astro
---
import { getEntry } from "astro:content";
import { Icon } from "astro-icon/components";
import Base from "../layouts/Base.astro";
import { modifieddate } from "../components/utilities/DateFormat.js";
const now = await getEntry("now", "now");
const { Content, remarkPluginFrontmatter } = await now.render();
let title = now.data.title;
let description = now.data.description;
---
<Base title={title} description={description}>
<article>
<h1>{title}</h1>
<p class="now">
The <a href="https://nownownow.com/about">"now page"</a> concept comes from
an idea by <a href="https://sive.rs">Derek Sivers</a> to have people communicate
what they're focused on <b>now</b> at this point in their lives.
</p>
<div class="time">
<Icon name="bi:calendar2-week-fill" />
<time datetime={now.data.date}>
<a href={`/${now.slug}`}
>Last updated {modifieddate(remarkPluginFrontmatter.lastModified)}</a
>
</time>
</div>
<div class="now">
<Content />
</div>
</article>
</Base>

That’s it. Now I have auto-generated last modified dates for any page I feel needs one, thanks to Astro using remark to render markdown and therefore making adding remark plugins super simple.

Last updated indicator

Footnotes

  1. You can put your remark plugin anywhere you want, as long as you reference the path correctly in astro.config.mjs.