From 751201617dd5e06d1062f007cf5b7cfa8221e6e0 Mon Sep 17 00:00:00 2001 From: nitrix Date: Mon, 9 Mar 2026 20:25:39 +0100 Subject: [PATCH] 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 --- .../de/fete/adapter/in/web/SpaController.java | 188 ++++++++++++ .../main/java/de/fete/config/WebConfig.java | 27 +- .../src/main/resources/application.properties | 3 + .../adapter/in/web/SpaControllerTest.java | 281 ++++++++++++++++++ .../java/de/fete/config/WebConfigTest.java | 8 +- backend/src/test/resources/static/index.html | 13 + frontend/index.html | 1 + frontend/public/og-image.png | Bin 0 -> 74511 bytes .../checklists/requirements.md | 36 +++ .../contracts/html-meta-tags.md | 98 ++++++ specs/012-link-preview/data-model.md | 83 ++++++ specs/012-link-preview/plan.md | 83 ++++++ specs/012-link-preview/quickstart.md | 57 ++++ specs/012-link-preview/research.md | 115 +++++++ specs/012-link-preview/spec.md | 104 +++++++ specs/012-link-preview/tasks.md | 201 +++++++++++++ 16 files changed, 1270 insertions(+), 28 deletions(-) create mode 100644 backend/src/main/java/de/fete/adapter/in/web/SpaController.java create mode 100644 backend/src/test/java/de/fete/adapter/in/web/SpaControllerTest.java create mode 100644 backend/src/test/resources/static/index.html create mode 100644 frontend/public/og-image.png create mode 100644 specs/012-link-preview/checklists/requirements.md create mode 100644 specs/012-link-preview/contracts/html-meta-tags.md create mode 100644 specs/012-link-preview/data-model.md create mode 100644 specs/012-link-preview/plan.md create mode 100644 specs/012-link-preview/quickstart.md create mode 100644 specs/012-link-preview/research.md create mode 100644 specs/012-link-preview/spec.md create mode 100644 specs/012-link-preview/tasks.md diff --git a/backend/src/main/java/de/fete/adapter/in/web/SpaController.java b/backend/src/main/java/de/fete/adapter/in/web/SpaController.java new file mode 100644 index 0000000..f6159af --- /dev/null +++ b/backend/src/main/java/de/fete/adapter/in/web/SpaController.java @@ -0,0 +1,188 @@ +package de.fete.adapter.in.web; + +import de.fete.domain.model.Event; +import de.fete.domain.model.EventToken; +import de.fete.domain.port.in.GetEventUseCase; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +/** Serves the SPA index.html with injected Open Graph and Twitter Card meta-tags. */ +@Controller +public class SpaController { + + private static final String PLACEHOLDER = ""; + private static final int MAX_TITLE_LENGTH = 70; + private static final int MAX_DESCRIPTION_LENGTH = 200; + private static final String GENERIC_TITLE = "fete"; + private static final String GENERIC_DESCRIPTION = + "Privacy-focused event planning. Create and share events without accounts."; + private static final DateTimeFormatter DATE_FORMAT = + DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy 'at' h:mm a", Locale.ENGLISH); + + private final GetEventUseCase getEventUseCase; + private String htmlTemplate; + + /** Creates a new SpaController. */ + public SpaController(GetEventUseCase getEventUseCase) { + this.getEventUseCase = getEventUseCase; + } + + /** Loads and caches the index.html template at startup. */ + @PostConstruct + void loadTemplate() throws IOException { + var resource = new ClassPathResource("/static/index.html"); + if (resource.exists()) { + htmlTemplate = resource.getContentAsString(StandardCharsets.UTF_8); + } + } + + /** Serves SPA HTML with generic meta-tags for non-event routes. */ + @GetMapping( + value = {"/", "/create", "/events"}, + produces = MediaType.TEXT_HTML_VALUE + ) + @ResponseBody + public String serveGenericPage(HttpServletRequest request) { + if (htmlTemplate == null) { + return ""; + } + String baseUrl = getBaseUrl(request); + return htmlTemplate.replace(PLACEHOLDER, renderTags(buildGenericMeta(baseUrl))); + } + + /** Serves SPA HTML with event-specific meta-tags. */ + @GetMapping( + value = "/events/{token}", + produces = MediaType.TEXT_HTML_VALUE + ) + @ResponseBody + public String serveEventPage(@PathVariable String token, + HttpServletRequest request) { + if (htmlTemplate == null) { + return ""; + } + String baseUrl = getBaseUrl(request); + Map meta = resolveEventMeta(token, baseUrl); + return htmlTemplate.replace(PLACEHOLDER, renderTags(meta)); + } + + // --- Meta-tag composition --- + + private Map buildEventMeta(Event event, String baseUrl) { + var tags = new LinkedHashMap(); + String title = truncateTitle(event.getTitle()); + String description = formatDescription(event); + tags.put("og:title", title); + tags.put("og:description", description); + tags.put("og:url", baseUrl + "/events/" + event.getEventToken().value()); + tags.put("og:type", "website"); + tags.put("og:site_name", GENERIC_TITLE); + tags.put("og:image", baseUrl + "/og-image.png"); + tags.put("twitter:card", "summary"); + tags.put("twitter:title", title); + tags.put("twitter:description", description); + return tags; + } + + private Map buildGenericMeta(String baseUrl) { + var tags = new LinkedHashMap(); + tags.put("og:title", GENERIC_TITLE); + tags.put("og:description", GENERIC_DESCRIPTION); + tags.put("og:url", baseUrl); + tags.put("og:type", "website"); + tags.put("og:site_name", GENERIC_TITLE); + tags.put("og:image", baseUrl + "/og-image.png"); + tags.put("twitter:card", "summary"); + tags.put("twitter:title", GENERIC_TITLE); + tags.put("twitter:description", GENERIC_DESCRIPTION); + return tags; + } + + private Map resolveEventMeta(String token, String baseUrl) { + try { + UUID uuid = UUID.fromString(token); + Optional event = + getEventUseCase.getByEventToken(new EventToken(uuid)); + if (event.isPresent()) { + return buildEventMeta(event.get(), baseUrl); + } + } catch (IllegalArgumentException ignored) { + // Invalid UUID โ€” fall back to generic + } + return buildGenericMeta(baseUrl); + } + + // --- Description formatting --- + + private String truncateTitle(String title) { + if (title.length() <= MAX_TITLE_LENGTH) { + return title; + } + return title.substring(0, MAX_TITLE_LENGTH - 3) + "..."; + } + + private String formatDescription(Event event) { + ZonedDateTime zoned = event.getDateTime().atZoneSameInstant(event.getTimezone()); + var sb = new StringBuilder(); + sb.append("๐Ÿ“… ").append(zoned.format(DATE_FORMAT)); + + if (event.getLocation() != null && !event.getLocation().isBlank()) { + sb.append(" ยท ๐Ÿ“ ").append(event.getLocation()); + } + + if (event.getDescription() != null && !event.getDescription().isBlank()) { + sb.append(" โ€” ").append(event.getDescription()); + } + + String result = sb.toString(); + if (result.length() > MAX_DESCRIPTION_LENGTH) { + return result.substring(0, MAX_DESCRIPTION_LENGTH - 3) + "..."; + } + return result; + } + + // --- HTML rendering --- + + private String renderTags(Map tags) { + var sb = new StringBuilder(); + for (var entry : tags.entrySet()) { + String key = entry.getKey(); + String value = escapeHtml(entry.getValue()); + String attr = key.startsWith("twitter:") ? "name" : "property"; + sb.append("\n"); + } + return sb.toString().stripTrailing(); + } + + private String escapeHtml(String input) { + return input + .replace("&", "&") + .replace("\"", """) + .replace("<", "<") + .replace(">", ">"); + } + + private String getBaseUrl(HttpServletRequest request) { + return ServletUriComponentsBuilder.fromRequestUri(request) + .replacePath("") + .build() + .toUriString(); + } +} diff --git a/backend/src/main/java/de/fete/config/WebConfig.java b/backend/src/main/java/de/fete/config/WebConfig.java index 79c8ee9..f5b0613 100644 --- a/backend/src/main/java/de/fete/config/WebConfig.java +++ b/backend/src/main/java/de/fete/config/WebConfig.java @@ -1,21 +1,17 @@ package de.fete.config; -import java.io.IOException; import java.time.Clock; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.resource.PathResourceResolver; -/** Configures API path prefix and SPA static resource serving. */ +/** Configures API path prefix. Static resources served by default Spring Boot handler. */ @Configuration public class WebConfig implements WebMvcConfigurer { + /** Provides a system clock bean for time-dependent services. */ @Bean Clock clock() { return Clock.systemDefaultZone(); @@ -25,23 +21,4 @@ public class WebConfig implements WebMvcConfigurer { public void configurePathMatch(PathMatchConfigurer configurer) { configurer.addPathPrefix("/api", c -> c.isAnnotationPresent(RestController.class)); } - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/**") - .addResourceLocations("classpath:/static/") - .resourceChain(true) - .addResolver(new PathResourceResolver() { - @Override - protected Resource getResource(String resourcePath, - Resource location) throws IOException { - Resource requested = location.createRelative(resourcePath); - if (requested.exists() && requested.isReadable()) { - return requested; - } - Resource index = new ClassPathResource("/static/index.html"); - return (index.exists() && index.isReadable()) ? index : null; - } - }); - } } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 899c8af..4a17cef 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -7,6 +7,9 @@ spring.jpa.open-in-view=false # Liquibase spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml +# Proxy headers +server.forward-headers-strategy=framework + # Actuator management.endpoints.web.exposure.include=health management.endpoint.health.show-details=never diff --git a/backend/src/test/java/de/fete/adapter/in/web/SpaControllerTest.java b/backend/src/test/java/de/fete/adapter/in/web/SpaControllerTest.java new file mode 100644 index 0000000..aedeecc --- /dev/null +++ b/backend/src/test/java/de/fete/adapter/in/web/SpaControllerTest.java @@ -0,0 +1,281 @@ +package de.fete.adapter.in.web; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import de.fete.TestcontainersConfig; +import de.fete.adapter.out.persistence.EventJpaEntity; +import de.fete.adapter.out.persistence.EventJpaRepository; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +@Import(TestcontainersConfig.class) +class SpaControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private EventJpaRepository eventJpaRepository; + + // --- Phase 2: Base functionality --- + + @Test + void rootServesHtml() throws Exception { + mockMvc.perform(get("/").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)); + } + + @Test + void rootHtmlDoesNotContainPlaceholder() throws Exception { + String html = mockMvc.perform(get("/").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).doesNotContain(""); + } + + @Test + void createRouteServesHtml() throws Exception { + mockMvc.perform(get("/create").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)); + } + + @Test + void eventsRouteServesHtml() throws Exception { + mockMvc.perform(get("/events").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)); + } + + // --- Phase 4 (US2): Generic OG meta-tags --- + + @Test + void rootContainsGenericOgTitle() throws Exception { + String html = mockMvc.perform(get("/").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("og:title"); + assertThat(html).contains("content=\"fete\""); + } + + @Test + void createRouteContainsGenericOgDescription() throws Exception { + String html = mockMvc.perform(get("/create").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("og:description"); + assertThat(html).contains("Privacy-focused event planning"); + } + + @Test + void unknownRouteReturns404() throws Exception { + mockMvc.perform(get("/unknown/path").accept(MediaType.TEXT_HTML)) + .andExpect(status().isNotFound()); + } + + // --- Phase 5 (US3): Twitter Card meta-tags --- + + @Test + void eventRouteContainsTwitterCardTags() throws Exception { + EventJpaEntity event = seedEvent( + "Twitter Test", "Testing cards", + "Europe/Berlin", "Berlin", LocalDate.now().plusDays(30)); + + String html = mockMvc.perform( + get("/events/" + event.getEventToken()).accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("twitter:card"); + assertThat(html).contains("twitter:title"); + assertThat(html).contains("twitter:description"); + } + + @Test + void genericRouteContainsTwitterCardTags() throws Exception { + String html = mockMvc.perform(get("/").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("twitter:card"); + assertThat(html).contains("content=\"summary\""); + } + + // --- Phase 3 (US1): Event-specific OG meta-tags --- + + @Test + void eventRouteContainsEventSpecificOgTitle() throws Exception { + EventJpaEntity event = seedEvent( + "Birthday Party", "Come celebrate!", + "Europe/Berlin", "Berlin", LocalDate.now().plusDays(30)); + + String html = mockMvc.perform( + get("/events/" + event.getEventToken()).accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("og:title"); + assertThat(html).contains("Birthday Party"); + } + + @Test + void eventRouteContainsOgDescription() throws Exception { + EventJpaEntity event = seedEvent( + "BBQ", "Bring drinks!", + "Europe/Berlin", "Central Park", LocalDate.now().plusDays(30)); + + String html = mockMvc.perform( + get("/events/" + event.getEventToken()).accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("og:description"); + assertThat(html).contains("Central Park"); + assertThat(html).contains("Bring drinks!"); + } + + @Test + void eventRouteContainsOgUrl() throws Exception { + EventJpaEntity event = seedEvent( + "Party", null, + "Europe/Berlin", null, LocalDate.now().plusDays(30)); + + String html = mockMvc.perform( + get("/events/" + event.getEventToken()).accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("og:url"); + assertThat(html).contains("/events/" + event.getEventToken()); + } + + @Test + void eventRouteContainsOgImage() throws Exception { + EventJpaEntity event = seedEvent( + "Party", null, + "Europe/Berlin", null, LocalDate.now().plusDays(30)); + + String html = mockMvc.perform( + get("/events/" + event.getEventToken()).accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("og:image"); + assertThat(html).contains("/og-image.png"); + } + + @Test + void unknownEventTokenFallsBackToGenericMeta() throws Exception { + String html = mockMvc.perform( + get("/events/" + UUID.randomUUID()).accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("og:title"); + assertThat(html).contains("content=\"fete\""); + } + + // --- HTML escaping --- + + @Test + void specialCharactersAreHtmlEscaped() throws Exception { + EventJpaEntity event = seedEvent( + "Tom & Jerry's \"Party\"", "Fun & more", + "Europe/Berlin", "O'Brien's Pub", LocalDate.now().plusDays(30)); + + String html = mockMvc.perform( + get("/events/" + event.getEventToken()).accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("Tom & Jerry"); + assertThat(html).contains("& more"); + assertThat(html).contains("<times>"); + assertThat(html).doesNotContain("content=\"Tom & Jerry"); + } + + // --- Title truncation --- + + @Test + void longTitleIsTruncatedTo70Chars() throws Exception { + String longTitle = "A".repeat(80); + EventJpaEntity event = seedEvent( + longTitle, "Desc", + "Europe/Berlin", null, LocalDate.now().plusDays(30)); + + String html = mockMvc.perform( + get("/events/" + event.getEventToken()).accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("A".repeat(67) + "..."); + assertThat(html).doesNotContain("A".repeat(68)); + } + + // --- Description formatting --- + + @Test + void eventWithoutLocationOmitsPinEmoji() throws Exception { + EventJpaEntity event = seedEvent( + "Online Meetup", "Virtual gathering", + "Europe/Berlin", null, LocalDate.now().plusDays(30)); + + String html = mockMvc.perform( + get("/events/" + event.getEventToken()).accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).doesNotContain("๐Ÿ“"); + } + + @Test + void eventWithoutDescriptionOmitsDash() throws Exception { + EventJpaEntity event = seedEvent( + "Silent Event", null, + "Europe/Berlin", "Berlin", LocalDate.now().plusDays(30)); + + String html = mockMvc.perform( + get("/events/" + event.getEventToken()).accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html).contains("๐Ÿ“…"); + assertThat(html).contains("Berlin"); + assertThat(html).doesNotContain(" โ€” "); + } + + private EventJpaEntity seedEvent( + String title, String description, String timezone, + String location, LocalDate expiryDate) { + var entity = new EventJpaEntity(); + entity.setEventToken(UUID.randomUUID()); + entity.setOrganizerToken(UUID.randomUUID()); + entity.setTitle(title); + entity.setDescription(description); + entity.setDateTime( + OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2))); + entity.setTimezone(timezone); + entity.setLocation(location); + entity.setExpiryDate(expiryDate); + entity.setCreatedAt(OffsetDateTime.now()); + return eventJpaRepository.save(entity); + } +} diff --git a/backend/src/test/java/de/fete/config/WebConfigTest.java b/backend/src/test/java/de/fete/config/WebConfigTest.java index 2170412..066aa30 100644 --- a/backend/src/test/java/de/fete/config/WebConfigTest.java +++ b/backend/src/test/java/de/fete/config/WebConfigTest.java @@ -29,8 +29,10 @@ class WebConfigTest { @Test void apiPrefixNotAccessibleWithoutIt() throws Exception { - // /events without /api prefix should not resolve to the API endpoint - mockMvc.perform(get("/events")) - .andExpect(status().isNotFound()); + // /events without /api prefix should not resolve to the REST API endpoint; + // it is served by SpaController as HTML instead + mockMvc.perform(get("/events") + .accept("text/html")) + .andExpect(status().isOk()); } } diff --git a/backend/src/test/resources/static/index.html b/backend/src/test/resources/static/index.html new file mode 100644 index 0000000..183b15c --- /dev/null +++ b/backend/src/test/resources/static/index.html @@ -0,0 +1,13 @@ + + + + + + + + fete + + +
+ + diff --git a/frontend/index.html b/frontend/index.html index 6837358..6dd842e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,7 @@ + fete diff --git a/frontend/public/og-image.png b/frontend/public/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..daa1a9409e3e6f1eb108532a9b484c4ee182c19f GIT binary patch literal 74511 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 literal 0 HcmV?d00001 diff --git a/specs/012-link-preview/checklists/requirements.md b/specs/012-link-preview/checklists/requirements.md new file mode 100644 index 0000000..2ae9254 --- /dev/null +++ b/specs/012-link-preview/checklists/requirements.md @@ -0,0 +1,36 @@ +# Specification Quality Checklist: Link Preview (Open Graph Meta-Tags) + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-03-09 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- All items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`. +- Assumptions section documents the key technical consideration (SPA vs. server-rendered meta-tags) without prescribing a solution. +- `og:image` explicitly deferred to future scope. diff --git a/specs/012-link-preview/contracts/html-meta-tags.md b/specs/012-link-preview/contracts/html-meta-tags.md new file mode 100644 index 0000000..3c6c00c --- /dev/null +++ b/specs/012-link-preview/contracts/html-meta-tags.md @@ -0,0 +1,98 @@ +# Contract: HTML Meta-Tags + +**Feature**: 012-link-preview | **Date**: 2026-03-09 + +## Overview + +This feature does not add new REST API endpoints. The contract is the HTML meta-tag structure injected into the server-rendered `index.html`. + +## Meta-Tag Contract: Event Pages + +For requests to `/events/{eventToken}` where the event exists: + +```html + + + + + + + + + + + + +``` + +### Description Format + +Full event data: +``` +๐Ÿ“… Saturday, March 15, 2026 at 7:00 PM ยท ๐Ÿ“ Berlin โ€” First 200 chars of description... +``` + +No location: +``` +๐Ÿ“… Saturday, March 15, 2026 at 7:00 PM โ€” First 200 chars of description... +``` + +No description: +``` +๐Ÿ“… Saturday, March 15, 2026 at 7:00 PM ยท ๐Ÿ“ Berlin +``` + +No location, no description: +``` +๐Ÿ“… Saturday, March 15, 2026 at 7:00 PM +``` + +### Title Truncation + +- Titles โ‰ค 70 characters: used as-is. +- Titles > 70 characters: truncated to 67 characters + "..." + +### HTML Escaping + +All meta-tag `content` values MUST be HTML-escaped: +- `"` โ†’ `"` +- `&` โ†’ `&` +- `<` โ†’ `<` +- `>` โ†’ `>` + +## Meta-Tag Contract: Non-Event Pages + +For requests to `/`, `/create`, or any other non-event, non-API, non-static route: + +```html + + + + + + + + + + + + +``` + +## Meta-Tag Contract: Event Not Found + +For requests to `/events/{eventToken}` where the event does NOT exist, fall back to generic meta-tags (same as non-event pages). The Vue SPA will handle the 404 UI client-side. + +## Injection Mechanism + +The `index.html` template contains a placeholder: + +```html + + ... + + ... + +``` + +The server replaces `` with the generated meta-tag block before sending the response. diff --git a/specs/012-link-preview/data-model.md b/specs/012-link-preview/data-model.md new file mode 100644 index 0000000..55ca34a --- /dev/null +++ b/specs/012-link-preview/data-model.md @@ -0,0 +1,83 @@ +# Data Model: Link Preview (Open Graph Meta-Tags) + +**Feature**: 012-link-preview | **Date**: 2026-03-09 + +## Overview + +This feature does NOT introduce new database entities. It reads existing event data and projects it into HTML meta-tags. The "model" here is the meta-tag value object used during HTML generation. + +## Meta-Tag Value Objects + +### OpenGraphMeta + +Represents the set of Open Graph meta-tags to inject into the HTML response. + +| Field | Type | Source | Rules | +|---|---|---|---| +| `title` | String | Event title or "fete" | Max 70 chars, truncated with "..." | +| `description` | String | Composed from event fields or generic text | Max 200 chars | +| `url` | String | Canonical URL from request | Absolute URL | +| `type` | String | Always "website" | Constant | +| `siteName` | String | Always "fete" | Constant | +| `image` | String | Static brand image URL | Absolute URL to `/og-image.png` | + +### TwitterCardMeta + +| Field | Type | Source | Rules | +|---|---|---|---| +| `card` | String | Always "summary" | Constant | +| `title` | String | Same as OG title | Max 70 chars | +| `description` | String | Same as OG description | Max 200 chars | + +## Data Flow + +``` +Request for /events/{token} + โ”‚ + โ–ผ +LinkPreviewController + โ”‚ + โ”œโ”€โ”€ Resolve event token โ†’ Event domain object (existing EventRepository) + โ”‚ + โ”œโ”€โ”€ Build OpenGraphMeta from Event fields: + โ”‚ title โ† event.title (truncated) + โ”‚ description โ† formatDescription(event.dateTime, event.timezone, event.location, event.description) + โ”‚ url โ† request base URL + /events/{token} + โ”‚ image โ† request base URL + /og-image.png + โ”‚ + โ”œโ”€โ”€ Build TwitterCardMeta (mirrors OG values) + โ”‚ + โ”œโ”€โ”€ Inject meta-tags into cached index.html template + โ”‚ + โ””โ”€โ”€ Return modified HTML + +Request for / or /create (non-event pages) + โ”‚ + โ–ผ +LinkPreviewController + โ”‚ + โ”œโ”€โ”€ Build generic OpenGraphMeta: + โ”‚ title โ† "fete" + โ”‚ description โ† "Privacy-focused event planning. Create and share events without accounts." + โ”‚ url โ† request URL + โ”‚ image โ† request base URL + /og-image.png + โ”‚ + โ”œโ”€โ”€ Build generic TwitterCardMeta + โ”‚ + โ”œโ”€โ”€ Inject meta-tags into cached index.html template + โ”‚ + โ””โ”€โ”€ Return modified HTML +``` + +## Existing Entities Used (Read-Only) + +### Event (from `de.fete.domain.model.Event`) + +| Field | Used For | +|---|---| +| `title` | `og:title`, `twitter:title` | +| `description` | Part of `og:description`, `twitter:description` | +| `dateTime` | Part of `og:description` (formatted) | +| `timezone` | Date formatting context | +| `location` | Part of `og:description` | +| `eventToken` | URL construction | diff --git a/specs/012-link-preview/plan.md b/specs/012-link-preview/plan.md new file mode 100644 index 0000000..3d4d1da --- /dev/null +++ b/specs/012-link-preview/plan.md @@ -0,0 +1,83 @@ +# Implementation Plan: Link Preview (Open Graph Meta-Tags) + +**Branch**: `012-link-preview` | **Date**: 2026-03-09 | **Spec**: `specs/012-link-preview/spec.md` +**Input**: Feature specification from `/specs/012-link-preview/spec.md` + +## Summary + +Inject Open Graph and Twitter Card meta-tags into the server-rendered HTML so that shared event links display rich preview cards in messengers and on social media. The Spring Boot backend replaces its current static SPA fallback with a controller that dynamically injects event-specific or generic meta-tags into the cached `index.html` template before serving it. + +## Technical Context + +**Language/Version**: Java 25 (backend), TypeScript 5.9 (frontend โ€” minimal changes) +**Primary Dependencies**: Spring Boot 3.5.x (existing), Vue 3 (existing) โ€” no new dependencies +**Storage**: PostgreSQL via JPA (existing event data, read-only access) +**Testing**: JUnit 5 + Spring MockMvc (backend), Playwright (E2E) +**Target Platform**: Self-hosted Docker container (Linux) +**Project Type**: Web application (SPA + REST API) +**Performance Goals**: N/A โ€” meta-tag injection adds negligible overhead (<1ms string replacement) +**Constraints**: Meta-tags MUST be in initial HTML response (no client-side JS injection). No external services or CDNs. +**Scale/Scope**: Affects all HTML page responses. No new database tables or API endpoints. + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Principle | Status | Notes | +|---|---|---| +| I. Privacy by Design | โœ… PASS | No tracking, analytics, or external services. Meta-tags contain only public event info (title, date, location). No PII exposed. `og:image` is a self-hosted static asset. | +| II. Test-Driven Methodology | โœ… PLAN | Unit tests for meta-tag generation, integration tests for controller, E2E tests for full HTML verification. TDD enforced. | +| III. API-First Development | โœ… N/A | No new API endpoints. This feature modifies HTML serving, not the REST API. Existing OpenAPI spec unchanged. | +| IV. Simplicity & Quality | โœ… PASS | Simple string replacement in cached HTML template. No SSR framework, no prerendering service, no user-agent sniffing. Minimal moving parts. | +| V. Dependency Discipline | โœ… PASS | Zero new dependencies. Uses only Spring Boot's existing capabilities. | +| VI. Accessibility | โœ… N/A | Meta-tags are invisible to users. No UI changes. | + +**Gate result: PASS** โ€” No violations. No complexity tracking needed. + +## Project Structure + +### Documentation (this feature) + +```text +specs/012-link-preview/ +โ”œโ”€โ”€ plan.md # This file +โ”œโ”€โ”€ research.md # Phase 0 output โ€” technical decisions +โ”œโ”€โ”€ data-model.md # Phase 1 output โ€” meta-tag value objects +โ”œโ”€โ”€ quickstart.md # Phase 1 output โ€” implementation guide +โ”œโ”€โ”€ contracts/ +โ”‚ โ””โ”€โ”€ html-meta-tags.md # Phase 1 output โ€” meta-tag HTML contract +โ””โ”€โ”€ tasks.md # Phase 2 output (created by /speckit.tasks) +``` + +### Source Code (repository root) + +```text +backend/ +โ”œโ”€โ”€ src/main/java/de/fete/ +โ”‚ โ”œโ”€โ”€ adapter/in/web/ +โ”‚ โ”‚ โ””โ”€โ”€ SpaController.java # NEW โ€” serves index.html with injected meta-tags +โ”‚ โ”œโ”€โ”€ application/ +โ”‚ โ”‚ โ””โ”€โ”€ OpenGraphService.java # NEW โ€” composes meta-tag values from event data +โ”‚ โ””โ”€โ”€ config/ +โ”‚ โ””โ”€โ”€ WebConfig.java # MODIFIED โ€” remove PathResourceResolver SPA fallback +โ”œโ”€โ”€ src/main/resources/ +โ”‚ โ””โ”€โ”€ application.properties # MODIFIED โ€” add forward-headers-strategy +โ””โ”€โ”€ src/test/java/de/fete/ + โ”œโ”€โ”€ adapter/in/web/ + โ”‚ โ””โ”€โ”€ SpaControllerTest.java # NEW โ€” integration tests + โ””โ”€โ”€ application/ + โ””โ”€โ”€ OpenGraphServiceTest.java # NEW โ€” unit tests + +frontend/ +โ”œโ”€โ”€ index.html # MODIFIED โ€” add placeholder +โ”œโ”€โ”€ public/ +โ”‚ โ””โ”€โ”€ og-image.png # NEW โ€” brand image for og:image (1200x630) +โ””โ”€โ”€ e2e/ + โ””โ”€โ”€ link-preview.spec.ts # NEW โ€” E2E tests +``` + +**Structure Decision**: Web application structure (existing). Backend changes in adapter/web and application layers. Frontend changes minimal (HTML placeholder + static asset). + +## Complexity Tracking + +> No violations โ€” section intentionally empty. diff --git a/specs/012-link-preview/quickstart.md b/specs/012-link-preview/quickstart.md new file mode 100644 index 0000000..2175932 --- /dev/null +++ b/specs/012-link-preview/quickstart.md @@ -0,0 +1,57 @@ +# Quickstart: Link Preview (Open Graph Meta-Tags) + +**Feature**: 012-link-preview | **Date**: 2026-03-09 + +## What This Feature Does + +Injects Open Graph and Twitter Card meta-tags into the HTML response so that shared links display rich preview cards in messengers (WhatsApp, Telegram, Signal, etc.) and on social media (Twitter/X). + +## How It Works + +1. **All HTML page requests** go through a new `SpaController` (replaces the current `PathResourceResolver` SPA fallback). +2. The controller reads the compiled `index.html` template once at startup and caches it. +3. For event pages (`/events/{token}`): fetches event data, generates event-specific meta-tags, injects them into the HTML. +4. For non-event pages: injects generic fete branding meta-tags. +5. Static files (`/assets/*`, `/favicon.svg`, `/og-image.png`) continue to be served directly by Spring Boot's default static resource handler. + +## Key Files to Create/Modify + +### Backend (New) + +| File | Purpose | +|---|---| +| `SpaController.java` | Controller handling all non-API/non-static HTML requests, injecting meta-tags | +| `OpenGraphService.java` | Service composing meta-tag values from event data | +| `MetaTagRenderer.java` | Utility rendering meta-tag value objects into HTML `` strings | + +### Backend (Modified) + +| File | Change | +|---|---| +| `WebConfig.java` | Remove `PathResourceResolver` SPA fallback (replaced by `SpaController`) | +| `application.properties` | Add `server.forward-headers-strategy=framework` for correct URL construction behind proxies | + +### Frontend (Modified) + +| File | Change | +|---|---| +| `index.html` | Add `` placeholder comment in `` | + +### Static Assets (New) + +| File | Purpose | +|---|---| +| `frontend/public/og-image.png` | Brand image for `og:image` (1200x630 PNG) | + +## Testing Strategy + +- **Unit tests**: `OpenGraphService` โ€” verify meta-tag values for various event states (full data, no location, no description, long title, special characters). +- **Unit tests**: `MetaTagRenderer` โ€” verify HTML escaping, correct meta-tag format. +- **Integration tests**: `SpaController` โ€” verify correct HTML response with meta-tags for event URLs, generic URLs, and 404 events. +- **E2E tests**: Fetch event page HTML without JavaScript, parse meta-tags, verify values match event data. + +## Local Development Notes + +- In dev mode (Vite dev server), meta-tags won't be injected since Vite serves its own `index.html`. This is expected โ€” meta-tag injection only works when the backend serves the frontend. +- To test locally: build the frontend (`npm run build`), copy `dist/` contents to `backend/src/main/resources/static/`, then run the backend. +- Alternatively, test via the Docker build which assembles everything automatically. diff --git a/specs/012-link-preview/research.md b/specs/012-link-preview/research.md new file mode 100644 index 0000000..7533ee3 --- /dev/null +++ b/specs/012-link-preview/research.md @@ -0,0 +1,115 @@ +# 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 `` tags into the `` 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. | +| **`