Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions src/components/patterns/AnnouncementBar/AnnouncementBar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Banner } from '@components/patterns';
import { BodyMd } from '@components/primitives';
import { getCollection } from 'astro:content';
import { getLangFromUrl, useTranslations } from '@i18n/utils';
import { getPubDate, sortByPubDateDesc } from '@/utils/rss';
import announcement from '@/config/announcement.json';

type BannerVariant = 'info' | 'warning';
Expand All @@ -22,26 +23,15 @@ const useCustom = !!announcement.title;

function getLatestBlog() {
return getCollection('blog').then((posts) => {
const sorted = posts
.filter((post) => post.id !== 'write-post')
.sort((a, b) => {
const aFile = a.id.split('/').pop() ?? '';
const bFile = b.id.split('/').pop() ?? '';
return bFile.localeCompare(aFile);
});
const sorted = sortByPubDateDesc(posts.filter((post) => post.id !== 'write-post'));
return sorted[0] ?? null;
});
}

const latestPost = !useCustom ? await getLatestBlog() : null;

function getPostDate(id: string) {
const filename = id.split('/').pop() ?? id;
return filename.substring(0, 10);
}

const blogSlug = latestPost ? latestPost.id.split('/').pop() : '';
const blogDate = latestPost ? getPostDate(latestPost.id) : '';
const blogDate = latestPost ? (getPubDate(latestPost)?.toISOString().substring(0, 10) ?? '') : '';
const blogTitle = latestPost?.data.title ?? '';
const blogDescription = latestPost?.data.description ?? '';
const blogVariant: BannerVariant = latestPost?.data.tags?.includes('security') ? 'warning' : 'info';
Expand Down
7 changes: 2 additions & 5 deletions src/components/patterns/RelatedContent/RelatedContent.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { CollectionEntry } from 'astro:content';
import './RelatedContent.css';
import { H2 } from '@/components/primitives';
import { PostCard } from '@/components/patterns';
import { formatPubDate } from '@/utils/rss';

