e-support Private KEP, Ioannina
Lead Developer
Bilingual website for a private citizen-services bureau in Ioannina: 12 service categories, JSON-LD with GEMI/VAT identifiers, an AEO layer with build-time llms.txt and a markdown mirror, and CI gates at every step. From design comps to Cloudflare production.

The system
e-support is a private KEP (citizen service center) in Ioannina: it handles EFKA (social security), DYPA (employment service), AADE (tax authority), KEPA (disability certification), benefits, certificates, vehicle, and pension matters, with in-person service in Ioannina and remote service nationwide. It needed a website that explains clearly what the business does (and what it is not: it is not a public authority), stands up in both Greek and English, and is easy to find for Google and AI assistants alike. Starting from the in-house Astro website-template, I delivered a fully static, bilingual site with the 12 service categories as a content collection, a lead form, and an AEO layer that keeps the machine-readable picture of the site permanently in sync with the site itself.
Architecture
- Static Astro 7 + Tailwind v4: plain static output with no adapter, TypeScript throughout, self-hosted Commissioner via the Astro Fonts API (no third-party font requests)
- Bilingual i18n: Greek at the root, English under
/en/, with Greek slugs in both locales (/ypiresies/, not/en/services/). A single helper (localePath()) builds every locale-prefixed href, and a structural-parity test fails the build if the two dictionaries diverge - Content collection with opt-in detail pages: the 12 service categories are markdown files, and a category gets its own guide page only when its file has a non-empty body. The same body-presence test drives
getStaticPaths, the listing’s links, and thellms.txtcatalog, so the three can never disagree by construction - AEO layer:
llms.txtandrobots.txtare build-time endpoints derived fromsite.config.ts, the i18n dictionaries, and the collection on every build, plus a committed markdown mirror (22index.mdtwins +llms-full.txt) with a<link rel="alternate" type="text/markdown">on every route - JSON-LD ProfessionalService: GEMI (business registry) identifier, VAT ID, localized
areaServed(city + country), split opening hours (morning + evening windows), and adisambiguatingDescriptionstating explicitly that the business is not a public authority - Two hosts, one build shape: production on Cloudflare Workers Static Assets via wrangler, staging as Docker (node:22-alpine → nginx:alpine) on Coolify, with
SITE_URLandSTAGING_NOINDEXas build args so staging can never claim production’s canonicals
My contribution
I delivered the site end-to-end as the sole developer: from design to DNS. Beyond the pages themselves, most of the work went into the gates that keep the site maintainable: a hardened placeholder gate that scans 7 files for scaffold strings that survived configuration (with comment-skipping and emptyValue sentinels so real values don’t trip false positives), a linkinator crawl of the entire built output, four Playwright suites (smoke, i18n, lead form, responsive), and a WCAG AA contrast audit script that measures the rendered pages. I also digitally recreated the logo, and wrote the per-route OG cards rendered via Playwright for the five top-level routes in both locales.
Stack details
The most interesting decision is that llms.txt is not a static file but a build-time endpoint: it is derived from the same inputs that produce the site (config, dictionaries, collection), so it cannot fall behind, and a broken input fails the build instead of silently serving a wrong catalog. GA4 loads only behind the cookie consent’s analytics toggle. The contact form goes through my own self-hosted forms service (rorsa-platform) with the public site key in the markup, no secrets in the repo, and the Cloudflare Turnstile token verified server-side. And the check:links gate crawls the entire built dist rather than just the homepage, with a computed skip list instead of a config file that can be silently shadowed.
Outcomes
- Live at esupport-ioannina.gr, in production on Cloudflare Workers Static Assets
- Bilingual: a full English mirror under
/en/with structurally guaranteed i18n parity - Full CI: types, unit tests, placeholder gate, build, dead-link crawl, four e2e suites
The challenge
The most interesting design challenge was the AEO layer. A hand-written llms.txt is correct the day it is written and suspect every day after. The solution was to make the catalog a derivative of the site instead of a description of it: buildLlmsTxt() reads site.config.ts, the i18n dictionaries, and the services collection through ordinary ES imports (no readFileSync, so the builder stays environment-agnostic), and runs on every build. The same holds for which services are advertised as detailed guides: the body-presence test that decides whether a category gets a detail page is the same test that builds the llms.txt catalog and the listing’s links. When the truth is defined in one place, three surfaces need no synchronisation, and a mistake becomes a build failure instead of silent drift.
Links
- Live: esupport-ioannina.gr
Related work
- Rorsa Tools: the MCP toolkit behind this site’s markdown mirror
- Κόμβος Υγείας & Dr Christos Kolettis: other Astro sites in the same family, without the AEO layer
Gallery


