Skip to content

Commit 8fbb2ca

Browse files
authored
Merge pull request #5 from pythonicode/feat/og-image
Add social preview image and Open Graph tags
2 parents 05e7e17 + 26b3cb7 commit 8fbb2ca

4 files changed

Lines changed: 97 additions & 0 deletions

File tree

‎index.html‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@
77
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
88
<meta name="theme-color" content="#f97316" />
99
<meta name="description" content="Analyze orienteering maps — import GPX tracks, draw routes, and export annotated maps." />
10+
<link rel="canonical" href="https://oanalysis.com/" />
11+
<meta property="og:type" content="website" />
12+
<meta property="og:site_name" content="O-Analysis" />
13+
<meta property="og:title" content="O-Analysis — orienteering map analysis" />
14+
<meta property="og:description" content="Analyze orienteering maps — import GPX tracks, draw routes, and export annotated maps." />
15+
<meta property="og:url" content="https://oanalysis.com/" />
16+
<meta property="og:image" content="https://oanalysis.com/og-image.png" />
17+
<meta property="og:image:width" content="1200" />
18+
<meta property="og:image:height" content="630" />
19+
<meta property="og:image:alt" content="A map with GPX tracks and numbered control markers, shown in O-Analysis" />
20+
<meta name="twitter:card" content="summary_large_image" />
21+
<meta name="twitter:title" content="O-Analysis — orienteering map analysis" />
22+
<meta name="twitter:description" content="Analyze orienteering maps — import GPX tracks, draw routes, and export annotated maps." />
23+
<meta name="twitter:image" content="https://oanalysis.com/og-image.png" />
1024
<meta name="application-name" content="O-Analysis" />
1125
<meta name="apple-mobile-web-app-capable" content="yes" />
1226
<meta name="apple-mobile-web-app-title" content="O-Analysis" />

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"scripts": {
77
"dev": "vite",
88
"generate-icons": "node scripts/generate-pwa-icons.mjs",
9+
"generate-og-image": "bun run scripts/generate-og-image.mjs",
910
"build": "bun run generate-icons && tsc -b && vite build",
1011
"lint": "oxlint",
1112
"test": "vitest run",

‎public/og-image.png‎

332 KB
Loading

‎scripts/generate-og-image.mjs‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { readFileSync, writeFileSync } from 'node:fs'
2+
import { dirname, join } from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
import sharp from 'sharp'
5+
6+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
7+
const WIDTH = 1200
8+
const HEIGHT = 630
9+
10+
// The social card shows a real map from the bundled sample project
11+
const sample = JSON.parse(readFileSync(join(root, 'public/sample.anal'), 'utf8'))
12+
const [, mapBase64] = sample.mapImage.src.split(',')
13+
const mapPng = await sharp(Buffer.from(mapBase64, 'base64'))
14+
.resize({ width: 960, withoutEnlargement: true })
15+
.jpeg({ quality: 88 })
16+
.toBuffer()
17+
const mapMeta = await sharp(mapPng).metadata()
18+
19+
const CARD_W = 430
20+
const CARD_H = Math.round((CARD_W * mapMeta.height) / mapMeta.width)
21+
const card = { x: 732, y: (HEIGHT - CARD_H) / 2 - 6 }
22+
const cardCentre = { x: card.x + CARD_W / 2, y: card.y + CARD_H / 2 }
23+
const toCardPx = (fx, fy) => ({ x: card.x + fx * CARD_W, y: card.y + fy * CARD_H })
24+
25+
// Orange control markers placed over the map, in fractions of the card size
26+
const markers = [
27+
{ label: 'A', fx: 0.3, fy: 0.42 },
28+
{ label: 'B', fx: 0.52, fy: 0.58 },
29+
{ label: 'C', fx: 0.74, fy: 0.36 },
30+
]
31+
32+
const logo = readFileSync(join(root, 'public/favicon.svg')).toString('base64')
33+
const mapDataUrl = `data:image/jpeg;base64,${mapPng.toString('base64')}`
34+
35+
const markerSvg = markers
36+
.map(({ label, fx, fy }) => {
37+
const p = toCardPx(fx, fy)
38+
return `
39+
<circle cx="${p.x}" cy="${p.y}" r="17" fill="#f97316" stroke="#fff" stroke-width="4"/>
40+
<text x="${p.x}" y="${p.y + 7}" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="20" font-weight="700" fill="#fff">${label}</text>`
41+
})
42+
.join('')
43+
44+
const svg = `
45+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}">
46+
<defs>
47+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
48+
<stop offset="0" stop-color="#16141b"/>
49+
<stop offset="1" stop-color="#262030"/>
50+
</linearGradient>
51+
<radialGradient id="glow" cx="0.85" cy="0.15" r="0.6">
52+
<stop offset="0" stop-color="#f97316" stop-opacity="0.35"/>
53+
<stop offset="1" stop-color="#f97316" stop-opacity="0"/>
54+
</radialGradient>
55+
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
56+
<feGaussianBlur stdDeviation="18"/>
57+
</filter>
58+
</defs>
59+
60+
<rect width="${WIDTH}" height="${HEIGHT}" fill="url(#bg)"/>
61+
<rect width="${WIDTH}" height="${HEIGHT}" fill="url(#glow)"/>
62+
63+
<image href="data:image/svg+xml;base64,${logo}" x="72" y="92" width="92" height="92"/>
64+
<text x="72" y="282" font-family="Helvetica, Arial, sans-serif" font-size="88" font-weight="700" fill="#fff7ed" letter-spacing="-2">O-Analysis</text>
65+
<text font-family="Helvetica, Arial, sans-serif" font-size="34" fill="#d6d3d1">
66+
<tspan x="72" y="340">Analyze and annotate your</tspan>
67+
<tspan x="72" y="384">orienteering sessions</tspan>
68+
</text>
69+
70+
<g transform="rotate(-3 ${cardCentre.x} ${cardCentre.y})">
71+
<rect x="${card.x + 10}" y="${card.y + 22}" width="${CARD_W}" height="${CARD_H}" rx="18" fill="#000" opacity="0.55" filter="url(#shadow)"/>
72+
<rect x="${card.x - 10}" y="${card.y - 10}" width="${CARD_W + 20}" height="${CARD_H + 20}" rx="18" fill="#fff"/>
73+
<image href="${mapDataUrl}" x="${card.x}" y="${card.y}" width="${CARD_W}" height="${CARD_H}" preserveAspectRatio="xMidYMid slice"/>
74+
${markerSvg}
75+
</g>
76+
77+
<text x="72" y="578" font-family="Helvetica, Arial, sans-serif" font-size="22" fill="#a8a29e">oanalysis.com</text>
78+
</svg>`
79+
80+
const png = await sharp(Buffer.from(svg)).png().toBuffer()
81+
writeFileSync(join(root, 'public/og-image.png'), png)
82+
console.log(`Generated og-image.png (${WIDTH}x${HEIGHT})`)

0 commit comments

Comments
 (0)