interface Props {
posts: CollectionEntry<'blog'>[];
Expand All @@ -20,11 +21,7 @@ const { posts, lang, title } = Astro.props;
<div class="related-content__grid">
{posts.map((p) => {
const pFilename = p.id.split('/').pop() ?? p.id;
const pDate = new Date(pFilename.substring(0, 10)).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
const pDate = formatPubDate(p, { month: 'long', day: 'numeric', year: 'numeric' });
const pAuthors = (p.data.authors ?? []).map((a) => ({
src: a.github ? `https://github.com/${a.github}.png` : '',
name: a.name ?? '',
Expand Down
1 change: 1 addition & 0 deletions src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
description: z.string().optional(),
date: z.coerce.date().optional(),
tags: z.array(z.string()).optional(),
cover: z.string().optional(),
authors: z
Expand Down
5 changes: 4 additions & 1 deletion src/content/blog/write-post.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ If you have an idea for a blog post, follow these steps to propose it and potent

1. **Create a new file**

Create a new file in the `src/content/blog` directory named using following the format: `YYYY-MM-DD-title.md`.
Create a new file in the `src/content/blog` directory named using following the format: `YYYY-MM-DD-title.md`. The date prefix is used as the publication date unless you set a `date` field in the front matter (see below).

1. **Add the required front matter**

Expand All @@ -30,6 +30,7 @@ If you have an idea for a blog post, follow these steps to propose it and potent
---
title: <your-title>
description: <description-of-your-post>
date: YYYY-MM-DD
tags: ['tag1', 'tag2']
authors:
- name: <your-name>
Expand All @@ -40,6 +41,8 @@ If you have an idea for a blog post, follow these steps to propose it and potent

The `github` property of an author is optional. Including your username only (not your full profile URL) will ensure that your blog post links out to it.

The `date` property is optional. When set, it determines the post's publication date (used for ordering, display, and feeds); otherwise the `YYYY-MM-DD` prefix in the filename is used.

The `cover` property is optional. If omitted, an Open Graph image will be automatically generated from the post title. If you want a custom cover image, place it in the `public` directory and reference its path (e.g. `/images/my-cover.jpg`).

1. **Add your content**
Expand Down
20 changes: 3 additions & 17 deletions src/pages/[lang]/blog/[...page].astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,14 @@ import type { TabItem } from '@components/primitives';
import { PostCard, PageHead, Pagination } from '@components/patterns';
import { getCollection, type CollectionEntry } from 'astro:content';
import type { PaginateFunction } from 'astro';
import { formatPubDate, sortByPubDateDesc } from '@/utils/rss';

export async function getStaticPaths({ paginate }: { paginate: PaginateFunction }) {
const PAGE_SIZE = 6;
const allPosts = await getCollection('blog');

return Object.keys(languages).flatMap((lang) => {
const langPosts = allPosts
.filter((post) => post.id !== 'write-post')
.sort((a, b) => {
const aFile = a.id.split('/').pop() ?? '';
const bFile = b.id.split('/').pop() ?? '';
return bFile.localeCompare(aFile);
});
const langPosts = sortByPubDateDesc(allPosts.filter((post) => post.id !== 'write-post'));

const allTags = [...new Set(langPosts.flatMap((p) => p.data.tags ?? []))];

Expand All @@ -35,15 +30,6 @@ const { page, allTags, langPosts, pageSize } = Astro.props;
const { lang } = Astro.params;
const pagePosts = page.data as CollectionEntry<'blog'>[];

function getDate(id: string) {
const filename = id.split('/').pop() ?? id;
return new Date(filename.substring(0, 10)).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}

function formatTagLabel(tag: string) {
const spaced = tag.replace(/-/g, ' ');
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
Expand All @@ -63,7 +49,7 @@ function getPostProps(post: CollectionEntry<'blog'>) {
src: a.github ? `https://github.com/${a.github}.png` : '',
name: a.name ?? '',
})),
avatarCaption: getDate(post.id),
avatarCaption: formatPubDate(post, { month: 'short', day: 'numeric', year: 'numeric' }),
};
}

Expand Down
17 changes: 5 additions & 12 deletions src/pages/[lang]/blog/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
WriteBanner,
RelatedContent,
} from '@/components/patterns';
import { formatPubDate, sortByPubDateDesc } from '@/utils/rss';

export async function getStaticPaths() {
const posts = await getCollection('blog');
Expand Down Expand Up @@ -40,20 +41,12 @@ const authorsList = (authors ?? []).map((a) => ({
src: a.github ? `https://github.com/${a.github}.png` : '',
name: a.name ?? '',
}));
const dateSource = filename.substring(0, 10);
const parsedDate = new Date(dateSource);
const date = Number.isNaN(parsedDate.getTime())
? undefined
: parsedDate.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
const date = formatPubDate(post, { month: 'long', day: 'numeric', year: 'numeric' });
const labels = tags ?? [];

const allPosts = (await getCollection('blog'))
.filter((p) => p.id !== post.id && p.id !== 'write-post')
.sort((a, b) => b.id.localeCompare(a.id));
const allPosts = sortByPubDateDesc(
(await getCollection('blog')).filter((p) => p.id !== post.id && p.id !== 'write-post')
);

const currentTags = new Set(tags ?? []);
const tagged = allPosts.filter((p) => p.data.tags?.some((tag) => currentTags.has(tag)));
Expand Down
8 changes: 4 additions & 4 deletions src/pages/feed.xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import type { APIRoute } from 'astro';
import {
getLinkedTitleContent,
getAuthorsCustomData,
getPubDateFromId,
getPubDate,
shouldIncludeInFeed,
sortByIdDateDesc,
sortByPubDateDesc,
} from '@/utils/rss';

export const GET: APIRoute = async (context) => {
const site = context.site as URL;
const blog = await getCollection('blog');
const sortedBlog = sortByIdDateDesc(blog.filter((post) => shouldIncludeInFeed(post.id)));
const sortedBlog = sortByPubDateDesc(blog.filter((post) => shouldIncludeInFeed(post.id)));

return rss({
title: 'The Express.js Blog',
Expand All @@ -21,7 +21,7 @@ export const GET: APIRoute = async (context) => {
items: sortedBlog.map((post) => ({
link: `/en/blog/${post.id}/`,
title: post.data.title,
pubDate: getPubDateFromId(post.id),
pubDate: getPubDate(post),
description: post.data.description,
content: getLinkedTitleContent(post.data.title, post.id, site),
categories: post.data.tags,
Expand Down
8 changes: 4 additions & 4 deletions src/pages/vulnerabilities.xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import type { APIRoute } from 'astro';
import {
getLinkedTitleContent,
getAuthorsCustomData,
getPubDateFromId,
getPubDate,
shouldIncludeInFeed,
sortByIdDateDesc,
sortByPubDateDesc,
} from '@/utils/rss';

export const GET: APIRoute = async (context) => {
const site = context.site as URL;
const blog = await getCollection('blog');
const securityPosts = sortByIdDateDesc(
const securityPosts = sortByPubDateDesc(
blog.filter(
(post) => shouldIncludeInFeed(post.id) && (post.data.tags ?? []).includes('security')
)
Expand All @@ -25,7 +25,7 @@ export const GET: APIRoute = async (context) => {
items: securityPosts.map((post) => ({
link: `/en/blog/${post.id}/`,
title: post.data.title,
pubDate: getPubDateFromId(post.id),
pubDate: getPubDate(post),
description: post.data.description,
content: getLinkedTitleContent(post.data.title, post.id, site),
categories: post.data.tags,
Expand Down
37 changes: 34 additions & 3 deletions src/utils/rss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export interface FeedAuthor {
github?: string;
}

export interface DatedPost {
id: string;
data?: { date?: Date | string };
}

export function getPubDateFromId(id: string): Date | undefined {
const filename = id.split('/').pop() ?? id;
const match = filename.match(/^(\d{4}-\d{2}-\d{2})/);
Expand All @@ -12,6 +17,32 @@ export function getPubDateFromId(id: string): Date | undefined {
return Number.isNaN(date.getTime()) ? undefined : date;
}

/**
* Resolves a post's publication date, preferring the `date` front matter field
* and falling back to the `YYYY-MM-DD` prefix in the filename.
*/
export function getPubDate(post: DatedPost): Date | undefined {
const frontmatter = post.data?.date;
if (frontmatter) {
const date = frontmatter instanceof Date ? frontmatter : new Date(frontmatter);
if (!Number.isNaN(date.getTime())) return date;
}
return getPubDateFromId(post.id);
}

/**
* Formats a post's publication date for display. Calendar dates are formatted in
* UTC so the rendered day matches the authored date regardless of the viewer's
* timezone. Returns undefined when no valid date can be resolved.
*/
export function formatPubDate(
post: DatedPost,
options: Intl.DateTimeFormatOptions
): string | undefined {
const date = getPubDate(post);
return date?.toLocaleDateString('en-US', { timeZone: 'UTC', ...options });
}

export function getAuthorsCustomData(authors?: FeedAuthor[]): string | undefined {
if (!authors?.length) return undefined;

Expand All @@ -23,10 +54,10 @@ export function getAuthorsCustomData(authors?: FeedAuthor[]): string | undefined
.join('');
}

export function sortByIdDateDesc<T extends { id: string }>(items: T[]): T[] {
export function sortByPubDateDesc<T extends DatedPost>(items: T[]): T[] {
return [...items].sort((a, b) => {
const aTime = getPubDateFromId(a.id)?.getTime() ?? 0;
const bTime = getPubDateFromId(b.id)?.getTime() ?? 0;
const aTime = getPubDate(a)?.getTime() ?? 0;
const bTime = getPubDate(b)?.getTime() ?? 0;

if (aTime === bTime) return a.id.localeCompare(b.id);
return bTime - aTime;
Expand Down
Loading