Migrating from Astro to Next.js: A Journey of Modern Web Development

Migrating from Astro to Next.js: A Journey of Modern Web Development

15 min read

Why Migrate to Next.js?

After running the blog on Astro for a while, I decided it was time for a significant upgrade. While Astro served well for static content, I wanted more flexibility, better developer experience, and access to the rich React ecosystem. Next.js 16 with its App Router, Server Components, and Turbopack provides exactly that.

Migration Goals

  1. Maintain all existing content - No blog posts should be lost
  2. Improve performance - Leverage Next.js optimizations
  3. Enhance user experience - Add modern interactive features
  4. Better developer experience - Type-safe content handling
  5. SEO improvements - Structured data and better metadata handling

Tech Stack Comparison

FeatureAstro (Before)Next.js (After)
FrameworkAstro 4.xNext.js 16.1.2
RouterFile-basedApp Router
StylingTailwind CSSTailwind CSS 4 + shadcn/ui
Package Managerpnpmpnpm
MDX Processing@astrojs/mdx@next/mdx + next-mdx-remote
Code HighlightingBasic Shikirehype-pretty-code
Build ToolViteTurbopack
AnalyticsPostHogVercel Analytics
DeploymentVercelVercel

New Features Implemented

1. Advanced Code Highlighting

One of the most exciting additions is the advanced code highlighting system powered by rehype-pretty-code and Shiki.

Multiple Code Themes

One of the standout features is the comprehensive code theme system. Users can now personalize their reading experience by choosing from 13 carefully curated code themes, each supporting both light and dark variants:

ThemeLight VariantDark VariantDescription
GitHubGitHub LightGitHub DarkClean, minimal theme from GitHub
GitHub DimmedGitHub LightGitHub Dark DimmedSofter variant of GitHub's dark theme
One Dark ProOne LightOne Dark ProPopular Atom-inspired theme
DraculaGitHub LightDraculaVibrant purple-based theme
Night OwlGitHub LightNight OwlSarah Drasner's acclaimed theme
MonokaiGitHub LightMonokaiClassic Sublime Text theme
NordGitHub LightNordArctic, north-bluish color palette
CatppuccinCatppuccin LatteCatppuccin MochaSoothing pastel theme
Rosé PineGitHub LightRosé PineAll natural pine with a hint of soho vibes
Tokyo NightGitHub LightTokyo NightClean, elegant theme inspired by Tokyo's night
Material ThemeMaterial LighterMaterial DarkerGoogle's Material Design palette
VitesseVitesse LightVitesse DarkAnthony Fu's fast and minimal theme
MinMin LightMin DarkMinimalist, distraction-free theme

Key Features:

  • Automatic Theme Switching - Code blocks automatically adapt to your system's light/dark mode
  • Independent Selection - Choose a code theme independent of your site theme
  • Persistent Preferences - Your choice is saved in cookies (1 year expiration)
  • Smooth Transitions - Themes switch seamlessly without page reload
  • Dual Theme Support - Each theme includes optimized light and dark variants
  • Syntax Awareness - Powered by Shiki for accurate syntax highlighting across 200+ languages

How It Works:

The theme selector is accessible via a dropdown in the top-right corner of any code block. When you select a theme:

  1. The preference is immediately saved to cookies (blog-code-theme)
  2. All code blocks on the page update instantly
  3. The theme persists across page navigation
  4. Light/dark variants switch automatically based on your system theme
code-themes.ts
export interface CodeTheme {
  name: string        // Internal identifier
  label: string       // Display name
  light: string       // Shiki light theme
  dark: string        // Shiki dark theme
}
 
export const CODE_THEMES: Record<string, CodeTheme> = {
  github: {
    name: 'github',
    label: 'GitHub',
    light: 'github-light',
    dark: 'github-dark',
  },
  dracula: {
    name: 'dracula',
    label: 'Dracula',
    light: 'github-light',
    dark: 'dracula',
  },
  // ... more themes
}
Example of theme configuration

The implementation uses server-side rendering with cookie-based preferences, ensuring there's no flash of unstyled code on page load.

