feat: OG/Twitter Card metadata for chat link previews; v0.1.18
CI / test (push) Has been skipped
CI / secrets-scan (push) Successful in 5s
CI / sast (push) Successful in 13s
CI / vuln-scan (push) Successful in 14s
CI / lint (push) Successful in 26s
CI / build-images (push) Has been skipped
CI / image-scan (push) Has been skipped
CI / push (push) Has been skipped
CI / secrets-scan (pull_request) Successful in 6s
CI / sast (pull_request) Successful in 14s
CI / vuln-scan (pull_request) Successful in 15s
CI / test (pull_request) Successful in 25s
CI / lint (pull_request) Successful in 27s
CI / build-images (pull_request) Successful in 5m7s
CI / image-scan (pull_request) Successful in 24s
CI / push (pull_request) Has been skipped

index.html grows the Open Graph and Twitter Card meta tags (title,
description, type, site_name, url, image + dimensions, alt) plus a
plain `<meta name="description">` for SEO. Same copy as the README
one-liner so every surface stays in sync.

Absolute URLs are required for Facebook+Twitter and strongly preferred
elsewhere, but the deployment URL isn't known at build time. Solved with
the same pattern config.js already uses: a `__OG_BASE_URL__` token in
index.html gets sed-substituted by the nginx entrypoint from a new
`APP_PUBLIC_URL` env var. Trailing-slash trim and sed-meta escaping are
both handled. Unset env = relative URLs (Slack/Discord/iMessage still
render fine, Facebook won't).

README gains a paragraph under Runtime Config documenting the new var.
This commit is contained in:
2026-05-18 15:49:38 -07:00
parent 348a6f65ea
commit 4359955c51
6 changed files with 77 additions and 5 deletions
+3
View File
@@ -91,9 +91,12 @@ The client is a static Vite build. Rather than baking the backend URL into the b
```bash
docker run -p 8080:80 \
-e APP_API_BASE_URL="https://api.example.com/api" \
-e APP_PUBLIC_URL="https://tehriehl.example.com" \
teh-riehl-client
```
`APP_PUBLIC_URL` is the site's absolute origin (no trailing slash). The entrypoint substitutes it into the Open Graph / Twitter Card meta tags in `index.html` so chat clients (Slack, Discord, iMessage, Twitter, etc.) can render link previews against fully-qualified URLs. Leave it unset for local Docker spin-ups; previews will fall back to relative paths, which most modern scrapers handle but Facebook does not.
Add a new runtime knob by extending the `AppConfig` interface in `packages/client/src/lib/runtimeConfig.ts`, adding the env var to `packages/client/docker/40-app-config.sh`, and documenting it here.
## Security model
+24 -3
View File
@@ -3,9 +3,15 @@
# Runtime config materializer.
#
# Runs once at container startup (via nginx:alpine's /docker-entrypoint.d/
# convention) and writes /usr/share/nginx/html/config.js from env vars BEFORE
# nginx starts serving. The app's index.html loads /config.js before the
# bundle, so window.__APP_CONFIG__ is populated by the time React mounts.
# convention) BEFORE nginx starts serving. Two jobs:
#
# 1. Writes /usr/share/nginx/html/config.js from env vars. The app's
# index.html loads /config.js before the bundle, so window.__APP_CONFIG__
# is populated by the time React mounts.
# 2. Substitutes the __OG_BASE_URL__ placeholder in index.html with the
# site's absolute origin, so the Open Graph / Twitter Card meta tags
# resolve to fully-qualified URLs (required by Facebook + Twitter,
# strongly recommended everywhere else for reliable chat previews).
#
# Add new runtime knobs by:
# 1. extending the AppConfig interface in src/lib/runtimeConfig.ts
@@ -15,15 +21,30 @@
set -eu
TARGET="/usr/share/nginx/html/config.js"
INDEX="/usr/share/nginx/html/index.html"
# Defaults match the dev-mode public/config.js so behavior is consistent if
# nothing is overridden (useful when running the image with no env vars).
APP_API_BASE_URL="${APP_API_BASE_URL:-/api}"
# Absolute origin used in OG/Twitter Card URLs. If unset, the placeholder
# is wiped to empty — meta URLs become relative ("/TehRiehlIncremental.png"),
# which most modern scrapers handle but some (Facebook) won't. Set this for
# real deployments.
APP_PUBLIC_URL="${APP_PUBLIC_URL:-}"
# Trim a trailing slash so we don't end up with `https://x.com//image.png`.
APP_PUBLIC_URL="${APP_PUBLIC_URL%/}"
cat > "$TARGET" <<EOF
window.__APP_CONFIG__ = {
apiBaseUrl: "$APP_API_BASE_URL"
};
EOF
# Escape characters that have meaning in sed's replacement string (/, &, \)
# so a URL like https://example.com doesn't break the substitution.
ESCAPED_URL=$(printf '%s' "$APP_PUBLIC_URL" | sed 's/[\/&]/\\&/g')
sed -i "s/__OG_BASE_URL__/${ESCAPED_URL}/g" "$INDEX"
echo "[entrypoint] $TARGET <- apiBaseUrl=$APP_API_BASE_URL"
echo "[entrypoint] $INDEX <- og:base=${APP_PUBLIC_URL:-(unset; URLs are relative)}"
+37
View File
@@ -6,6 +6,43 @@
<meta name="theme-color" content="#070912" />
<link rel="icon" type="image/png" href="/TehRiehlIncremental.png" />
<title>TehRiehlIncremental</title>
<meta
name="description"
content="A meta/dev-themed incremental game. Lines of Code, Commits, Coffee, Closed Tickets, Releases, and Tech Debt — built on a TMT-compatible schema."
/>
<!--
Social-share metadata for chat previews (Slack, Discord, iMessage,
Telegram, Twitter, etc.). The `__OG_BASE_URL__` token is rewritten
to the absolute origin (e.g. https://tehriehl.dev) by the container
entrypoint script using the APP_PUBLIC_URL env var — same pattern
as config.js, so one image targets any deployment. In dev the
token survives as-is; that only matters for external scrapers, not
for the browser viewing the page, so it's harmless.
-->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="TehRiehlIncremental" />
<meta property="og:title" content="TehRiehlIncremental" />
<meta
property="og:description"
content="A meta/dev-themed incremental game. Lines of Code, Commits, Coffee, Closed Tickets, Releases, and Tech Debt — built on a TMT-compatible schema."
/>
<meta property="og:url" content="__OG_BASE_URL__/" />
<meta property="og:image" content="__OG_BASE_URL__/TehRiehlIncremental.png" />
<meta property="og:image:width" content="340" />
<meta property="og:image:height" content="340" />
<meta
property="og:image:alt"
content="TehRiehlIncremental logo — a stylised dev-themed brace icon."
/>
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="TehRiehlIncremental" />
<meta
name="twitter:description"
content="A meta/dev-themed incremental game. Lines of Code, Commits, Coffee, Closed Tickets, Releases, and Tech Debt — built on a TMT-compatible schema."
/>
<meta name="twitter:image" content="__OG_BASE_URL__/TehRiehlIncremental.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@teh-riehl/client",
"version": "0.1.17",
"version": "0.1.18",
"private": true,
"type": "module",
"scripts": {
+11
View File
@@ -10,6 +10,17 @@ export interface PatchNote {
* Vite/tsc can resolve them without bundler magic. Newest entry first.
*/
export const patchNotes: ReadonlyArray<PatchNote> = [
{
version: '0.1.18',
date: '2026-05-18',
body: [
'## v0.1.18 — Link previews in chat',
'',
'- **Open Graph + Twitter Card meta tags** added to `index.html`, so dropping the game link into Slack / Discord / iMessage / Telegram / Twitter etc. now renders a real preview card with title, description, and the brace-logo thumbnail instead of a bare URL.',
"- **Deploy knob:** the container entrypoint substitutes `__OG_BASE_URL__` in `index.html` with the new `APP_PUBLIC_URL` env var at startup (same pattern as `APP_API_BASE_URL` → `config.js`). One immutable image still targets any environment. Unset = relative URLs in the meta tags, which most scrapers handle but Facebook doesn't.",
'- **README** picked up the new env var under the Runtime Config section.',
].join('\n'),
},
{
version: '0.1.17',
date: '2026-05-18',
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@teh-riehl/server",
"version": "0.1.17",
"version": "0.1.18",
"private": true,
"scripts": {
"build": "nest build",