---
title: e-support Private KEP, Ioannina
description: >-
  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.
lang: en
source: https://jimrarras.com/en/projects/esupport-ioannina/
fetched: '2026-09-19T05:53:17.415Z'
---

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.

- **12** service categories on one page

- **4** step-by-step guides

- **2** languages, with every page mirrored

## Stack
## The situation

e-support is a private citizen-services bureau in Ioannina. It handles the cases that go through the state’s digital portals on the citizen’s behalf, in person in the city and remotely across the country. It needed a site that explains clearly what it takes on, states outright that it is not a public authority, and stands up in English too. I delivered it as the sole developer, from the design comps to the DNS.

## A visitor can see at once whether their case is covered

![The e-support services page: a dark green header and menu above the services heading, and a photograph of a service desk stacked with folders](https://jimrarras.com/images/projects/esupport-ioannina/services.webp)

The services page: the twelve categories start below the photograph

People do not search for “private citizen-services bureau”, they search for “pension”, “disability certification”, “benefit”. The twelve categories are written in the words the visitor already uses, so they can tell in ten seconds whether their case is on the list.

How it works

- **The categories are content, not code:** each one is a markdown file in a content collection, in Greek and English. A new category is a new file, not a template change.
- **Fully static:** Astro 7 with Tailwind v4, no adapter and no server: every page is served as a file.
- **The typeface is self-hosted:** Commissioner loads through Astro’s Fonts API, with no third-party request.
- **OG cards and the logo:** per-route cards for the five main routes in both languages, rendered through Playwright, and the logo redrawn from the printed mark.

## A guide for the cases that confuse people

Four of the twelve categories have a guide page of their own: social security, tax, disability certification and pensions.

The guide starts from why the case is hard and ends at the documents that come out of it. The visitor arrives at the office knowing what to ask for.

![The social-security guide page: a link back to all services, two introductory paragraphs, and a list of what the bureau handles with each document name in bold](https://jimrarras.com/images/projects/esupport-ioannina/service-detail.webp)

The social-security guide: the problem first, then the list of documents

How it works

- **One truth about which categories have a guide:** a category gets a page only when its file has a non-empty body. The same check drives the build’s routes, the listing’s links and the `llms.txt` catalogue, so the three cannot disagree. Today four of the twelve have a body, and the other eight appear without a link.
- **Greek slugs in both languages:** the English page lives under `/en/` but keeps the Greek path, so one shared link works for both readers.
- **A single helper** builds every locale-prefixed href, and a structural-parity test fails the build if the two dictionaries diverge in keys, types or array lengths.

## A message instead of a phone queue

A visitor does not have to ring to ask whether their case is taken on: they pick a category, write their question and send it.

The opening hours are shown as they really are, with the evening window separate from the morning one, so nobody turns up at a closed door.

![The contact page: a card with the bureau's details and day-by-day opening hours beside a form with name, phone, email, service category, message and a consent checkbox](https://jimrarras.com/images/projects/esupport-ioannina/contact.webp)

The contact page: the bureau's details beside the form

How it works

- **The form goes through my own self-hosted forms service**, with the public site key in the markup and no secret in the repository, and with Cloudflare Turnstile verified on the server side.
- **Consent:** an explicit checkbox for storing the details, linked to the privacy policy, and GA4 that loads only behind the cookie consent’s analytics toggle.
- **JSON-LD ProfessionalService:** a business-registry identifier, the VAT ID, a localized `areaServed` for city and country, separate morning and evening opening hours, and a `disambiguatingDescription` stating that the business is not a public authority.

## It does not break quietly

A site that can fall behind without anyone saying so is worse than one that breaks. What exists in two copies here is derived from the one, and what could drift is caught by a gate before production.

How it works

- **A mirror for machines:** 22 committed `index.md` twins and one `llms-full.txt`, with a `<link rel="alternate" type="text/markdown">` on every route, so an AI assistant reads markdown, not HTML.
- **Build-time endpoints:** `llms.txt` and `robots.txt` are derived from `site.config.ts`, the i18n dictionaries and the collection, through ordinary imports so the builder stays environment-independent.
- **Placeholder gate:** it scans 7 files for scaffold strings that survived configuration, skipping comments and honouring sentinels for deliberately empty values.
- **The order of CI:** types, unit tests, placeholder gate, build, a dead-link crawl of the built output, and then the e2e suites.
- **Seven Playwright suites:** smoke, i18n, the form, responsive, an axe sweep over every page, and the display-preferences panel with its state matrix, plus a colour-contrast script over the rendered pages.
- **Two hosts, one build shape:** production on Cloudflare Workers Static Assets through wrangler, staging as Docker on Coolify, with `SITE_URL` and `STAGING_NOINDEX` as build args so staging cannot claim production’s canonicals.

## Links

`llms.txt` is not a file somebody once wrote by hand: it is produced on every build from the same data as the pages, so a broken input fails the build instead of serving a wrong catalogue.

## Related work

- Live: [esupport-ioannina.gr](https://esupport-ioannina.gr/)

- 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
