Skip to content

Commit 1818f02

Browse files
committed
Fix description and image on social cards
1 parent 5408868 commit 1818f02

6 files changed

Lines changed: 140 additions & 9 deletions

File tree

src/components/Head/Social.astro

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const site = resolveSiteUrl(Astro)
2121
<meta property="og:title" content={title} />
2222
<meta property="og:description" content={description} />
2323
<meta property="og:image" content={getSocialImageLink(path, site)} />
24+
<meta property="og:image:width" content="1200" />
25+
<meta property="og:image:height" content="630" />
2426
<meta property="og:image:alt" content={`Social card for ${title}`} />
2527
{/* Canonical URL of the page */}
2628
<meta property="og:url" content={absoluteUrl(path, site)} />
@@ -30,3 +32,7 @@ const site = resolveSiteUrl(Astro)
3032
<meta name="twitter:card" content="summary_large_image" />
3133
<meta name="twitter:site" content="@webstackdev" />
3234
<meta name="twitter:creator" content="@webstackdev" />
35+
<meta name="twitter:title" content={title} />
36+
<meta name="twitter:description" content={description} />
37+
<meta name="twitter:image" content={getSocialImageLink(path, site)} />
38+
<meta name="twitter:image:alt" content={`Social card for ${title}`} />
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { beforeEach, describe, expect, test } from 'vitest'
2+
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
3+
import { JSDOM } from 'jsdom'
4+
5+
describe('Social (Astro)', () => {
6+
let container: AstroContainer
7+
8+
beforeEach(async () => {
9+
container = await AstroContainer.create()
10+
})
11+
12+
test('renders explicit Open Graph and Twitter image metadata', async () => {
13+
const Social = (await import('@components/Head/Social.astro')).default
14+
const title = 'Platform Engineering for Modern Teams'
15+
const description =
16+
'Platform engineer Kevin Brown builds resilient delivery platforms, cloud foundations, and better developer experience.'
17+
18+
const response = await container.renderToResponse(Social, {
19+
props: {
20+
title,
21+
description,
22+
path: '/',
23+
},
24+
request: new Request('https://example.com/'),
25+
partial: false,
26+
})
27+
28+
const renderedHtml = await response.text()
29+
const document = new JSDOM(renderedHtml).window.document
30+
const socialImageUrl = 'https://example.com/api/social-card?slug=home'
31+
32+
expect(document.querySelector('meta[property="og:title"]')?.getAttribute('content')).toBe(title)
33+
expect(document.querySelector('meta[property="og:description"]')?.getAttribute('content')).toBe(
34+
description
35+
)
36+
expect(document.querySelector('meta[property="og:image"]')?.getAttribute('content')).toBe(
37+
socialImageUrl
38+
)
39+
expect(document.querySelector('meta[property="og:image:width"]')?.getAttribute('content')).toBe(
40+
'1200'
41+
)
42+
expect(document.querySelector('meta[property="og:image:height"]')?.getAttribute('content')).toBe(
43+
'630'
44+
)
45+
expect(document.querySelector('meta[name="twitter:title"]')?.getAttribute('content')).toBe(
46+
title
47+
)
48+
expect(document.querySelector('meta[name="twitter:description"]')?.getAttribute('content')).toBe(
49+
description
50+
)
51+
expect(document.querySelector('meta[name="twitter:image"]')?.getAttribute('content')).toBe(
52+
socialImageUrl
53+
)
54+
})
55+
})

