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.

πŸ“… Published: September 12, 2025 ✏️ Updated: October 8, 2025 By Ojaswi Athghara
#blog-migration #nuxt-content #dev.to #web-dev #seo #ssg #frontend

I Rebuilt My Blog: From Dev.to to My Own Website (13,500+ Lines of Code Later)

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:

  1. Create a custom reading experience
  2. Implement smart recommendations
  3. Build proper categorization with SEO-friendly URLs
  4. Add pagination for better performance
  5. Customize code syntax highlighting
  6. Control image optimization
  7. 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:

  1. Migrated 16 blog posts from dev.to
  2. Standardized frontmatter format
  3. Optimized images (moved to ImageKit CDN)
  4. Added proper metadata for SEO
  5. 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 states
  • BlogPagination.vue - SEO-friendly pagination
  • BlogCategories.vue - Filter by categories
  • BlogPreviewCard.vue - Beautiful blog previews
  • BlogRecommendations.vue - Smart content recommendations
  • BlogsWrapper.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:

  1. Proper meta tags
  2. Open Graph images
  3. Structured data (JSON-LD)
  4. Canonical URLs
  5. Sitemap generation
  6. 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:

  1. Added canonical URLs pointing to my site
  2. Submitted new sitemap to Google
  3. 301 redirects from old dev.to URLs (added redirect meta)
  4. Updated all backlinks in my network
  5. 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:

  1. Set up infrastructure
  2. Migrate 2-3 posts
  3. Test thoroughly
  4. 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

Related Blogs

Ojaswi Athghara

SDE, 4+ Years

Β© ojaswiat.com 2025-2027