Fix: Missing H1 heading
Missing an H1 heading forces search engine crawlers to parse your body text and guess your page's primary topic. The <h1> tag is the highest-weight semantic element in the HTML document body. When you omit it, you lose control over how Google maps search queries to your document.
Developers building heavily componentized React or Vue applications frequently strip semantic HTML in favor of utility classes. If you rely on a <div> styled with Tailwind classes like text-5xl font-bold tracking-tight, crawlers read it as standard paragraph text.
The Indxel h1-present rule treats a missing H1 as a critical failure with an 8/100 severity weight. It blocks CI builds. Fixing this isn't about typography; it's about providing an explicit, machine-readable topic definition for the Googlebot parser. Without it, you bleed relevance on your primary keywords and force search engines to auto-generate snippets based on unpredictable DOM traversal.
How do you detect missing H1s?
Run npx indxel check against your local build or live URL to flag pages missing an <h1> element. The CLI parses the final rendered DOM, catching missing tags caused by client-side rendering delays or missing props.
npx indxel check http://localhost:3000
Running Indxel SEO audit...
❌ /pricing
h1-present: Missing <h1> heading on page.
Fix: Add exactly one semantic <h1> element.
❌ /blog/how-to-ship
h1-present: Styled text detected instead of semantic heading.
Found: <div class="text-4xl font-bold">...</div>
Score: 92/100
2 critical errors found. Build failed.The h1-present rule is marked as critical. By default, any page returning a missing H1 will exit the Indxel CLI with a non-zero status code (exit 1), deliberately failing your build pipeline.
How do you fix a missing H1?
Replace your heavily styled <div> or <p> tags with a semantic <h1> element that contains your target keyword. The CSS styling can remain identical. Search engines read the HTML tag, not the CSS tree.
The CSS utility class trap
The most common cause of the h1-present error is abstracting typography into generic components that default to div or span.
// ❌ BAD: Visually a heading, semantically a paragraph
export function PageHeader({ title }: { title: string }) {
return (
<div className="text-4xl font-extrabold text-gray-900 tracking-tight">
{title}
</div>
);
}
// ✅ GOOD: Exact same styling, correct semantics
export function PageHeader({ title }: { title: string }) {
return (
<h1 className="text-4xl font-extrabold text-gray-900 tracking-tight">
{title}
</h1>
);
}Next.js App Router implementation
In the Next.js App Router, developers often mistakenly place the H1 in a shared layout.tsx file. This violates the multiple-h1 rule if child pages also define an H1, or forces every route sharing that layout to have the exact same H1 topic.
The H1 belongs in the page.tsx file, mapping exactly to the specific route's content.
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { getPost } from '@/lib/db';
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return (
<article className="max-w-2xl mx-auto py-10">
<header className="mb-8">
{/* The H1 is specific to this exact URL */}
<h1 className="text-3xl font-bold text-slate-900">
{post.title}
</h1>
<time className="text-sm text-slate-500">{post.date}</time>
</header>
<div className="prose">
{post.content}
</div>
</article>
);
}Syncing H1 with the Indxel SDK
While the H1 lives in the document body and the <title> lives in the <head>, they should be tightly coupled. If your H1 says "Next.js Authentication" but your title tag says "Login Tutorial", search engines receive conflicting topic signals.
When using the Indxel SDK to generate metadata, pass the exact same string to your H1 component to ensure a 1:1 signal match.
// app/features/analytics/page.tsx
import { defineSEO } from '@indxel/sdk';
const PAGE_TOPIC = "Real-time SEO Analytics Dashboard";
export const metadata = defineSEO({
title: PAGE_TOPIC,
description: "Monitor your core web vitals and crawl errors in real-time.",
});
export default function AnalyticsFeaturePage() {
return (
<main>
{/* Sync the DOM heading perfectly with the head metadata */}
<h1 className="text-5xl font-black">{PAGE_TOPIC}</h1>
<p>Dashboards that update before Google Search Console does.</p>
</main>
);
}How do you prevent missing H1s in CI?
Add npx indxel check --ci to your build pipeline to fail builds when pull requests introduce pages without an H1. This guarantees no unoptimized page ever reaches production.
GitHub Actions
Add the Indxel CLI check as a step in your CI workflow. It runs against your local build output before deployment.
name: SEO Guard
on: [push, pull_request]
jobs:
validate-seo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Run Indxel SEO Check
# Scans the .next build output directly
run: npx indxel check .next --ci --diffUsing the --diff flag ensures Indxel only audits pages modified in the current pull request. This reduces CI time to milliseconds and prevents legacy pages from blocking new feature work.
Vercel Build Command
If you bypass GitHub Actions and deploy directly to Vercel, intercept the build command in your project settings.
Navigate to Settings > General > Build & Development Settings and override the Build Command:
npx indxel check ./src && next buildIf the Indxel check fails due to an h1-present error, the Vercel build aborts immediately, preventing the degraded page from going live.
What are common H1 edge cases?
Developers frequently break the h1-present rule on dynamic routes where data fetching fails, resulting in an empty heading tag.
Empty tags evaluate as missing
If your database query returns an empty string or undefined for a title prop, React renders <h1></h1>. The Indxel parser flags empty tags as h1-present: ERROR. The tag must contain text nodes.
// ❌ BAD: Renders <h1></h1> if post.title is undefined
export default function Page({ post }) {
return <h1 className="text-xl">{post?.title}</h1>;
}
// ✅ GOOD: Provides a fallback or handles the error boundary
export default function Page({ post }) {
if (!post) return <NotFound />;
return <h1 className="text-xl">{post.title || "Untitled Post"}</h1>;
}Visually hidden H1s
Design systems occasionally require pages without a visible title, such as landing pages dominated by hero videos. Removing the H1 entirely degrades SEO. The correct approach is to render the H1 but hide it visually using CSS techniques that preserve screen reader and crawler access.
Do not use display: none or visibility: hidden. Googlebot ignores elements with these properties. Use the sr-only utility class.
// ✅ GOOD: Accessible and indexable, but visually hidden
export default function VideoLandingPage() {
return (
<main>
<h1 className="sr-only">Enterprise Video Hosting Platform</h1>
<HeroVideoPlayer src="/hero.mp4" />
</main>
);
}The sr-only class applies position: absolute, width: 1px, height: 1px, and clip: rect(0, 0, 0, 0). Crawlers parse this text normally, and screen readers announce it, but it takes up zero visual space in your layout.
Client-side Hydration Delays
If your application is an SPA (Single Page Application) that fetches the page title via a client-side useEffect, the H1 might not exist in the initial HTML payload. While Googlebot executes JavaScript, there is a rendering queue delay. To guarantee immediate indexing, H1s must be server-side rendered (SSR) or statically generated (SSG).
Related rules
The h1-present rule operates alongside several other document structure checks in the Indxel suite:
multiple-h1: Flags pages containing more than one<h1>tag. While HTML5 technically allows multiple H1s, it dilutes the primary topic signal.missing-title: Verifies the<head>contains a<title>tag. The title tag and H1 should ideally carry the same intent.thin-content: Fails pages with an H1 but fewer than 300 words of indexable body text.
FAQ
Should every page have an H1?
Yes. Every indexable page must have exactly one H1 heading. It acts as the definitive, on-page topic declaration for search engines and assistive technologies.
Can I have more than one H1?
Technically yes, but it dilutes the topic signal and triggers the Indxel multiple-h1 warning. Limit yourself to one H1 per page and use H2-H6 tags to structure sub-sections hierarchically.
Does the H1 have to match the title tag exactly?
No, but they must align in intent. The title tag optimized for the SERP (Search Engine Results Page) can be 60 characters, while the H1 optimized for the page UI can be longer or slightly rephrased.
Does Google care if my H1 is an image?
Yes. If you wrap an <h1> around an <img> tag (like a company logo), you must provide descriptive alt text on the image. Indxel will flag an H1 containing an image with a missing or empty alt attribute as a failure, because crawlers extract no text value from it.
Frequently asked questions
Should every page have an H1?
Yes. Every indexable page should have exactly one H1 heading. It's the strongest on-page signal for your page's main topic.
Can I have more than one H1?
Technically yes, but it's not recommended. Multiple H1s dilute the topic signal. Indxel flags multiple H1s as a warning. Use H2-H6 for sub-sections.