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
62 changes: 49 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@js-temporal/polyfill": "^0.5.1",
"@types/html-minifier-terser": "^7.0.2",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.13.2",
"@types/prismjs": "^1.26.3",
"canvas": "^3.0.1",
"eslint": "^10.5.0",
"globals": "^17.6.0",
"html-minifier-terser": "^7.2.0",
"husky": "^9.1.7",
"js-yaml": "^4.1.0",
"js-yaml": "^5.2.3",
"lightningcss": "^1.32.0",
"lint-staged": "^17.0.7",
"marked": "^18.0.7",
Expand Down
6 changes: 3 additions & 3 deletions src/build/feeds/create-atom-feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ export async function createAtomFeed(posts: Post[]): Promise<void> {
<title>${TITLE}</title>
<link href="${new URL(fileHref, BUILD.baseUrl).href}" rel="self" />
<link href="${BUILD.baseUrl}" />
<updated>${BUILD.date.toISOString()}</updated>
<updated>${BUILD.date.toString({ smallestUnit: 'millisecond' })}</updated>
<id>${BUILD.baseUrl}</id>
<author>
<name>${AUTHOR.name}</name>
<email>${AUTHOR.email}</email>
</author>
${map(posts, (post) => {
const date = post.meta.date.toISOString();
const updated = post.meta.updated?.toISOString() || date;
const date = post.meta.date.toString();
const updated = post.meta.updated?.toString() || date;
return html`
<entry>
<title>${post.meta.title}</title>
Expand Down
6 changes: 3 additions & 3 deletions src/build/feeds/create-rss-feed.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Post } from '#types';
import path from 'node:path';
import { formatDateUtcString } from '../../utils/date.js';
import { html, map } from '../../utils/html.js';
import { createFile } from '../../utils/node.js';
import { AUTHOR, BUILD, TITLE } from '../../config.js';
Expand All @@ -9,7 +10,7 @@ const FILE_NAME = 'rss.xml';

export async function createRssFeed(posts: Post[]): Promise<void> {
const fileHref = new URL(FILE_NAME, BUILD.baseUrl).href;
const buildDate = BUILD.date.toUTCString();
const buildDate = formatDateUtcString(BUILD.date);

// It is important there is no whitespace before the XML tag.
const contents = html`<?xml version="1.0" encoding="utf-8"?>
Expand All @@ -30,13 +31,12 @@ export async function createRssFeed(posts: Post[]): Promise<void> {
<category>Web</category>
<lastBuildDate>${buildDate}</lastBuildDate>
${map(posts, (post) => {
const date = post.meta.date.toUTCString();
return html`
<item>
<title>${post.meta.title}</title>
<link>${post.meta.url}</link>
<guid>${post.meta.url}</guid>
<pubDate>${date}</pubDate>
<pubDate>${formatDateUtcString(post.meta.date)}</pubDate>
<description>${post.meta.description}></description>
<content:encoded><![CDATA[${post.content}]]></content:encoded>
<author>${AUTHOR.email} (${AUTHOR.name})</author>
Expand Down
4 changes: 2 additions & 2 deletions src/build/feeds/create-sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { PageData, RenderedPages } from '#types';
import path from 'node:path';
import { formatDateIso } from '../../utils/date.js';
import { formatPlainDate } from '../../utils/date.js';
import { html, map } from '../../utils/html.js';
import { createFile } from '../../utils/node.js';
import { BUILD } from '../../config.js';
Expand All @@ -12,7 +12,7 @@ function pagesToSitemapEntries(pages: PageData[]): string {
({ url, updated, date }) => html`
<url>
<loc>${url}</loc>
<lastmod>${formatDateIso(updated || date || BUILD.date)}</lastmod>
<lastmod>${formatPlainDate(updated || date || BUILD.date)}</lastmod>
</url>
`,
);
Expand Down
50 changes: 30 additions & 20 deletions src/build/posts/get-posts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ParsedFrontMatter, Post } from '#types';
import { promises as fs } from 'node:fs';
import yamlToJs from 'js-yaml';
import { Temporal } from '@js-temporal/polyfill';
import { load } from 'js-yaml';
import { convert } from 'quote-quote';
import { findFilesByExtension } from '../../utils/node.js';
import { isObject } from '../../utils/object.js';
Expand All @@ -11,6 +12,9 @@ import { parseMarkdown } from './parse-markdown.js';
// Markdown front matter is at the top of the file between triple-dashed lines.
const frontMatterRegex = /^---([\s\S]*?)---/;

// Verify a string is in the format of `yyyy-mm-dd hh:mm:ss`.
const dateTimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;

/**
* Return all Markdown files transformed to posts.
*/
Expand All @@ -19,7 +23,9 @@ export async function getPosts(): Promise<Post[]> {
const result = await Promise.all(
mdFiles.map((file) => transformMarkdownFileToPost(file)),
);
return result.sort((a, b) => b.meta.date.getTime() - a.meta.date.getTime());
return result.sort((a, b) =>
Temporal.Instant.compare(b.meta.date, a.meta.date),
);
}

async function transformMarkdownFileToPost(file: string): Promise<Post> {
Expand All @@ -40,29 +46,33 @@ async function transformMarkdownFileToPost(file: string): Promise<Post> {
};
}

function parseFrontMatter(fm: string | undefined): ParsedFrontMatter {
if (fm) {
const parsed = yamlToJs.load(fm);
if (isValidFrontMatter(parsed)) {
return parsed;
function parseFrontMatter(rawFm: string | undefined): ParsedFrontMatter {
if (rawFm) {
const fm = load(rawFm);

if (
isObject(fm) &&
'title' in fm &&
'date' in fm &&
dateTimeRegex.test(fm.date as string) &&
(!('tags' in fm) || isValidTags(fm.tags))
) {
fm.date = stringToUtcDateTime(fm.date as string);

if ('updated' in fm && dateTimeRegex.test(fm.updated as string)) {
fm.updated = stringToUtcDateTime(fm.updated as string);
}

return fm as ParsedFrontMatter;
}
}
throw new Error('Invalid front matter');
}

/**
* Check if front matter has all required properties in the correct format. This
* ensures data is complete, but also parsed as expected by `js-yaml`.
*/
function isValidFrontMatter(fm: unknown): fm is ParsedFrontMatter {
return (
isObject(fm) &&
'title' in fm &&
'date' in fm &&
fm.date instanceof Date &&
(!('tags' in fm) || isValidTags(fm.tags)) &&
(!('updated' in fm) || fm.updated instanceof Date)
);
function stringToUtcDateTime(fmTimestamp: string): Temporal.Instant {
return Temporal.PlainDateTime.from(fmTimestamp.replace(' ', 'T'))
.toZonedDateTime('UTC')
.toInstant();
}

function isValidTags(tags: unknown): tags is string[] {
Expand Down
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Temporal } from '@js-temporal/polyfill';
import { BASE_URL_DEV, BASE_URL_PROD } from './constants.js';

type Env = 'DEV' | 'TEST' | 'PROD';
Expand All @@ -14,7 +15,7 @@ export const AUTHOR = {

export const BUILD = {
...getBaseUrlAndEnv(),
date: new Date(),
date: Temporal.Now.instant(),
};

function getBaseUrlAndEnv(): { env: Env; baseUrl: string } {
Expand Down
4 changes: 2 additions & 2 deletions src/includes/post-preview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Post } from '#types';
import { formatDateIso, formatDateShort } from '../utils/date.js';
import { formatDateShort, formatPlainDate } from '../utils/date.js';
import { html, when } from '../utils/html.js';
import { tagList } from './tag-list.js';

Expand All @@ -8,7 +8,7 @@ function showParagraphWithDate(post: Post): string {
'<p>',
(paragraphTag) => html`
${paragraphTag}
<time datetime="${formatDateIso(post.meta.date)}">
<time datetime="${formatPlainDate(post.meta.date)}">
${formatDateShort(post.meta.date)} —&nbsp;
</time>
`,
Expand Down
6 changes: 4 additions & 2 deletions src/layouts/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ function getPageMetadata(data: PageData): PageMetadata {
? `Banner that introduces the blog post by its title: ${post.meta.title}`
: 'Profile picture of Tom Herni—a front-end engineer passionate about designing and building creative solutions for the web';

const date = data.date?.toISOString();
const date = data.date?.toString();
const updated =
data.updated?.toISOString() || date || BUILD.date.toISOString();
data.updated?.toString() ||
date ||
BUILD.date.toString({ smallestUnit: 'millisecond' });

return {
title,
Expand Down
4 changes: 2 additions & 2 deletions src/layouts/post.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Layout } from '#types';
import { profilePicture } from '../includes/profile-picture.js';
import { tagList } from '../includes/tag-list.js';
import { formatDateIso, formatDateLong } from '../utils/date.js';
import { formatDateLong, formatPlainDate } from '../utils/date.js';
import { html, when } from '../utils/html.js';
import baseLayout from './base.js';

Expand All @@ -23,7 +23,7 @@ const layout: Layout = (data) => {
<header>
<h1>${post.meta.title}</h1>
<div class="datetime">
<time datetime="${formatDateIso(post.meta.date)}">
<time datetime="${formatPlainDate(post.meta.date)}">
${formatDateLong(post.meta.date)}
</time>
&nbsp;&bull;&nbsp; ${post.meta.timeToRead} min read
Expand Down
2 changes: 1 addition & 1 deletion src/static/blog/and-so-it-begins/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: And So It Begins
date: 2024-01-03 17:00:00
date: 2024-01-03 16:00:00
tags:
- blog
- tomherni.dev
Expand Down
2 changes: 1 addition & 1 deletion src/static/blog/curly-quotes-with-quote-quote/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Curly Quotes with QuoteQuote
date: 2025-04-17 10:16:53
date: 2025-04-17 08:16:53
tags:
- quote-quote
- github
Expand Down
2 changes: 1 addition & 1 deletion src/static/blog/thanks-ai-you-ruined-the-em-dash/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Thanks AI, You Ruined the Em Dash
date: 2026-08-03 22:24:00
date: 2026-08-03 20:24:00
tags:
- ai
---
Expand Down
2 changes: 1 addition & 1 deletion src/static/blog/the-specificity-of-slotted/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: The Specificity Of ::slotted()
date: 2024-02-08 15:34:00
date: 2024-02-08 14:34:00
tags:
- css
- web components
Expand Down
Loading