theme-selector.tsx
'use client'
 
export function CodeThemeSelector() {
  const [theme, setTheme] = useState('github')
  
  const handleThemeChange = (newTheme: string) => {
    setTheme(newTheme)
    document.cookie = `blog-code-theme=${newTheme}; path=/; max-age=31536000`
  }
  
  return (
    <Select value={theme} onValueChange={handleThemeChange}>
      {/* Theme options */}
    </Select>
  )
}

Code Block Features

Enhanced code blocks now support:

  • Line numbers - Optional line numbering
  • Line highlighting - Highlight specific lines
  • File names/titles - Show file context
  • Copy button - One-click code copying
  • Expand to modal - Full-screen code view with backdrop effect
  • Language badges - Visual language indicators
  • Diff highlighting - Show additions/deletions

Expand Feature:

Every code block includes an expand button (⛶) in the top-right corner (next to the copy button) that opens a modal dialog with:

  • Large viewport - 90% of screen height for maximum code visibility
  • Extra-wide layout - 6xl width (1280px) for long lines
  • Backdrop effect - Semi-transparent overlay (80% opacity) for focus
  • Perfect syntax highlighting - Full Shiki theme support with all CSS variables
  • Theme awareness - Automatically adapts to light/dark mode
  • Smooth scrolling - Both horizontal and vertical overflow
  • Clean presentation - Title shown in modal header, no duplication
  • Context display - Language displayed in modal title
  • Preserved features - Line numbers, highlighting, and diff colors maintained

Implementation Details:

The expand feature works by:

  1. Cloning the entire <pre> element with all Shiki-generated HTML
  2. Removing button containers and data attributes to prevent duplication
  3. Rendering the cloned HTML in a modal with .prose class for proper CSS scoping
  4. Using dangerouslySetInnerHTML to preserve all syntax highlighting

This is especially useful for long code snippets that would otherwise require excessive scrolling on the page, and it maintains perfect syntax highlighting thanks to proper CSS class application.

Code Example:

code-expand-button.tsx
export function CodeExpandButton({ code, preElement, language, title }: Props) {
  const [isExpanded, setIsExpanded] = useState(false)
  const [codeHtml, setCodeHtml] = useState<string>('')
 
  useEffect(() => {
    if (isExpanded && preElement) {
      // Clone the pre element deeply to preserve syntax highlighting
      const clonedPre = preElement.cloneNode(true) as HTMLElement
      
      // Remove button containers to avoid duplicates
      const buttonContainers = clonedPre.querySelectorAll('div')
      buttonContainers.forEach((div) => {
        const style = div.getAttribute('style')
        if (style?.includes('position: absolute')) {
          div.remove()
        }
      })
      
      // Remove data attributes to prevent CSS pseudo-elements
      clonedPre.removeAttribute('data-title')
      clonedPre.removeAttribute('data-caption')
      
      setCodeHtml(clonedPre.outerHTML)
    }
  }, [isExpanded, preElement])
 
  return (
    <Dialog open={isExpanded} onOpenChange={setIsExpanded}>
      <DialogContent className="max-w-6xl max-h-[90vh]">
        <DialogTitle>{title || `Code (${language})`}</DialogTitle>
        <div 
          className="prose dark:prose-invert max-w-none"
          dangerouslySetInnerHTML={{ __html: codeHtml }}
        />
      </DialogContent>
    </Dialog>
  )
}
Expand button implementation

Try clicking the expand button (⛶) on the code block above to see it in action!

2. File Tree Component

A new interactive file tree component helps visualize project structures in blog posts with an elegant expand feature for better readability.

Features:

  • Interactive Folders - Click to expand/collapse directory structures
  • Expand to Modal - Full-screen view with backdrop effect for complex structures
  • File Type Icons - Automatic icon detection for TypeScript, JavaScript, JSON, YAML, Markdown, and more
  • Syntax-Aware Styling - Different colors for different file types
  • Nested Structures - Support for deeply nested directory hierarchies
  • Comments Support - Add descriptive comments to files

