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

116 lines
5.3 KiB
Markdown

# 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.