feat: optional dates, otherwise fetched from git

This commit is contained in:
Lewis Wynne 2026-01-31 23:39:29 +00:00
parent 4d9e3c56da
commit cc6eff22a8
8 changed files with 90 additions and 31 deletions

View file

@ -7,7 +7,6 @@ const md = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/md' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
pinned: z.boolean().optional(),
category: z.string().optional(),
draft: z.boolean().optional(),

44
www/src/lib/git.ts Normal file
View file

@ -0,0 +1,44 @@
import { execSync } from 'node:child_process';
export function getGitCreationDate(filePath: string): Date {
try {
// Get the oldest commit for this file (first commit that added it)
const timestamp = execSync(
`git log --follow --diff-filter=A --format=%cI -- "${filePath}"`,
{ encoding: 'utf8' }
).trim();
return timestamp ? new Date(timestamp) : new Date(0);
} catch {
return new Date(0);
}
}
export function getGitLastModifiedDate(filePath: string): Date {
try {
const timestamp = execSync(
`git log -1 --format=%cI -- "${filePath}"`,
{ encoding: 'utf8' }
).trim();
return timestamp ? new Date(timestamp) : new Date(0);
} catch {
return new Date(0);
}
}
export interface GitDates {
created: Date;
updated: Date | null; // null if never updated (created === lastModified)
}
export function getGitDates(filePath: string): GitDates {
const created = getGitCreationDate(filePath);
const lastModified = getGitLastModifiedDate(filePath);
// If dates are the same (same commit), there's no update
const hasUpdate = created.getTime() !== lastModified.getTime();
return {
created,
updated: hasUpdate ? lastModified : null,
};
}

View file