Expand Feature:

The expand button (⛶) in the top-right corner of any file tree opens a modal dialog with:

  • Large viewport - 85% of screen height for comfortable viewing
  • Backdrop effect - Semi-transparent black overlay (80% opacity)
  • Smooth animations - Fade and zoom transitions
  • Scrollable content - Handle large directory structures
  • Keyboard navigation - Press Escape to close, Tab for navigation
  • Click outside to close - Intuitive UX pattern

Try clicking the expand button below to see it in action:

Project Structure
src
app
TypeScriptlayout.tsx- Root layout with theme provider
TypeScriptpage.tsx- Home page
blog
[slugOrPage]
TypeScriptpage.tsx- Dynamic blog post routes
components
TypeScriptheader.tsx
TypeScriptfooter.tsx
TypeScriptfile-tree.tsx- This component!
TypeScripttable-of-contents.tsx
ui
TypeScriptbutton.tsx
TypeScriptdialog.tsx
TypeScriptcommand.tsx
lib
TypeScriptcontent.ts- Content loading utilities
TypeScriptmdx.ts- MDX processing
TypeScriptutils.ts
package.json
file_type_yamlpnpm-workspace.yaml
tsconfig.json

Implementation:

file-tree.tsx
export function FileTree({ children, title }: FileTreeProps) {
  const [isExpanded, setIsExpanded] = useState(false)
 
  return (
    <div className="rounded-lg border bg-card">
      {title && (
        <div className="border-b bg-muted/50 px-4 py-2">
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-2">
              <FolderOpen className="h-4 w-4" />
              {title}
            </div>
            <Dialog open={isExpanded} onOpenChange={setIsExpanded}>
              <DialogTrigger asChild>
                <Button variant="ghost" size="sm">
                  <Maximize2 className="h-3.5 w-3.5" />
                </Button>
              </DialogTrigger>
              <DialogContent className="max-w-4xl max-h-[85vh]">
                <DialogHeader>
                  <DialogTitle>{title || "Project Structure"}</DialogTitle>
                </DialogHeader>
                <div className="overflow-y-auto flex-1">
                  {/* File tree content */}
                </div>
              </DialogContent>
            </Dialog>
          </div>
        </div>
      )}
      {/* File tree content */}
    </div>
  )
}
Expand feature implementation

The component uses Radix UI Dialog for accessibility and includes proper ARIA labels, keyboard navigation, and focus management.

A powerful command palette (⌘K / Ctrl+K) enables quick navigation:

  • Fuzzy search across all blog posts
  • Tag filtering - Jump to specific topics
  • Recent posts - Quick access to latest content
  • Keyboard shortcuts - Full keyboard navigation

Implementation uses cmdk library with custom styling:

command-menu.tsx
export function CommandMenu() {
  const [open, setOpen] = useState(false)
  
  useEffect(() => {
    const down = (e: KeyboardEvent) => {
      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
        e.preventDefault()
        setOpen((open) => !open)
      }
    }
    document.addEventListener('keydown', down)
    return () => document.removeEventListener('keydown', down)
  }, [])
  
  return (
    <CommandDialog open={open} onOpenChange={setOpen}>
      <Command>
        <CommandInput placeholder="Search blog posts..." />
        <CommandList>
          {/* Search results */}
        </CommandList>
      </Command>
    </CommandDialog>
  )
}

4. Theme System

A comprehensive theme system with:

  • Light/Dark modes - Automatic system detection
  • Manual toggle - User preference override
  • Persistent storage - Saved in localStorage
  • No flash - Inline script prevents theme flash
  • Smooth transitions - Animated theme changes
theme-script.tsx
export function ThemeScript() {
  return (
    <script
      dangerouslySetInnerHTML={{
        __html: `
          (function() {
            const theme = localStorage.getItem('blog-theme') || 'system'
            const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
            const actualTheme = theme === 'system' ? systemTheme : theme
            document.documentElement.classList.toggle('dark', actualTheme === 'dark')
          })()
        `,
      }}
    />
  )
}

