I recently overhauled this blog service from the ground up. "Overhauled" sounds impressive, but let's be honest—most of it was me staring at my monitor at 3 AM muttering "why isn't this working?"
Still, I'm pretty happy with the results. This isn't just a changelog; it's a record of the technical decisions I made while walking the tightrope between system stability and user experience.
1. Log Everything, Block Nothing (Fire-and-Forget)
As a service grows, tracking "who did what and when" becomes essential. The problem? If logging slows down your actual service, you've got your priorities backwards.
The Problem
Synchronously inserting a log entry every time someone posts a comment? Users get frustrated by the lag, and you get buried in complaints.
The Solution: Async Fire-and-Forget Pattern
// app/lib/activityLogger.js
export async function writeLog({ category, action, details }) {
try {
await sql`
INSERT INTO activity_logs (category, action, details, created_at)
VALUES (${category}, ${action}, ${JSON.stringify(details)}, NOW())
`;
} catch (error) {
// Log failure = service outage? That's a comedy sketch.
console.error('[ActivityLogger] Failed:', error.message);
}
}
The principle is simple: if logging fails, the user's action should still succeed. Wrapped everything in try-catch so even if the log database explodes, the service keeps running. Having your service crash because of logging would be embarrassing.
2. Highlight Comments: I Wanted My Own Medium
You know that feature in Medium or e-book apps where you drag text and instantly leave a note? I wanted that. Leaving a comment at the bottom of the page saying "loved paragraph 3" just feels... disconnected from the context.
The Technical Challenge: DOM Is Messier Than You Think
I thought I'd just store "from character X to character Y." But HTML isn't that simple. With nested <p>, <strong>, and <em>tags, calculating pure text-based positions gets surprisingly tricky.
The Solution: TreeWalker for Text Nodes Only
// hooks/use-highlight-renderer.js
function findNodeAtOffset(container, targetOffset) {
const walker = document.createTreeWalker(
container,
NodeFilter.SHOW_TEXT,
null,
false
);
let currentOffset = 0;
let node;
while ((node = walker.nextNode())) {
const nodeLength = node.textContent.length;
if (currentOffset + nodeLength >= targetOffset) {
return { node, offset: targetOffset - currentOffset };
}
currentOffset += nodeLength;
}
return null;
}
Using the TreeWalker API to iterate through text nodes only means accurate positioning regardless of DOM complexity. Highlights stay in place even after React re-renders. Getting this right took more coffee than I'd like to admit.
3. Version Control: A Safety Net for Writers
Ever accidentally selected all and deleted your entire post? (I have.) Ever regretted changing a sentence thinking "the old one was better"? (Definitely have.)
Simple auto-save wasn't enough. If auto-save automatically saves your mistakes, what's the point?
The Solution: Two Tracks of Saving
// app/lib/actions/drafts.js
if (isManualSave) {
// Manual save = create immutable snapshot
await sql`
INSERT INTO post_drafts (post_id, content, version_number, is_manual_save)
VALUES (${postId}, ${content}, ${nextVersion}, TRUE)
`;
} else {
// Auto-save = overwrite current state
await sql`
UPDATE post_drafts
SET c ${content}, updated_at = NOW()
WHERE id = ${currentDraftId}
`;
}
Auto-save: Quietly overwrites the current state. Saves database space.
Manual save: Creates a new version. Time machine functionality.
This way, the database doesn't bloat from endless auto-saves, but you can still travel back to any important checkpoint. Consider it a mental health feature for writers.
4. The Lies Your View Count Tells
Does "100 views" really mean 100 people read your article? That includes people who bounced after one second. Kind of deflating, right?
Metrics I Introduced
Scroll Depth: Did they reach 25%, 50%, or 100% of the article?
Time on Page: How long did they actually stay?
Completion Rate: What percentage read to the end?
Now I can identify articles with "high views but rock-bottom completion rates." Is the intro boring? Was the title clickbait? The data tells me. (It also objectively proves when my writing isn't engaging, but... that's just honest feedback, I guess.)
Retrospective: Lessons Learned
1. Practical Separation Over Perfect Abstraction
I initially tried to abstract all logging into one unified system. But auth logs, error logs, and behavior logs serve different purposes. Separating them into logAuth, logSystem, etc. made maintenance way easier than forcing them together.
2. Clear Boundaries Between Frontend and Backend
For the highlight feature, "coordinate calculation" belongs to the frontend; "data persistence" belongs to the backend. Using Next.js Server Actions to clearly divide responsibilities cut complexity significantly.
3. UX Lives in the Details
A well-designed database schema matters, but so does the little tooltip animation that pops up when you finish dragging, or that friendly error message when something goes wrong. Those details define how your service feels.
Wrapping Up
Technology exists to serve users. The goal of this update wasn't flashy features—it was data integrity and thoughtful user experience.
The late-night debugging sessions will continue. That's just the process of building something more robust and more human.

Leave a comment