Case study · City portal / multi-instance engine

Wirtualny Otwock 2.0 — one config file runs an entire county

Nobody needed a ninth news portal in Otwock. The county has 125,000 residents and eight outlets fighting over the same clicks. We walked in with a brand from 2007, but instead of writing the same stories again we dropped a technology bomb. Wirtualny Otwock is not a website. It is an engine, and the city portal is its by-product.

wirtualnyotwock.pl →
Wirtualny Otwock — home page of the Otwock county portal
5 days
From kickoff to the launch of the whole portal
1 656
Business listings from 9 municipalities, ready to claim
100%
Configuration from one file — zero engine code changes
44
MCP server tools for AI assistants

Eight portals, one red ocean

All that remained of the old Wirtualny Otwock was a domain, a logo, a full colour set and a screenshot from 2007. No code, no database, plenty of sentiment. Launching a ninth news site would have meant fighting over scraps of attention in a region where eight players already cover the same road works. We went the other way: we do not write news, we take over its tributaries, and we build what none of them has — a business directory, league data and guides to the county.

Not a website. An engine.

Rule #1: nothing local in the code

  • Name, domain, colours, municipalities, categories, data sources, pricing and enabled modules — all in one instance config file
  • 23 module switches drive navigation, the home page and routing; a disabled module vanishes from the portal along with its URLs
  • The brand palette is injected as CSS variables — one compiled stylesheet serves any number of instances
  • The test is brutally simple: a second instance means a new config file and a logo. If engine code has to be touched, something local leaked and goes back for a fix

A directory that built itself

20 categories, 153 specialities, nine municipalities

  • 1,656 business listings from nine municipalities — collected, described and ready for owners to claim
  • We publish in batches, only after checking description, address and photos. Quality before the counter on the home page
  • Instead of a subcategory tree — 137 specialities with pages of their own, like “dentists in Otwock”. Attributes such as parking stay filters, because combinatorics is a filter, not content
  • Photos are sourced with our own tooling, and a company with no website gets a frame from an image bank prepared for this region
Otwock county business directory — categories with photos and counters

Three leagues, three temperamental APIs, one ruthless module

An adapter instead of a hard-wired integration

  • Every organiser keeps their data differently — and every one of them in its own digital middle ages
  • Adapter architecture settled it — three sources, three adapters, and the rest of the system knows nothing about them
  • The result: 12 competitions at one address — local ones and the regional leagues our clubs play in. Together 144 teams, 797 matches and 1,236 players, refreshed with no human involved
  • When a source dies, the fan sees no error, just the last safe copy. A new league in the county is a config entry, not a module rewrite
League module — tables, fixtures and results of local competitions

We took the tributaries instead of fighting for the ocean

Competitors as a source, traffic returns to authors

  • We scan 36 headline sources and 16 event feeds: portals, the offices of eight municipalities, the county hall, libraries, culture centres, police and clubs
  • The system catches duplicate events from different offices and merges them into one, recognising the same venues under different names
  • We show the title and the source, and a click hands the traffic to the original author. Zero copied text, zero enemies
  • Regional headlines feed the editorial plan, which writes guides and context — and never touches news
Latest from the region — a headline aggregator from local portals and institutions

Jobs and company posts for free, because that is the promise

The local job market in one place

  • Job offers from local companies are free and are not a paid add-on to any package
  • An adapter for the national job database adds listings from the county labour office
  • A company runs its own channel of updates and promotions, with a dedicated RSS feed and a daily cap against spam
  • An employer radar shows who is hiring, who is starting and who is accelerating — a sales signal drawn from our own data
Jobs module — offers from local companies and the labour office

We built a portal for people and for machines

Probably the first city portal in Poland with its own MCP server

Fewer and fewer people type queries into a search box — they ask their language models instead. An ordinary website feeds such a model text to guess from. We exposed a native MCP server: 44 tools, 15 of which read the directory, events and job offers without any key at all. It gets better. A local business owner generates a token in their panel and hands the listing to their AI assistant — which posts jobs on their behalf, fixes the description, updates opening hours, straight through the API. Write rules match the web forms, every change returns as a field-by-field diff, deletion is soft and confirmed, and the listing itself cannot be deleted through the API. We wrote up how to do this yourself.

County events calendar — data merged from sixteen sources

We measure, we do not declare

An audit before launch, not after an incident

  • We measure every page on a phone with a weak signal — what counts is the moment the reader sees content, not a lab score
  • The layout does not jump under your thumb: images, typefaces and ad slots reserve their space before they load
  • The code goes through a security audit before launch and after every new module — findings are fixed and documented before anything reaches production
  • Contextual advertising with no cookies: only a campaign identifier goes to the provider, the matching stays in the reader's browser
  • An account exports its own data and deletes itself, with anonymisation instead of an empty row in the database

What it was built from

Five of our projects folded into one engine

Nothing here was built from scratch, and that is the whole point. The directory, payments, photo processing and deployment came from Slub24, built over years for the same kind of directory. Editorial discipline came from ChceBycPiekna: a layered language model with a fallback provider, a unique image for every piece, an author and category on every article. The article layout arrived from the City24 city portal. Advertising runs on our own ContentLink, the same one serving the other portals in the network. Photos go through a pipeline shared with ScaleTry, and local businesses are offered the FlashAI agent in the same place. The building blocks for the next stage — personal accounts, friends, messaging, events with sign-ups and a local marketplace — sit ready in LoveMerci, built on the same stack. The city portal is the by-product. The main product is the engine that will build the next one.

We took the past off its old domain. We did not build a ninth news portal, we built a city-portal engine — and Otwock is its first instance. The next town is a config file, not another project.

Frequently asked questions

How does this portal differ from a local news site?

It does not publish its own news. It aggregates headlines from 36 local sources and sends traffic back to the originals, while building what the region lacked: a directory of businesses from nine municipalities, data from twelve league competitions, an events calendar, job listings and guides to the county.

What does a portal gain from its own MCP server?

An AI assistant reads the directory, events and job offers without a key, and a business owner who generates a token in the panel can have their assistant publish an offer or fix the listing description. Write rules are shared with the web forms, every change returns as a field-by-field diff, and deletion is soft and requires confirmation.

Will the same engine run a portal for another town?

That is why it exists. Name, domain, colours, municipalities, categories, data sources and the module set are configuration rather than code, and the brand palette comes in as CSS variables. The next instance means a new config file, logo and favicons. If it needs anything more, we treat that as a design flaw to fix.

How long did the build take?

Five days from kickoff to launch, timed to the start of the local league season. In that window we built the directory, league, aggregator, events, jobs, accounts, advertising and MCP modules, and the whole thing passed a performance and security audit before it went to production.

Have a local brand or directory to revive?

We will stand up a portal on a ready engine: a business directory, external API data through adapters, accounts, cookieless advertising and an MCP server for AI assistants. Configuration instead of writing everything again.