How I fixed Google ignoring 160 pages: migrating from Vite SPA to Next.js SSG
I've been building Convertify — a free image converter
that supports HEIC, WebP, AVIF, PNG, JPG and 20+ formats.
The problem: after weeks of work, Google had indexed exactly 2 pages out of 160+.
Why Google ignored my pages
My app was a Vite React SPA. Every route looked like this in HTML:
<!DOCTYPE html>
<html>
<head>
<title>Convertify</title>
</head>
<body>
<div id="root"></div>
<script src="/assets/index.js"></script>
</body>
</html>
Google crawls without executing JavaScript. So every single page —
/heic-to-jpg, /webp-to-png, /avif-to-jpg — returned identical empty HTML.
Google saw them all as duplicates and indexed only the homepage.
The fix: Next.js App Router + generateStaticParams
I migrated to Next.js and used generateStaticParams() to pre-render
all format pairs at build time:
export async function generateStaticParams() {
const formats = ['heic', 'jpg', 'jpeg', 'png', 'webp', 'avif', 'gif', 'bmp', 'tiff']
const params = []
for (const from of formats) {
for (const to of formats) {
if (from !== to) params.push({ slug: `${from}-to-${to}` })
}
}
return params
}
Result: 186 static HTML pages generated at build time.
Server components for SEO content
The key insight: not everything needs to be a client component.
I split the page into two parts:
Client (interactive): DropZone, Settings, ConvertButton, FilesViewer
Server (static HTML): FAQ, HowToConvert, SupportedFormats, Header, Footer
For server components I replaced MUI Accordion with native HTML <details>/<summary>:
// No 'use client', no JavaScript, pure HTML
const FAQ = ({ from, to }: { from?: string; to?: string }) => {
return (
<dl>
{faqs.map((faq, i) => (
<details key={i}>
<summary>{faq.question}</summary>
<dd>{faq.answer}</dd>
</details>
))}
</dl>
)
}
Google gets full FAQ content without running any JavaScript.
Dynamic metadata per page
Instead of react-helmet-async, Next.js handles metadata server-side:
export async function generateMetadata({ params }) {
const [from, to] = params.slug.split('-to-')
return {
title: `Convert ${from.toUpperCase()} to ${to.toUpperCase()} Online Free — Convertify`,
description: `Free online ${from.toUpperCase()} to ${to.toUpperCase()} converter.`,
alternates: { canonical: `https://convertifyapp.net/${params.slug}` },
}
}
Every page now has unique title, description and canonical tag in the HTML
— no JavaScript required.
Results
- Before: 2 indexed pages
- After: 186 static pages pre-rendered
- PageSpeed Desktop: 100/100 Performance, 100/100 SEO
- PageSpeed Mobile: 83/100 Performance, 100/100 SEO
The mobile performance hit comes from MUI (emotion) on the client side.
Working on reducing that next.
Lessons learned
- SPAs are SEO poison for content pages. Use SSG or SSR.
- Split server/client components — only interactive parts need JavaScript.
-
Native HTML elements (
<details>,<summary>) work great for accordions and are fully server-renderable. - generateStaticParams is incredibly powerful for programmatic SEO pages.
If you're building a tool with many landing pages — don't make the same
mistake I did. Start with Next.js from day one.
Check out Convertify if you need to convert
HEIC, WebP, AVIF or any other image format — free, no signup, no limits.
Top comments (0)