Personal Note & Opening
Hey guys, it has truly been a minute since I've actually truly written something on this platform. And I don't just mean coming here for a quick post and checking in with the rest of the dev community. I mean, having something substantive to share and write about.
Some time ago, I also wrote something brief about how I wanted to take a step back from posting/sharing stuff online so that I could really focus on doing the actual work. The truth is, I've always felt more comfortable doing the work and then allowing the end results of that work to speak for me. I've never really been comfortable with the idea of self-promotion and self-advertising.
After weeks of not sharing much, I'm happy to be back this weekend to discuss a recent freelance project I've been working on. As part of an inspiration, I've also decided to title my project as "Satori PageBuilder".
If you truly want to know why I chose that word "Satori", well, then I suppose you'd have to follow me here and stay tuned for my future posts when I'm finally ready to debut and share my custom collection of "Satori UI". It is still a work-in-progress, so I don't think I am fully ready to showcase the suite of UI components just yet.
In a nutshell, "Satori" is a Japanese word that also means "enlightened" or "to be enlightened". It also means "understanding". It is a word that resonated deeply with everything that I have experienced in my nearly 20 years as a freelancer.
Alright, my goal for today is not to dive into that topic, so we'll have plenty of time in the near future to discuss more about that. For now, let's just focus on my recent developer experience when it comes to building my first ever PageBuilder (and crafting a better drag-and-drop UX)
A Blogger’s Perspective
Before I even wrote a single line of code for the Satori PageBuilder, I have considered myself to be a Power User of the web. For as long as I could remember, I have been using platforms like blogger.com, myspace, WordPress (currently still), Weebly, WIX, Facebook's Notes, LinkedIn's article publisher, and so many more. I've quite literally tried everything there is on the web, trying to find not only the most comprehensive page-building tool available, but also one that would provide the kind of smooth UX that content creators like myself needed. At my height as a featured technical writer, I even paid for premium page-building plugins/tools. But I was never truly impressed by any single one of them. For the longest time, it always seemed like they all had their strengths, but there is still something missing in terms of the UX that they could provide.
Even though I could've tried to build my own PageBuilder, I never really saw the need, or felt the incentive to want to build something from scratch. Not when there are readily available tools and plugins to choose from. Sure, maybe paying $75 a year or $200+ a year for a top plugin might seem a little much at times, I just never really felt the need to do it.
At least not until my freelance client approached me about 5 weeks ago.
Why Build a PageBuilder?
When my client first approached me with this idea that he had about wanting to build a PageBuilder for their current Software-as-a-Service (SaaS), I was honestly excited (and maybe a little intimidated) by the opportunity that it presented. You see, after spending so many years working on complex Content Management Systems (CMS), I've never had a real opportunity to build a PageBuilder of my own. I mean, you know my background by now. I've used them for a long time, but I've never actually built one.
But here I am, thinking to myself, "Hey, you know what, I've built far more complex full-scale websites that can cost up to 30K - 40K. I mean, how hard would it be to build a PageBuilder...right...?"
(Yes, go ahead and laugh. I am sure every dev has had that thought at least once. The most famous last words ever 😅🤦♂️)
Spoiler: There’s a world of difference between using something and building it. We, as users, only see what the creators want us to see. The real complexity lurks beneath the surface. I mean, I've been pursuing and practising UX and front-end development for close to two decades, and I already know this.
To my client’s credit, he trusted me even after I told him I had never built one before. I think that mutual honesty and trust are what got this project off the ground; that, and a shared drive to make something better than just “good enough.”
Sidebar: For context, I was juggling this with a post-diploma course, a final school project, and working off a Raspberry Pi 4B with 8GB of RAM (yes, really). Oh, and the project started just as my ADHD brain 🧠 was hitting peak summer chaos. If not for AI tools like ChatGPT (shoutout 📣 to GPT-4.1), I honestly don’t think I’d have gotten to v1.0 in five weeks. Five years ago, this would have been a two- or three-month job, easy.
There’s something funny about building modern software on much less capable hardware. While you’re waiting for your little Pi to catch up (or unfreeze, again), you start to appreciate every edge you can get. For me, that secret weapon turned out to be AI and not just for writing code.
My Real Workflow: Human × AI
If you’d told me a year ago that I’d be pair-programming with an AI every day, I probably would’ve laughed. I was a late adopter, not because I doubted the tech, but because I was honestly worried I’d become too dependent, or maybe lose some of that “scrappy problem solver” instinct.
But it wasn’t until my Raspberry Pi kept freezing, and I found myself facing problem after problem with limited hardware, that I truly began to appreciate just how powerful an AI companion could be.
What surprised me most is that GPT-4.1 became more than just a coding assistant. It was there for my morning routine, regularly helping me plan my day, break down sprint goals, and brainstorm solutions before I even wrote a single line of code. (I’ve even got a photo of my “morning mission brief” routine with my tablet and a cup of ginger tea — that’s how real it became!)
Without a laptop or a MacBook, every morning I’d wake up early, grab my tablet, and spend 1.5 to 2 hours working through tasks with GPT-4.1. That meant I could clear up to 10–20% of my daily workload before breakfast. Add that up, and in a typical week, it’s like gaining a whole extra day of progress — all because of a smart workflow and a bit of AI magic.
AI wasn’t just there for code. It was my brainstorming partner, my sounding board, and, honestly, my best teammate & development buddy during the solo grind.
Frankly, there were days when the stress from personal challenges and my chronic anxiety disorder felt overwhelming. On those days, this “AI tool” became something more. It helped me to maintain my sanity, gave me a sense of momentum, and let me fight that much harder to maintain the highest possible quality in my work, even when everything else felt shaky and uncertain.
AI Can’t Ship v1.0: Only You Can
There was a moment, right in the middle of this journey, that really stuck with me. I had just finished my first working version of the drag-and-drop feature—the part that lets you move content around visually. Before diving into the next part of the project that also required a similar drag-and-drop experience, I decided to stop for a second and take a breath.
Up until then, I’ll be honest: I had gotten pretty good at searching for code snippets, leaning on GPT-4.1 for “how-to” solutions, and pasting fixes directly into my files. But it was in that in-between moment, code working but not understood, that it hit me:
I was operating on blind faith. The code worked, but I couldn’t really tell you why it worked. That was a humbling self-realisation.
So I did something that’s easy to skip when you’re in a rush: I stepped back. I made myself pause and dive into how the critical parts of react-dnd
actually functioned, beyond the tutorials and the AI’s step-by-step instructions.
Don’t get me wrong: GPT-4.1 was incredible at unblocking me, showing me solutions, and helping me debug when I was stuck. But the “aha” moments, the true leaps in UX, only happened when I put in the effort to understand what I was building. It was on me to bring the critical thinking, the problem-solving, and the empathy to craft a drag-and-drop experience that felt good to real users.
AI can help you get there faster, but it can’t make those calls for you. The craft still has to come from you.
A Craftsman’s Approach, Even in 2025
One of the biggest lessons I’ve learned, especially when you’re doing your best to build something truly meaningful on modest hardware, is that speed isn’t everything. The most rewarding (and honestly, the best) work happened when I permitted myself to slow down.
It’s easy to get caught up chasing velocity—cranking out features as fast as possible, especially when everything around you is moving at startup pace. But there’s something deeply satisfying about slow, thoughtful, and deliberate development. That’s when real progress happens. Sometimes, top, premium quality work requires us to take a more “surgical approach” rather than speed.
Over time, I found myself naturally settling into a set of philosophies: a kind of personal development manifesto. Here are a few that guided me through this project:
Go slow to go far. Sometimes the fastest way to finish is to resist the urge to rush.
Embrace “surgical” work. When you can’t brute-force your way through, you learn to make every move count.
Patterns are your friend, but not your prison. Find workflows and rituals that work for you, but don’t be afraid to evolve them as the project changes.
Reflect often. The best ideas come when you pause to ask, “Is this still the right [best] way?”
(Right around Sprint 3.6.*, I had discovered a workflow that was working extremely well for me, so I decided to note it down in Notion)
### Dev Workflow
With the assistance of ChatGPT 4.1 these last few days, we have
managed to developed a proven workflow when working on this Sprint.
1. Refer to Unlayer
2. Add relevant properties to `blockData`.
3. Create default values under `block-factory.tsx`
4. Pass properties from `renderContentBlock`
5. Set global state in block’s `onClick` handler.
6. Update content blocks with style generation.
7. Add `set[Content]Config` to `contentManager`.
8. Create `[Content]Styles` or `[Content]Options` section.
9. Add conditional rendering to `sidebar-panel.tsx`
10. Add serialization function to export to `cssContent`
format/structure.
11. Add the selector value to raw HTML.
12. Pass/extract `blockData` values for JSON export/import (not
styles, but options, configurations).
But here’s the real talk: there were plenty of days when I was seriously tempted to rush. Financial pressures were very real because this was my main source of income, and I genuinely needed more work. Some days, I felt desperate for that next gig. And yet, as tempting as it was to sprint toward that “finish line”, I made myself slow down and focus on doing the job right, not just fast.
That doesn’t mean my output was perfect. I still got feedback. I still had to make corrections and improvements. But here’s what stood out: I spent absolutely ZERO time on rework. In five weeks, there were hardly any moments where I had to tear things down and start over. That wasn’t luck, it was the direct result of taking my time, thinking things through, and not cutting corners.
If there’s one ethos that I kept coming back to, it’s this:
"Building something fast doesn’t make you the best. Building it once and building it well almost guarantees you’ll be faster than the next guy, who’s stuck redoing their work again and again to get it right."
The Reality of Crafting a (Not Just Using) Drag-and-Drop UX
Most of us have used drag-and-drop before. Making it feel good as a builder? That’s a whole different challenge.
Visualizing Drag-and-Drop: My “Mail Sorting Warehouse” Moment
I remember one day, sketching out a mail-sorting warehouse as a metaphor for how blocks should move around in a page builder. Every block is a package, every drop area is a sorting bin, and my job was to make sure each package landed in the right place, in the right order—every time. (I’ve included the photo of that sketch below. Trust me, sometimes the most “developer” thing you can do is grab a pen and draw it out.)
The Building Blocks: useDrag
vs. useDrop
If you’re new to building drag-and-drop UIs, there are two key concepts:
Drag sources (useDrag
):
These are the things you pick up and move (like a package on the warehouse floor).
const [{ isDragging }, dragRef] = useDrag({
type: "BLOCK_TYPE", // e.g., "CARD", "BLOCK", etc.
item: {
// Info about what's being dragged (its "passport")
id: uniqueBlockId,
type: "BLOCK_TYPE",
payload: yourBlockData,
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
// Then attach dragRef to the element you want to be draggable
return <div ref={dragRef}>{/* ...content... */}</div>;
Drop targets (useDrop
):
These are the places you can drop those things.
const [{ isOver, canDrop }, dropRef] = useDrop({
accept: ["BLOCK_TYPE"], // What kinds of items this drop area accepts
drop: (item, monitor) => {
// What happens when something is dropped here
handleDrop(item);
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
// Attach dropRef to your drop area
return <div ref={dropRef}>{/* ...drop area... */}</div>;
You don’t always have to pick just one. Sometimes, the same component needs to be both a drag source and a drop target. (Though, heads up: combining them isn’t always the most performant move—found that out the hard way.) But it’s possible, and sometimes necessary.
What’s in a Payload? (Or: Don’t Travel Without Your Documents)
Every time you pick up a block and move it, there’s an invisible “travel document” (aka: payload) that tells the system what you’re holding. It’s like crossing borders—you need the right paperwork.
Here’s a (simplified) pseudocode version of the kind of “passport” I used for each drag action:
interface BlockPayload {
// If moving between columns, reference the other column's block ID
otherColumnBlockId?: string;
// The type of block (e.g., "TEXT", "IMAGE", or a custom type)
blockType: string;
// Unique ID for the specific block being moved
designBlockId: string;
// Optional: reference to the DOM element (for focus, toolbars, etc.)
blockRef?: RefObject<HTMLElement>;
// The main content data (think: the package you’re delivering)
contentBlock: ContentBlock;
}
This payload acts as a contract between every drag-and-drop operation. If your travel document has missing information, you’re not getting through airport security.
Drop Areas vs. Drop Zones: Designing the “Where” of Drag-and-Drop
When you’re building a drag-and-drop interface, not every drop area is created equal. There’s a difference between a general drop area (like an entire canvas or a column) and a drop zone (those specific, often highlighted spots that say, “Drop right here!”).
Drop Areas:
Here’s a look at one of my early drop areas: the ColumnCell
. Notice how the whole area gets a highlight when you’re hovering a block over it. This is a “general” drop area—it covers the entire cell, and it’s great for quickly tossing blocks into columns without worrying about the exact position.
Designated DropZones:
Now, compare that with a drop zone: here, you see a custom button block drag preview hovering over a very specific, thin DesignDropZone
. These are the little hot spots between blocks that let you insert content precisely where you want it, not just “somewhere in the column.”
Behind the Scenes: The Code
General drop areas (like the canvas or a whole column) use useDrop()
hooks that accept almost any block, and typically just “append” or “insert” at the end or into an empty space.
Designated drop zones are more targeted: each one is paired with a specific spot in your content structure (e.g., “insert between block A and block B”). The code calls a specialised handleDrop()
function that knows exactly where in the array/tree to insert the new block.
Why does this matter?
Because, as a user, you want control: sometimes “just drop it anywhere” is fine, and sometimes you need pixel-perfect precision. As a builder, you have to design for both.
What I Learned:
- General drop areas are great for speed and simplicity, but they don’t give users fine-grained control.
- Designated drop zones take more work to implement, but they deliver a much better experience when it comes to reordering and inserting content “just so.”
- Visually, it helps to make drop zones bold and obvious when they’re active, so users know exactly where their block will land.
Pro tip: The best UIs often blend both: use general drop areas for new users or “quick add,” and designated drop zones for power users who care about details.
Inspiration: Why I Built My Own designTree
It was around late June, specifically during Session 3.5.4: Cross-Canvas Block Management & UX Finalization (17th–20th June, according to my now-priceless dev logs), that I hit a key insight:
I needed a reliable, centralised way to track every block’s state, position, and relationships in my PageBuilder.
The Problem
Most UI builders rely on deeply nested arrays/objects to model layouts. That works, but it can get messy, especially when you want to move blocks around, sync between canvas and columns, or just find a block quickly.
I realised what I needed was a “single source of truth” for everything in the design view. In the same way a browser uses a DOM tree to keep track of every node, I needed a design tree.
The Solution: Enter designTree
I decided to use a flat
Map<contentBlockId, DesignTreeEntry>
.-
Why?
- Fast lookups: Find any block instantly—no recursion.
- Easy updates: When a block moves, update its entry. No headaches.
- Syncable: It’s easy to sort and sync the tree with your visual content, so what you see is always what’s really there.
- Single-level: No complex nesting—every block is a top-level entry, with metadata about its current “location” (canvas vs columns).
Pseudocode: What’s in a DesignTreeEntry
?
Here’s a simplified, commented version of the interface I designed:
interface DesignTreeEntry {
// Unique ID for the content block (stays the same, no matter where you move it)
contentBlockId: string;
// The ID for the design wrapper (can change if you move it between places)
designBlockId: string;
// Ref to the outer block (used for drag, hover, focus, etc.)
designBlockRef: RefObject<HTMLDivElement>;
// Ref to the actual content (like the text or image component)
contentBlockRef: RefObject<HTMLElement>;
// What kind of content block is this? ("heading", "columns", etc.)
contentBlockType: string;
// Where is this block right now? ("canvas" or "columns")
location: "canvas" | "columns";
// If inside columns, what is the column index? (optional)
colIndex?: number;
}
Think of it as the "address book" for every block in my builder. At any time, I can open the
designTree
, find a block by its unique content ID, and know exactly where it is, what it is, and how to interact with it.
Why It Matters
- No matter how complex the UI or how many blocks you drag around, everything stays in sync and easy to debug.
- Whenever content is saved, deleted, or modified, the
designTree
ensures that the visual state matches the data model, hence no “ghost blocks,” no lost content.
Pro tip: If you’re ever struggling with managing complex layouts, try a flat map/tree approach. It might just save your sanity (and your sprint deadlines).
Lessons Learned (and Why I Log Everything Now)
Slow Is Not Lazy. It’s Precision!
In a world that prizes speed, taking things slow can feel almost rebellious. But here’s what I learned:
“Slow” isn’t lazy, and it isn’t “whatever happens, happens.”
It’s about being deliberate. Methodical. Surgical.
It's about moving with intention, purpose, not just momentum.
Some of my most valuable progress came when I forced myself to slow down, step back, and really think about what I was building, not just rushing to check an item on the list.
Slow is how you avoid avoidable mistakes, reduce rework, and create something you can be truly proud of.
Our Circumstances Don’t Define Us. Our Work Does.
It’s tempting to let our circumstances or limitations (hardware, health, life stress, whatever) become part of our professional identity. I’ll admit:
- Building a production-ready PageBuilder on a Raspberry Pi wasn’t glamorous.
- Balancing freelance stress with personal challenges was hard.
- There were days I felt like an underdog, or even an impostor.
But I also learned something important:
None of those things define who you are or how your work will be remembered.
What does matter is what you create, the value you deliver, and the quality you stand behind.
Let your results speak for you, not your gear, your LinkedIn profile, or your backstory.
Dev Logs: The Game-Changer I Didn’t See Coming
Here’s a confession:
For most of my freelance career (even during my best, highest-paid years), I never kept a dev log.
Not once. It just never seemed necessary, or I convinced myself I’d remember everything.
I was wrong.
Keeping detailed, daily dev logs on this project changed everything:
- It kept me organised and honest.
- It made handover and debugging a breeze.
- It let me track my growth, spot patterns, and catch recurring pain points before they became real problems.
(Fun fact: When I tried to upload a screenshot of my dev log, dev.to told me the image was too big. Had to split it in half. Turns out, documenting everything means you sometimes outgrow the platform’s limits. Worth it!)
Honestly? I wish I’d started doing this years ago.
(And yes, while I’m happy to share more snippets or screenshots, I’ll probably keep the full logs private for now — there’s something powerful about having a “for your eyes only” record of the real process.)
Tip: Even if you’re not a “journaling” type, try it for one project. You might be surprised by how much clarity and momentum it brings.
AI & The Truth About “Vibe Coding” vs. Real Development
There’s a lot of hype, memes, and honestly, a fair bit of panic lately about the rise of “vibe coding”. There is this idea that you can just vibe with an AI and crank out production apps without understanding a thing. Some folks claim that developers are becoming obsolete, or that tools like Lovable (or the latest “no-code” darling) can do it all for you.
Let’s get real for a second.
As someone who’s worked through every line of my own PageBuilder—on a Raspberry Pi, no less—I can tell you one thing that is true:
AI is an incredible partner. It accelerates your workflow, offers new perspectives, and lets you clear roadblocks faster than ever.
But AI is not a replacement for craft, judgment, or actual experience.
Vibe Coding vs. AI-Accelerated Development: What’s the Difference?
The difference is human guardrails.
Vibe Coding:
This is when you blindly trust the AI to generate code, solutions, or even whole apps, without really understanding or questioning what’s happening. It can feel like magic…until you hit a wall, or something breaks in production, and you have no clue why.
- It’s tempting, especially when you’re tired or in a hurry.
- I’ve fallen into this myself; earlier in this very project, when I realised I didn’t truly understand what I was building until I hit pause and dug in.
AI-Accelerated Development:
This is about collaboration, using AI as a partner, but bringing your own experience, curiosity, and care to the process.
- You sanity-check every step.
- You ask “why,” not just “how.”
- You add empathy, judgment, and the willingness to slow down, refactor, or rethink a feature when it matters.
In my experience, even with all the power of GPT-4.1 and similar tools, building something real, something you’d actually hand over to a client or deploy in production, remains a shared effort. At best, it’s a 50/50 split:
AI brings speed and breadth, but you bring depth, vision, and the responsibility to make it right.
Bottom line:
You can’t vibe your way to excellence. Tools are just tools. It’s the human in the loop, the builder, the craftsperson, the one who cares, that makes the difference between shipping an app and shipping something that lasts.
What Makes a True UX Artisan in 2025?
If there’s one thing I’ve learned: over all these years, in all these projects, it’s that the best builders are not just coders, or even designers. They’re artisans. And the defining trait of a true UX artisan? Relentlessly thinking like the end user.
It’s a bit like a carpenter shaping a bespoke bench. He doesn’t just carve and assemble the wood—he sits on it, tests how it feels, then carves and sands some more. He keeps returning, tweaking, and reworking, not stopping until that bench isn’t just beautiful, but comfortable, usable, and right for whoever sits down.
Every meaningful UX I’ve helped to build started with that same quiet habit:
I would put myself in the user’s shoes before writing a single line of code, and again after every new feature.
A UX artisan isn’t just a designer. We are the builders who take a vision or an idea and turn it into something better, layer by layer, always testing, always refining.
When my freelance client came to me and said, “I want something like Unlayer,” I didn’t just hear “copy this.”
I heard, “Let’s craft the ideal PageBuilder, one that users will truly love.”
So I drew on my past as a creator and a power user, asking,
"What would make this the most comfortable, powerful, and joyful tool it could be?”
And then, like that careful carpenter, I built, tested, refined, and rebuilt—until it didn’t just work, it felt right.
A UX artisan isn’t satisfied with “it works.”
We keep shaping, testing, and caring, until it feels like it was made just for you.
Final Thoughts & An Invitation
For Potential Clients & Collaborators
My next big goal, a proper portfolio showcase (v2.0), is still on the horizon. I’m in no rush. When it’s ready, it’ll be something I can be truly proud of, not just something to tick a box.
But this article?
This was never meant as a detailed technical manual. It’s an inside look at how I work, think, and what I care about: my approach, my philosophy, my struggles, and what I’ve learned along the way.
If you’re reading this as a founder, business owner, or potential collaborator:
- I won’t pretend I can work for free right now. Life’s real, and I need the income.
- But I’m not only here for a paycheck. Some of my most meaningful collaborations have come from clients who paid less, but valued trust, transparency, and a shared mission.
- Yes, I hope to land those S$10K–S$20K projects one day soon. But I’m just as open to working with those who have tight budgets, if we’re aligned in vision and values.
So if you’re looking for someone who cares as much about the journey as the result, someone who wants to build things that matter, let’s connect.
You can always drop me an email if you have a project idea that you want to turn into reality, and we can discuss how to proceed.
Reach me at d2d.weizhi@gmail.com
To My Fellow Developers, Makers, and Builders
I want to leave you with this:
You don’t need to be loud to shine.
You don’t have to self-promote, boast, or go viral to prove your value. Sometimes, the most powerful thing you can do is focus on your craft, care deeply about your work, and let the results speak for themselves.
Resilience, grit, and passion matter, especially when things are hard or when you’re building on a shoestring budget (or a Raspberry Pi!).
If you ever feel like you’re struggling to stand out, or worried you’re not being noticed, remember:
Your best work will find its audience.
Focus on creating something real and meaningful.
Let your craft speak for you. That’s how I’ve always tried to do it, and eventually, it'll speak louder than you ever could.
Top comments (0)