All articles
Iz praksecmsclient portalprocess

Building a custom website your client can edit without breaking it

Lea Gobo's custom website went live on 19 October 2026 with a CMS built into the site, and the problems that mattered were about what the editor could find, see and trust.

A client with no technical background can edit a custom website safely if the editor lives inside the site and offers only changes the designer has already checked. That is how I built leagobo.com for Lea Gobo, a feng shui consultant in Croatia, and it went live on 19 October 2026. What hurt most was not the technology. It was the gap between how I imagine a CMS being used and how an editor uses one.

An embedded CMS is a content management system that runs inside the same application as the website, so the site and its editor share one codebase, one deployment and one database. Lea's runs on Payload 3 inside a Next.js 16 app. Below are the decisions I would repeat and the mistakes I would not.

The situation: a WordPress site, a new brand and a client who wanted to edit everything

Lea's old website was WordPress with Elementor and Polylang: 23 pages and 28 articles in two languages, as I recorded it on 22 September 2026. At the kickoff on 18 September she asked for a site she could change herself, from text and images to articles and her list of recommendations. I'm Jurica Stublić, an independent web designer and developer in Zagreb, Croatia, and I built it as custom work rather than another WordPress theme.

WordPress is the right answer when the editor already knows it, or when you need a plugin on day one that has no custom equivalent worth building. Three things pointed the other way for Lea. Her brand system rules out shadows, gradients and card grids. A design made of fields per section cannot be dragged out of shape. And core, theme and plugin updates should not be her job.

Once the CMS existed, I compared it with WordPress on 36 points an editor touches, such as pasting from Word, trash, revisions and the link picker. I did this on paper, from the code and the documentation. I was ahead on several: both languages live in one document, and links are validated when you save.

I was behind where it hurts. With no trash, a deleted article was gone for good. The editor could not see revision history. A new hero photo went live unseen because that section had no draft. I built the ten most important gaps on 25 September and checked them in a browser.

What I got wrong at the kickoff: a promise is scope

I promised a CMS in a meeting before I had decided whether it was in scope. On 18 September I told Lea she would be able to change texts, images, articles and recommendations herself. On 22 September I logged it as phase two, after launch, because the first delivery was due on 5 October and she changes content rarely. On 24 September I reversed that and put the CMS into the first delivery.

The log entry was right about the effort and wrong about what had already been agreed. What I say in a call is scope, and what I write down afterwards is only a revision. The better move was to say “phase two, separate proposal” in the room, or to put the CMS into the proposal as its own line.

How the changes arrived: numbered requests in a portal

Every round of changes from Lea came through the client portal as a numbered list: ten points on 7 October, six on 8 October. I kept each list as a document in the repository, so every point had a number I could cite. That is how I saw that two of her points shared one cause, which I come back to below.

The portal also carried the one thing that stalled. Access to her domain was the only item on the materials checklist that had not arrived after a week, because a client cannot simply “send DNS access”. The instructions for that request were not concrete enough, and a request for an outside tool needs them step by step.

How can a client change a custom website without breaking its design?

She can change everything that is content and nothing that is structure, unless I unlock it. Each page is a set of fields per section, so an editor fills in a heading, a paragraph and a photo, and the layout stays as designed. The article editor offers only what the site can display: paragraphs, two heading levels, bold, italic, links, lists, quotes and images.

Layout is the one exception, and it is deliberate. Each section with text has four positions for the text block: left, centre, right or hidden. The default is the current design, so nothing changes until someone changes it. Long text always stays left-aligned, because centred paragraphs read badly.

This is a variants model, not a page builder. She picks among layouts I have composed and checked on desktop and phone. The choice is locked until the design is approved and the project is handed over, and I unlock it with one checkbox. The server rejects a change while it is locked, so hiding the button is not the only protection.

The lock taught me two things about Payload 3.90. A field-level permission drops the locked field when an editor restores an older version, so the lock is enforced in a hook. And restoring writes straight to the live record, which could take an article offline, so it now always lands in a draft.

The hero video follows the same rule. It has a separate version for phones, and Lea replaces it herself in the CMS; the clip on the live site today is a temporary one. The code decides everything technical: the video pauses off screen, autoplay stays muted, and visitors with reduced motion get a still frame.

The upload that looked fixed: what a real editor found

Two days after I declared the image upload fixed, the client showed me that I had fixed the wrong thing. On 12 October Lea told me the CMS would not accept her images. It did accept them, but the admin showed an empty file icon instead of a thumbnail. I had built the thumbnail address from the file name, while Payload 3.90 stores browser uploads in a folder named by a key.

