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
5 changes: 2 additions & 3 deletions src/components/BlogMarkdownLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -30,7 +29,7 @@ export const BlogMarkdownLayout: React.FC<BlogMarkdownLayoutProps> = ({
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 (
Expand Down
39 changes: 11 additions & 28 deletions src/lib/blog.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,23 +12,15 @@ export interface BlogPostWithSlug extends BlogPost {
slug: string;
}

export async function getAllPosts(): Promise<BlogPostWithSlug[]> {
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));
}
3 changes: 2 additions & 1 deletion src/lib/rss.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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),
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/about/blog/document-cameras/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
35 changes: 32 additions & 3 deletions src/pages/about/blog/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 (
<article className={styles.blogPostCard}>
Expand Down Expand Up @@ -80,6 +82,33 @@ export default function BlogIndex({ posts }: BlogIndexProps) {
);
}

async function getAllPosts(): Promise<BlogPostWithSlug[]> {
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<BlogIndexProps> = async () => {
const posts = await getAllPosts();

Expand Down
2 changes: 1 addition & 1 deletion src/pages/about/blog/introducing-ai-grading/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
};
Expand Down
2 changes: 1 addition & 1 deletion src/pages/about/blog/linting-pit-of-success/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
};
Expand Down
2 changes: 1 addition & 1 deletion src/pages/about/blog/self-enrollment-overhaul/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
};
Expand Down
2 changes: 1 addition & 1 deletion src/pages/about/blog/stylish-mustaches/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
};
Expand Down
Loading