This post suits for readers who use Next.js to develop websites or want to maintain or optimize SEO performance. The knowledge and core concepts of SEO are the same.
The content is based on my own experience. Please refer to other websites and books for more advanced SEO knowledge and information. If there are any errors or questions, please send an email to inform us. Thank you!
Table of Contents
- Introduction
- Migrate From Hugo to Next.js
- Successful Migration from Hugo to Next.js: SEO and Traffic Maintained
- SEO Key Points
- Final Thoughts
Introduction
Building and designing my own personal website has always been a dream of mine.
Back when I was still getting familiar with software development, I started with WordPress to build my site. It was a great starting point, but I found myself craving more flexibility — I wanted greater control over how content was structured and displayed on the front end. That led me to explore other tools that would give me the creative freedom I was looking for.
In 2019, thanks to a recommendation from a former colleague, I discovered Hugo — a popular Jamstack framework known for its speed and SEO-friendly static site generation. Using Hugo, I rebuilt my personal website and began writing more consistently. Over time, the site grew from around 10,000 views on WordPress to over 300,000 page views today, with an average of 50+ daily visitors. (Most of my blog posts are written in Traditional Chinese.)
However, as a developer with a backend background in Python and recently transitioning into frontend development with JavaScript, I found Hugo’s reliance on Go (Golang) to be a bit of a hurdle. Customizing themes and experimenting with new UI ideas became increasingly limiting.
That’s what pushed me to take the next step — to find a path where I could keep learning modern frontend technologies while also having the flexibility to design and shape my personal site’s UI exactly the way I envision it.
Migrate From Hugo to Next.js
Back when I was still in grad school, the modern frontend frameworks — React, Vue, and Angular — hadn't even been released yet. At that time, we were building websites using plain HTML, CSS, and JavaScript (or jQuery), typically combined with MVC architecture and backend technologies.
Fast forward to a couple of years ago, I began transitioning from backend to frontend development. When I first started learning Next.js, I was completely unfamiliar with different rendering strategies like CSR, SSR, SSG, and ISR. The nuances — and sometimes conflicts — between CSR and SSR gave me quite a headache. Compared to React.js, which uses a straightforward CSR model, and Hugo, which is based on static site generation (SSG), jumping straight into Next.js felt overwhelming. I tried multiple times to rebuild my Hugo blog with Next.js, but the learning curve — especially around rendering — kept holding me back.
There were a few major hurdles:
- My frontend knowledge was still limited, especially regarding rendering strategies, which made development difficult.
- I had little experience optimizing for SEO, and I was concerned that the SEO performance wouldn’t match what Hugo had been effortlessly handling for me.
- URL structure changes could seriously affect search engine rankings.
My blog, built with Hugo, has been running for about 5–6 years and has accumulated over 300,000 page views, with around 50+ daily visitors. Many of the articles rank well on Google, thanks to Hugo's excellent SEO handling.
One critical SEO requirement is preserving the original URL structure. Any change could result in 404 Page Not Found errors or require redirects — both of which can hurt Google rankings.
The original URL structure in Hugo looked like this:
- Chinese homepage:
/
- English homepage:
/en/
- Chinese posts:
/<year>/<month>/<slug>
- English posts:
/en/<year>/<month>/<slug>
However, when I tried rebuilding the site using Next.js and its dynamic routing system (e.g., /[lang]/page.js
), my URLs changed. For example, a post originally at /2024/04/post
became /zh-TW/2024/04/post
. That change broke the existing URLs and caused Google to lose track of the pages.
Eventually, I discovered a powerful feature in Next.js: Catch-all Segments. By defining a dynamic route like /[...routes]/page.js
and using generateStaticParams
, I could define flexible route arrays, regardless of language or slug format.
For example:
- Passing
['en', '2024', '04', 'post']
gives/en/2024/04/post
- Passing
['2023', '01', 'post']
gives/2023/01/post
This approach allowed me to maintain the original URL structure (like /2024/04/post
), ensuring existing search engine links would still work.
After figuring this out, I finally made the decision to migrate from Hugo to Next.js — and officially started the process of rebuilding my site.
Successful Migration from Hugo to Next.js: SEO and Traffic Maintained
After the full site refactor using Next.js, the official relaunch was on May 26, 2025. As of June 5, it has been nearly two weeks since going live. During this time, I closely monitored performance through Google Analytics (GA) and Google Search Console (GSC). The results so far have been quite positive—both traffic and SEO rankings have remained stable, indicating a smooth transition.
This is especially reassuring considering a previous attempt to migrate using Nuxt.js resulted in a significant SEO drop. So seeing these stable numbers now feels like a small victory.
Daily users and pageviews remain steady post-launch.
Search Console Metrics
In Google Search Console, both performance and indexing results mirrored what I observed in GA:
No major dip in impressions or clicks after switching to Next.js.
More valid pages are now detected and indexed.
Some of the non-indexed pages are likely outdated or contain old broken paths from earlier versions of the site. I plan to manually clean those up in future revisions.
Key Takeways
- No major drop in search rankings or traffic
- No 404 errors from old links
- Increased number of pages indexed by Google robots
Overall, this migration wasn’t a waste of effort—it actually paid off. It not only maintained the existing SEO performance but also gave me valuable hands-on experience in SEO tuning and working with Next.js.
I’ve also been deepening my understanding of SEO, including tools and best practices such as Schema.org
markup, Google Lighthouse
audits, and structured data enhancements. At the same time, I’m re-evaluating my site’s content to ensure it's still engaging and relevant to readers.
Sure, my blog doesn’t pull thousands or tens of thousands of visitors a day like an e-commerce site. But unlike the failed Nuxt.js migration where traffic and rankings plummeted, this time the outcome was solid.
In the next section, I’ll share some of the key lessons and techniques I used to preserve and optimize SEO during the transition—hopefully, they’ll help others avoid common pitfalls.
SEO Key Points
The Next.js version: 14.2.5
1. Metadata
Metadata provides structured information about each webpage, helping search engines like Google understand the content, language, author, and how the page should appear when shared on social platforms.
For every page you want indexed by Google—especially blog posts—it's recommended to define unique metadata. Here are the key components I used in my implementation:
- Basic Info: title, description, and keywords. (Although the impact of keywords has decreased, it's still advisable to include around five.)
- Favicon: A small tab icon to increase brand recognition and professionalism.
- Additional Info: Such as generator, authors, and creator.
- Robots.txt Configuration: Instructs search engine bots on which pages to index.
- Alternates: Useful for multilingual websites; specify language-specific URLs. Be sure the canonical URL is correct, or your shared links might lead to the wrong page.
- openGraph (OG) & Twitter Tags: Define how your content appears when shared across platforms.
Defining clear, relevant metadata for each page is crucial to strong SEO performance. Here is a sample metadata object implemented in Next.js (v14.2.5)
based on my personal website:
const metadata = {
// Basic Info
title: "About - @Mina Influence", // iPhone - Apple
description: "Some description here.",
keywords: ["keywords1", "keywords2", "keywords字3", "keywords4", "keywords5"],
// Additional Info
generator: "Next.js 14.2.5",
// Favicon
icons: {
icon: "/static/favicon.jpg",
shortcut: "/static/favicon.jpg",
apple: "/static/favicon.jpg",
},
// Put website name
applicationName: "@Mina Influence",
referrer: "origin-when-cross-origin",
// Additional Info
authors: [
{ name: "Mina" },
{ name: "@Mina Influence", url: "https://minayu.site" },
],
creator: "Mina",
publisher: "Mina",
// Safari or mobile browsers will detect if match the format of Email, Address, phone.
formatDetection: {
email: false,
address: false,
telephone: false,
},
// Robots.txt for Google Crawler
robots: {
index: true,
follow: true,
nocache: false,
googleBot: {
index: true,
follow: true,
noimageindex: false,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
// for multilingual settings.
alternates: {
canonical: "https://minayu.site/about",
languages: {
en: "https://minayu.site/en/about",
},
},
// openGraph (OG) & Twitter Tags
openGraph: {
title: "About - @Mina Influence",
description: "Record the information about this site.",
url: "https://minayu.site/about",
type: "article",
publishedTime: "",
authors: ["Mina Yu"],
siteName: "@Mina Influence",
images: [
{
url: "https://<test-website>/static/img.jpg", // Must be an absolute URL
width: 1200,
height: 630,
},
],
locale: "zh-TW",
},
// Twitter
twitter: {
card: "summary",
title: "About - @Mina Influence",
description: "Record the information about this site.",
siteId: "@MingJungYU",
creator: "@MingJungYU",
creatorId: "",
images: {
url: "https://<test-website>/static/img.jpg",
alt: "@Mina Influence Image",
},
},
};
2. Pages need SEO should use SSR or SSG
There are several frontend rendering methods like CSR, SSR, SSG, ISR, and Hydration. If you're not focused on frontend development, they can be hard to grasp. Let's skip the technical differences and remember the key point:
Pages that need SEO should use SSR or SSG.
If you're just building a personal website and unfamiliar with rendering methods, it's easier to use a Jamstack framework like Hugo that uses SSG by default. Choose a popular one with good community support.
For Next.js, configure the project like this for static export:
1. Configuration in next.config.mjs
const nextConfig = {
output: "export",
distDir: "out",
// If add this line, the generated structure will be /blog/index.html or /2024/05/11/post/index.html when visit /blog/ or /blog, Github Page will direct to /blog/index.html。
trailingSlash: true,
// Change to your host url when deploying, this will direct to corract CSS/JS routes.
assetPrefix: isDev ? "" : "<hostname>",
};
export default nextConfig;
Run npm run build
to generate the static site in the /out/
folder.
2. Define static route paramters
All routes must be pre-generated. Next.js will automatically create static paths based on your folder structure.
If using dynamic routes like [id]
or [...routes]
, you must define them using generateStaticParams
. Otherwise, the page won't be found (404 error).
Examples:
// /divination/[method]/layout.js
const METHOD = ["runes", "tarot", "lenormand"];
export async function generateStaticParams() {
const params = [];
METHODS.forEach((method) => {
params.push({ method });
});
return params;
}
export default async function Layout({ children }) {
return children;
}
// /[...routes]/layout.js
export async function generateStaticParams() {
const allPosts = await getAllPosts();
const posts = allPosts.map((post) => {
// For loop each post and define the route path
return {
routes: [post.lang, post.year, post.month,post.slug ];,
};
});
// data:
// [
// {routes: ['2024', '05', '11', 'my-first-post']},
// {routes: ['2024', '06', '11', 'my-second-post']},
// {routes: ['2024', '07', '02', 'my-third-post']}, ...
// ]
return [...posts];
}
3. Avoid whole page CSR
If you add "use client"
to the entire page, it becomes CSR and loses SEO benefits. Instead:
Keep the main page as SSR/SSG
Extract components that use hooks (e.g., Carousel) into separate files with "use client"
, then import them back.
// carousel.js
"use client";
export function Carousel() {
return <>{/* TODO: Carousel */}</>;
}
// page.js
import { Carousel } from "@app/component/carousel";
export default function Page({ params: { lng } }) {
return (
<>
<Header />
<Navbar />
<Carousel />
<Posts />
<Categories />
<Footer />
</>
);
}
4. Check pages successfully statically generated
How to check if your page was successfully statically generated:
- Run
npm run build
to generate static site. - Open the HTML file in
/out/
with a text editor (not a browser) - If you see
<head>
metadata and full<body>
content, it's a valid static site (Make sure the metadata in head tag set correspondingly with you defined not the default value set in layout.js. Next.js will use the default or metadata in home page if it gets defined metadata failed.) - If you only see a single empty
<div>
, it's CSR and won't be SEO-friendly
3. Suggestion: Avoid using Chinese in URL routes (slug)
Since I migrated my blog several times, some posts (converted from WordPress .md files) didn’t have defined slugs and used Chinese filenames instead. This caused encoding issues.
- URL appears as:
/2024/05/11/我的第一篇文章
- But becomes:
/2024/05/11/%E6%88%91%E7%9A%84%E7%AC%AC%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0
in actual requests
Browsers and frameworks like Next.js try to handle encoding, but I still ran into decoding issues when building static pages. So I decided to define all slugs in English to avoid errors.
Also, Chinese URLs don’t perform well in SEO. I recommend defining English slugs manually:
/2024/05/11/我的第一篇文章
→/2024/05/11/my-first-post
/blog/categories/旅行日記
→/blog/categories/travel-diary
That way, you avoid encoding problems and improve SEO.
If your framework handles Chinese URLs flawlessly, no need to change. I only did this after hitting multiple encoding issues.
4. Enable sitemap.xml and robots.txt for SEO
sitemap
A sitemap helps search engines understand your site's structure and update frequency. It’s an XML file usually located at the root: public/sitemap.xml
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/</loc>
<lastmod>2025-06-03</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
To auto-generate it in Next.js, use the next-sitemap package.
1. Create next-sitemap.config.js
and configure it
2. additionalPaths: additional path needs to define or re-define existing paths.
The sitemap URLs automatically generated by the next-sitemap package with the current date. I don't know how to set the lastmod
for different websites.
I checked the official documentation and found that you can overwrite the information of the existing paths by adding the paths again in additionalPaths
, no duplicate path issues.
So I overwrite the posts lastmod by re-defining the paths and information as follows:
module.exports = {
siteUrl: "https://<your-domain>",
generateRobotsTxt: true,
outDir: "public",
changefreq: "weekly",
priority: 0.8,
exclude: ["/blog/page/*", "/blog/category/*/page/*"],
additionalPaths: async (config) => {
// In layout.js, I have defined the paths before, but I want to overwrite the lastmod based on post date after auto-generating with current date.
const posts = await getAllPosts();
return posts.map((post) => {
const prefix = post.lang === "en" ? "/en" : "";
return {
loc: `${prefix}/${post.year}/${post.month}/${post.slug}`,
lastmod: post.date.toISOString(),
};
});
},
};
3. Add prebuild sitemap to your package.json scripts:
"scripts": {
"prebuild": "next-sitemap",
"build": "next build"
}
4. Running npm run build will generate the sitemap before building the static site.
robot.txt
The robots.txt
file tells search engines which paths to crawl. You can either:
Auto-generate it with next-sitemap (set generateRobotsTxt: true
), or
Create it manually in your public/
folder:
User-agent: *
Allow: /
Host: https://<your-domain>
Sitemap: https://<your-domain>/sitemap.xml
Make sure to replace the host with your actual domain and adjust exclusions if needed.
5. Deployment Settings: assetPrefix, basePath, trailingSlash, CNAME, .nojekyll
When deploying your site, the URL will differ from the local http://localhost:3000
. It might use a custom domain or have a subpath like /staging/
.
1. CNAME
If you're using a custom domain, create a CNAME
file in the public
folder (no file extension) with your domain as the only line inside.
2. .nojekyll
Next.js puts assets in _next/
. GitHub Pages uses Jekyll by default, which ignores folders starting with _
. To fix this, create a .nojekyll
file (empty, no extension) in the public
folder.
3. Next.js Config
// next.config.mjs
const nextConfig = {
output: "export",
distDir: "out",
trailingSlash: true, // Ensures URLs like /blog/ work properly
assetPrefix: isDev ? "" : "https://<hostname>/", // For static asset URLs
// basePath: "/staging", // Use this if your site is deployed under a subpath
};
export default nextConfig;
If images or CSS/JS files aren’t loading, check your config of assetPrefix
and make sure .nojekyll
is in place.
6. SEO Monitoring Tools: GA & Search Console
Once your site is live, use these tools to track traffic and SEO performance:
Google Analytics (GA)
Generate a GA tracking ID and add this code in your Next.js layout <head>
. Replace G-XXXXXXXXXX with your GA ID:
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
{/* GA4 Script */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
strategy="afterInteractive"
/>
<Script id="ga4-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX', {
page_path: window.location.pathname,
});
`}
</Script>
</head>
<body>{children}</body>
</html>
);
}
Google Search Console
Submit your sitemap URL in the Search Console to help Google index your site correctly. Auto monitoring SEO metrics through the tool.
Final Thoughts
I’m not an expert in frontend or SEO—just sharing my personal experience. I hope this guide helps you deploy your Next.js site and improve its SEO effectively!