ClientProduction2026

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.

Stack

  • Astro 7
  • Tailwind v4
  • Cloudflare Workers
  • TypeScript
  • Playwright + vitest
  • llms.txt / AEO
  • JSON-LD
  • rorsa-platform forms
e-support Private KEP, Ioannina

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 the llms.txt catalog, so the three can never disagree by construction
  • AEO layer: llms.txt and robots.txt are build-time endpoints derived from site.config.ts, the i18n dictionaries, and the collection on every build, plus a committed markdown mirror (22 index.md twins + 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 a disambiguatingDescription stating 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_URL and STAGING_NOINDEX as 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.

  • 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
Get in touch

Display preferences

Text

Text size
Line spacing
Letter spacing

Color

Contrast

Motion and images

Reading

Reading aid
No changes