This Astro project is a local-services website with city pages, service pages and pages combining the two. A flooring installation page for Barcelona shares a structure with other locations, but availability and page content still need to come from the right records.
The site separates those inputs: JSON describes cities and services, Markdown holds longer page content, and shared Astro templates assemble them. There are 83 Markdown records across the three collections. That count describes stored content, not a promise that every possible city/service combination is published.
A page exists only when the service is available
The route builder checks the city, the service and that city's service settings. All three must allow the combination.
For example, the retained configuration enables Barcelona and its local service flags, but the global painting service is inactive:
| Combination | City active | Service active | City allows service | Route generated |
|---|---|---|---|---|
| Barcelona + laminate installation | Yes | Yes | Yes | Yes |
| Barcelona + home painting | Yes | No | Yes | No |
The disabled service does not need its content deleted. Changing availability in the data is enough to prevent its route being generated.
For an enabled combination, the template looks up the Markdown entry with matching city and service slugs, the URL identifiers. Its text and metadata come from that entry, with template fallbacks where content is absent.
The collections use Zod schemas to require fields such as title, description and hero text. Missing required frontmatter can be caught when content is processed, before every template has to deal with it differently.
One question pool, one selection for the page
The project notes describe the earlier FAQ implementation as five repeated questions with city and service names substituted. The current pool holds 78 questions across ten categories, with service-specific and general questions.
A real flooring question in that pool is “¿Qué grosor de suelo laminado debo elegir para mi vivienda?”, asking what thickness to choose for a home's laminate floor. Service associations let that question belong to flooring rather than relying on a generic question with a new city name inserted.
The city-service selector asks for five questions: up to three relevant to the service, then general questions to fill the remaining places. It uses the city and service identifiers as a repeatable shuffle seed. Rebuilding with unchanged inputs keeps the order stable instead of replacing the questions randomly.
const pageFAQs = getPageFAQs(service.slug, city.slug, 5);The same array goes to the visible FAQ section and generateFAQSchema(). That second function creates JSON-LD, machine-readable information describing the page's questions and answers.
Select the questions once
FAQSectionVisible questionsRender the selected questions and answers.
generateFAQSchema(pageFAQs)JSON-LDDescribe those same questions in structured data.
Selecting once is the useful constraint here. A visitor and a system reading the structured data receive the same selected answers. Editing an answer in the pool updates both through the shared input.
A seed does not make every page's copy unique, and a small eligible pool can return fewer than five questions. The result is a content system with explicit availability and repeatable assembly. Search performance and the quality of the individual answers need their own evaluation.
Project sources & checks
astro-seo-website: src/content/config.ts, src/utils/faq-utils.ts, src/data/faqs.json and src/pages/[city]/[service].astro.
Counted 66 city-service, 11 city and 6 service Markdown records; FAQ data contains 78 questions across 10 categories.
Configured content counts are not published-page counts. Routes filter active cities, active services and city-level availability; no traffic uplift is claimed.