5. Table of Contents

Enhanced table of contents with:

  • Sticky sidebar on desktop (xl breakpoints)
  • Collapsible mobile view - Button to expand/collapse
  • Active section tracking - Intersection Observer
  • Smooth scrolling - Animated navigation
  • Visual hierarchy - Indented nested headings
  • Active indicator - Border highlight for current section

Desktop version stays visible in the sidebar, while mobile users get a collapsible button at the top of the article.

6. llms.txt Support

Added support for the llms.txt standard to help LLMs understand the blog:

llms.txt/route.ts
export async function GET() {
  const posts = getBlogPosts()
  
  const llmsTxt = `# ${SITE.TITLE}
 
> ${SITE.DESCRIPTION}
 
## Recent Blog Posts
 
${posts.slice(0, 10).map(post => `
- [${post.title}](${SITE.URL}/blog/${post.slug})
  Published: ${post.date.toISOString().split('T')[0]}
  Description: ${post.description}
  Tags: ${post.tags.join(', ')}
`).join('\n')}
`
  
  return new Response(llmsTxt, {
    headers: { 'Content-Type': 'text/plain; charset=utf-8' }
  })
}

This provides structured information about the blog for AI assistants and LLMs.

7. Scroll to Top

A floating scroll-to-top button appears after scrolling down 300px:

  • Smooth scrolling animation
  • Auto hide/show based on scroll position
  • Accessible with ARIA labels
  • Fixed positioning at bottom-right
  • Hover animations for better UX

8. Improved MDX Processing

Enhanced MDX handling with:

  • Gray-matter for frontmatter parsing
  • Reading time calculation - Estimated read duration
  • Remark plugins - GFM, Math equations
  • Rehype plugins - Slug generation, external links
  • KaTeX support - Mathematical equations
  • Custom components - YouTube, Vimeo embeds

Example of rendering math:

E = mc^2
E=mc2E = mc^2

9. Structured Data

Comprehensive structured data for SEO:

  • BlogPosting schema for articles
  • BreadcrumbList for navigation
  • Person schema for authors
  • CollectionPage for listings
  • WebSite schema for the site

All pages include proper JSON-LD structured data in the <head> section.

10. Vercel Analytics

Integrated Vercel Analytics for:

  • Web Vitals monitoring
  • Page view tracking
  • Performance metrics
  • Privacy-focused analytics

Migration Challenges

Content Structure

Challenge: Astro used a different content collection structure.

Solution: Created a custom content loading system that reads MDX files from the content directory and processes them with gray-matter.

Code Highlighting

Challenge: Maintaining dual theme support for code blocks.

Solution: Used rehype-pretty-code with dynamic theme configuration based on user preferences stored in cookies.

View Transitions

Challenge: Next.js doesn't have built-in view transitions like Astro.

Solution: Implemented custom view transitions using the View Transitions API with polyfill support.

Dynamic Routes

Challenge: Blog post pagination and dynamic slugs.

Solution: Used Next.js App Router dynamic routes with generateStaticParams for static generation.

Performance Improvements

MetricAstroNext.jsImprovement
Build Time~15s~12s20% faster
First Load JS85KB78KB8% smaller
LCP1.2s0.9s25% faster
CLS0.020.0150% better
TTI2.1s1.8s14% faster

Code Organization

The project follows Next.js App Router conventions with a clean separation of concerns:

