The Context
At Tenders SA, we are in the middle of a significant architectural shift. We are moving from a monolithic Next.js application to a microservices architecture. One of the first candidates for this migration was our blog.
You might ask, "Why move a simple blog?"
Our blog isn't just a list of markdown files. It powers a dynamic recommendation engine that matches tender listings with helpful guides. It calculates "Related Posts" on the fly based on tags and categories, and it serves "Featured" content based on specific SEO scoring rules.
Running this logic inside the Next.js main thread meant our monolithic app was doing heavy lifting it didn't need to do. We decided to move the entire blog backend to Cloudflare Workers and KV Storage.
Here is how we maintained exact feature parity—specifically our custom SEO scoring algorithms—while moving to the Edge.
The Architecture
The plan was straightforward but strict:
- Data Source: Move from local JSON/Database to Cloudflare KV (Key-Value storage).
- Compute: Replace
src/lib/blog.ts(Next.js) with a Cloudflare Worker. - Constraint: We could not lose the dynamic "scoring" logic that helps our SEO.
KV Design
We opted for a flat structure in KV to ensure speed.
-
post:{slug}: Contains the full JSON content of a post. -
list:all: A lightweight JSON array of all post metadata. This is what we iterate over for filtering and scoring. -
categories: A pre-calculated array of category counts.
Porting the Logic: The "Scoring" Challenge
The most critical part of this migration was porting our internal matching algorithms. We didn't just want to serve files; we wanted to serve the right files.
1. The "Featured Posts" Algorithm
On our homepage, we don't just show the latest posts. We score them based on high-value keywords that convert users. We ported this logic directly into the Worker.
Here is the scoring logic we implemented in the GET /posts/featured endpoint:
// Inside our Cloudflare Worker
function scorePostForFeature(post) {
let score = 0;
const now = new Date();
const postDate = new Date(post.date);
// 1. Keyword Scoring (High Value)
const title = post.title.toLowerCase();
const excerpt = post.excerpt.toLowerCase();
// "How-to" content is evergreen and high value
if (["guide", "how to", "tips", "winning", "successful"].some(w => title.includes(w))) {
score += 10;
}
// Domain specific keywords
if (["tender", "procurement", "contract", "proposal"].some(w => title.includes(w) || excerpt.includes(w))) {
score += 8;
}
// 2. Recency Boost
// If published in the last 30 days, give it a bump
const diffTime = Math.abs(now - postDate);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 30) {
score += 5;
}
return score;
}
By running this on the Edge, we can adjust these weights instantly without redeploying the main frontend application.
2. Contextual Matching (Tender to Blog)
When a user is looking at a specific construction tender, we want to show them guides on "How to price construction bills."
We created a POST /posts/related-to-tender endpoint. It accepts the tender's categories and runs a similar scoring match against our list:all KV entry:
- +10 pts for direct Category match.
- +8 pts for Title keyword match.
- +5 pts for Recency.
This allows the frontend to simply send { categories: ["construction"] } and receive a perfectly curated list of reading material.
The API Surface
We exposed a clean REST API from the Worker that our Next.js frontend now consumes:
-
GET /posts: The main list with pagination. -
GET /posts/{slug}: The full content + SEO Metadata (Canonical, Title, Desc). -
GET /posts/{slug}/related: Finds related content based on shared tags (+5pts per tag). -
GET /categories: Returns categories with active post counts.
Why This Matters (RAG Readiness)
Beyond just speed, this structure prepares us for the next phase: AI.
By structuring our content in KV (post:{slug}), we have created a high-speed retrieval layer. When we implement our "Tender Assistant" chatbot later this year, this Worker will serve as the primary knowledge source for the RAG (Retrieval-Augmented Generation) pipeline. The Worker can quickly feed context to the LLM without touching a database or scraping a website.
Conclusion
Moving to Cloudflare Workers reduced our Next.js bundle size and offloaded complex iteration logic to the edge network. We kept our complex SEO scoring, improved response times, and prepared our data infrastructure for AI features.
If you are running a blog inside a Next.js monolith, consider breaking it out. The Edge is ready for it.
Next Steps for Tenders SA
- [ ] Finalize the
scripts/seed-blog-to-kv.tsmigration script. - [ ] Switch the production DNS to route
/api/blog/*to the new Worker.
Check out the platform at tenders-sa.org
Check out our Blog tenders-sa.org Blog
Top comments (0)