Improve your SEO score from F to A
An SEO score of F (< 60/100) means your pages lack the structural metadata search engines require for indexing. Indxel evaluates 20 rules against a 100-point scale. Missing a <title> tag (title-present) costs 5 points. Omitting a canonical URL (canonical-url) drops 10 points. Missing an Open Graph image (og-image) deducts 8 points. If your site scores an F, you are failing multiple critical checks simultaneously, guaranteeing poor indexing and broken social sharing previews.
You fix an F score using a strict priority queue. First, resolve the 5 critical errors (title-present, description-present, og-image, canonical-url, h1-present). These account for 36 points and trigger CI pipeline failures. Fixing them immediately pushes an F (e.g., 54/100) to a B (90/100). Second, resolve high-weight warnings (5+ points) like title-length and image-alt-text. Finally, clear the remaining low-weight warnings to achieve a perfect 100/100 A grade.
How do you detect a failing SEO score?
Run the Indxel CLI against your local development server or production URL. The CLI outputs your aggregate score, grades the run, and lists every rule violation by severity and file path.
npx indxel check http://localhost:3000Indxel Validation Report
========================
URL: http://localhost:3000
Pages scanned: 47
Score: 54/100 (Grade: F)
Status: FAILED
Critical Errors (0 points awarded):
✖ title-present Missing <title> tag in <head> /blog/how-to-code
✖ description-present Missing <meta name="description"> /blog/how-to-code
✖ canonical-url Missing <link rel="canonical"> /pricing
✖ og-image Missing <meta property="og:image"> /about
Warnings (50% points awarded):
⚠ title-length Title exceeds 60 characters (68) /blog/how-to-code
⚠ image-alt-text Missing alt attribute on <img> /features
Run `npx indxel check --help` for more options.Critical rule violations (Status: ERROR) award 0 points. Warnings award 50% of the rule's available points. You cannot achieve an A grade (90+) until all critical errors are resolved.
How to fix an F score systematically
Stop guessing which tags matter. Fix the 5 critical rules first. These rules carry the heaviest point weights and act as hard blockers for crawler indexing.
1. Next.js App Router (React Server Components)
In the Next.js App Router, metadata is handled via the exported metadata object or generateMetadata function. A common cause for an F score is relying on the root layout.tsx for all metadata, resulting in duplicate or missing tags on deep routes.
Bad: Returning an empty page or relying solely on default layout metadata. This triggers title-present, description-present, and canonical-url errors.
// app/blog/[slug]/page.tsx
// ❌ SCORE: F (Missing specific title, description, og-image, canonical)
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}Good: Exporting generateMetadata to populate all 5 critical rules. This immediately recovers 36 points.
// app/blog/[slug]/page.tsx
// ✅ SCORE: A (All criticals and high-weight warnings satisfied)
import { Metadata } from 'next';
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const post = await getPost(params.slug);
const url = `https://yourdomain.com/blog/${params.slug}`;
return {
title: post.title, // Satisfies title-present (5pts)
description: post.excerpt, // Satisfies description-present (5pts)
alternates: {
canonical: url, // Satisfies canonical-url (10pts)
},
openGraph: {
title: post.title,
description: post.excerpt,
url: url,
siteName: 'Your SaaS',
images: [
{
url: post.ogImageUrl, // Satisfies og-image (8pts)
width: 1200,
height: 630,
},
],
type: 'article',
},
};
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return (
<article>
{/* Satisfies h1-present (8pts) */}
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}2. Next.js Pages Router
If you maintain a Pages Router application, you must inject metadata using the next/head component.
Bad: A page missing the <Head> component entirely.
// pages/features.tsx
// ❌ SCORE: F
export default function Features() {
return (
<div>
<h2>Our Features</h2>
<p>Fast and reliable.</p>
</div>
);
}Good: Injecting the exact tags required to pass the critical Indxel checks.
// pages/features.tsx
// ✅ SCORE: A
import Head from 'next/head';
export default function Features() {
return (
<>
<Head>
<title>Features | Your SaaS</title>
<meta name="description" content="Explore the features that make our platform fast and reliable." />
<link rel="canonical" href="https://yourdomain.com/features" />
<meta property="og:image" content="https://yourdomain.com/images/og-features.png" />
</Head>
<main>
<h1>Our Features</h1>
<p>Fast and reliable.</p>
</main>
</>
);
}3. Plain HTML
If you generate static HTML, you must physically write the tags into the <head> of your document.
<!-- ✅ SCORE: A -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- title-present (5pts) -->
<title>Pricing | Your SaaS</title>
<!-- description-present (5pts) -->
<meta name="description" content="Simple, transparent pricing for teams of all sizes.">
<!-- canonical-url (10pts) -->
<link rel="canonical" href="https://yourdomain.com/pricing">
<!-- og-image (8pts) -->
<meta property="og:image" content="https://yourdomain.com/assets/og-pricing.png">
</head>
<body>
<!-- h1-present (8pts) -->
<h1>Pricing Plans</h1>
</body>
</html>4. The Indxel SDK approach
The fastest way to guarantee an A score across your entire application is to use the Indxel SDK. The defineSEO utility enforces the presence of critical tags at compile time via TypeScript types. If you forget the canonical URL, your build fails before Indxel even runs.
// lib/seo.ts
import { defineSEO } from '@indxel/core';
export const getSeo = defineSEO({
siteName: 'Your SaaS',
baseUrl: 'https://yourdomain.com',
defaultOgImage: '/images/default-og.png',
});// app/about/page.tsx
import { getSeo } from '@/lib/seo';
export const metadata = getSeo({
title: 'About Us',
description: 'Learn about our mission to build developer-first SEO tools.',
path: '/about', // Automatically resolves to canonical: https://yourdomain.com/about
// ogImage falls back to defaultOgImage, satisfying the og-image rule
});
export default function About() {
return <h1>About Us</h1>;
}How to prevent score regressions in CI
Once you reach a 90+ score, lock it in. Add the --ci flag to your build pipeline. CI mode ignores the 0-100 numeric score and evaluates strict rule severity. If any of the 5 critical rules return an ERROR status, the CLI exits with code 1 and fails your build.
Do not use the standard npx indxel check in CI. A missing favicon drops your score to 98/100, which is technically a failure if you enforce a perfect score. The --ci flag ensures your build only fails on critical structural errors that actually harm search indexing.
GitHub Actions Integration
Add Indxel to your test or build workflow. This ensures no developer can merge a PR that introduces an F score.
# .github/workflows/seo-check.yml
name: Indxel SEO Guard
on:
pull_request:
branches: [main]
jobs:
validate-seo:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Start local server
run: npm run start &
env:
PORT: 3000
- name: Wait for server
run: npx wait-on http://localhost:3000
- name: Run Indxel CI guard
run: npx indxel check http://localhost:3000 --ciVercel Build Integration
To block bad deployments directly on Vercel, modify your build command in vercel.json or the Vercel dashboard.
{
"buildCommand": "next build && npx indxel check .next/server/app --ci"
}What edge cases cause persistent F scores?
Fixing the static tags is straightforward, but dynamic applications introduce edge cases that bypass basic implementations.
1. Dynamic route fetching failures
If generateMetadata fetches data from a CMS or database, network failures or missing records often result in null metadata. Next.js gracefully falls back to the root layout, but the canonical URL and specific Open Graph images will be missing, triggering critical errors.
Fix: Always provide explicit fallbacks in your dynamic metadata functions.
export async function generateMetadata({ params }): Promise<Metadata> {
try {
const data = await fetchCMS(params.slug);
return buildMetadata(data);
} catch (error) {
// Fallback prevents F score on fetch failure
return {
title: 'Post Not Found',
description: 'This content is unavailable.',
alternates: { canonical: `https://yourdomain.com/blog/${params.slug}` }
};
}
}2. Client-side rendering (SPA)
If you render your application entirely on the client (e.g., standard React via Vite) without Server-Side Rendering (SSR) or Static Site Generation (SSG), the Indxel crawler sees an empty <div id="root"></div>. All critical rules will fail because the tags do not exist in the initial HTML payload.
Fix: Migrate to Next.js/Remix, or implement pre-rendering during your Vite build process so crawlers receive populated HTML.
3. Duplicate canonicals on paginated routes
Applying a single canonical URL to an entire paginated series (e.g., /blog?page=1, /blog?page=2) using the root /blog URL causes search engines to ignore pages 2 and beyond. Indxel catches this if the canonical URL does not match the exact resolved path.
Fix: Ensure your canonical URL builder includes search parameters if they mutate the page content.
const url = searchParams.page
? `https://yourdomain.com/blog?page=${searchParams.page}`
: `https://yourdomain.com/blog`;Related rules
An F score is rarely caused by a single rule violation. It is an aggregate failure. When fixing a low score, you will frequently interact with these specific Indxel rules:
missing-title: Triggers when the<title>tag is completely absent.missing-meta-description: Triggers when the<meta name="description">tag is omitted.missing-canonical-url: Triggers when<link rel="canonical">is absent, risking duplicate content penalties.missing-og-image: Triggers when social preview images are undefined.missing-h1: Triggers when the page lacks a primary<h1>heading.
FAQ
How is the Indxel score calculated?
The Indxel score is a 100-point system distributed across 20 rules. Passed rules receive full points. Warnings receive 50% of their point value. Critical errors receive 0 points. Grades are assigned mathematically: A >= 90, B >= 80, C >= 70, D >= 60, F < 60.
What score do I need to pass CI?
CI mode (--ci) does not evaluate the numeric score. It fails the build if and only if any critical rule has an ERROR status. The 5 critical rules are title-present, description-present, og-image, canonical-url, and h1-present.
Can I disable specific rules?
Yes. You can bypass specific checks by passing the disabledRules array to the validation options or defining them in your indxel.config.json file. Disabling critical rules means CI will not catch those errors, exposing your production site to indexing failures.
{
"disabledRules": ["image-alt-text", "favicon"]
}Why does my score drop on dynamic routes?
Dynamic routes often lack hardcoded metadata and rely on asynchronous data fetching. If the fetch fails, or if the developer forgets to map the fetched data to the metadata object (specifically the canonical URL and Open Graph properties), the route fails critical checks and the score plummets. Ensure generateMetadata covers all 5 critical rules.
Frequently asked questions
How is the Indxel score calculated?
Each of the 20 rules has a weight (totaling 100). Passed rules get full credit. Warnings get 50% credit. Errors get 0 credit. Grades: A >= 90, B >= 80, C >= 70, D >= 60, F < 60.
What score do I need to pass CI?
CI mode (--ci flag) doesn't use the numeric score. It fails the build if any critical rule has status ERROR. There are 5 critical rules: title-present, description-present, og-image, canonical-url, and h1-present.
Can I disable specific rules?
Yes. Pass disabledRules to the validation options or use the CLI config file. But disabling critical rules means CI won't catch those errors.