Skip to content

Commit 3bad20c

Browse files
Merge pull request #96 from john-kurkowski/astro-seo
- Adopt `astro-seo` for canonical, description, Open Graph, Twitter, profile, and article metadata - Make social preview image handling explicit across posts, including required alt text for image-backed posts and a new 1200x630 fallback image - Use Netlify deploy-preview hosts for absolute metadata URLs so preview crawlers can fetch branch-only assets - Add TypeScript metadata tests covering descriptions, social images, fallback metadata, and article Open Graph output
2 parents b296a28 + c768b16 commit 3bad20c

23 files changed

Lines changed: 284 additions & 99 deletions

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22

33
- After changing any file, before ending the turn, run `npm run check:fix`. Fix
44
any reported issues until the command passes cleanly.
5+
- When changing application code, content schemas, build behavior, or tests,
6+
also run `npm test`.
7+
- Mirror source paths when naming tests for application modules. For example,
8+
tests for `src/config/site.ts` belong in `test/config/site.test.ts`.

astro.config.mts renamed to astro.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import mdx from "@astrojs/mdx"
33
import sitemap from "@astrojs/sitemap"
44
import tailwindcss from "@tailwindcss/vite"
55
import { defineConfig } from "astro/config"
6+
import { getSiteUrl } from "./src/config/site"
67

