As a content writer and UX strategist without a programming background, when I started thinking about my own website, my options were naturally limited to a few familiar tools: WordPress, Substack, Ghost, and Webflow. Each comes with its own philosophy, its own constraints, and its own ceiling — the point you reach before you hit either a wall of limitations or a pricing tier that starts to sting.
So I chose a different route: I decided to build my own web application from scratch.
And I did — without being a professional developer. This piece is about how that went, what broke along the way, and why, despite it all, I still think it was the right decision.
Why not a ready-made platform?
My background in UX comes, in part, from long and often frustrating encounters with CMS systems. At the same time, I work with content — I know how it should look, how it should read, and how it should guide a reader. Sooner or later, any off-the-shelf platform would have forced a compromise between what I wanted to show and what the platform would allow.
I wanted full control over typography. I wanted my own tagging system — not a plugin, not an external service. I'm trained as a translator, so I wanted the site to be bilingual from the ground up, not as a layer added on top or something handled on the fly. And I wanted the entire stack to be mine: code, database, hosting, and domain.
This wasn't a reaction against WordPress. For some, it's an excellent tool. But if you spend your days designing flows and writing product documentation, building your own tool becomes a form of practice. You learn differently when there isn't a plugin to reach for.
How I built it with AI
NEONEON was created with substantial support from Claude, Anthropic's language model. I'm explicit about this because the way we work with AI deserves attention, not omission.
I didn't use AI as a code generator to copy and paste from without understanding. I used it more like a patient, highly capable technical collaborator — someone who understands architecture, anticipates consequences, and can explain why something works the way it does. At the same time, it's a collaborator that occasionally gets things wrong on the first pass and requires careful oversight.
In practice, Claude contributed to designing the application architecture, writing components, configuring authentication, debugging production issues, and implementing successive iterations. The work happened through Claude Cowork, a desktop tool that gives the model direct access to local project files — no copying code into chat, no loss of context between sessions.
This approach has a name: AI-assisted development. And it's increasingly a viable path for people with domain expertise — designers, strategists, editors — who want to build their own tools without years of engineering experience.
Stack: what and why
Next.js 16 with the App Router — a React framework for server-side rendering. The choice was partly technical, partly philosophical: I wanted a fast, indexable site, not a single-page app that loads for three seconds and shows a spinner.
Supabase for the database and authentication. Cloud-hosted PostgreSQL with a UI that doesn't require SQL for everyday use — ideal for a project maintained by a linguist. It stores articles, comments, contact messages, and tags.
TipTap as the content editor in the admin panel. A WYSIWYG editor with support for headings, lists, images, and embedded YouTube videos. What you see in the editor closely matches what appears on the site.
Vercel for hosting provides automatic deployment on every Git push to the main branch, meaning a bug fix reaches production within minutes.
Next-intl supports bilingual content. Polish content is located under /pl/..., English under /en/..., with language detected automatically via the browser's Accept-Language header. The interface is translated via JSON files, while article content is stored separately in the database.
Tailwind CSS is used with custom colour tokens: paper, ink, muted, border. The site uses Cormorant Garamond as the serif typeface and DM Sans for the UI, replacing the default fonts from create-next-app.
Framer Motion provides subtle entry animations. The site doesn't try to impress; it simply feels alive.
Design decisions that mattered
Typography as a foundation
The site is intentionally text-first, so typography wasn't decorative, it was fundamental. Cormorant Garamond brings a literary tone, while DM Sans provides a neutral, readable counterpoint for the interface.
Details matter: heading line-height (H1—H3) is set to 1.2, appropriate for large serif type. The default 1.8 used for body text was 30—40% too loose for headings, especially on mobile where multi-line headings became difficult to read. Top margins before H2 and H3 were halved, because the rhythm of text on screen isn't the same as in print.
Tags stored in the database, not config files
The initial version translated tags via messages/pl.json and messages/en.json. That meant every new tag required a code change and deployment, which wasn't sustainable.
The solution: a name_en column in the tags table in Supabase. The admin panel now has two fields per tag — Polish and English. TagBadge reads the current locale and displays the appropriate label, with a fallback to Polish if English is missing. No deployment is needed.
Instagram embeds and client-side navigation
Instagram embeds worked on full page loads but disappeared during client-side navigation, because Next.js doesn't reload the page and the Instagram script runs only once.
The solution wasn't obvious: TiptapRenderer remained a Server Component (to avoid hydration errors), while a separate EmbedActivator — a Client Component returning null — recreates <script> nodes in useEffect and calls window.instgrm?.Embeds.process() if the library is already cached.
Additionally, there was a 43px gap under embeds caused by a newline character in TipTap generating a line box at line-height: 1.8. A CSS :has(iframe) rule targets only blocks containing embeds, leaving regular code blocks untouched.
Favicon without configuration
I removed the default favicon.ico from create-next-app and added two PNG files (180×180) in the app/ directory. Next.js App Router automatically generates the appropriate <link rel="icon"> and <link rel="apple-touch-icon"> tags, without any configuration required.
Admin panel without localisation
The login page and admin panel live under /login and /admin, without locale prefixes. This is intentional: there's a single user (me), and adding /pl/login and /en/login would introduce complexity without adding value.
Mistakes that taught me something
A few things went wrong along the way. They're worth listing, as mistakes tend to be more instructive than successes.
Conflict between middleware.ts and proxy.ts. Next.js 16 replaced middleware.ts with proxy.ts. The project already used proxy.ts for language detection, and I mistakenly added middleware.ts again, causing a server error. Lesson: check existing conventions before adding new files.
Sender address in Resend. Resend allows sending from onboarding@resend.dev only to the account's registered email. Sending contact form emails externally required a verified domain. Lesson: email service limitations are often buried in the fine print.
Async Server Component inside a Client Component. TagBadge was an async Server Component using await getLocale(). After ArticleCard was marked with 'use client', Next.js threw an error. TagBadge had to be rewritten as a Client Component using useLocale(). Lesson: the boundary between Server and Client Components is strict and needs to be managed deliberately.
Nested links. ArticleCard wrapped the entire card in a <Link>, while TagBadge inside also rendered a <Link>. HTML doesn't allow <a> inside <a>, which caused hydration errors. The fix: move tags outside the main link. Lesson: clickable cards and clickable elements inside them are separate UI patterns with different structural requirements.
What this means
NEONEON is a practical example of something that's increasingly true: building a fully custom web application is within reach for people who aren't professional developers — provided they have clarity about what they want and the patience to iterate.
Every improvement came from observation: what doesn't work, what feels off, what can be done better. That's UX work, regardless of whether you're doing it on someone else's product or your own.
The site isn't finished. No good site ever is.
