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
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:
@@ -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
|
||||
|
||||
@@ -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)}"
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@teh-riehl/client",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@teh-riehl/server",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
|
||||
Reference in New Issue
Block a user