Jakub Kubík
Email me

All notes

note · websites · ~9 min read

How ozjuliana.sk was built: a redesign without WordPress, with its own news admin

Why a small non-profit’s website runs without WordPress or a database, how its news admin with secure phone login works, and how the redesign was verified.

Published
topic
Websites for small organisations
status
finished

The hosting admin has a “set up DNS” button. Click it and it quietly rewrites the records a browser uses to find the domain’s server: they suddenly point straight at the hosting server, bypassing the CDN (the network of servers that delivers and protects the site). The site keeps running, so nobody notices. That’s exactly what happened to ozjuliana.sk, and it wasn’t a person who caught it but a script that only reads the live site after every deployment. A small non-profit’s website has more safety nets like that than you’d expect, and this note is about why they’re there.

Občianske združenie Juliána is a Slovak non-profit that collects clothes, shoes, toys and household goods all year round and delivers them to people in need. I’ve been building and running its website, ozjuliana.sk, as a volunteer since April 2026. The brief sounds simple: explain what the non-profit does and how people can help, and let someone with no technical background post news about a collection, photos included, from a phone. The numbers at a glance are in the case study; this is the longer story.

Why not WordPress, and why JSON files with no database

The site runs on cheap shared hosting behind a CDN. The server has PHP and nothing else: no SSH (remote command line), no Git, and deployment goes over FTPS (file transfer over an encrypted connection). WordPress would run there, but it would bring regular core and plugin updates, a database and endless login attempts at a well-known address. Meanwhile the non-profit adds an article with photos once every few weeks. A need like that isn’t worth maintaining a whole content management system for.

The result is custom PHP with no framework and no Composer. The public pages are templates that work without JavaScript too. Three small scripts only add animations, the consent banner, an intro screen with the logo and a photo viewer.

There’s no database for the articles either. Each article is a single JSON file (a text format for structured data), its photos sit next to it in a folder, and the GD library resizes them on upload. SQLite would have worked as well, but with a few dozen articles it wouldn’t speed anything up, and files have two practical advantages: all the site’s content backs up by copying two folders, and in an emergency an article can be fixed over FTP in a plain text editor.

Writes always go to a temporary file that’s only then renamed into place, so an interrupted save never leaves a half-written article behind.

{
    "slug": "clothing-drive-2026",
    "title": "Clothing drive 2026",
    "status": "published",
    "published_at": "2026-09-18T12:00:00+02:00",
    "text": "Thank you to everyone who contributed.\n\nThe drive continues every Friday.",
    "cover": { "big": "uploads/…-big.jpg", "thumb": "uploads/…-thumb.jpg", "alt": "Sorting donated clothes" },
    "gallery": []
}

The article is made up; the real files also record when they were created and last edited. Every evening a backup pulls the content from the server: it only reads, over FTPS, and keeps 30 daily snapshots. If anything goes wrong, an email arrives.

Two-factor login in plain PHP

The article admin is the only place where the site’s content can be changed, so it requires two-factor login: a password and a six-digit code from Google Authenticator. Without the second factor, an account can’t even be created.

TOTP, the time-based one-time code (RFC 6238), is a surprisingly small algorithm: an HMAC (a cryptographic fingerprint) of the secret key and the number of the current 30-second window, from which six digits are picked out. On hosting without Composer, it’s simpler to write it in plain PHP from standard functions than to upload a library and its dependencies by hand. The implementation is checked against the test values straight from the RFC, and a dependency-free test suite runs before every deployment.

Around that sits the usual kit: passwords stored as bcrypt hashes, an anti-forgery token (CSRF) in every form, sessions with a limited lifetime, a counter of failed attempts, and a first account that can’t be created without access to the server. The specific settings are deliberately left out here; they’d help an attacker more than a reader.

An admin built for a phone and a non-technical editor

Content is managed almost entirely from a phone, so the admin is designed from a width of 320 px up: bigger buttons and checkboxes, touch targets of at least 44 px, and input fields with text of at least 16 px so that an iPhone doesn’t zoom in on tap. It works without JavaScript, so not even an older browser can break it.

The form has only what’s needed: headline, date, text, cover photo, gallery, photo captions and a switch between draft and published. The text is plain text, and a blank line starts a new paragraph. No editor full of buttons slipping hidden HTML into the article. Photos from the phone are rotated according to the EXIF data the camera writes into them, resized and saved without metadata. An audit checked all 84 photos on the live site, and not one of them has GPS coordinates.

