I spent four months building a React app. Polished UI, fast load times, clean code. Then I checked Google Search Console and nearly threw my laptop out the window. Zero indexed pages. Zero. Turns out I had been shipping a beautifully crafted ghost town, and every JavaScript developer I've talked to has hit this wall at least once.
If you're building with React, Next.js, Vue, or any JS-heavy stack, the SEO checklist for JavaScript apps is different from what most tutorials teach. The WordPress-era advice will quietly mislead you. Let me share the exact checklist I now run through on every project before it ships.
SEO Checklist Step 1: Solve the JavaScript Rendering Problem First
The first item on any SEO checklist for JavaScript developers is rendering. Googlebot can execute JavaScript, but it does not always wait for it. By the time it crawls your page, your useEffect() has not fired, your API call has not returned, and your <title> tag is still an empty string.
Run this quick audit to check what Googlebot actually sees:
# Simulate Googlebot's initial HTML fetch (no JS execution)
curl -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
https://yoursite.com | grep -E "<title>|<meta name=\"description\"|<h1>"
If that command returns empty tags or nothing at all, you have a rendering problem. The fix depends on your stack, but the three paths forward are:
- SSR (Server-Side Rendering): render HTML on the server per request (Next.js, Nuxt, SvelteKit)
- SSG (Static Site Generation): pre-render at build time for content that does not change per user
- Dynamic rendering: serve pre-rendered HTML to bots, client-rendered pages to users
For most content-driven JavaScript apps, SSG is the sweet spot. Here is the Next.js pattern:
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
const post = await fetchPost(params.slug);
return {
props: {
title: post.title,
description: post.excerpt,
content: post.body,
},
revalidate: 60, // ISR: regenerate every 60 seconds
};
}
export async function getStaticPaths() {
const posts = await fetchAllSlugs();
return {
paths: posts.map((p) => ({ params: { slug: p.slug } })),
fallback: "blocking",
};
}
Result: Googlebot fetches fully-formed HTML. Title, description, and H1 are all present before a single line of client JavaScript runs. Rendering is always step one on the SEO checklist for JavaScript projects.
SEO Checklist Step 2: Meta Tags on Every Route, Not Just the Homepage
Every page in your JavaScript app needs a minimum viable set of meta tags. Not just the homepage, every dynamic route. This is one of the most commonly skipped items on any JavaScript SEO checklist.
Here is a copy-paste template for Next.js using the App Router:
// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
const post = await fetchPost(params.slug);
return {
title: `${post.title} | My Site`,
description: post.excerpt.slice(0, 155), // Google truncates at ~155 chars
openGraph: {
title: post.title,
description: post.excerpt,
url: `https://mysite.com/blog/${params.slug}`,
type: "article",
publishedTime: post.createdAt,
images: [
{
url: post.ogImage || "https://mysite.com/default-og.png",
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
},
alternates: {
canonical: `https://mysite.com/blog/${params.slug}`,
},
};
}
What each field does for your JavaScript SEO:
-
title: shows in SERPs and browser tabs; keep under 60 characters -
description: your pitch in search results; Google may rewrite it, but write it anyway -
canonical: prevents duplicate content penalties when your content appears at multiple URLs -
openGraph: controls how your link looks when shared on Slack, LinkedIn, and Discord
Quick validation check: paste your URL into [opengraph.xyz] to see what social platforms will render before you go live.
SEO Checklist Step 3: Structured Data, the Signal Most JavaScript Devs Skip
Schema markup is JSON-LD you embed in your page that tells Google exactly what type of content it is looking at. Blog post? Product? Recipe? FAQ? Google uses structured data to generate rich results like star ratings, article dates, and breadcrumbs that push your JavaScript app's listing above plain blue links.
This is consistently the most skipped item on the SEO checklist for JavaScript projects, and it has one of the highest return-on-effort ratios.
Here is a reusable helper for article schema:
// lib/schema.js
export function generateArticleSchema({
title,
description,
datePublished,
dateModified,
author,
url,
imageUrl,
}) {
return {
"@context": "https://schema.org",
"@type": "Article",
headline: title,
description: description,
datePublished: datePublished,
dateModified: dateModified || datePublished,
author: {
"@type": "Person",
name: author,
},
publisher: {
"@type": "Organization",
name: "My Site",
logo: {
"@type": "ImageObject",
url: "https://mysite.com/logo.png",
},
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": url,
},
image: imageUrl,
};
}
// Usage in your page component
export default function BlogPost({ post }) {
const schema = generateArticleSchema({
title: post.title,
description: post.excerpt,
datePublished: post.createdAt,
author: post.authorName,
url: `https://mysite.com/blog/${post.slug}`,
imageUrl: post.ogImage,
});
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
{/* rest of your page */}
</>
);
}
Validate it at [Google's Rich Results Test]. It shows you exactly what Google sees and flags any schema errors before they hurt your rankings.
SEO Checklist Step 4: Automate the Audit So Nothing Gets Skipped
The real problem with any SEO checklist for JavaScript apps is not knowing what to check — it is remembering to check it before every single deploy. I started automating my SEO audit as part of my CI pipeline, and I have not shipped a missing meta tag since.
I came across power-seo while looking for a Node.js-compatible audit tool I could run headlessly in CI. It checks meta completeness, structured data validity, canonical tags, and more from a single CLI call, which made it easy to drop into a pre-deploy script:
npm install -D @power-seo/core
// scripts/seo-audit.js
const { audit } = require("@power-seo/core");
const urls = [
"https://staging.mysite.com/",
"https://staging.mysite.com/blog/my-first-post",
"https://staging.mysite.com/about",
];
async function runAudit() {
for (const url of urls) {
const result = await audit(url);
if (result.score < 80) {
console.error(`SEO score too low for ${url}: ${result.score}`);
process.exit(1); // Fail the CI step
}
console.log(`SEO OK: ${url} scored ${result.score}`);
}
}
runAudit();
Add it to your package.json:
{
"scripts": {
"seo:audit": "node scripts/seo-audit.js",
"predeploy": "npm run seo:audit"
}
}
Now every npm run deploy runs the SEO checklist for your JavaScript app automatically. If any page scores below your threshold, the deploy stops. No more invisible pages slipping through.
Key Takeaways
-
Rendering is the foundation. No amount of perfect meta tags saves you if Googlebot sees an empty
<div id="root"></div>. Solve SSR or SSG before anything else on the checklist. - Every route needs its own meta tags. Dynamic JavaScript pages (blog posts, product pages, user profiles) need programmatic meta generation, not hardcoded titles.
- Structured data is low-effort, high-reward. Article schema takes 20 minutes to implement and can earn rich results that significantly improve your click-through rate.
- Automate the checklist. Human checklists get skipped under deadline pressure. A CI check that fails the build does not get skipped.
If you want to try the automated SEO checklist approach for your JavaScript app, here's the repo: [https://github.com/CyberCraftBD/power-seo]
Your Turn
What is the sneakiest SEO bug you have shipped to production in a JavaScript app? Mine was a noindex meta tag left over from a staging environment that made it all the way to launch, three weeks before I noticed.
Drop your war story in the comments. I am genuinely curious: does the rendering issue or the missing meta tags cause more grief for JavaScript developers in practice? And if you are on Nuxt, SvelteKit, or Remix, did these patterns translate cleanly for you, or did you have to solve it a different way?
Top comments (0)