78
const parsePublishTime: RemarkPlugin = () => (_tree, file) => {
89
const maybeDateString = /\d+-\d+-\d+/.exec(file.path)
@@ -30,7 +31,7 @@ export default defineConfig({
3031
},
3132
},
3233

33-
site: "https://johnkurkowski.com",
34+
site: getSiteUrl(),
3435
integrations: [mdx(), sitemap()],
3536

3637
vite: {

package-lock.json

Lines changed: 10 additions & 79 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "john-kurkowski.github.io",
33
"version": "1.0.0",
4+
"type": "module",
45
"description": "Source for https://johnkurkowski.com.",
56
"scripts": {
67
"astro": "astro",
@@ -11,7 +12,7 @@
1112
"prepare": "astro sync",
1213
"preview": "astro preview --host",
1314
"start": "astro dev --host",
14-
"test": "npm run check",
15+
"test": "npm run check && npm run build && node --test 'test/**/*.test.ts'",
1516
"type-check": "astro check"
1617
},
1718
"repository": {
@@ -28,6 +29,7 @@
2829
"@astrojs/sitemap": "^3.7.3",
2930
"@tailwindcss/vite": "^4.3.1",
3031
"astro": "^6.4.6",
32+
"astro-seo": "^1.1.0",
3133
"tailwindcss": "^4.3.1"
3234
},
3335
"devDependencies": {

public/avatar-social.jpeg

155 KB
Loading

src/components/layouts/Seo.astro

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
---
2+
import { SEO } from "astro-seo"
3+
24
const { frontmatter, siteMetadata } = Astro.props
35
let title: string
46
if (frontmatter.title?.includes(siteMetadata.title)) {
@@ -12,20 +14,87 @@ if (frontmatter.title?.includes(siteMetadata.title)) {
1214
const date = frontmatter.date ? new Date(frontmatter.date) : null
1315
1416
const description = frontmatter.description || siteMetadata.description
17+
const isArticle = Boolean(date)
18+
const siteUrl = Astro.site || new URL(Astro.url.origin)
19+
const canonical = new URL(Astro.url.pathname, siteUrl).href
20+
const defaultImage = new URL("/avatar-social.jpeg", siteUrl).href
21+
const defaultImageDimensions = {
22+
height: 630,
23+
width: 1200,
24+
}
25+
const image = frontmatter.image
26+
const postImageUrls = import.meta.glob(
27+
"../../content/posts/**/*.{png,jpg,jpeg,webp}",
28+
{
29+
eager: true,
30+
import: "default",
31+
query: "?url",
32+
},
33+
) as Record<string, string>
34+
const rawImagePath =
35+
typeof image === "string" ? image.replace(/^\.\//, "/") : ""
36+
const imageSrc =
37+
typeof image === "string"
38+
? Object.entries(postImageUrls).find(([path]) =>
39+
path.endsWith(rawImagePath),
40+
)?.[1]
41+
: image?.src
42+
const imageUrl = new URL(imageSrc || defaultImage, siteUrl).href
43+
const imageAlt = image ? frontmatter.imageAlt : "John Kurkowski"
44+
const imageDimensions = image
45+
? {
46+
height: image.height,
47+
width: image.width,
48+
}
49+
: defaultImageDimensions
50+
const article = date
51+
? {
52+
authors: [siteMetadata.title],
53+
publishedTime: date.toISOString(),
54+
tags: frontmatter.tags,
55+
}
56+
: undefined
1557
---
1658

17-
<title>{title}</title>
18-
<meta property="og:title" content={title}>
59+
<SEO
60+
charset="utf-8"
61+
canonical={canonical}
62+
description={description}
63+
openGraph={{
64+
basic: {
65+
image: imageUrl,
66+
title,
67+
type: isArticle ? "article" : "website",
68+
url: canonical,
69+
},
70+
optional: {
71+
description,
72+
siteName: siteMetadata.title,
73+
},
74+
image: {
75+
alt: imageAlt,
76+
height: imageDimensions.height,
77+
width: imageDimensions.width,
78+
},
79+
...(article ? { article } : {}),
80+
}}
81+
title={title}
82+
twitter={{
83+
card: "summary_large_image",
84+
creator: "@bluu",
85+
description,
86+
image: imageUrl,
87+
imageAlt,
88+
site: "@bluu",
89+
title,
90+
}}
91+
/>
1992

2093
<meta name="author" content={siteMetadata.title}>
21-
<meta property="og:author" content={siteMetadata.url}>
22-
23-
<meta name="description" content={description}>
24-
<meta name="twitter:description" content={description}>
94+
<meta property="og:author" content={canonical}>
2595

2696
{date && <meta http-equiv='date' content={date.toISOString()} />}
2797

28-
<meta charset="utf-8">
2998
<meta content="John" property="profile:first_name">
3099
<meta content="Kurkowski" property="profile:last_name">
31100

@@ -52,8 +121,3 @@ const description = frontmatter.description || siteMetadata.description
52121

53122
<link href="/favicon.ico" rel="shortcut icon">
54123
<link href="/avatar@2X.jpeg" rel="apple-touch-icon">
55-
<meta content="/avatar@2X.jpeg" property="og:image">
56-
<meta content="/avatar@2X.jpeg" name="twitter:image:src">
57-
<meta content="summary" name="twitter:card">
58-
<meta content="@bluu" name="twitter:site">
59-
<meta content="@bluu" name="twitter:creator">

src/config/site.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
export const productionSiteUrl = "https://johnkurkowski.com"
2+
3+
/**
4+
* Social metadata needs absolute URLs, but Netlify deploy previews must point at
5+
* their preview host so crawlers can fetch assets before they exist in
6+
* production.
7+
*/
8+
export function getSiteUrl(env = process.env) {
9+
return (
10+
env.DEPLOY_PRIME_URL ||
11+
env.DEPLOY_URL ||
12+
env.URL ||
13+
productionSiteUrl
14+
).replace(/\/$/, "")
15+
}
16+
117
export const siteMetadata = {
218
description:
319
"With 14+ years in the game, I help frontend teams ship incrementally, with test coverage confidence, without rewrites. Debug any app, existing or legacy. Collaborate on distributed teams via docs, code review, and mentorship.",

src/content.config.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@ import { z } from "astro/zod"
77
const posts = defineCollection({
88
loader: glob({ pattern: "**/[^_]*.{md,mdx}", base: "./src/content/posts" }),
99

10-
schema: z.object({
11-
description: z.string(),
12-
title: z.string(),
13-
categories: z.array(z.string()),
14-
tags: z.array(z.string()),
15-
}),
10+
schema: ({ image }) =>
11+
z
12+
.object({
13+
description: z.string(),
14+
title: z.string(),
15+
categories: z.array(z.string()),
16+
tags: z.array(z.string()),
17+
image: image().nullable(),
18+
imageAlt: z.string().optional(),
19+
})
20+
.refine((data) => data.image === null || data.imageAlt, {
21+
message: "imageAlt is required when image is set",
22+
path: ["imageAlt"],
23+
}),
1624
})
1725

1826
export const collections = { posts }

src/content/posts/2011-10-06-avoid-git-first-drafts.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ description:
77
Here are pages rife with debate on using git pull vs. git pull --rebase. I’ve
88
decided for myself there are times for both, that is, there are times for
99
merge commits and not.
10+
image: null
1011
---
1112

1213
Here are

src/content/posts/2013-05-27-accumulating-multiple-failures-in-a-ValidationNEL.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ description: >-
88
To preface, Gravity likes to use 2 very predictable patterns throughout its
99
Scala codebase, which is very important for a TIMTOWTDI language like Scala:
1010
the for-comprehension and ValidationNEL.
11+
image: null
1112
---
1213

1314
This is an adaptation of an internal blog post I made for

0 commit comments

Comments
 (0)