I fixed the address and tested on production with a temporary editor account: a 7.3 MB phone photo, a PNG, a WebP, file names with accents. Every thumbnail showed. On 14 October she wrote that when she tried to change an image, all she saw was an icon.

The originals in storage had 0 bytes. The browser uploads the full image to Vercel Blob, then Payload re-processes it with sharp and writes the result back. For browser uploads that result sits in a temporary file, while the buffer the Blob adapter reads stays empty, so the last step overwrote each original with an empty file. Thumbnails were fine because they come from the sharp step. I had tested what the bug looked like, not what it did.

A hook now fills the buffer from the temporary file and refuses a genuinely empty upload. Twelve of her images were affected, the first on 6 October. I recovered five from orphaned files in storage, and seven were lost, so she uploaded them again. None was on a published page.

One trap followed. The Vercel Blob CDN caches a file for a year at its path and ignores query strings, so a repaired file has to go to a new path. For the same reason I switched off the editor's built-in crop, which wrote over the original.

What she saw was not what visitors saw, and what she could not find

Most of the other problems were about trust. On 8 October Lea told me the phone and desktop versions of her site did not match. On her computer she was signed in to the CMS, so the site showed her unpublished drafts, while her phone showed the published pages. Four pages held drafts she had never published. The draft view now carries a banner that cannot be dismissed.

The second kind was text she could not edit. On 7 October she reported that her Contact edits were not showing. The cause was not caching: the button label she had changed was saved correctly and never displayed, because the page drew that label from code.

On 12 and 14 October she asked why she could not change texts that I had changed. So I walked every public page in both languages, extracted each visible string and label, and compared them with the database. The table ran to roughly 150 rows, and 37 pieces of text existed only in code. They became 63 fields on a settings page called Website texts, where an empty field means the default text. A card on the admin start page answers “Where do I edit what?”.

Booking and newsletter: forms need an owner too

A form is content in disguise: someone owns the hours it offers and the list it fills. The booking for Lea's intro call, which lasts 15 minutes, is my own code, not a third-party widget. Its hours come from a settings page: a weekly schedule, closed date ranges with a private note, how many days ahead to offer and the minimum notice.

There is one window per day on purpose. A lunch break goes into her Google Calendar, and the booking page reads that calendar and never offers busy time. The first version had a trap: changing the hours in the CMS changed the footer text but not the slots, which came from code. A field that changes a label and not the behaviour is worse than no field, so on 2 October I moved the rules into the CMS.

The newsletter form had a worse problem. It returned success and stored nothing, because signups went to a Resend audience whose ID had never been configured. I found it on 30 September, before launch. Signups now go into a list in the CMS, where Lea can mark an unsubscribe, delete an address on request and download everything as a CSV. The addresses sit in the site's own database in Frankfurt, and the privacy page says so.

What I would do differently

I would change four things. First, I would put the CMS into the proposal, or say “phase two, separate proposal” in the call. Second, I would test every editor feature the way the editor uses it: upload, close, reopen, edit, place on a page, replace. I uploaded a photo and looked at the thumbnail, and that is where I stopped.

Third, I would treat every visible string as content from day one, instead of finding 37 that existed only in code. Fourth, I would put a real editor in front of the CMS earlier. Lea was editing on her own from 6 October, and nearly every serious problem came from her use of it.

I would not build a CMS at all for a client who changes a page twice a year. Files in a repository do that job with less to maintain, and that was my first plan for this site. I also have no results to report: the site went live on 19 October 2026, and it is too early for traffic or enquiry figures.

If your own team will edit a custom website, the questions are the same in London or Copenhagen: who edits, what can they break, and where do they find things. The websites service page sets out scope and investment, and the FESCA write-up shows how I run a client portal.

About the author

Jurica Stublić

Jurica Stublić

Web designer and developer · 4 years · 50+ projects

I build custom web and mobile apps, web shops and websites, and I stay on the project after launch. Alongside client work I develop my own product: ai.webica.hr.

Read next

A slot is open right now, get in touch about working together

If you have an idea worth doing properly, the first step is a 30 minute conversation.

No proposal and no pressure. You tell me what you're building, I ask the questions nobody has asked you yet, and by the end you know whether it's feasible, roughly what it costs and what the next step is. Whether or not we end up working together.

Jurica Stublić u radu na projektu