@ -1,22 +1,44 @@
import path from 'node:path';
import type { CollectionEntry } from 'astro:content';
import { getGitDates, type GitDates } from './git';
type Post = CollectionEntry<'md'>;
export interface PostWithDates extends Post {
dates: GitDates;
}
export function getSlug(postId: string): string {
const parts = postId.split('/');
return parts[parts.length - 1];
}
function sortPosts(posts: Post[]): Post[] {
function getPostFilePath(post: Post): string {
return path.join(process.cwd(), 'src/content/md', `${post.id}.md`);
}
export function enrichPostWithDates(post: Post): PostWithDates {
const filePath = getPostFilePath(post);
return {
...post,
dates: getGitDates(filePath),
};
}
export function enrichPostsWithDates(posts: Post[]): PostWithDates[] {
return posts.map(enrichPostWithDates);
}
function sortPosts(posts: PostWithDates[]): PostWithDates[] {
return posts.slice().sort((a, b) => {
if (a.data.pinned && !b.data.pinned) return -1;
if (!a.data.pinned && b.data.pinned) return 1;
return b.data.date.getTime() - a.data.date.getTime();
return b.dates.created.getTime() - a.dates.created.getTime();
});
}
export function organizePostsByCategory(posts: Post[]): {
grouped: Record<string, Post[]>;
export function organizePostsByCategory(posts: PostWithDates[]): {
grouped: Record<string, PostWithDates[]>;
categories: string[];
} {
const grouped = posts.reduce((acc, post) => {
@ -24,7 +46,7 @@ export function organizePostsByCategory(posts: Post[]): {
if (!acc[category]) acc[category] = [];
acc[category].push(post);
return acc;
}, {} as Record<string, Post[]>);
}, {} as Record<string, PostWithDates[]>);
const categories = Object.keys(grouped).sort((a, b) => {
if (a === 'md') return -1;

View file

@ -1,17 +1,8 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import yaml from 'js-yaml';
import { sortByPinnedThenDate } from './format';
function getGitDate(filePath: string): Date {
try {
const timestamp = execSync(`git log -1 --format=%cI -- "${filePath}"`, { encoding: 'utf8' }).trim();
return timestamp ? new Date(timestamp) : new Date(0);
} catch {
return new Date(0);
}
}
import { getGitLastModifiedDate } from './git';
export interface TxtFile {
name: string;
@ -45,7 +36,7 @@ export function getTxtFiles(): TxtFile[] {
.filter(file => file.endsWith('.txt'))
.map(name => ({
name,
date: getGitDate(path.join(txtDir, name)),
date: getGitLastModifiedDate(path.join(txtDir, name)),
pinned: pinnedSet.has(name),
}));
return sortByPinnedThenDate(files);

View file

@ -5,27 +5,28 @@ import { getCollection, render } from 'astro:content';
import { requireAdminSession } from '../../lib/auth';
import Layout from '../../layouts/Layout.astro';
import { formatDate } from '../../lib/format';
import { getSlug } from '../../lib/md';
import { getSlug, enrichPostWithDates } from '../../lib/md';
const { session, error } = await requireAdminSession(Astro.request);
if (error) return error;
if (!session) return Astro.redirect('/api/auth/signin');
const slug = Astro.params.slug;
const posts = await getCollection('md', ({ data }) => data.draft === true);
const post = posts.find(p => getSlug(p.id) === slug);
const rawPosts = await getCollection('md', ({ data }) => data.draft === true);
const rawPost = rawPosts.find(p => getSlug(p.id) === slug);
if (!post) {
if (!rawPost) {
return new Response('Not found', { status: 404 });
}
const post = enrichPostWithDates(rawPost);
const { Content } = await render(post);
---
<Layout title={`${post.data.title} - lewis m.w.`}>
<article>
<h1>{post.data.title}</h1>
<p class="muted" style="margin-top: 0;">{formatDate(post.data.date)}</p>
<p class="muted" style="margin-top: 0;">{formatDate(post.dates.created)}{post.dates.updated && ` (updated ${formatDate(post.dates.updated)})`}</p>
<Content />
</article>
</Layout>

View file

@ -1,18 +1,19 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import type { APIContext } from 'astro';
import { getSlug } from '../lib/md';
import { getSlug, enrichPostsWithDates } from '../lib/md';
import { getTxtFiles } from '../lib/txt';
export async function GET(context: APIContext) {
const posts = await getCollection('md', ({ data }) => data.draft !== true);
const rawPosts = await getCollection('md', ({ data }) => data.draft !== true);
const posts = enrichPostsWithDates(rawPosts);
const bookmarks = await getCollection('bookmarks');
const txtFiles = getTxtFiles();
const items = [
...posts.map(post => ({
title: post.data.title,
pubDate: post.data.date,
pubDate: post.dates.created,
link: `/md/${getSlug(post.id)}`,
description: post.data.title,
})),

View file

@ -2,13 +2,13 @@
import { getCollection, render } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import { formatDate } from '../../lib/format';
import { getSlug } from '../../lib/md';
import { getSlug, enrichPostWithDates } from '../../lib/md';
export async function getStaticPaths() {
const posts = await getCollection('md', ({ data }) => data.draft !== true);
return posts.map(post => ({
params: { slug: getSlug(post.id) },
props: { post }
props: { post: enrichPostWithDates(post) }
}));
}
@ -19,7 +19,7 @@ const { Content } = await render(post);
<article>
<h1>{post.data.title}</h1>
<p class="muted" style="margin-top: 0;">{formatDate(post.data.date)}</p>
<p class="muted" style="margin-top: 0;">{formatDate(post.dates.created)}{post.dates.updated && ` (updated ${formatDate(post.dates.updated)})`}</p>
<Content />
</article>
</Layout>

View file

@ -2,9 +2,10 @@
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import { formatListItem } from '../../lib/format';
import { organizePostsByCategory, getSlug } from '../../lib/md';
import { organizePostsByCategory, getSlug, enrichPostsWithDates } from '../../lib/md';
const posts = await getCollection('md', ({ data }) => data.draft !== true);
const rawPosts = await getCollection('md', ({ data }) => data.draft !== true);
const posts = enrichPostsWithDates(rawPosts);
const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
---
<Layout title="md - lewis m.w.">
@ -12,7 +13,7 @@ const { grouped, categories: sortedCategories } = organizePostsByCategory(posts)
{sortedCategories.map(category => (
<details open>
<summary>{category}</summary>
<pre set:html={grouped[category].map(post => formatListItem(post.data.date, `/md/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })).join('\n')} />
<pre set:html={grouped[category].map(post => formatListItem(post.dates.created, `/md/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })).join('\n')} />
</details>
))}
</Layout>