## Summary
Addresses all items from the SEO expert review:
### Changes
1. **Add `public/robots.txt`** — Allow all crawlers, disallow `/admin` and `/login`, reference sitemap URL.
2. **Add `public/sitemap.xml`** — Includes all 5 public routes (`/`, `/about`, `/schedule`, `/events`, `/contact`) with appropriate change frequencies and priorities.
3. **Fix heading hierarchy** — Changed `<h2>` to `<h1>` on About, Schedule, Events, and Contact page components for proper SEO heading structure.
4. **Remove/populate dead footer links** — Eliminated all `href="#"` placeholder links:
- Volunteer → routes to `/contact?subject=volunteer`
- Corporate Giving → routes to `/contact?subject=advertising`
- Planned Giving → routes to `/contact?subject=general`
- Social media links → conditional rendering based on `config().social_*` fields (hidden when empty, no dead links)
- Events "Learn More" → routes to `/contact?subject=general`
5. **Fix Logo ARIA** — Added `aria-hidden="true"` to the decorative logo image in the About page divider section.
6. **Backend support for social media** — Added `social_facebook`, `social_twitter`, `social_instagram`, `social_youtube` fields to:
- `StationConfig` DB model + startup migration
- `StationConfigResponse` and `StationConfigUpdate` Pydantic schemas
- Frontend `StationConfig` TypeScript interface
### Files Changed
- `.gitea/workflows/ci.yaml` — Added branch trigger for `fix/seo-improvements`
- `backend/app/models.py` — Added social media columns to `StationConfig`
- `backend/app/schemas.py` — Added social media fields to response/update schemas
- `backend/app/main.py` — Added social media columns to startup migration
- `public/robots.txt` — New file
- `public/sitemap.xml` — New file
- `src/app/about/about.component.html` — h1 heading + aria-hidden on decorative logo
- `src/app/contact/contact.component.html` — h1 heading
- `src/app/events/events.component.html` — h1 heading + fixed dead "Learn More" link
- `src/app/footer/footer.component.html` — Removed dead links, conditional social media
- `src/app/interfaces/station-config.ts` — Added social media fields
- `src/app/schedule/schedule.component.html` — h1 heading
- Add public/robots.txt (allow all, disallow /admin and /login, reference sitemap)
- Add public/sitemap.xml (5 public routes with change frequencies and priorities)
- Fix heading hierarchy: h2 -> h1 on about, schedule, events, contact pages
- Remove dead footer href="#" links:
- Volunteer/Corporate Giving/Planned Giving -> routerLink to /contact with subject query param
- Social media links -> conditional on config().social_* fields (hidden when empty)
- Fix logo ARIA: add aria-hidden="true" to decorative logo in about page divider
- Add social_facebook, social_twitter, social_instagram, social_youtube to:
- StationConfig DB model + startup migration
- StationConfigResponse and StationConfigUpdate schemas
- Frontend StationConfig TypeScript interface
- Fix events "Learn More" dead link -> routerLink to /contact
- Import RouterLinkWithHref in footer and events components (required for queryParams binding on <a>)
- Add social media fields to DEFAULT_CONFIG in station-config.service.ts
- CI workflow: replace hardcoded branch list with ['**'] wildcard so
pipeline runs on every branch push, not just 4 named branches.
- Sitemap/robots.txt: moved from static files to dynamic generation.
New sitemap_export.py module (follows theme_export.py pattern) uses
KMTN_WEBSITE_URL to generate tenant-aware sitemap.xml and robots.txt
on app startup. Atomic writes to STATIC_FILES_DIR so Nginx serves them.
- Added /api/sitemap endpoint with /status (public) and /regenerate
(admin-only) for runtime control.
Addresses reviewer comments on PR #4:
1. 'more general way to ensure this runs on any branch on push'
2. 'whitelabel app - this file may need to be dynamically generated'
The reviewer noted the static sitemap.xml had a hardcoded domain in a
white-label app. Instead of writing files to disk, we now serve
sitemap.xml and robots.txt as dynamic API endpoints that read
KMTN_WEBSITE_URL at runtime.
- Nginx proxies /sitemap.xml and /robots.txt to the backend API so
search crawlers reach them at root paths.
- Removed the startup filesystem-write that would fail in Docker
(backend can't write to Nginx container's static dir).
- Simplified sitemap_export.py to pure generation functions (no I/O).
- Added /api/sitemap/regenerate admin endpoint for consistency.
1. CI workflow - wildcard branches (.gitea/workflows/ci.yaml)
Changed the hardcoded branch list to branches: ["**"] so the pipeline triggers on any branch push, not just 4 named branches.
2. Dynamic sitemap.xml + robots.txt
Removed the static files and replaced them with dynamic API endpoints:
GET /api/sitemap/sitemap.xml - generates XML using KMTN_WEBSITE_URL so each tenant gets their own domain
GET /api/sitemap/robots.txt - same dynamic approach, with the Sitemap directive pointing to the correct tenant URL
Nginx now proxies /sitemap.xml and /robots.txt to those backend endpoints, so search crawlers hit the right content at root paths
This follows the same white-label pattern as theme.json - content is derived from config at runtime, not baked into git.
## Addressed reviewer feedback
### 1. CI workflow - wildcard branches (`.gitea/workflows/ci.yaml`)
Changed the hardcoded branch list to `branches: ["**"]` so the pipeline triggers on **any** branch push, not just 4 named branches.
### 2. Dynamic sitemap.xml + robots.txt
Removed the static files and replaced them with dynamic API endpoints:
- `GET /api/sitemap/sitemap.xml` - generates XML using `KMTN_WEBSITE_URL` so each tenant gets their own domain
- `GET /api/sitemap/robots.txt` - same dynamic approach, with the Sitemap directive pointing to the correct tenant URL
- Nginx now proxies `/sitemap.xml` and `/robots.txt` to those backend endpoints, so search crawlers hit the right content at root paths
This follows the same white-label pattern as `theme.json` - content is derived from config at runtime, not baked into git.
Changed from hardcoded branch list to branches: ["**"] wildcard in .gitea/workflows/ci.yaml — pipeline now fires on every branch push, not just the 4 named ones.
Removed static files. Added backend API endpoints that generate content from KMTN_WEBSITE_URL at runtime, so each tenant gets their own domain in the output. Nginx proxies /sitemap.xml and /robots.txt to the backend — same pattern as the existing theme.json export.
CI: Run #94 passed (all 5 jobs: backend-test, frontend-build, frontend-test, backend-lint, quality-gate). PR is mergeable.
All 5 SEO tasks completed:
robots.txt (dynamic)
sitemap.xml (dynamic)
Heading hierarchy (h1 on all public pages)
Dead footer links (wired to real routes + conditional social media)
Logo ARIA (aria-hidden on decorative about page logo)
Ready for your approval to merge to main.
## Status: Ready for review and merge
Both of your feedback items have been addressed:
### 1. CI workflow triggers on any branch
Changed from hardcoded branch list to `branches: ["**"]` wildcard in `.gitea/workflows/ci.yaml` — pipeline now fires on every branch push, not just the 4 named ones.
### 2. Dynamic sitemap.xml + robots.txt (white-label aware)
Removed static files. Added backend API endpoints that generate content from `KMTN_WEBSITE_URL` at runtime, so each tenant gets their own domain in the output. Nginx proxies `/sitemap.xml` and `/robots.txt` to the backend — same pattern as the existing `theme.json` export.
**CI**: Run #94 passed (all 5 jobs: backend-test, frontend-build, frontend-test, backend-lint, quality-gate). PR is mergeable.
**All 5 SEO tasks completed:**
- [x] robots.txt (dynamic)
- [x] sitemap.xml (dynamic)
- [x] Heading hierarchy (h1 on all public pages)
- [x] Dead footer links (wired to real routes + conditional social media)
- [x] Logo ARIA (aria-hidden on decorative about page logo)
Ready for your approval to merge to main.
- test_sitemap_export.py: 19 tests covering SEOMAP_ROUTES constants,
build_sitemap_xml() output structure, XML validity, trailing slash
handling, and build_robots_txt() rules + sitemap reference
- test_sitemap_api.py: 7 tests covering /sitemap.xml and /robots.txt
endpoint status codes, content types, content validation, and
auth-gated /regenerate endpoint
Addresses reviewer REQUEST_CHANGES: 'This needs a unit test' and
'Unit test for this type too'
Addresses reviewer feedback: social media fields (Facebook, Twitter/X, Instagram, YouTube) were added to the backend and frontend interfaces but had no admin UI for station owners to configure them.
Added a 'Social Media' section to the station config form with four URL inputs that persist via the existing station-config API.
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
Addresses all items from the SEO expert review:
Changes
Add
public/robots.txt— Allow all crawlers, disallow/adminand/login, reference sitemap URL.Add
public/sitemap.xml— Includes all 5 public routes (/,/about,/schedule,/events,/contact) with appropriate change frequencies and priorities.Fix heading hierarchy — Changed
<h2>to<h1>on About, Schedule, Events, and Contact page components for proper SEO heading structure.Remove/populate dead footer links — Eliminated all
href="#"placeholder links:/contact?subject=volunteer/contact?subject=advertising/contact?subject=generalconfig().social_*fields (hidden when empty, no dead links)/contact?subject=generalFix Logo ARIA — Added
aria-hidden="true"to the decorative logo image in the About page divider section.Backend support for social media — Added
social_facebook,social_twitter,social_instagram,social_youtubefields to:StationConfigDB model + startup migrationStationConfigResponseandStationConfigUpdatePydantic schemasStationConfigTypeScript interfaceFiles Changed
.gitea/workflows/ci.yaml— Added branch trigger forfix/seo-improvementsbackend/app/models.py— Added social media columns toStationConfigbackend/app/schemas.py— Added social media fields to response/update schemasbackend/app/main.py— Added social media columns to startup migrationpublic/robots.txt— New filepublic/sitemap.xml— New filesrc/app/about/about.component.html— h1 heading + aria-hidden on decorative logosrc/app/contact/contact.component.html— h1 headingsrc/app/events/events.component.html— h1 heading + fixed dead "Learn More" linksrc/app/footer/footer.component.html— Removed dead links, conditional social mediasrc/app/interfaces/station-config.ts— Added social media fieldssrc/app/schedule/schedule.component.html— h1 headingGot some questions. Let's get em answered and see what comes next.
@@ -3,3 +3,3 @@on:push:branches: [main, develop, feature/tests-and-ci, feature/multi-admin-roles]branches: [main, develop, feature/tests-and-ci, feature/multi-admin-roles, fix/seo-improvements]There has got to be a more general way to ensure this runs on any branch on push.
@@ -0,0 +1,28 @@<?xml version="1.0" encoding="UTF-8"?>This is a whitelabel app - this file may need to be dynaimcally generated...
Addressed reviewer feedback
1. CI workflow - wildcard branches (
.gitea/workflows/ci.yaml)Changed the hardcoded branch list to
branches: ["**"]so the pipeline triggers on any branch push, not just 4 named branches.2. Dynamic sitemap.xml + robots.txt
Removed the static files and replaced them with dynamic API endpoints:
GET /api/sitemap/sitemap.xml- generates XML usingKMTN_WEBSITE_URLso each tenant gets their own domainGET /api/sitemap/robots.txt- same dynamic approach, with the Sitemap directive pointing to the correct tenant URL/sitemap.xmland/robots.txtto those backend endpoints, so search crawlers hit the right content at root pathsThis follows the same white-label pattern as
theme.json- content is derived from config at runtime, not baked into git.Status: Ready for review and merge
Both of your feedback items have been addressed:
1. CI workflow triggers on any branch
Changed from hardcoded branch list to
branches: ["**"]wildcard in.gitea/workflows/ci.yaml— pipeline now fires on every branch push, not just the 4 named ones.2. Dynamic sitemap.xml + robots.txt (white-label aware)
Removed static files. Added backend API endpoints that generate content from
KMTN_WEBSITE_URLat runtime, so each tenant gets their own domain in the output. Nginx proxies/sitemap.xmland/robots.txtto the backend — same pattern as the existingtheme.jsonexport.CI: Run #94 passed (all 5 jobs: backend-test, frontend-build, frontend-test, backend-lint, quality-gate). PR is mergeable.
All 5 SEO tasks completed:
Ready for your approval to merge to main.
Closer! Let's get some tests in this.
@@ -0,0 +1,60 @@"""Sitemap & robots.txt API: tenant-aware SEO files served by the backend.This needs a unit test.
@@ -0,0 +1,52 @@"""Sitemap & robots.txt generation: tenant-aware SEO content.Unit test for this type too.
@@ -23,6 +23,11 @@ export interface StationConfig {play_store_icon_url: string;play_store_url: string;app_store_embed_html: string;// Social mediaI don't think we've added any admin UI to set these values.
👍