Files
fete/specs/012-link-preview/research.md
nitrix 751201617d
All checks were successful
CI / backend-test (push) Successful in 1m9s
CI / frontend-test (push) Successful in 23s
CI / frontend-e2e (push) Successful in 1m12s
CI / build-and-publish (push) Has been skipped
Add Open Graph and Twitter Card meta-tags for link previews
Replace PathResourceResolver SPA fallback with SpaController that
injects OG/Twitter meta-tags into cached index.html template.
Event pages get event-specific tags (title, date, location),
all other pages get generic fete branding. Includes og-image.png
brand asset and forward-headers-strategy for proxy support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:25:39 +01:00

5.3 KiB

Research: Link Preview (Open Graph Meta-Tags)

Feature: 012-link-preview | Date: 2026-03-09

R1: How to Serve Dynamic Meta-Tags from a Vue SPA

Problem

Vue SPA serves a single index.html for all routes via Spring Boot's PathResourceResolver fallback in WebConfig.java. Social media crawlers (WhatsApp, Telegram, Signal, Twitter/X) do NOT execute JavaScript — they only read the initial HTML response. The current index.html contains no Open Graph meta-tags.

Decision: Server-Side HTML Template Injection

Intercept HTML page requests in the Spring Boot backend. Before serving index.html, parse the route, fetch event data if applicable, and inject <meta> tags into the <head> section.

Rationale

  • No new dependencies: Uses Spring Boot's existing resource serving + simple string manipulation.
  • No SSR framework needed: Avoids adding Nuxt, Vite SSR, or a prerendering service.
  • Universal: Works for all clients (not just crawlers), improving SEO for all visitors.
  • Simple: The backend already serves index.html for all non-API/non-static routes. We just need to modify what HTML is returned.

Alternatives Considered

Alternative Rejected Because
Vue SSR (Nuxt/Vite SSR) Massive architectural change. Overkill for injecting a few meta-tags. Violates KISS.
Prerendering service (prerender.io, rendertron) External dependency that may phone home. Violates Privacy by Design. Adds operational complexity.
User-agent sniffing Fragile — crawler UA strings change frequently. Serving different content to crawlers vs. users is considered cloaking by some search engines.
Static prerendering at build time Events are dynamic — created at runtime. Cannot prerender at build time.
<noscript> fallback Crawlers don't read <noscript> content for meta-tags. Only <meta> tags in <head> are parsed.

R2: Implementation Strategy — Where to Inject

Decision: Custom Controller Replacing SPA Fallback

Replace the current PathResourceResolver SPA fallback in WebConfig.java with a dedicated @Controller that:

  1. Reads the compiled index.html from classpath:/static/index.html once at startup (cached as a template string).
  2. For requests matching /events/{token}: fetches the event from the database, generates meta-tags, injects them into the HTML template.
  3. For all other non-API, non-static-file requests: injects generic fete meta-tags.
  4. Returns the modified HTML with Content-Type: text/html.

Rationale

  • The existing PathResourceResolver approach cannot modify the HTML content — it only resolves files.
  • A controller gives full programmatic control over the response.
  • Template caching avoids repeated file I/O.
  • Event lookup is a single DB query (already exists via EventRepository).

Template Injection Point

The index.html will contain a placeholder comment <!-- OG_META_TAGS --> in the <head> section. The controller replaces this placeholder with the generated meta-tags. This is done in the Vite source index.html and preserved through the build.

R3: Meta-Tag Content Strategy

Decision: Structured Description Format

For event pages, og:description follows this pattern:

📅 {formatted date} · 📍 {location} — {truncated description}

If location is missing:

📅 {formatted date} — {truncated description}

If description is missing:

📅 {formatted date} · 📍 {location}

Date format: EEEE, MMMM d, yyyy 'at' h:mm a (e.g., "Saturday, March 15, 2026 at 7:00 PM") using the event's timezone.

Title truncation

og:title = event title, truncated to 70 characters with "..." suffix if exceeded.

Description truncation

Total og:description max 200 characters. The event description portion is truncated to fit within this limit after the date/location prefix.

R4: Brand Image for og:image

Decision: Use Existing Favicon SVG

The project already has a favicon.svg (tada emoji). For og:image, we'll create a PNG version (1200x630 recommended for OG) as a static asset.

Rationale

  • SVG is not universally supported as og:image (WhatsApp and some crawlers require raster formats).
  • A simple static PNG avoids runtime image generation complexity.
  • The brand image is the same for all pages (event-specific images are out of scope per spec).

Implementation

  • Add a static og-image.png (1200x630) to frontend/public/ so it's included in the build output.
  • The og:image URL will be an absolute URL: {baseUrl}/og-image.png.
  • The image needs to be created manually (design task) or generated from the favicon.

R5: Absolute URL Construction

Decision: Derive from Request

The og:url and og:image tags require absolute URLs. These will be constructed from the incoming HTTP request's scheme, host, and port using ServletUriComponentsBuilder.

Rationale

  • Works correctly behind reverse proxies when X-Forwarded-* headers are configured (Spring Boot handles this by default with server.forward-headers-strategy=framework).
  • No need for hardcoded base URL configuration.
  • Adapts automatically to different deployment environments.

Note

Spring Boot's server.forward-headers-strategy should be set to framework in production to trust proxy headers. This is typically already handled in containerized deployments.