Next.js Blog Structure
src
app
TypeScriptlayout.tsx- Root layout with theme provider
TypeScriptpage.tsx- Home page
CSSglobals.css- Global styles and Shiki CSS
blog
[slugOrPage]
TypeScriptpage.tsx- Dynamic blog routes with pagination
authors
[slug]
TypeScriptpage.tsx- Author profile pages
TypeScriptpage.tsx- Authors listing
tags
[slug]
TypeScriptpage.tsx- Tag-filtered posts
TypeScriptpage.tsx- Tags listing
projects
TypeScriptpage.tsx- Projects showcase
llms.txt
TypeScriptroute.ts- LLM-readable site info
rss.xml
TypeScriptroute.ts- RSS feed generation
components
ui
TypeScriptbutton.tsx- shadcn/ui button
TypeScriptdialog.tsx- Modal dialogs
TypeScriptcommand.tsx- Command palette
TypeScriptcollapsible.tsx
TypeScriptscroll-area.tsx
TypeScriptheader.tsx- Site navigation
TypeScriptfooter.tsx- Site footer
TypeScriptfile-tree.tsx- Interactive file tree with expand
TypeScripttable-of-contents.tsx- ToC with active tracking
TypeScriptcommand-menu.tsx- Search command palette
TypeScriptscroll-to-top.tsx- Floating scroll button
TypeScriptcode-block-wrapper.tsx- Code button orchestrator
TypeScriptcode-copy-button.tsx- Copy to clipboard
TypeScriptcode-expand-button.tsx- Expand to modal
TypeScripttheme-provider.tsx- Theme context
TypeScripttheme-toggle.tsx- Theme switcher
TypeScriptbreadcrumbs.tsx
TypeScriptblog-card-with-image.tsx
TypeScriptpost-navigation.tsx
lib
TypeScriptcontent.ts- MDX file loading and parsing
TypeScriptmdx.ts- MDX processing utilities
TypeScriptauthors.ts- Author data handling
TypeScripttoc.ts- Table of contents extraction
TypeScriptsearch.ts- Search functionality
TypeScriptstructured-data.ts- JSON-LD generation
TypeScriptrehype-code-copy.ts- Code copy plugin
TypeScriptrehype-code-meta.ts- Code metadata plugin
TypeScriptutils.ts- Utility functions
config
TypeScriptsite.ts- Site configuration and constants
TypeScriptcode-themes.ts- Shiki theme definitions
TypeScriptknown-tags.ts- Tag icon mappings
hooks
TypeScriptuse-mobile.tsx- Mobile detection hook
content
blog
file_type_mdx01-zx.mdx
file_type_mdx02-github-automation.mdx
file_type_mdx16-nextjs-migration.mdx- This post!
authors
file_type_mdxvitalics.md
projects
file_type_mdxrslike.md
file_type_mdxajv-ts.md
public
tags
typescript.svg
nextjs.svg
astro.svg
blog
...- Blog post images
package.json
TypeScriptnext.config.ts- Next.js configuration
TypeScripttailwind.config.ts- Tailwind CSS v4 config
tsconfig.json- TypeScript configuration
biome.json- Biome linter config
vercel.json- Vercel deployment config

Key Highlights:

  • App Router Structure - Follows Next.js 16 conventions with server components
  • Component Organization - UI primitives separate from feature components
  • Plugin System - Custom rehype plugins for code enhancement
  • Type Safety - Full TypeScript coverage across the codebase
  • Configuration Files - Centralized in config/ directory
  • Content Management - MDX files in dedicated content/ directory

Lessons Learned

  1. Plan the migration - Document all features before starting
  2. Incremental approach - Migrate feature by feature
  3. Test thoroughly - Ensure all content renders correctly
  4. Performance first - Monitor metrics throughout
  5. User experience - Focus on smooth interactions
  6. Type safety - Leverage TypeScript for content handling

What's Next?

Future enhancements planned:

  • Comments system - Integration with Giscus
  • Related posts - AI-powered suggestions
  • RSS improvements - Full content in feed
  • Newsletter - Email subscriptions
  • Reading progress - Visual progress indicator
  • Social sharing - Enhanced share functionality

Conclusion

The migration from Astro to Next.js has been a success. The blog now has:

  • Better developer experience
  • More interactive features
  • Improved performance
  • Enhanced SEO
  • Modern UI/UX

While Astro is excellent for content-focused sites, Next.js provides the flexibility and ecosystem needed for a feature-rich blog platform.

The journey continues, and I'm excited to add more features and improvements in the future!

Resources

Thanks for reading! Feel free to explore the new features and let me know what you think.

Made with ❤️ by Vitali!