diff --git a/src/components/BlogMarkdownLayout.tsx b/src/components/BlogMarkdownLayout.tsx index 5620dba0..4371ed86 100644 --- a/src/components/BlogMarkdownLayout.tsx +++ b/src/components/BlogMarkdownLayout.tsx @@ -5,9 +5,8 @@ import ReactMarkdown from "react-markdown"; import { MDXProvider } from "@mdx-js/react"; import Head from "next/head"; import Modal from "react-bootstrap/Modal"; -import { format } from "date-fns"; - import mdxComponents from "../lib/mdxComponents"; +import { formatBlogDate } from "../lib/blog"; import { PageBanner } from "./Banner"; import { TagList } from "./Tag"; @@ -30,7 +29,7 @@ export const BlogMarkdownLayout: React.FC = ({ const { title, summary, date, author, tags, ogImage } = meta; if (!title) throw new Error("Missing title"); - const formattedDate = date ? format(new Date(date), "MMMM d, yyyy") : null; + const formattedDate = date ? formatBlogDate(date) : null; const hasMeta = formattedDate || author || (tags && tags.length > 0); return ( diff --git a/src/lib/blog.ts b/src/lib/blog.ts index 7a750458..cb3c13bd 100644 --- a/src/lib/blog.ts +++ b/src/lib/blog.ts @@ -1,13 +1,4 @@ -import fs from "fs"; -import path from "path"; - -const postsDirectory = path.join( - process.cwd(), - "src", - "pages", - "about", - "blog", -); +import { parseISO } from "date-fns"; export interface BlogPost { title: string; @@ -21,23 +12,15 @@ export interface BlogPostWithSlug extends BlogPost { slug: string; } -export async function getAllPosts(): Promise { - const items = fs.readdirSync(postsDirectory, { withFileTypes: true }); - const slugs = items - .filter((item) => item.isDirectory()) - .map((item) => item.name); - const posts = await Promise.all( - slugs.map(async (slug) => { - const { meta } = (await import( - `../pages/about/blog/${slug}/index.mdx` - )) as { meta: BlogPost }; - return { slug, ...meta }; - }), - ); - - const sortedPosts = posts.toSorted( - (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), - ); +const BLOG_DATE_FORMATTER = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", + day: "numeric", + // timeZone is set so server and client render the same calendar day. + // Revisit if we ever display times along with dates. + timeZone: "America/Chicago", +}); - return sortedPosts; +export function formatBlogDate(date: string): string { + return BLOG_DATE_FORMATTER.format(parseISO(date)); } diff --git a/src/lib/rss.ts b/src/lib/rss.ts index 59f02836..7f76417b 100644 --- a/src/lib/rss.ts +++ b/src/lib/rss.ts @@ -1,6 +1,7 @@ import fs from "fs/promises"; import path from "path"; import RSS from "rss"; +import { parseISO } from "date-fns"; import { BlogPostWithSlug } from "./blog"; const SITE_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL @@ -28,7 +29,7 @@ export async function generateRssFeed( guid: `${BLOG_URL}/${post.slug}`, categories: post.tags || [], author: post.author, - date: new Date(post.date), + date: parseISO(post.date), }); } diff --git a/src/pages/about/blog/document-cameras/index.mdx b/src/pages/about/blog/document-cameras/index.mdx index 07be6fb3..f479e28c 100644 --- a/src/pages/about/blog/document-cameras/index.mdx +++ b/src/pages/about/blog/document-cameras/index.mdx @@ -17,7 +17,7 @@ import ptExamLabel from "./pt-exam-label.png"; export const meta = { title: "Document cameras make CBTF exams more flexible", - date: "2026-04-21", + date: "2026-04-21T00:00:00-05:00", author: "Jim Sosnowski", tags: ["CBTF", "Case study"], summary: diff --git a/src/pages/about/blog/index.tsx b/src/pages/about/blog/index.tsx index 59f2f1b5..347f3864 100644 --- a/src/pages/about/blog/index.tsx +++ b/src/pages/about/blog/index.tsx @@ -1,13 +1,15 @@ +import fs from "fs"; +import path from "path"; import React from "react"; import Head from "next/head"; import Link from "next/link"; import { GetStaticProps } from "next"; +import { parseISO } from "date-fns"; import { PageBanner } from "../../../components/Banner"; import { Heading } from "../../../components/Heading"; import Stack from "../../../components/Stack"; import { TagList } from "../../../components/Tag"; -import { format } from "date-fns"; -import { getAllPosts, BlogPostWithSlug } from "../../../lib/blog"; +import { BlogPost, BlogPostWithSlug, formatBlogDate } from "../../../lib/blog"; import { generateRssFeed } from "../../../lib/rss"; import styles from "./index.module.scss"; @@ -17,7 +19,7 @@ interface BlogIndexProps { } const BlogPostCard = ({ post }: { post: BlogPostWithSlug }) => { - const formattedDate = format(new Date(post.date), "MMMM d, yyyy"); + const formattedDate = formatBlogDate(post.date); return (
@@ -80,6 +82,33 @@ export default function BlogIndex({ posts }: BlogIndexProps) { ); } +async function getAllPosts(): Promise { + const postsDirectory = path.join( + process.cwd(), + "src", + "pages", + "about", + "blog", + ); + + const items = fs.readdirSync(postsDirectory, { withFileTypes: true }); + const slugs = items + .filter((item) => item.isDirectory()) + .map((item) => item.name); + const posts = await Promise.all( + slugs.map(async (slug) => { + const { meta } = (await import(`./${slug}/index.mdx`)) as { + meta: BlogPost; + }; + return { slug, ...meta }; + }), + ); + + return posts.toSorted( + (a, b) => parseISO(b.date).getTime() - parseISO(a.date).getTime(), + ); +} + export const getStaticProps: GetStaticProps = async () => { const posts = await getAllPosts(); diff --git a/src/pages/about/blog/introducing-ai-grading/index.mdx b/src/pages/about/blog/introducing-ai-grading/index.mdx index dfe30208..d5ae7f5e 100644 --- a/src/pages/about/blog/introducing-ai-grading/index.mdx +++ b/src/pages/about/blog/introducing-ai-grading/index.mdx @@ -13,7 +13,7 @@ import submissionExplanation from "./submission-explanation.png"; export const meta = { title: "Introducing AI grading", - date: "2026-05-21", + date: "2026-05-21T13:32:00-05:00", author: "Miguel Aenlle", tags: ["RELEASE"], }; diff --git a/src/pages/about/blog/linting-pit-of-success/index.mdx b/src/pages/about/blog/linting-pit-of-success/index.mdx index 24c37dd9..a455620e 100644 --- a/src/pages/about/blog/linting-pit-of-success/index.mdx +++ b/src/pages/about/blog/linting-pit-of-success/index.mdx @@ -10,7 +10,7 @@ import reviewFunnel from "./review-funnel.png"; export const meta = { title: "Linting into 'The Pit of Success'", - date: "2025-11-26", + date: "2025-11-26T00:00:00-06:00", author: "Peter Stenger", tags: ["Technical", "Development"], }; diff --git a/src/pages/about/blog/self-enrollment-overhaul/index.mdx b/src/pages/about/blog/self-enrollment-overhaul/index.mdx index 7259e0f5..5ab3883b 100644 --- a/src/pages/about/blog/self-enrollment-overhaul/index.mdx +++ b/src/pages/about/blog/self-enrollment-overhaul/index.mdx @@ -16,7 +16,7 @@ import studentsPage from "./students_page.png"; export const meta = { title: "Overhauled enrollment management", - date: "2026-01-01", + date: "2026-01-01T00:00:00-06:00", author: "Peter Stenger", tags: ["Release"], }; diff --git a/src/pages/about/blog/stylish-mustaches/index.mdx b/src/pages/about/blog/stylish-mustaches/index.mdx index 17027a48..ea96f93d 100644 --- a/src/pages/about/blog/stylish-mustaches/index.mdx +++ b/src/pages/about/blog/stylish-mustaches/index.mdx @@ -12,7 +12,7 @@ import linterScreenshot from "./linter.png"; export const meta = { title: "Stylish mustaches", - date: "2026-02-25", + date: "2026-02-25T00:00:00-06:00", author: "Peter Stenger", tags: ["Technical", "Development"], };