Implements Phase 2 of the SEO optimization plan: route-level metadata service and JSON-LD structured data for enhanced SERP presence and social sharing.
SeoService — Dynamic Metadata Management
Per-route document titles (e.g., KMountain Flower Radio — About)
Meta descriptions pulled from station config per route
Canonical URLs, Open Graph tags, Twitter Card tags
## Summary
Implements Phase 2 of the SEO optimization plan: **route-level metadata service** and **JSON-LD structured data** for enhanced SERP presence and social sharing.
### SeoService — Dynamic Metadata Management
- Per-route document titles (e.g., `KMountain Flower Radio — About`)
- Meta descriptions pulled from station config per route
- Canonical URLs, Open Graph tags, Twitter Card tags
- Input sanitization (HTML stripping, URL validation, attribute escaping)
- `noindex` robots directives for `/login` and `/admin` routes
- Centralized router watcher via `SeoService.watchRouter()` — zero per-component boilerplate
- 22 unit tests covering all functionality
### StructuredDataService + StructuredDataComponent — JSON-LD
- **Organization schema** (`NCBOrganization`) site-wide with name, URL, logo, contact point, address
- **WebSite schema** with publisher info and logo
- **WebPage schema** per-route with breadcrumbs
- **Event schema** dynamically on `/events` page for each upcoming event
- **RadioSeason schema** on `/schedule` page with show episodes
### Architecture
- `SeoService` is injected once in `app-root` and auto-watches router navigation
- `StructuredDataComponent` is a standalone component that writes `<script type="application/ld+json">` tags to `<head>`
- `EventsComponent` and `ScheduleComponent` feed their data to `StructuredDataService` for dynamic schema injection
- All services are `providedIn: root` singletons with signal-based reactivity
### Files Changed
- `src/app/interfaces/seo-metadata.ts` (new)
- `src/app/interfaces/structured-data.ts` (new)
- `src/app/services/seo.service.ts` (new)
- `src/app/services/seo.service.spec.ts` (new)
- `src/app/services/structured-data.service.ts` (new)
- `src/app/structured-data/structured-data.component.ts` (new)
- `src/app/app.ts` (updated)
- `src/app/app.html` (updated)
- `src/app/events/events.component.ts` (updated)
- `src/app/schedule/schedule.component.ts` (updated)
### Test Plan
- [x] 22 unit tests for SeoService pass (Vitest)
- [ ] Frontend build passes
- [ ] Backend tests pass (unchanged)
Add SeoService for dynamic metadata management:
- Per-route document titles (e.g., 'KMountain Flower Radio — About')
- Meta descriptions from station config per route
- Canonical URLs, Open Graph tags, Twitter Card tags
- Input sanitization (HTML stripping, URL validation, attribute escaping)
- noindex robots directives for /login and /admin routes
- Centralized router watcher (zero per-component boilerplate)
Add StructuredDataService + StructuredDataComponent for JSON-LD:
- Organization schema (NCBOrganization) site-wide with contact info
- WebSite schema with publisher info
- WebPage schema per-route with breadcrumbs
- Dynamic Event schema on /events page
- RadioSeason schema on /schedule page
Wire up SeoService.watchRouter() in app-root ngOnInit.
Wire up StructuredDataComponent in app-root template.
Wire up EventsComponent and ScheduleComponent to feed data to
StructuredDataService for dynamic schema injection.
Includes full test suite for SeoService (22 tests).
When running on the test site (https://kmtn.westerntechnologies.duckdns.org) should we see this metadata somewhere in the markup when the page is done rendering?
Yes — once this PR is merged, you will see all the SEO metadata in the rendered page markup. Here is what gets injected into <head> at runtime:
1. Dynamic <title> per route
The Angular Title service updates the document title on each route change. E.g., on /about the title becomes KMountain Flower Radio — About. Visible in the browser tab and in DevTools <title> element.
2. <meta> tags (visible in DevTools <head>)
<meta name="description" ...> — unique description per route
<meta property="og:title" ...>, <meta property="og:description" ...> — Open Graph for social sharing
<meta name="robots" content="noindex"> — on /login and /admin
3. JSON-LD <script> tags (visible in DevTools <head>)
Organization schema (site-wide)
WebSite schema with search action
WebPage schema per route
Event schemas on /events (dynamic from API data)
RadioSeason schema on /schedule
Important caveat: The test site (kmtn.westerntechnologies.duckdns.org) is currently running main, which does not yet include PR #6. After merge + redeploy, you can verify the metadata by:
Opening DevTools (F12) > Elements tab > <head>
Looking for <meta> tags with og:, twitter:, and description attributes
Looking for <script type="application/ld+json"> tags
Note that View Page Source (Ctrl+U) shows the original server HTML — Angular injects these tags at runtime, so you need DevTools Elements panel to see the final rendered <head>.
All major search engines (Google, Bing) execute JavaScript and index these dynamic meta tags correctly. This is standard Angular SSR-less behavior that works fine for SEO.
Yes — once this PR is merged, you will see all the SEO metadata in the rendered page markup. Here is what gets injected into `<head>` at runtime:
**1. Dynamic `<title>` per route**
The Angular `Title` service updates the document title on each route change. E.g., on `/about` the title becomes `KMountain Flower Radio — About`. Visible in the browser tab and in DevTools `<title>` element.
**2. `<meta>` tags (visible in DevTools `<head>`)**
- `<meta name="description" ...>` — unique description per route
- `<meta property="og:title" ...>`, `<meta property="og:description" ...>` — Open Graph for social sharing
- `<meta name="twitter:card" ...>`, `<meta name="twitter:title" ...>` — Twitter cards
- `<link rel="canonical" ...>` — canonical URL
- `<meta name="robots" content="noindex">` — on `/login` and `/admin`
**3. JSON-LD `<script>` tags (visible in DevTools `<head>`)**
- Organization schema (site-wide)
- WebSite schema with search action
- WebPage schema per route
- Event schemas on `/events` (dynamic from API data)
- RadioSeason schema on `/schedule`
**Important caveat:** The test site (`kmtn.westerntechnologies.duckdns.org`) is currently running `main`, which does not yet include PR #6. After merge + redeploy, you can verify the metadata by:
1. Opening DevTools (F12) > Elements tab > `<head>`
2. Looking for `<meta>` tags with `og:`, `twitter:`, and `description` attributes
3. Looking for `<script type="application/ld+json">` tags
Note that `View Page Source` (Ctrl+U) shows the *original* server HTML — Angular injects these tags at runtime, so you need DevTools Elements panel to see the final rendered `<head>`.
All major search engines (Google, Bing) execute JavaScript and index these dynamic meta tags correctly. This is standard Angular SSR-less behavior that works fine for SEO.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Implements Phase 2 of the SEO optimization plan: route-level metadata service and JSON-LD structured data for enhanced SERP presence and social sharing.
SeoService — Dynamic Metadata Management
KMountain Flower Radio — About)noindexrobots directives for/loginand/adminroutesSeoService.watchRouter()— zero per-component boilerplateStructuredDataService + StructuredDataComponent — JSON-LD
NCBOrganization) site-wide with name, URL, logo, contact point, address/eventspage for each upcoming event/schedulepage with show episodesArchitecture
SeoServiceis injected once inapp-rootand auto-watches router navigationStructuredDataComponentis a standalone component that writes<script type="application/ld+json">tags to<head>EventsComponentandScheduleComponentfeed their data toStructuredDataServicefor dynamic schema injectionprovidedIn: rootsingletons with signal-based reactivityFiles Changed
src/app/interfaces/seo-metadata.ts(new)src/app/interfaces/structured-data.ts(new)src/app/services/seo.service.ts(new)src/app/services/seo.service.spec.ts(new)src/app/services/structured-data.service.ts(new)src/app/structured-data/structured-data.component.ts(new)src/app/app.ts(updated)src/app/app.html(updated)src/app/events/events.component.ts(updated)src/app/schedule/schedule.component.ts(updated)Test Plan
When running on the test site (https://kmtn.westerntechnologies.duckdns.org) should we see this metadata somewhere in the markup when the page is done rendering?
Yes — once this PR is merged, you will see all the SEO metadata in the rendered page markup. Here is what gets injected into
<head>at runtime:1. Dynamic
<title>per routeThe Angular
Titleservice updates the document title on each route change. E.g., on/aboutthe title becomesKMountain Flower Radio — About. Visible in the browser tab and in DevTools<title>element.2.
<meta>tags (visible in DevTools<head>)<meta name="description" ...>— unique description per route<meta property="og:title" ...>,<meta property="og:description" ...>— Open Graph for social sharing<meta name="twitter:card" ...>,<meta name="twitter:title" ...>— Twitter cards<link rel="canonical" ...>— canonical URL<meta name="robots" content="noindex">— on/loginand/admin3. JSON-LD
<script>tags (visible in DevTools<head>)/events(dynamic from API data)/scheduleImportant caveat: The test site (
kmtn.westerntechnologies.duckdns.org) is currently runningmain, which does not yet include PR #6. After merge + redeploy, you can verify the metadata by:<head><meta>tags withog:,twitter:, anddescriptionattributes<script type="application/ld+json">tagsNote that
View Page Source(Ctrl+U) shows the original server HTML — Angular injects these tags at runtime, so you need DevTools Elements panel to see the final rendered<head>.All major search engines (Google, Bing) execute JavaScript and index these dynamic meta tags correctly. This is standard Angular SSR-less behavior that works fine for SEO.
👍