feat: optional dates, otherwise fetched from git
This commit is contained in:
parent
4d9e3c56da
commit
cc6eff22a8
8 changed files with 90 additions and 31 deletions
|
|
@ -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
44
www/src/lib/git.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue