Skip to content

Commit 7187301

Browse files
committed
Add OpenGraph, article meta, and build timestamp
1 parent 05b43a7 commit 7187301

6 files changed

Lines changed: 125 additions & 52 deletions

File tree

pages/about.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ The "nocturnal scribbles" part is literal - I do my best work between 10pm and 4
3737

3838
# This blog
3939

40-
Custom static site generator, built in one night with Bun and TypeScript. No frameworks, no tracking, no analytics, no bullshit. Just markdown files in git that become HTML.
40+
[Custom static site generator](https://github.com/AviDuda/nocturnal-scribbles), built in one night with Bun and TypeScript. No frameworks, no tracking, no analytics, no bullshit. Just markdown files in git that become HTML.
4141

4242
# Get in touch
4343

posts/the-night-shift-begins.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ So absolutely no promises about any kind of schedule or what I'll actually talk
6161

6262
# Interested? Hit the RSS bell, err, button
6363

64-
Look, there's no analytics, no tracking, no newsletter or anything. I want a good old site with no bullshit like this. It's all running on a very simple blog generator I built in one night for this purpose.
64+
Look, there's no analytics, no tracking, no newsletter or anything. I want a good old site with no bullshit like this. It's all running on a [very simple blog generator](https://github.com/AviDuda/nocturnal-scribbles) I built in one night for this purpose.
6565

6666
There's the main RSS feed and also per-tag feeds if you're interested only in specific areas.
6767

src/dev.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,8 @@ describe("Dev Server", () => {
406406
// Write a markdown file to trigger rebuild
407407
writeFileSync(join(TEST_DIRS.watchDir, "test-post.md"), "# Test");
408408

409-
// Wait for fs event to propagate
410-
await Bun.sleep(150);
409+
// Wait for fs event to propagate (longer timeout for CI reliability)
410+
await Bun.sleep(300);
411411

412412
expect(rebuildCalled).toBe(true);
413413

@@ -432,7 +432,7 @@ describe("Dev Server", () => {
432432
);
433433

434434
// Wait to ensure no rebuild is triggered
435-
await Bun.sleep(150);
435+
await Bun.sleep(300);
436436

437437
expect(rebuildCalled).toBe(false);
438438

src/templates.test.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe("templates", () => {
3737
join(TEMPLATES_DIR, "base.html"),
3838
`<!DOCTYPE html>
3939
<html>
40-
<head><title>{{TITLE}}</title><meta name="description" content="{{DESCRIPTION}}"></head>
40+
<head><title>{{TITLE}}</title><meta name="description" content="{{DESCRIPTION}}">{{ARTICLE_META}}</head>
4141
<body>{{CONTENT}}</body>
4242
</html>`,
4343
);
@@ -166,13 +166,12 @@ describe("templates", () => {
166166

167167
describe("applyBaseTemplate", () => {
168168
test("should replace title and content placeholders", async () => {
169-
const result = await applyBaseTemplate(
170-
"<p>Hello</p>",
171-
"Test Title",
172-
"Test description",
173-
{},
174-
TEMPLATES_DIR,
175-
);
169+
const result = await applyBaseTemplate({
170+
content: "<p>Hello</p>",
171+
title: "Test Title",
172+
description: "Test description",
173+
templatesDir: TEMPLATES_DIR,
174+
});
176175

177176
expect(result).toContain("<title>Test Title</title>");
178177
expect(result).toContain('content="Test description"');
@@ -196,6 +195,32 @@ describe("templates", () => {
196195
expect(result).toContain("/static/style-abc123.css");
197196
expect(result).not.toContain("/static/style.css");
198197
});
198+
199+
test("should include article meta tags when publishedTime is provided", async () => {
200+
const result = await applyBaseTemplate({
201+
content: "<p>Post content</p>",
202+
title: "Test Post",
203+
templatesDir: TEMPLATES_DIR,
204+
ogType: "article",
205+
publishedTime: "2025-01-15T00:00:00.000Z",
206+
});
207+
208+
expect(result).toContain(
209+
'article:published_time" content="2025-01-15T00:00:00.000Z"',
210+
);
211+
expect(result).toContain('article:author" content="aviraccoon"');
212+
});
213+
214+
test("should not include article meta tags when publishedTime is not provided", async () => {
215+
const result = await applyBaseTemplate({
216+
content: "<p>Page content</p>",
217+
title: "Test Page",
218+
templatesDir: TEMPLATES_DIR,
219+
});
220+
221+
expect(result).not.toContain("article:published_time");
222+
expect(result).not.toContain("article:author");
223+
});
199224
});
200225

201226
describe("renderPostListItem", () => {

src/templates.ts

Lines changed: 79 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
HOMEPAGE_POST_LIMIT,
99
ROOT_DIR,
1010
SITE_DESCRIPTION,
11+
SITE_TITLE,
12+
SITE_URL,
1113
TEMPLATES_DIR,
1214
} from "./config";
1315
import { groupPostsByYearMonth } from "./content";
@@ -69,14 +71,38 @@ export async function readTemplate(
6971
* Wraps content in the base HTML template with title and description.
7072
* Replaces static asset references with hashed versions.
7173
*/
74+
type BaseTemplateOptions = {
75+
content: string;
76+
title: string;
77+
description?: string;
78+
assetMap?: StaticAssetMap;
79+
templatesDir?: string;
80+
rootDir?: string;
81+
canonicalPath?: string;
82+
ogType?: "website" | "article";
83+
/** ISO date string for article:published_time (posts only) */
84+
publishedTime?: string;
85+
};
86+
7287
export async function applyBaseTemplate(
73-
content: string,
74-
title: string,
75-
description = "",
76-
assetMap: StaticAssetMap = {},
77-
templatesDir = TEMPLATES_DIR,
78-
rootDir = ROOT_DIR,
88+
options: BaseTemplateOptions,
7989
): Promise<string> {
90+
const {
91+
content,
92+
title,
93+
description = "",
94+
assetMap = {},
95+
templatesDir = TEMPLATES_DIR,
96+
rootDir = ROOT_DIR,
97+
canonicalPath = "/",
98+
ogType = "website",
99+
publishedTime,
100+
} = options;
101+
102+
const articleMeta = publishedTime
103+
? `\n <meta property="article:published_time" content="${publishedTime}">\n <meta property="article:author" content="aviraccoon">`
104+
: "";
105+
80106
let baseTemplate = await readTemplate("base.html", templatesDir);
81107

82108
// Process {{INCLUDE:path}} directives
@@ -88,9 +114,15 @@ export async function applyBaseTemplate(
88114
}
89115

90116
return baseTemplate
91-
.replace("{{TITLE}}", title)
92-
.replace("{{DESCRIPTION}}", description)
93-
.replace("{{CONTENT}}", content);
117+
.replaceAll("{{TITLE}}", title)
118+
.replaceAll("{{DESCRIPTION}}", description)
119+
.replace("{{CONTENT}}", content)
120+
.replace("{{BUILD_TIME}}", new Date().toISOString())
121+
.replace("{{CANONICAL_URL}}", `${SITE_URL}${canonicalPath}`)
122+
.replace("{{OG_TYPE}}", ogType)
123+
.replace("{{SITE_TITLE}}", SITE_TITLE)
124+
.replace("{{SITE_URL}}", SITE_URL)
125+
.replace("{{ARTICLE_META}}", articleMeta);
94126
}
95127

96128
/**
@@ -160,13 +192,16 @@ export async function generatePostPage(
160192
postHtml = postHtml.replace(/{{#TAGS}}[\s\S]*?{{\/TAGS}}/g, "");
161193
}
162194

163-
return applyBaseTemplate(
164-
postHtml,
165-
post.frontmatter.title,
166-
post.frontmatter.description || "",
195+
return applyBaseTemplate({
196+
content: postHtml,
197+
title: post.frontmatter.title,
198+
description: post.frontmatter.description,
167199
assetMap,
168200
templatesDir,
169-
);
201+
canonicalPath: `/posts/${post.slug}/`,
202+
ogType: "article",
203+
publishedTime: new Date(post.frontmatter.date).toISOString(),
204+
});
170205
}
171206

172207
/**
@@ -183,13 +218,14 @@ export async function generateSimplePage(
183218
.replace("{{TITLE}}", page.frontmatter.title)
184219
.replace("{{CONTENT}}", page.html);
185220

186-
return applyBaseTemplate(
187-
pageHtml,
188-
page.frontmatter.title,
189-
page.frontmatter.description || "",
221+
return applyBaseTemplate({
222+
content: pageHtml,
223+
title: page.frontmatter.title,
224+
description: page.frontmatter.description,
190225
assetMap,
191226
templatesDir,
192-
);
227+
canonicalPath: `/${page.slug}/`,
228+
});
193229
}
194230

195231
/**
@@ -225,13 +261,14 @@ export async function generateIndexPage(
225261
);
226262
}
227263

228-
return applyBaseTemplate(
229-
indexContent,
230-
"Home",
231-
SITE_DESCRIPTION,
264+
return applyBaseTemplate({
265+
content: indexContent,
266+
title: "Home",
267+
description: SITE_DESCRIPTION,
232268
assetMap,
233269
templatesDir,
234-
);
270+
canonicalPath: "/",
271+
});
235272
}
236273

237274
/**
@@ -256,13 +293,14 @@ export async function generateTagPage(
256293
.replace("{{POST_COUNT_PLURAL}}", postCountPlural)
257294
.replace("{{POSTS}}", `<ul class="post-list">${postsList}</ul>`);
258295

259-
return applyBaseTemplate(
260-
tagContent,
261-
`Tag: ${tag}`,
262-
`Posts tagged with ${tag}`,
296+
return applyBaseTemplate({
297+
content: tagContent,
298+
title: `Tag: ${tag}`,
299+
description: `Posts tagged with ${tag}`,
263300
assetMap,
264301
templatesDir,
265-
);
302+
canonicalPath: `/tags/${tag}/`,
303+
});
266304
}
267305

268306
/**
@@ -288,13 +326,14 @@ export async function generateTagsIndexPage(
288326

289327
const tagsContent = tagsIndexTemplate.replace("{{TAGS}}", tagsList);
290328

291-
return applyBaseTemplate(
292-
tagsContent,
293-
"Tags",
294-
"All tags used in posts",
329+
return applyBaseTemplate({
330+
content: tagsContent,
331+
title: "Tags",
332+
description: "All tags used in posts",
295333
assetMap,
296334
templatesDir,
297-
);
335+
canonicalPath: "/tags/",
336+
});
298337
}
299338

300339
/**
@@ -354,11 +393,12 @@ export async function generateArchivePage(
354393
.replace("{{POST_COUNT_PLURAL}}", postCountPlural)
355394
.replace("{{CONTENT}}", archiveContent);
356395

357-
return applyBaseTemplate(
358-
archiveHtml,
359-
"Archive",
360-
"All posts organized by date",
396+
return applyBaseTemplate({
397+
content: archiveHtml,
398+
title: "Archive",
399+
description: "All posts organized by date",
361400
assetMap,
362401
templatesDir,
363-
);
402+
canonicalPath: "/archive/",
403+
});
364404
}

templates/base.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>{{TITLE}} - aviraccoon's nocturnal scribbles</title>
77
<meta name="description" content="{{DESCRIPTION}}">
8+
<meta name="generator" content="nocturnal-scribbles - https://github.com/AviDuda/nocturnal-scribbles">
9+
<meta property="og:title" content="{{TITLE}}">
10+
<meta property="og:description" content="{{DESCRIPTION}}">
11+
<meta property="og:type" content="{{OG_TYPE}}">
12+
<meta property="og:url" content="{{CANONICAL_URL}}">
13+
<meta property="og:site_name" content="{{SITE_TITLE}}">
14+
<meta property="og:image" content="{{SITE_URL}}/icon-512.png">{{ARTICLE_META}}
815
<link rel="stylesheet" href="/static/style.css">
916
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
1017
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
@@ -38,4 +45,5 @@ <h1><a href="/">aviraccoon's nocturnal scribbles</a></h1>
3845
<script>{{INCLUDE:scripts/theme.js}}</script>
3946
<script>{{INCLUDE:scripts/tooltip.js}}</script>
4047
</body>
48+
<!-- Built: {{BUILD_TIME}} -->
4149
</html>

0 commit comments

Comments
 (0)