Skip to content
Kulular Teknoloji, Kulular Bilişim Teknolojileri Limited Şirketi

← Writing

How to build a corporate website: notes from our own site

18 September 2026Updated 1 October 20265 minVersion 1.1

Summary

Static generation means a website's pages are built once at build time, not when a visitor arrives. The server does not recompute the page on every request. It serves ready-made HTML. This approach improves speed, reduces server load and allows pages to be cached. The cost is that when content changes, the page has to be rebuilt and the cache has to be cleared.

Author

Tarık İsmet ALKAN →

Organisation

Kulular Bilişim Teknolojileri Limited Şirketi

A software company's website is the first piece of software the company writes. Visitors who open it do not see the code, but they see the result. If a page loads slowly, if a Turkish sentence is left behind in another language, if the contact form quietly fails, they take it as a sign of how the company works.

So we did not build the site like a brochure. We built it like a small product. Below I write about which decisions we made, why we made them and where we got things wrong.

Static generation

The site is written in Next.js. In the first version, pages were regenerated on the server for every request. The cause was an innocent line. The root layout read a request header to work out the language. Next treats every page that does this as dynamic.

The result was that Cloudflare could not cache a single page. Every visit went all the way to the server at Hetzner. When we measured locally, time to first byte was a few milliseconds, so the code was not slow. The problem would have appeared at scale.

The fix was to take the language from the address, not from the request. Each language has its own root layout, and the language is clear from the folder the page sits in. Today all of the roughly three hundred pages are generated once, at build time.

Seven languages and consistency

The site is live in Turkish, English, French, Portuguese, German, Spanish and Arabic. The translation itself is the easy part. The hard part is that when one text changes, it has to change in seven places at once.

Most of the errors we found in the first reviews came from the same pattern. A long chain of conditions picked text by language. When it forgot a language, it quietly fell through to the last branch, and the last branch was usually Turkish. That is how a Turkish sentence ended up in the middle of a French page.

We closed this off with a helper function. Every place that picks text must supply all seven languages at once. If one language is missing, the code does not compile. The error is caught before it goes live.

Academic titles were a similar problem. In one translation the title "Doç. Dr." had turned into professor. The title is no longer translated in any language. It is taken from the Turkish record, and a test checks this on every push.

Tests

We did not set up a heavy test framework. We wrote nine tests with Node's own test module. Each one catches an error we have actually run into before.

They include checking that the title stays the same in every language, that no text contains an em dash, that the brand name is not translated, that every Turkish post has a counterpart in six languages and that legal texts are not summarised in translation. The last test is crude, but it works. If a legal page in one language is shorter than sixty per cent of the Turkish version, it has most likely been summarised.

On every push to GitHub, type checking, linting, these tests and a production build all run. If any of them breaks, the release does not go live.

Security

The content security policy is strict. No scripts, fonts or images can be loaded from an external domain. Fonts come from our own server, so the visitor's browser does not send a request to a third party when it opens the page. Google Analytics loads only if the visitor gives consent.

The working protocol screen on the home page sends the text the visitor types to a language model. This is the easiest part of the site to abuse. Someone who wants to use a paid model for free could call the endpoint directly. We block this with several layers. We check that the request comes from our own page. We use a short-lived, single-use signed ticket. There are hourly and daily limits per address and a daily cap for the whole site. There is also a limit on how many requests can go to the model at the same time.

Deployment

The code lives on GitHub. Coolify runs on the server at Hetzner. On every push to the main branch, it rebuilds the site as a Docker image and puts it live. A deployment takes about two and a half minutes.

Cloudflare sits in front. Once we moved to static generation, the HTML could be cached too, and pages are kept there for up to an hour.

Where we got it wrong

The cache was a source of speed, but it was also a source of confusion. We released a new version and Coolify reported success, but the home page still came up in its old form. The reason was simple. Cloudflare would not ask for a new copy until the one it held had expired.

The right fix is to purge the cache automatically at the end of every deployment. A release only counts as finished when it is finished where the visitor sees it, not just on the server.

Then there is accessibility. Automated scans found no errors on the pages. But the small labels on the home page sat on top of a semi-transparent photo, and the scanning tool cannot measure contrast over a photo. We measured the pixels behind the labels separately, and the ratio was around 3:1. When we faded the photo slightly and made the labels a shade darker, it rose above 5:1. Even when the tool says "no errors", you still need to look.

Why we are telling you this

It is easy to tell a client "we deliver working software". When we write openly about what we did on this site, it also becomes easier to see what that sentence rests on.

Frequently asked

01Did a corporate site really need seven languages?
Few of the partners we look for in European projects, or of our contacts abroad, read Turkish. The real cost of seven languages is not translation. It is consistency. We left that to tests, not to human attention.
02Why didn't you use an off-the-shelf site builder?
The working protocol screen on the home page sends a request to a language model, and that request has to be protected against abuse. With off-the-shelf tools it was hard to build that protection and a strict content security policy together. In our own code both are visible and auditable.
03Why does the old page sometimes appear after a release?
Pages are kept in the Cloudflare cache for up to an hour. Even after a new version goes live, the cached copy can be served until it expires. The fix is to purge the cache automatically after every deployment.

Sources

  1. 01Next.js documentation, static and dynamic rendering
  2. 02MDN, Content Security Policy
  3. 03W3C, Web Content Accessibility Guidelines 2.1
  4. 04Cloudflare documentation, purging the cache

Suggested citation

Tarık İsmet Alkan. “How to build a corporate website: notes from our own site”. Kulular Teknoloji, version 1.1, 1 October 2026. https://kulular.com.tr/en/writing/how-we-built-this-site

Kulular Teknoloji

We build this for organisations

Kulular Teknoloji is a software company based in Ankara. We build bespoke software, AI and data systems for organisations. If you have a similar need, tell us briefly about your project and we will plan a first meeting together.

  • Software architecture
  • Next.js
  • Multilingual content
  • Deployment