· 2 min read
A marketing site where every section is editable
by Furkan Durna · Project: HARDWEY Web
#react #express #sqlite
HARDWEY is an app for buying shares in artists' brands. Its marketing site changes often: FAQ answers, founder bios, partners, legal pages. The goal was that none of those changes should need a developer or a redeploy.
One table, one request
The content model is deliberately simple. Each section (hero, intro, FAQ, founders, partners, the legal pages and so on) is one row in a SQLite table: a unique section name, the content as JSON, and an update timestamp maintained by a database trigger. Saves are upserts, so creating and updating a section are the same operation.
The site fetches everything with a single request when it loads. A small React hook caches the response in memory for five minutes and gives each component its own section. When an admin saves, the cache is invalidated and every component using the hook refetches.
The home page loads its hero immediately and lazy-loads the rest, so one content request and one small first bundle get the page on screen.
Content that outlives its shape
Sections change shape over time. A field becomes a list, a single image becomes several. Instead of migrating stored data, each section has a normalizer on the client that coerces whatever is stored into the current shape:
- It moves legacy fields into their new structure.
- It generates missing IDs.
- It pads lists that the layout expects to have a fixed number of entries.
Every component also has default content, so a missing or empty section degrades to placeholder content instead of a broken page.
The admin app
The admin app ships in the same Vite bundle but is lazy-loaded behind its own route, so visitors never download it. Each section has a dedicated editor, and an "Advanced JSON" mode allows editing the raw content for anything the forms don't cover. Invalid JSON is rejected before it is saved. If a section doesn't exist yet, the editor starts from its template, so new sections can be created from the admin app.
Images are uploaded through the API with a type check and a size limit (5 MB by default). They're saved under random file names and served with long-lived cache headers, since a name never gets reused.
Keeping the editor locked
Admin access uses short-lived access tokens and longer-lived refresh tokens, both in httpOnly cookies with SameSite=strict, so page scripts can't read them and other sites can't send them. Every write also needs a CSRF token fetched from the API and sent in a header. Passwords are hashed with bcrypt, logins are rate-limited, and the initial admin account can't be created without an explicit password in the environment.
Hosting quirks
The server is written to run behind Passenger on shared hosting as well as on its own. Behind that kind of proxy, requests can arrive with or without the /api prefix, so the server mounts the same routes under both instead of depending on the proxy configuration. It only starts its own listener when it isn't running under Passenger. SQLite runs in write-ahead log mode, which lets the site keep reading content while an admin is saving.
The source is on GitHub.