The Architect's Dilemma: Orchestrating a Federated Blog Engine with AI
The Architect's Dilemma: Orchestrating a Federated Blog Engine with AI
The "I Made This (But Did I?)" Complex
I have a confession. Last week, I deployed a fully functional application: a federated blog engine capable of subscribing to external feeds, rendering custom themes, and running smoothly on a device with less processing power than my smart fridge. The code is clean, the PWA manifest is compliant, and the SEO tags are correct.
And I felt absolutely hollow.
I didn't write the for loops. I didn't wrestle with CSS grid alignment until 3 AM. I architected the system, but I treated my IDE like a voice-activated assistant, prompting an LLM to generate implementation details while I sipped coffee. This is the new reality of software engineering. We are transitioning from bricklayers to site foremen, and the loss of "touch" with the raw materials induces a specific kind of vertigo.
Today, I want to dissect that feeling by walking through the construction of Z-Engine (a nod to a similar project called ZLOG). We'll build a lightweight, federated blogging system designed to run on a Raspberry Pi 3.
The System Design: Federation on a Potato
The goal is simple but ambitious: a blog engine that hosts my content but can also "subscribe" to other blogs, pulling their content into my feed as if it were native. All on low-end hardware.
Here is the architecture:
- Core: Node.js with Fastify (lower overhead than Express).
- Database: SQLite (file-based, perfect for Pis).
- Caching: In-memory LRU (vital for slow I/O).
- Federation: A background worker that polls RSS/JSON feeds.
The "Black Box" Risk
When you ask an AI to "build a blog system," it gives you a generic mess. The senior developer's job is to enforce constraints. We are not writing the code, but we are writing the spec. If you don't understand the internal logic, you have created technical debt before you've even committed to Git.
Implementation: The TypeScript Core
Let's start with the data structure. I instructed the model to create a strict interface for our "Federated Post." Note the origin field, which distinguishes between local and remote content.
// types/blog.ts
export interface Author {
name: string;
avatarUrl?: string;
siteUrl: string;
}
export interface BlogPost {
id: string;
title: string;
slug: string;
content: string;
excerpt: string;
publishedAt: Date;
tags: string[];
// The Federation logic
origin: 'local' | 'remote';
originalUrl?: string;
sourceFeedId?: string;
author: Author;
}
export interface FeedSubscription {
id: string;
feedUrl: string;
categoryMapping: string; // Map their "tech" to my "engineering"
lastFetchedAt: Date;
errorCount: number;
}The Aggregation Engine
This is where things get interesting. We need a worker that pulls data without killing the Pi's CPU. I asked the AI for a "resilient fetcher," and it gave me a naive Promise.all.
And that is exactly where the senior dev steps in. Promise.all on 50 feeds will spike the memory and probably crash a Pi 3. We need concurrency control. I refactored the output to use a semaphore pattern.
// services/feed-fetcher.ts
import Parser from 'rss-parser';
import { FeedSubscription, BlogPost } from '../types/blog';
import { db } from '../db';
import pLimit from 'p-limit'; // The concurrency limiter
const parser = new Parser();
const limit = pLimit(3); // Only fetch 3 feeds at a time for the Pi's sake
export class FeedAggregator {
async syncFeeds(feeds: FeedSubscription[]): Promise<void> {
const tasks = feeds.map(feed =>
limit(() => this.processSingleFeed(feed))
);
const results = await Promise.allSettled(tasks);
// Log failures without crashing the process
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`Failed to sync ${feeds[index].feedUrl}:`, result.reason);
}
});
}
private async processSingleFeed(feed: FeedSubscription): Promise<void> {
try {
const remoteData = await parser.parseURL(feed.feedUrl);
const posts: BlogPost[] = remoteData.items.map(item => ({
id: `remote_${item.guid || item.link}`,
title: item.title || 'Untitled',
slug: this.slugify(item.title || 'untitled'),
content: item.content || '',
excerpt: item.contentSnippet?.substring(0, 150) || '',
publishedAt: new Date(item.pubDate || new Date()),
tags: [feed.categoryMapping],
origin: 'remote',
originalUrl: item.link,
sourceFeedId: feed.id,
author: {
name: remoteData.title || 'Unknown',
siteUrl: remoteData.link || ''
}
}));
await db.upsertPosts(posts);
await db.updateFeedStatus(feed.id, new Date(), 0);
} catch (error) {
await db.incrementFeedError(feed.id);
throw error;
}
}
private slugify(text: string): string {
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
}
}This p-limit addition is the difference between a toy and a system. AI generates code that works in a vacuum. System thinkers write code that survives production.
Performance: The Raspberry Pi Constraint
Running modern web apps on a Pi 3 is like running a marathon in flip-flops. It is possible, but you have to be deliberate. The AI suggested using TypeORM. I vetoed that immediately.
Instead, we used better-sqlite3 with a custom caching layer. Here is the middleware I enforced to keep Time-To-First-Byte (TTFB) low.
// middleware/cache.ts
import { FastifyRequest, FastifyReply } from 'fastify';
import { LRUCache } from 'lru-cache';
// Cache for 5 minutes, max 100 items (RAM is precious)
const pageCache = new LRUCache<string, string>({
max: 100,
ttl: 1000 * 60 * 5,
});
export const cacheMiddleware = async (req: FastifyRequest, reply: FastifyReply) => {
if (req.method !== 'GET') return;
const key = req.url;
const cached = pageCache.get(key);
if (cached) {
reply.header('X-Cache', 'HIT');
reply.send(cached);
return reply;
}
reply.header('X-Cache', 'MISS');
const originalSend = reply.send.bind(reply);
reply.send = (payload: any) => {
if (reply.statusCode === 200) {
pageCache.set(key, payload);
}
return originalSend(payload);
};
};The Bittersweet Part
The creator of ZLOG mentioned a "strange bitterness" knowing they didn't write every logic path. I feel this too. There is a specific dopamine hit in solving a race condition that prompting simply does not provide.
But look at the trade-off. By using AI for the grunt work (generating the PWA service worker, writing RSS parser boilerplate, handling CSS media queries), I focused on:
- Resilience: Handling network failures during federation.
- Resource Constraints: Optimizing for the Raspberry Pi.
- User Experience: Making the integrated view feel cohesive.
The code is "mine" in the sense that I directed it, audited it, and shipped it. But the emotional ownership is different. It feels less like crafting a watch and more like designing a factory line.
Conclusion
Building a federated blog engine on a Raspberry Pi using AI is a decent microcosm of modern engineering. The barrier to entry has dropped to the floor. You can build complex, integrated systems by speaking natural language.
But if you don't know why the AI chose Promise.all, or why SQLite acts up on concurrent writes, you are not a developer. You are a passenger. The bittersweet feeling is the growing pains of our industry evolving. We are moving up the abstraction ladder.
The code works. The Pi is humming. The feeds are syncing. And I have time to write this article instead of debugging a missing semicolon. Perhaps that is a fair trade.