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