One decision came straight from who runs the site. A browser in which someone has already logged in successfully gets its own attempt counter. So a stranger’s login attempts can lock out unknown devices, but not the phone the content is actually posted from.

The admin also comes with a short guide in Slovak. Its most useful tip isn’t technical: an article’s first sentence should say when, where, what and how much, because that sentence becomes the snippet on cards and the description for search engines.

Section 109(8) of the Slovak Electronic Communications Act requires consent before storing data on a visitor’s device, unless it’s strictly necessary for the service to work. The site is built so that, without consent, it needs nothing of the kind: the public pages set no cookies and start no session, and fonts load from the site’s own server, not from Google Fonts.

Google Analytics loads only after a click on “Accept”. “Accept” and “Reject” are equal buttons on the first layer, the choice can be changed at any time through a link in the footer, and withdrawing consent deletes the analytics cookies. Every choice is logged on the server as a record of consent: a random identifier, the time, the version and the choice, with no IP address. The exception is the embedded Google map on the contact page, which loads straight away; the privacy page says so openly.

A redesign you can measure

The redesign came in September, under the working title “Calm Elegance”. It was preceded by an audit of the old version, and the results were sobering: 29 different font sizes, input borders with a 1.24:1 contrast ratio, four findings from axe, the automated accessibility checker, and navigation that overflowed by a few pixels at some phone widths.

The work had two phases. The first was a refactor with a single rule: no visible change. A shared template, a single contact form module, colours and sizes as variables.

After every commit, the new version was compared with the old one: the HTML output of 41 scenarios byte for byte on PHP 8.4 and 8.5, and screenshots pixel for pixel. At the end of the phase the outputs were identical, apart from one fix approved in advance, and 450 screenshots (15 pages × 10 widths × 3 modes: normal, without JavaScript and with reduced motion) differed by 0 pixels. The whole suite ran without admin rights: PHP in WebAssembly and headless Chrome driven by Playwright.

Only then did the redesign itself begin. Every intended deviation was written down up front, and the comparison normalised it; everything else had to stay identical. That way, a page that wasn’t meant to change can’t be changed by accident.

The result: a type scale of 12 steps (the pre-redesign audit counted 29 values), form borders with contrast above 3:1, zero axe violations (WCAG 2 accessibility rules, levels A and AA) across 30 runs on 10 pages, and no horizontal overflow anywhere from 320 to 1440 px, checked pixel by pixel in the riskiest ranges. On mobile, the section about the non-profit’s mission shrank from about 1,000 to 450 px, and the logo in the phone header grew from 20 × 20 to 36 × 32 px, so it’s finally legible.

What broke, and how it was fixed

The audit before the redesign found bugs in the original admin that ordinary clicking around never reveals. An apostrophe in a headline broke the delete confirmation dialog, so the article was deleted without asking. An invalid UTF-8 character in an account name led to an empty accounts file being written. And on PHP 8.5, a deprecated image function printed a warning where the redirect after saving an article should have been. All three bugs were fixed and verified before launch.

Right after launch came a security audit of the live site in two rounds, the second with two independent adversarial code reviews. It produced 38 findings, but neither round found a way into the admin without logging in. The most instructive finding: of 16 simultaneous attempts with a wrong password, not one was blocked, because an attempt was only counted after the password check. Today the attempt is counted under a lock before the check. The code fixes shipped in five pull requests the same day.

The last bug, the one with the button from the start, wasn’t in the code. After a single click, the domain pointed straight at the hosting server and the site kept working, just without the CDN and its protection. What gave it away was the script that checks the live site after every deployment, by reading only: its certificate check failed. The records were put back the very next morning. Without that script, the site could have run like that for months without anyone noticing.

What I take away

  • A small non-profit doesn’t need a content management system; it needs one good form. With a few dozen articles, files can stand in for a database, as long as writes are atomic and backups automatic.
  • Designing an admin starts with the question of who runs it, and on what device. Security follows from that too, for example a trusted browser with its own attempt counter.
  • A refactor without measurement rests on an impression. Byte and pixel parity turns “I don’t think anything changed” into proof.
  • An audit pays off twice: on the old code before the changes, and on the live site after launch. Each one found something different.
  • A deployment check that only reads the live site catches mistakes outside the code too.

The “set up DNS” button is still there in the hosting admin. So is the script that will catch it at the next deployment.