Introduction
Displaying the last modified time of blog posts is a valuable feature that helps readers understand when content was last updated. This guide demonstrates how to implement this functionality in an Astro blog using a custom remark plugin. We will explore two approaches: using Git commit history and using filesystem modification times.
Overview
The implementation involves several key steps:
- Creating a custom remark plugin to extract modification times
- Configuring Astro to use the plugin
- Passing the modification time through the rendering pipeline
- Adding internationalization support
- Displaying the modification time in the UI
Method 1: Git-Based Modification Time
This approach uses Git commit history to determine when a file was last modified. It is ideal for production environments where content changes are tracked through version control.
Step 1: Create the Remark Plugin
Create a new file at src/plugins/remark-modified-time.mjs:
import { execSync } from "child_process";
export function remarkModifiedTime() {
return function (tree, file) {
const filepath = file.history[0];
try {
const result = execSync(`git log -1 --pretty="format:%cI" "${filepath}"`);
file.data.astro.frontmatter.lastModified = result.toString().trim();
} catch (e) {
console.warn(`Failed to get git modification time for ${filepath}`, e);
}
};
}
This plugin executes a Git command to retrieve the ISO 8601 timestamp of the last commit that modified the file. The %cI format specifier returns the committer date in strict ISO 8601 format.
Step 2: Register the Plugin
Update astro.config.mjs to import and register the plugin:
import { defineConfig } from "astro/config";
import { remarkModifiedTime } from "./src/plugins/remark-modified-time.mjs";
export default defineConfig({
markdown: {
remarkPlugins: [remarkModifiedTime],
// ... other plugins
},
});
Advantages and Limitations
Advantages:
- Reflects actual content changes tracked in version control
- Consistent across different environments
- Suitable for collaborative projects
Limitations:
- Requires Git repository
- Does not update until changes are committed
- Slower than filesystem-based approach due to command execution
Method 2: Filesystem-Based Modification Time
This approach uses the filesystem’s modification timestamp (mtime). It is ideal for development environments where you want to see changes immediately without committing.
Step 1: Create the Remark Plugin
Replace the contents of src/plugins/remark-modified-time.mjs with:
import { statSync } from "fs";
export function remarkModifiedTime() {
return function (tree, file) {
const filepath = file.history[0];
try {
const stats = statSync(filepath);
file.data.astro.frontmatter.lastModified = stats.mtime.toISOString();
} catch (e) {
console.warn(`Failed to get modification time for ${filepath}`, e);
}
};
}
This plugin uses Node.js’s statSync function to retrieve file statistics and converts the modification time to ISO 8601 format using toISOString().
Step 2: Register the Plugin
The registration process is identical to Method 1. Ensure the plugin is imported and added to the remarkPlugins array in astro.config.mjs.
Advantages and Limitations
Advantages:
- Updates immediately when files are saved
- Faster execution (no external commands)
- Works without Git repository
Limitations:
- Filesystem timestamps can be unreliable (affected by file operations, deployments)
- May not reflect actual content changes
- Can differ across environments
Integrating Modification Time into Your Blog
Once the plugin is configured, you need to pass the modification time through your rendering pipeline and display it in your UI.
Step 1: Extract Modification Time in Page Component
Update your post page component (e.g., src/pages/posts/[...slug].astro) to extract the modification time from the remark plugin frontmatter:
---
import { getCollection, render } from "astro:content";
import PostLayout from "../../layouts/PostLayout.astro";
export async function getStaticPaths() {
const postEntries = await getCollection("posts");
return postEntries.map((entry) => ({
params: { slug: entry.slug },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content, headings, remarkPluginFrontmatter } = await render(entry);
---
<PostLayout
title={entry.data.title}
subTitle={entry.data.description}
published={entry.data.published}
lastModified={remarkPluginFrontmatter?.lastModified}
headings={headings}
>
<Content />
</PostLayout>
The key change is extracting remarkPluginFrontmatter from the render() function and passing lastModified to the layout component.
Step 2: Update Layout Component
Modify src/layouts/PostLayout.astro to accept and pass through the lastModified prop:
---
interface Props {
title?: string;
subTitle?: string;
published?: Date;
lastModified?: string;
headings?: MarkdownHeading[];
// ... other props
}
const {
title,
subTitle,
published,
lastModified,
headings = [],
} = Astro.props;
---
<Main title={title} subTitle={subTitle}>
<div class="content-layout">
<div class="article">
<Markdown>
<slot />
</Markdown>
{published && title && (
<CopyRight
title={title}
published={published}
lastModified={lastModified}
/>
)}
</div>
</div>
</Main>
Step 3: Add Internationalization Support
To support multiple languages, add translation keys for the “Last Modified” label.
Update src/locales/keys.ts:
enum I18nKeys {
copy_right_author = "copy_right_author",
copy_right_publish_date = "copy_right_publish_date",
copy_right_last_modified_date = "copy_right_last_modified_date",
copy_right_license = "copy_right_license",
// ... other keys
}
export default I18nKeys;
Add translations in src/locales/languages/en.ts:
export const en: Translation = {
[key.copy_right_author]: "Author",
[key.copy_right_publish_date]: "Publish Date",
[key.copy_right_last_modified_date]: "Last Modified",
[key.copy_right_license]: "License",
// ... other translations
};
Add translations in src/locales/languages/zh_cn.ts:
export const zh_CN: Translation = {
[key.copy_right_author]: "作者",
[key.copy_right_publish_date]: "发布日期",
[key.copy_right_last_modified_date]: "最后修改",
[key.copy_right_license]: "许可证",
// ... other translations
};
Step 4: Display Modification Time in UI
Update src/components/misc/CopyRight.astro to display the modification time:
---
import { Icon } from "astro-icon/components";
import YukinaConfig from "../../../yukina.config";
import I18nKeys from "../../locales/keys";
import { i18n } from "../../locales/translation";
import { formatDate } from "../../utils/date";
export interface Props {
title: string;
published: Date;
lastModified?: string | Date;
license?: {
name: string;
url?: string;
};
sourceLink?: string;
author?: string;
}
const { title, published, lastModified, license, author, sourceLink } = Astro.props;
---
<div class="copyright-container">
<div class="copyright-content">
<div class="title-section">
<p>{title}</p>
<a href={sourceLink ?? ""}>{sourceLink ?? ""}</a>
</div>
<div class="metadata-section">
<div class="metadata-item">
<span>{i18n(I18nKeys.copy_right_author)}</span>
<p>{author ?? YukinaConfig.username}</p>
</div>
<div class="metadata-item">
<span>{i18n(I18nKeys.copy_right_publish_date)}</span>
<p>{formatDate(published, YukinaConfig.locale)}</p>
</div>
{lastModified && (
<div class="metadata-item">
<span>{i18n(I18nKeys.copy_right_last_modified_date)}</span>
<p>{formatDate(new Date(lastModified), YukinaConfig.locale)}</p>
</div>
)}
<div class="metadata-item">
<span>{i18n(I18nKeys.copy_right_license)}</span>
{license && license.url ? (
<a href={license.url} target="_blank">{license.name}</a>
) : license ? (
<p>{license.name}</p>
) : (
<a href={YukinaConfig.license.url} target="_blank">
{YukinaConfig.license.name}
</a>
)}
</div>
</div>
</div>
</div>
The conditional rendering {lastModified && (...)} ensures the modification time is only displayed when available. The formatDate utility function formats the timestamp according to the configured locale.
Testing the Implementation
After implementing the changes, you can verify the functionality:
- Development Server: Start your Astro development server with
npm run dev - Check Console: Verify there are no errors in the console output
- View Posts: Navigate to a blog post and verify the “Last Modified” field appears in the copyright section
- Test Updates:
- For Git method: Modify a post, commit the change, and verify the timestamp updates
- For filesystem method: Modify a post, save the file, and verify the timestamp updates immediately
Choosing Between Methods
Consider the following factors when selecting an approach:
Use Git-Based Method When:
- You want to track actual content changes
- Your content is managed through version control
- You need consistency across deployments
- You are working in a team environment
Use Filesystem-Based Method When:
- You want immediate feedback during development
- You do not use Git for content management
- You prioritize performance over accuracy
- You are working in a local development environment
Hybrid Approach: You can also implement a hybrid solution that uses filesystem times in development and Git times in production by checking the environment:
import { statSync } from "fs";
import { execSync } from "child_process";
export function remarkModifiedTime() {
return function (tree, file) {
const filepath = file.history[0];
const isDev = process.env.NODE_ENV === "development";
try {
if (isDev) {
// Use filesystem time in development
const stats = statSync(filepath);
file.data.astro.frontmatter.lastModified = stats.mtime.toISOString();
} else {
// Use Git time in production
const result = execSync(`git log -1 --pretty="format:%cI" "${filepath}"`);
file.data.astro.frontmatter.lastModified = result.toString().trim();
}
} catch (e) {
console.warn(`Failed to get modification time for ${filepath}`, e);
}
};
}
Conclusion
Implementing last modified timestamps in an Astro blog enhances the user experience by providing transparency about content freshness. Both the Git-based and filesystem-based approaches have their merits, and the choice depends on your specific requirements and workflow. By following this guide, you can successfully integrate modification times into your Astro blog and provide readers with valuable metadata about your content.