src/content/contact.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
{
3535
"network": "linkedin",
3636
"name": "webstack-builders",
37-
"url": "https://www.linkedin.com/company/webstack-builders",
37+
"url": "https://www.linkedin.com/in/kevin-brown-developer/",
3838
"order": 2,
3939
"displayName": "LinkedIn",
4040
"iconName": "linkedin",

src/pages/api/social-card/__tests__/index.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest'
22
import { GET } from '@pages/api/social-card'
33

4+
const avatarFetchMock = vi.hoisted(() => vi.fn())
5+
6+
vi.mock('@assets/images/avatars/kevin-brown.webp', () => ({
7+
default: {
8+
src: '/_astro/kevin-brown.test.webp',
9+
},
10+
}))
11+
412
type CollectionFixture = Array<{
513
id: string
614
data: {
@@ -33,6 +41,8 @@ vi.mock('astro-og-canvas', () => ({
3341
generateOpenGraphImage: generateOpenGraphImageMock,
3442
}))
3543

44+
vi.stubGlobal('fetch', avatarFetchMock)
45+
3646
const buildRequest = (url: string) =>
3747
GET({
3848
request: new Request(url),
@@ -69,6 +79,11 @@ describe('Social Card API - GET /api/social-card', () => {
6979
beforeEach(() => {
7080
generateOpenGraphImageMock.mockReset()
7181
generateOpenGraphImageMock.mockResolvedValue(Buffer.from('mock-image'))
82+
avatarFetchMock.mockReset()
83+
avatarFetchMock.mockResolvedValue({
84+
ok: true,
85+
arrayBuffer: async () => Uint8Array.from([1, 2, 3]).buffer,
86+
})
7287
seedCollections()
7388
})
7489

@@ -83,6 +98,9 @@ describe('Social Card API - GET /api/social-card', () => {
8398
expect.objectContaining({
8499
title: 'Sample Article',
85100
description: 'Article Description',
101+
logo: expect.objectContaining({
102+
size: [140, 140],
103+
}),
86104
})
87105
)
88106

@@ -104,6 +122,31 @@ describe('Social Card API - GET /api/social-card', () => {
104122
)
105123
})
106124

125+
it('uses updated platform engineering defaults for the homepage slug', async () => {
126+
const response = await buildRequest('http://localhost/api/social-card?slug=home')
127+
128+
expect(response.status).toBe(200)
129+
expect(generateOpenGraphImageMock).toHaveBeenCalledWith(
130+
expect.objectContaining({
131+
title: 'Platform Engineering by Kevin Brown',
132+
description:
133+
'Platform engineer helping teams harden delivery, modernize cloud platforms, and improve developer experience.',
134+
})
135+
)
136+
})
137+
138+
it('uses the Kevin Brown avatar file when generating a card', async () => {
139+
await buildRequest('http://localhost/api/social-card?slug=home')
140+
141+
expect(generateOpenGraphImageMock).toHaveBeenCalledWith(
142+
expect.objectContaining({
143+
logo: expect.objectContaining({
144+
path: expect.stringMatching(/kevin-brown\.webp$/u),
145+
}),
146+
})
147+
)
148+
})
149+
107150
it('returns an error response when generation fails', async () => {
108151
generateOpenGraphImageMock.mockRejectedValueOnce(new Error('boom'))
109152

src/pages/api/social-card/index.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
import { fileURLToPath } from 'node:url'
1+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
2+
import { tmpdir } from 'node:os'
3+
import { join } from 'node:path'
24
import type { APIRoute } from 'astro'
35
import { getCollection } from 'astro:content'
46
import { generateOpenGraphImage } from 'astro-og-canvas'
7+
import kevinBrownAvatar from '@assets/images/avatars/kevin-brown.webp'
58
import { buildApiErrorResponse, handleApiFunctionError } from '@pages/api/_utils/errors'
69
import { createApiFunctionContext } from '@pages/api/_utils/requestContext'
710

811
export const prerender = false
912

1013
const ROUTE = '/api/social-card'
11-
const DEFAULT_TITLE = 'Webstack Builders'
12-
const DEFAULT_DESCRIPTION = 'Professional Web Development Services'
13-
const LOGO_PATH = fileURLToPath(new URL('../../../../assets/images/site/logo.png', import.meta.url))
14+
const DEFAULT_TITLE = 'Platform Engineering by Kevin Brown'
15+
const DEFAULT_DESCRIPTION =
16+
'Platform engineer helping teams harden delivery, modernize cloud platforms, and improve developer experience.'
17+
const AVATAR_CACHE_DIR = join(tmpdir(), 'webstackbuilders-social-card')
18+
const AVATAR_CACHE_PATH = join(AVATAR_CACHE_DIR, 'kevin-brown.webp')
1419

1520
type CollectionKey = 'articles' | 'caseStudies' | 'services' | 'downloads'
1621
type PaletteKey = 'articles' | 'case-studies' | 'services' | 'downloads' | 'default'
@@ -56,6 +61,26 @@ const gradientPalette: Record<PaletteKey, [number, number, number][]> = {
5661
],
5762
}
5863

64+
const getAvatarUrl = (requestUrl: URL): URL => new URL(kevinBrownAvatar.src, requestUrl)
65+
66+
const ensureAvatarFile = async (requestUrl: URL): Promise<string> => {
67+
try {
68+
await readFile(AVATAR_CACHE_PATH)
69+
return AVATAR_CACHE_PATH
70+
} catch {
71+
const response = await fetch(getAvatarUrl(requestUrl))
72+
73+
if (!response.ok) {
74+
throw new Error(`Unable to load avatar image: ${response.status} ${response.statusText}`)
75+
}
76+
77+
const avatarBuffer = Buffer.from(await response.arrayBuffer())
78+
await mkdir(AVATAR_CACHE_DIR, { recursive: true })
79+
await writeFile(AVATAR_CACHE_PATH, avatarBuffer)
80+
return AVATAR_CACHE_PATH
81+
}
82+
}
83+
5984
/** Normalize slug parameters to a consistent format */
6085
const normalizeSlug = (value: string | null): string => {
6186
if (!value) return 'home'
@@ -119,6 +144,7 @@ export const GET: APIRoute = async ({ request, clientAddress, cookies }) => {
119144
const slug = normalizeSlug(url.searchParams.get('slug'))
120145
const titleOverride = url.searchParams.get('title')
121146
const descriptionOverride = url.searchParams.get('description')
147+
const avatarPath = await ensureAvatarFile(url)
122148

123149
const contentIndex = await buildContentIndex()
124150
const matchedEntry = contentIndex[slug]
@@ -138,8 +164,8 @@ export const GET: APIRoute = async ({ request, clientAddress, cookies }) => {
138164
bgGradient: gradientPalette[palette],
139165
padding: 80,
140166
logo: {
141-
path: LOGO_PATH,
142-
size: [180],
167+
path: avatarPath,
168+
size: [140, 140],
143169
},
144170
font: {
145171
title: {

src/pages/index.astro

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import Newsletter from '@components/CallToAction/Newsletter/index.astro'
1111
import Skills from '@components/Home/Skills/index.astro'
1212
import Testimonials from '@components/Testimonials/index.astro'
1313
14-
const pageDescription = 'Webstack Builders, Inc. Home Page'
15-
const pageTitle = 'Web Development & Digital Solutions - Webstack Builders, Inc.'
14+
const pageDescription =
15+
'Platform engineer Kevin Brown builds resilient delivery platforms, cloud foundations, and better developer experience.'
16+
const pageTitle = 'Platform Engineering for Modern Teams'
1617
const sectionClasses = 'container mx-auto px-4 sm:px-6 lg:px-8 py-4 md:py-8 lg:py-12'
1718
---
1819

0 commit comments

Comments
 (0)