Blog
How we rebuilt editandoideas.com: static, bilingual, and with no inline scripts.

In short. This site is static: every URL is generated as HTML at build time with React Router, and there is no application server and no forms. Everything comes from a single route map —prerendering, hreflang, sitemap, language switcher, and breadcrumbs—, editorial rules are enforced by automated tests, and the content security policy allows no inline scripts. Here is why, and what we would do differently.
Why a static site
The site does not take in data: no forms, no accounts, no database. Contact happens over email and WhatsApp, and the project calculator runs entirely in the browser. Under those conditions, an application server would only add attack surface and one more thing to maintain.
So every URL is prerendered at build time: React Router 8 with Vite and React 19, running without a server, produces a complete HTML file per page and per language. A search engine gets the full content in the first response, without depending on JavaScript, and the host only has to serve files.
One route map
The decision that has saved us the most work is having a single source of truth for URLs. One file declares every page and its Spanish and English path, and everything else is derived from it:
- The list of pages to prerender.
- Reciprocal hreflang links between languages, plus x-default.
- The sitemap.xml, with the real last-modified date of each page.
- The language switcher, which takes you to the equivalent page rather than the home page.
- Breadcrumbs and their structured data.
- Internal links, which are written by page identifier and never as literal strings.
Routes are tested, not reviewed
React Router needs its own route declaration, so there are two lists describing the same thing. Instead of trusting someone to keep them in sync, a test compares them and fails the build if they drift. The symptom it prevents is the worst one: a URL that shows up in the menu and returns a 404 in production.
The same goes for redirects from the previous site: a test checks that every destination exists. When we reorganized services into Development, Design, and Consulting, old URLs kept reaching their equivalent in a single hop.
Two languages that never leak into each other
Spanish lives at the root and English under /en. The language always comes from the URL, never from the browser: a shared link has to show the same thing to everyone.
There is a less obvious detail. At build time every page renders in the same process, and a global, mutable language setting is enough for a page to come out in the wrong language if the render order changes. So we use one fixed i18n instance per language instead of switching languages on the fly, which makes that bug impossible by construction.
A security policy with no exceptions
The site’s content security policy only allows scripts served from its own domain, plus the analytics loader. No unsafe-inline.
The catch is that React Router embeds the app bootstrap as inline scripts, and the policy blocks them. One version shipped that way: the HTML was correct, but no JavaScript ran, so the mobile menu and the calculator were dead. The fix was a post-build step that moves every inline script into its own file and stops the build if any remain. We would rather see a red build than a deployment that looks fine and does not work.
Analytics follows the same logic: it does not load until the visitor accepts it in the cookie notice.
Editorial rules that run as tests
A rule written in a document gets broken by week three; a rule that fails the build does not. So content rules are automated tests too, and together with the technical ones they now add up to more than 350:
- Both translations have exactly the same keys: half a section cannot go missing in English.
- Spanish stays neutral: a list of regional terms fails the test if any of them shows up.
- No string is left empty or filled with placeholder text.
- Phrases from the old site that no longer represent the company cannot come back as positioning.
- Clients are only named with explicit permission.
Apache and the details nobody sees
The site is published to Apache hosting over rsync, and the deployment retries because the connection to the server drops now and then. The Apache configuration is not written by hand: it is generated on every build from the redirect list, the same way the sitemap comes from the route map.
That configuration handles small things with a large effect: a single form for each URL, with no trailing slash; the bare domain redirected to www; and a 404 page that returns a real 404, not an empty page with a 200 status that would tell Google any address exists.
What we would keep and what we would change
We would keep the single route map and the habit of turning every rule into a test: those two decisions are what let the site absorb frequent changes without breaking.
We would change two things. The sitemap’s last-modified date started out as the build date, so every page claimed to have changed on every deployment; now each page takes the date of the last real change to its content, and it should have been that way from the start. And we would have recorded a performance baseline on day one, so every change could be compared against a starting point.