Skip to content
vp.Ven Petrov
WorkWritingAboutAt a glance
← Projects & notes
Astro local-services websiteProject story · 3 min read

A local-services site assembled from city, service and FAQ records

The Astro site describes flooring and other local services across different cities. Shared templates check availability, read the matching content and select one FAQ set for both visitors and structured data.

Ven PetrovImplemented website

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

pageFAQs = getPageFAQs(service.slug, city.slug, 5)
FAQSectionVisible questions

Render the selected questions and answers.

generateFAQSchema(pageFAQs)JSON-LD

Describe those same questions in structured data.

The service and city slugs seed the selection. Rebuilding an unchanged page keeps that selection stable.

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
Local repository and code review

astro-seo-website: src/content/config.ts, src/utils/faq-utils.ts, src/data/faqs.json and src/pages/[city]/[service].astro.

Source inventory

Counted 66 city-service, 11 city and 6 service Markdown records; FAQ data contains 78 questions across 10 categories.

Scope of result

Configured content counts are not published-page counts. Routes filter active cities, active services and city-level availability; no traffic uplift is claimed.

On this page
A page exists only when the service is availableOne question pool, one selection for the pageSources & checks
AstroTypeScriptContent modelingSEO

Connected to this

All writing
3D Spaces website migration · Project story

Moving 3D Spaces to Next.js while its email stayed on the old server

Sociedades Inactivas enquiries · Technical note

Saving an enquiry before trying to send its email

Ven PetrovSoftware engineer · Barcelona
GitHub ↗LinkedIn ↗Email ↗

Find a project or a note

17 results

Transfutur ERP · ExperienceERP for transport and logisticsAs the sole engineer, I built an ERP from zero for transport and logistics. It supported roughly €5–6 million in annual invoicing across two offices, and I kept it running for about a year.Financial review lab · Project storyExploring a financial report with Python and FastAPII started with one income change, then opened up the same 54-page report: products, regions, cash flow, core adjustments and a dataset whose figures lead back to the source.Crude-oil narrative tracker · Project storyCrude-oil research from global news and market dataI wanted help following global events that could matter for crude oil. The prototype combines news research with calculated market and position values, while keeping recent assessments separate from the longer numeric history.MNQ time-series research · ExperimentTime-series prediction experimentsAn exploration of high-resolution market data: choosing what to acquire, checking its limits, comparing time intervals and testing whether a prediction survived later data and the cost of acting on it.NQ/MNQ backtest engine · Technical noteA missing closing bar left the simulator’s short position openThe strategy reset for a new day while a shared counter still counted yesterday’s short. A small replay shows why closing the position has to happen before clearing its state.Small-cap edge research · Project storyTurning small-cap trading ideas into repeatable researchI asked Claude Code for a fresh research system that could test named price patterns, compare later periods and carry its methodology into new sessions. One candidate shows how features, entry timing and costs fit together.Gadgeon Europe applications · ExperienceConnected-device dashboards and mobile apps at Gadgeon EuropeI worked on Ionic and Angular applications that brought several APIs into dashboards for energy monitoring and connected-device control, with releases and maintenance across multiple client versions.ctxforge · Project storyctxforge: discovering what to build before codingA context discovery framework that asks an LLM to act like a senior engineering interviewer: understand the idea, uncover requirements and agree the scope before an agent starts building.Sociedades Inactivas enquiries · Technical noteSaving an enquiry before trying to send its emailOn the Sociedades Inactivas catalogue, a visitor can ask about a company or send a general enquiry. The form separates saving that request from delivering its notification, including when the visitor retries.Astro local-services website · Project storyA local-services site assembled from city, service and FAQ recordsThe Astro site describes flooring and other local services across different cities. Shared templates check availability, read the matching content and select one FAQ set for both visitors and structured data.3D Spaces website migration · Project storyMoving 3D Spaces to Next.js while its email stayed on the old serverI rebuilt the virtual-tour website with AI coding collaborators, preserving its content and URLs. The cutover also had to protect the mailbox, enquiry flow and existing analytics history.Capture CRM · Project storyConnecting classified listings to contacts in a small CRMA Chrome extension and Next.js dashboard bring classified listings, platform profiles and contact history together. Saving a listing again can add the person who was missing from an earlier import.Product-description comparison bench · ExperimentComparing one model call with separate vision and writing stepsA small product-description tool lets me inspect the visual analysis before the copy is written, change the model used for each step and compare request measurements.Between Us · Project storyBetween Us: why I kept the first take and widened the shotI compared dialogue takes, asked for wider shots and more physical acting, then changed the workflow when its studio limited the scenes I wanted to make.Restaurant-service voice agent · ExperimentDesigning a restaurant-service call around the real offerI worked through a Spanish outbound agent prompt, corrected a free visit to the actual paid service and compared how two drafts handled transfers and bookings.Vapi and Telnyx voice integration · Technical noteThe Vapi agent could speak, but it could not hear me clearlyMy Telnyx number connected to the assistant, but the caller audio was distorted. Three codec tests recovered the connection without fixing that original problem.Warloop: Echoes of Solmara · Project storyWarloop: turning generated character art into a playable prototypeGood-looking character images became tiny figures, a backwards walk and misplaced bosses in the game. I worked through the gap between generated art and playable behavior.
esc to close · Tab to move through results