Which kind of trouble is this, and who do I talk to about it tonight?
Law firm in Pitești, founded in 1995. Next.js, Firestore, Claude, Calendly.
Six seconds down one of the 140 generated pages, the one for divorce and division of property in Câmpulung. The headline, the sub-line naming the court, the paragraph placing the case at Judecătoria Câmpulung with appeal to Tribunalul Argeș, the court panel and the FAQ built from the same fact. The towns and the courts are real public institutions and come from the app’s own data layer. Every frame here was taken from a throwaway copy of the build with no credentials in it: the attorney’s name and the phone number are invented substitutions and the prices were removed before it was built.
A law firm in Pitești, founded in 1995, that had spent years on a page built from a template. What it needed was not a nicer brochure. It was a way for somebody in trouble at nine in the evening to work out which kind of trouble it is, find the page about it, and reach the right lawyer. Fourteen practice areas sit under that, crossed against ten localities into 140 generated pages, each one anchored on the court that would actually hear the case. The staff write and rewrite the public side themselves, in place, without asking us.
The problem
Legal trouble does not arrive with a label on it. A person knows the bank is taking money, or a letter came from the tax office, or the divorce is going to be contested, and none of that maps cleanly onto the words a law firm uses to organise itself. Fourteen practice areas is an accurate map of the firm and a useless map of a worried person.
The second problem is time. A contravention fine can be contested for fifteen days and a tax assessment answered in forty five, both of which the firm’s own pages now state in their FAQ. Five of the fourteen areas carry a deadline like that in the app’s data, and a protection order is counted in hours rather than weeks. A list of services tells a person none of this. It waits to be searched.
What we built
01
One conversation instead of a menu
A bar sits at the bottom of every page and opens by itself. It asks, in Romanian only, what the problem is. Behind it is a map of all fourteen areas, each with what the firm does in it, the keywords people actually type, and whether the area carries a deadline. It is given five things it can do to the page rather than words to say: open a practice page, scroll to a section, prefill the contact form with the reason, offer two to four buttons to tap, and end the conversation. The list of destinations is an enum of the fourteen slugs, so there is nothing to invent.
The panel as it opens, over one of the practice pages. Every word in the frame is the app’s own shipped copy: the greeting and the four category buttons are hardcoded defaults rather than anything written for the photograph, and nothing was called to produce it. The name on the header is an invented substitution for the firm’s own.
02
Four exchanges, then a price list, then a day
The fifth thing it can do ends the conversation. The prompt requires it by the fourth message with no exception, and if it has not fired by then the server appends a second instruction telling it to fire now. When it does, the visitor is pushed to the consultation page, the input locks, and a cooldown of twenty four hours starts. Ten messages is the ceiling behind that, fifteen an hour per address, twelve a session, and a daily spend cap above them. So the product is four exchanges at most, then a redirect, then a day of silence. It is enforced twice, which is not an accident.
03
A brief for the call, and where the certainty stops
Pick a consultation package and the conversation is posted to Claude Haiku, which writes two or three sentences in the third person describing the situation. That text is handed to Calendly as the prefilled answer to the first custom question on the event type. Whether it lands depends on that question existing, which is account configuration rather than code, so what we can say is that the summary is written and handed over, and no further. Every failure along the path is silent on purpose: the field simply stays empty.
04
Fourteen pages, written rather than generated
Each practice area has its own page: a hero, body copy, an FAQ accordion, the related areas, and a sidebar that stays with you down the page. Structured data marks up the firm, both attorneys and every one of the fourteen, with an FAQ and a breadcrumb graph per page. These are the pages the conversation routes to and the pages the location layer is crossed against, so they are the spine of everything below them.
One of the fourteen, on a phone and on a laptop. On the wide capture the body runs beside the sidebar that carries the contact card and the cross-links to the other thirteen. The name and the number on that card are invented substitutions for the firm’s own, and the consultation price it normally shows was taken out before this copy was built.
05
A page for every area in every town
Ten localities crossed against the fourteen areas: 140 pages, plus a hub for each locality and an index above them, all resolved at build time with dynamic params switched off, so nothing is made on request. 185 routes are prerendered and 173 URLs go into the sitemap. What each page turns on is not the town name. It is the court. A divorce filed in Câmpulung is heard at Judecătoria Câmpulung and not in Pitești, so every page carries a fact the other 139 cannot.
One of the 140, on a phone and on a laptop. The sub-line and the answer-lead paragraph place the case at Judecătoria Câmpulung with appeal to Tribunalul Argeș, and the rest of the copy varies on that. The town, the court and the tribunal are real and come from the app’s own data layer. The attorney’s name and the phone number are invented substitutions and the price line was removed before the build.
06
The pages had to learn Romanian grammar
“În fața Judecătoria Pitești” is a grammatical error. It has to be “Judecătoriei”, because Romanian inflects the article, so a twenty six line function in the location data puts every court name into the genitive by four rules, one per court type. Every locality also carries a preposition, since “avocat drept penal în Costești” is wrong where “la Costești” is right, and “la” takes the accusative, which is the plain form again. A generated page that gets its own language’s declensions wrong announces exactly what it is.
The location FAQ, opened at the question only a jurisdictional data layer can answer. Both case forms are in the one answer: the genitive after “competența revine”, and the plain form after “la”. The courts named are real; nothing personal is on this screen.
07
The firm writes its own site
Signed in, every heading, paragraph, image, button and link on the public pages becomes editable where it sits. Click the sentence and type; it saves when you click away and Escape puts the original back. Six primitives cover it and 118 keys are wired to it. Undo and redo are per field and live in the tab, so a reload ends them, and the document behind a field holds one value, overwritten in place, with no revision beside it.
Edit mode on a laptop, with the sidebar paragraph mid-edit inside its gold dashed outline and the admin bar carrying undo, redo, the inbox, the dashboard and the new-page button. Sign-in was replaced with a local flag in the copy this was photographed from, so no account and no database were involved.
08
Eight layouts, drawn rather than listed
Staff can also build whole new pages. The layout is picked by looking at it: eight cards in a carousel, three visible at a time, each one a block skeleton drawn in code from six named colours rather than a screenshot of the template it stands for. Naming and a URL make the page as a draft, which is then edited in place like every other page on the site.
The carousel, on a phone and on a laptop. Eight layouts, three of them in view, tracked by the dots, with the caption for the selected one underneath. The dialog was opened for the photograph and no page was created.
09
Their own numbers, counted in the building
Visits, page views, and the five things that count as an outcome for a law firm: taps on the phone number, on WhatsApp, on the email address, forms sent, and the chat opened. Rolled up per day in Bucharest time, with eight ranges, a year-on-year compare, two time charts, two donuts, a top-pages bar and a CSV export. One of the five counters has read zero since the day the panel started opening by itself, because the event only fires when somebody opens it by hand.
The dashboard on a phone and on a laptop, showing what it does rather than what the firm has: the ranges, the four counting cards, and the five channel counters under them. Every visit, page view, conversion, chart and table row on this screen was invented for the photograph, and none of it is the firm’s traffic. The counter reading zero is the one described above, and the fixture was written to leave it at zero rather than tidy it up.
10
An inbox that is a workflow
Messages from both forms land in one place with a status of new, read or resolved, timestamped at each step, a notes field beside them and a delete. An email goes to the office at the same time. A honeypot field, a too-fast-to-be-human check and a limit on links keep the spam out without asking somebody in distress to identify traffic lights.
The inbox on a phone and on a laptop, one message open on the wide capture. All five messages, and every name, address and number with them, were written for this photograph. The live listener was replaced with that fixture, so no real enquiry was ever loaded onto a screen that was about to be photographed.
A page that only swaps the town name into otherwise identical copy is a doorway page. So all 140 of them are anchored on the court that would actually hear the case.
The whole editing loop on a phone in six seconds. Switch edit mode on, tap the sub-heading, type over it, press Escape and the original sentence comes back. Sign-in was replaced with a local flag in the copy this was filmed from and the save path was a no-op, so nothing was written to any database.
The parts worth telling
Written to be read by machines as well as by people
The site publishes a plain-text summary of itself at /llms.txt and a longer one at /llms-full.txt, both generated from the same data layer that generates the pages, so the lists of practice areas and covered towns cannot drift out of step with the pages themselves. The robots file names twelve AI crawlers one per line and allows each explicitly, with a comment beside each, on the stated reasoning that several treat a named allow as the signal and that one per line keeps the intent auditable.
Half a megabyte of JavaScript, and the one line that put it there
On 4 March a 94-file commit split the Firebase config apart to keep the SDK out of the public bundle. On 16 March, a commit that let the chat bar hide itself the moment the firm switches it off added one static import of the fat config to a component in the root layout. Measured at the last commit: 506 KB of gzipped JavaScript on the homepage, 150 KB of it Firebase that no member of the public uses. Nothing in the repository could have caught that. There is no bundle budget, no test and no continuous integration.
Undo is not history
The editor’s undo and redo are held in a ref map, per field, in one tab. They are genuinely useful and they end when the page reloads. This page used to say the editor kept a history a change could be walked back through. It does not, and the sentence has been taken out.
The redirects came out of the measurement
The baseline snapshot was taken to be a before figure, and it also found nine dead URLs that real traffic was hitting: English spellings of the legal pages, and diacritics arriving URL-encoded. Those nine are permanent redirects in the config now, with a comment citing the file the numbers came from. The same pass caught a fault in the tracker itself, a phone number arriving in the field meant for page paths.
How it works
Fourteen practice areas, with keywords and deadlines
Ten localities, and the court that serves each
What the visitor types
One typed data layer, and a conversation that acts on the page
140 generated pages, ten hubs, 173 sitemap URLs
A practice page, opened mid-sentence
A contact form with the reason already written
Numbers
140
generated pages: ten localities across fourteen areas, each anchored on its own court
4 → 24 h
exchanges before the chat pushes to the price list, then a day with the input locked
441 / 857 / 19
visits, page views and contact taps over the 95 days to 24 August 2026, counted deliberately before the SEO layer went in
23,941
lines of TypeScript across 171 files, in 38 commits over 11 working days
Built with
Next.js 16
React 19
TypeScript
Tailwind 4
Firestore
Firebase Auth
Firebase Admin
Claude
Calendly
Zod
React Hook Form
Nodemailer
GSAP and Lenis
Recharts
Vercel
Launched, and a client’s business. The location layer went in on 24 August 2026 and the 95-day baseline in the numbers above was taken the same day, on purpose, as a before figure. The after has not been measured, so nothing here claims a result. Three things are honestly unfinished: there are no tests and no continuous integration, the linter reports 37 errors, and the Firebase regression above is still in the bundle. What the build changed for the firm is theirs to tell.