I Rebuilt My Blog: From Dev.to to My Own Website (13,500+ Lines of Code Later)
Complete website overhaul journey: migrating from dev.to to self-hosted blog with Nuxt Content, building custom UI components, implementing SEO-friendly pagination and categories, and achieving SSG. A developer's guide to owning your content.

When I Realized I Didn't Own My Content
I had 16 comprehensive DSA guides on dev.to. Good reach, nice community, zero setup. But every time I wanted to customize somethingβthe layout, the reading experience, the recommendationsβI hit a wall.
"What if dev.to changes their API? What if they shut down? What if I want my own design?"
That's when I decided: it's time to own my content. Time to build my own blog platform.
Three weeks, 77 files changed, 13,584 insertions, and 1,160 deletions laterβI have a blog that's truly mine.
In this post, I'll walk you through the entire migration journey: the architecture decisions, the technical challenges, the SEO considerations, and the lessons learned from building a production-ready blog from scratch.
The Problem with Third-Party Platforms
What I Had on Dev.to
// Old approach: Fetch from dev.to API
export default defineEventHandler(async (event) => {
return await $fetch<TBlogPost[]>(`${DEV_TO_URI}/me`, {
headers: {
"api-key": config.blogProfileSecret,
},
});
});
Pros:
- Zero maintenance
- Built-in community
- Good SEO out of the box
- Quick to set up
Cons:
- Limited customization
- No control over UI/UX
- Dependent on their API
- Can't optimize for my brand
- No control over content structure
- Limited analytics integration
The Breaking Point
I wanted to:
- Create a custom reading experience
- Implement smart recommendations
- Build proper categorization with SEO-friendly URLs
- Add pagination for better performance
- Customize code syntax highlighting
- Control image optimization
- Own my content permanently
Third-party platforms couldn't give me this level of control.
The Architecture Decision
Evaluating Options
Option 1: WordPress
- β Too heavy for a developer portfolio
- β PHP when I'm deep in TypeScript
- β Great for non-developers
Option 2: Ghost
- β Clean, modern
- β Another service to pay for
- β Still not complete control
Option 3: Gatsby/Next.js + MDX
- β Full control
- β More setup needed
- β React ecosystem - I'm not a fan of React
Option 4: Nuxt Content (My Choice)
- β Already using Nuxt 3
- β File-based CMS
- β Built-in markdown support
- β TypeScript-first
- β SSG out of the box
- β Zero API needed
Why Nuxt Content Won
// content.config.ts - Type-safe content schema
export default defineContentConfig({
collections: {
blogs: defineCollection({
type: "page",
source: "**/*.md",
schema: z.object({
title: z.string(),
description: z.string(),
published_at: z.coerce.date(),
updated_at: z.coerce.date().optional(),
image: z.string().optional(),
tags: z.array(z.string()),
primary_category: web-development
secondary_categories: z.array(z.string()).default([]),
author: z.string(),
}),
indexes: [
{ fields: ["primary_category"], name: "idx_primary_category" },
{ fields: ["published_at"], name: "idx_published_at" },
],
}),
},
});
The killer features:
- Type-safe with Zod validation
- Database-style indexes for performance
- Markdown with Vue components
- Auto-imports and composables
- Static generation ready
The Migration: Phase by Phase
Phase 1: Content Structure (Week 1)
Goal: Set up content infrastructure
# New folder structure
content/
βββ array-problems-two-pointers-prefix.md
βββ binary-search-beyond-arrays.md
βββ dynamic-programming-5-patterns.md
βββ graph-algorithms-bfs-dfs.md
βββ ... 12 more DSA guides
What I did:
- Migrated 16 blog posts from dev.to
- Standardized frontmatter format
- Optimized images (moved to ImageKit CDN)
- Added proper metadata for SEO
- Implemented consistent heading structure
Challenge: Converting dev.to's markdown to Nuxt Content
- dev.to has custom liquid tags
- Had to convert image references
- Needed to restructure internal links
Solution:
// Custom ProseImg component for optimized images
<template>
<img :src="src" :alt="alt" loading="lazy" decoding="async" />
</template>
Phase 2: Blog UI Components (Week 2)
Goal: Build production-ready blog reading experience
New components created:
BlogNavigation.vue- Category navigation with active statesBlogPagination.vue- SEO-friendly paginationBlogCategories.vue- Filter by categoriesBlogPreviewCard.vue- Beautiful blog previewsBlogRecommendations.vue- Smart content recommendationsBlogsWrapper.vue- Responsive blog layout
The Navigation Component:
<!-- BlogNavigation.vue - Smart category navigation -->
<script setup lang="ts">
const route = useRoute();
const categories = [
{ slug: "all", name: "All Topics", icon: "i-lucide:layout-grid" },
{ slug: "algorithms", name: "Algorithms", icon: "i-lucide:binary" },
{ slug: "data-structures", name: "Data Structures", icon: "i-lucide:boxes" },
{ slug: "python", name: "Python", icon: "i-lucide:code" },
];
function isActive(slug: string) {
if (slug === "all") {
return !route.params.category;
}
return route.params.category === slug;
}
</script>
Design principles:
- Mobile-first responsive
- Dark mode support
- Smooth animations
- Accessible keyboard navigation
- Fast loading with lazy loading
Phase 3: Routing & SEO (Week 2-3)
Goal: SEO-friendly URLs with proper static generation
Old structure (dev.to):
/blog β fetches from API
New structure (self-hosted):
/blog β All blogs
/blog/algorithms/ β Category page
/blog/algorithms/1 β Paginated category
/blog/[slug] β Individual blog post
Dynamic route implementation:
<!-- pages/blog/[category]/[page].vue -->
<script setup lang="ts">
const route = useRoute();
const category = route.params.category as string;
const page = Number.parseInt(route.params.page as string) || 1;
const perPage = 9;
// Query with filtering and pagination
const { data: blogs } = await useAsyncData(
`blogs-${category}-${page}`,
() => queryCollection("blogs")
.where("primary_category", "==", category)
.order("published_at", "desc")
.limit(perPage)
.skip((page - 1) * perPage)
.all()
);
</script>
SEO enhancements:
- Proper meta tags
- Open Graph images
- Structured data (JSON-LD)
- Canonical URLs
- Sitemap generation
- RSS feed
Phase 4: Code Highlighting & Styling (Week 3)
Goal: Beautiful code blocks that enhance learning
Custom blog CSS:
/* blog.css - 188 lines of custom styling */
.blog-content pre {
@apply rounded-lg overflow-x-auto;
@apply bg-gray-900 dark:bg-gray-950;
@apply p-4 my-6;
}
.blog-content code {
@apply font-mono text-sm;
@apply px-1.5 py-0.5 rounded;
@apply bg-gray-100 dark:bg-gray-800;
}
.blog-content h2 {
@apply text-2xl font-bold mt-8 mb-4;
@apply border-b border-gray-200 dark:border-gray-800 pb-2;
}
Features added:
- Syntax highlighting with Shiki
- Line numbers for code blocks
- Copy button for code
- Responsive tables
- Custom blockquotes
- Optimized typography
Phase 5: Performance & SSG (Week 3)
Goal: Lightning-fast static site
Nuxt config optimization:
export default defineNuxtConfig({
modules: ["@nuxt/content"],
content: {
highlight: {
theme: {
default: "github-light",
dark: "github-dark",
},
},
},
nitro: {
prerender: {
routes: ["/blog", "/blog/algorithms", "/blog/data-structures"],
crawlLinks: true,
},
},
routeRules: {
"/blog/**": { swr: 3600 }, // Cache for 1 hour
},
});
Performance wins:
- β Pre-rendered static pages
- β Optimized images with CDN
- β Code splitting by route
- β Lazy-loaded components
- β Prefetching for faster navigation
Results:
- First Contentful Paint: < 1s
- Time to Interactive: < 2s
- Lighthouse Score: 95+
The Technical Challenges I Faced
Challenge 1: Pagination with Static Generation
Problem: How to statically generate all paginated pages?
Solution:
// Generate routes at build time
export default defineNuxtConfig({
hooks: {
"pages:extend": function (pages) {
const categories = ["algorithms", "data-structures", "python"];
const postsPerCategory = { "algorithms": 8, "data-structures": 5, "python": 3 };
const perPage = 9;
categories.forEach((category) => {
const totalPosts = postsPerCategory[category as const];
const totalPages = Math.ceil(totalPosts / perPage);
for (let page = 1; page <= totalPages; page++) {
pages.push({
path: `/blog/${category}/${page}`,
file: "~/pages/blog/[category]/[page].vue",
});
}
});
},
},
});
Challenge 2: Smart Recommendations
Problem: Show relevant blog recommendations without machine learning
Solution:
// Recommendation algorithm based on categories and tags
function getRecommendations(currentPost: Blog, allPosts: Blog[], limit = 3) {
return allPosts
.filter((post) => post.id !== currentPost.id)
.map((post) => {
let score = 0;
// Same primary category: +3 points
if (post.primary_category === currentPost.primary_category) {
score += 3;
}
// Shared tags: +1 per tag
const sharedTags = post.tags.filter((tag) =>
currentPost.tags.includes(tag)
);
score += sharedTags.length;
// Secondary category match: +1 point
if (currentPost.secondary_categories.includes(post.primary_category)) {
score += 1;
}
return { post, score };
})
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map(({ post }) => post);
}
Challenge 3: SEO for Migrated Content
Problem: Preserve dev.to SEO juice while migrating
Solution:
- Added canonical URLs pointing to my site
- Submitted new sitemap to Google
- 301 redirects from old dev.to URLs (added redirect meta)
- Updated all backlinks in my network
- Rich snippets with structured data
<!-- SEO meta tags -->
<script setup lang="ts">
useSeoMeta({
title: blog.title,
description: blog.description,
ogTitle: blog.title,
ogDescription: blog.description,
ogImage: blog.image,
ogType: "article",
articlePublishedTime: blog.published_at,
articleModifiedTime: blog.updated_at,
articleAuthor: blog.author,
articleTag: blog.tags,
});
</script>
The Migration Checklist
If you're planning a similar migration, here's my battle-tested checklist:
Pre-Migration (Week 0)
- Audit existing content (I had 16 posts)
- Choose your tech stack (Nuxt Content, Next.js, Gatsby, etc.)
- Design URL structure
- Plan content schema
- Set up CDN for images
Week 1: Infrastructure
- Install and configure CMS (Nuxt Content)
- Set up content schema with validation
- Create folder structure
- Migrate first blog post as proof of concept
- Test markdown rendering
Week 2: UI Development
- Build blog list page
- Create individual blog post layout
- Implement navigation components
- Add pagination
- Design category filtering
- Mobile responsiveness
- Dark mode support
Week 3: Polish & SEO
- Code syntax highlighting
- Image optimization
- SEO meta tags
- Open Graph images
- Sitemap generation
- RSS feed
- Performance optimization
- Analytics integration
Week 4: Launch
- Migrate all content
- Set up 301 redirects
- Submit new sitemap
- Update social media links
- Announce migration
- Monitor analytics
Lessons Learned
1. Start with Content Schema
Don't wing it. Define your schema early:
// This saved me countless refactors
const blogSchema = z.object({
title: z.string(),
description: z.string(),
published_at: z.coerce.date(),
tags: z.array(z.string()),
primary_category: web-development
secondary_categories: z.array(z.string()).default([]),
});
2. SEO is Not Optional
- Proper meta tags from day one
- Structured data for rich snippets
- Semantic HTML structure
- Fast loading times matter
3. Developer Experience Matters
<!-- Vue components in Markdown = Game changer -->
# My Blog Post
<BlogAlert type="info">
This is a custom Vue component inside markdown!
</BlogAlert>
4. Performance Budget
Set performance budgets early:
- First Contentful Paint < 1.5s
- Time to Interactive < 3s
- Total page weight < 500KB
- Lighthouse score > 90
5. Own Your Images
Using ImageKit CDN:
- Automatic optimization
- Responsive images
- WebP conversion
- Global CDN
The Results: Was It Worth It?
Before (dev.to):
- β No control over UI
- β Limited customization
- β Dependent on external service
- β Zero maintenance
- β Built-in audience
After (Self-hosted):
- β Complete UI/UX control
- β Custom features (recommendations, categories)
- β Own my content forever
- β SEO optimized for my brand
- β Faster loading (SSG)
- β Better reading experience
- β Integration with my portfolio
- β οΈ Need to maintain (worth it!)
Traffic impact:
- First week: 30% drop (expected during migration)
- Week 2-3: Back to baseline
- Month 2: 40% increase (better SEO, faster site)
Development time:
- Total: ~60 hours over 3 weeks
- 77 files changed
- 13,584 lines added
- 1,160 lines removed
Worth it? Absolutely. I now own my content, my design, and my destiny.
The Tech Stack Breakdown
Core Technologies
- Framework: Nuxt 3 (Vue.js)
- CMS: Nuxt Content v3
- Styling: Tailwind CSS + Custom CSS
- TypeScript: Full type safety
- Validation: Zod schemas
- Syntax Highlighting: Shiki
New Dependencies Added
{
"@nuxt/content": "^3.x",
"zod": "^3.x",
"shiki": "^1.x"
}
Infrastructure
- Hosting: Vercel/Netlify (SSG)
- CDN: ImageKit for images
- Analytics: Custom solution
- Comments: Planned for Phase 2
What's Next?
Phase 2: Planned Features
1. Interactive Code Playground
<CodePlayground language="python">
def fibonacci(n):
return n if n <= 1 else fibonacci(n-1) + fibonacci(n-2)
</CodePlayground>
2. Table of Contents
- Auto-generated from headings
- Sticky scroll navigation
- Progress indicator
3. Reading Time & Progress
// Auto-calculate reading time
const readingTime = Math.ceil(wordCount / 200); // words per minute
4. Comment System
- Self-hosted comments
- GitHub Discussions integration
- Or Giscus
5. Search
- Full-text search
- Fuzzy matching
- Search by tags/categories
6. Newsletter Integration
- Subscribe to new posts
- Email digest
- RSS to email
My Migration Tips for You
1. Don't Migrate Everything at Once
Start with:
- Set up infrastructure
- Migrate 2-3 posts
- Test thoroughly
- Then migrate rest
2. Preserve URLs If Possible
// Redirect old dev.to URLs
// server/middleware/redirects.ts:
export default defineEventHandler((event) => {
const url = getRequestURL(event);
// dev.to URL pattern: /ojaswiat/post-slug-xyz
if (url.pathname.startsWith("/ojaswiat/")) {
const slug = url.pathname.replace("/ojaswiat/", "");
return sendRedirect(event, `/blog/${slug}`, 301);
}
});
3. Set Up Analytics First
Know your baseline before migration:
- Page views
- Bounce rate
- Time on page
- Traffic sources
4. Test on Real Devices
# Test on different devices
npm run build
npm run preview
# Then test on phone, tablet, desktop
5. Have a Rollback Plan
Keep dev.to content live until:
- All content migrated
- SEO stabilized
- Traffic recovered
- Zero critical bugs
Common Migration Pitfalls
Pitfall 1: Forgetting Image Optimization
<!-- BAD: Direct image links -->
<img src="https://raw.githubusercontent.com/user/huge-image.png" />
<!-- GOOD: CDN with optimization -->
<img src="https://cdn.imagekit.io/user/image.jpg?tr=w-800,f-webp" />
Pitfall 2: Ignoring Mobile
60% of traffic is mobile. Test mobile-first!
Pitfall 3: Not Setting Up Redirects
Lost all SEO juice because no 301 redirects? Don't be that person.
Pitfall 4: Skipping Performance Testing
# Always test before launch
npm run build
npm run preview
lighthouse http://localhost:3000/blog
The Developer's Perspective
What I Loved
File-based CMS:
# Just create a file
touch content/my-new-post.md
# It's automatically available!
Type Safety:
// TypeScript knows your content structure
const blog = await queryCollection("blogs")
.where("primary_category", "==", "algorithms")
.first();
// blog.title β
TypeScript knows this exists
// blog.invalid β TypeScript error!
Hot Reload: Edit markdown β See changes instantly β No build step!
What Was Hard
1. Learning Curve:
- Nuxt Content query syntax
- Understanding static generation
- SEO best practices
2. Edge Cases:
- Special characters in URLs
- Code blocks with HTML
- Nested lists rendering
3. Migration Tedium:
- Manually fixing image URLs
- Converting liquid tags
- Cleaning up frontmatter
But once set up? Smooth sailing! β΅
Conclusion: Own Your Content
After three weeks of intense development, I have:
- β 16 comprehensive DSA blogs on my own domain
- β Complete control over design and features
- β SEO-optimized blog platform
- β Lightning-fast static site
- β Beautiful reading experience
- β Foundation for future content
The verdict: If you're serious about content creation and have dev skills, build your own platform. The initial time investment pays off in control, customization, and peace of mind.
For developers:
- You control every pixel
- You decide features
- You own your content
- You learn a ton
For content creators:
- Third-party platforms are fine to start
- But plan your exit strategy
- Own your content eventually
The best time to own your platform? Yesterday. The second best time? Today.
My blog is now truly mineβfrom the URL structure to the syntax highlighting theme. And that feels amazing.
Ready to take control of your content? Start with one blog post. Then build from there.
If you found this migration story helpful or are planning your own blog overhaul, I'd love to hear about it! Connect with me on Twitter or LinkedIn.
Support My Work
If this guide helped you with this topic, I'd really appreciate your support! Creating comprehensive, free content like this takes significant time and effort. Your support helps me continue sharing knowledge and creating more helpful resources for developers.
β Buy me a coffee - Every contribution, big or small, means the world to me and keeps me motivated to create more content!
Cover image by Markus Winkler on Unsplash