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>
116 lines
5.3 KiB
Markdown
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.
|