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>
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.htmlfor 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:
- Reads the compiled
index.htmlfromclasspath:/static/index.htmlonce at startup (cached as a template string). - For requests matching
/events/{token}: fetches the event from the database, generates meta-tags, injects them into the HTML template. - For all other non-API, non-static-file requests: injects generic fete meta-tags.
- Returns the modified HTML with
Content-Type: text/html.
Rationale
- The existing
PathResourceResolverapproach 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) tofrontend/public/so it's included in the build output. - The
og:imageURL 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 withserver.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.