HTML for SEO: The Tags, Page Structure and CSS That Matter
Updated November 2, 2026: removed previously reported platform-data figures pending verification of the supporting methodology and publication permissions.
Use clear HTML structure, accurate metadata, accessible content and responsive CSS for your SEO pages.
HTML for SEO comes down to a handful of tags (title, meta description, canonical, robots, hreflang, headings, alt text, links and structured data) plus a page structure that puts the main content in clean, semantic, server-rendered HTML. Get those right and search engines and AI crawlers can read, index and quote the page; get one wrong and it can drop out of results entirely.
Most technical SEO problems end up as a line of HTML: a stray noindex, a canonical pointing at the wrong URL, a navigation built from <div onclick> that crawlers cannot follow, an image without dimensions that pushes the page around as it loads. If you can read the markup, you can find these yourself and write a ticket a developer can act on in one pass.
You do not need to build pages to do SEO, but you do need to read them. This guide covers the HTML and CSS that affect crawling, indexing, snippets, page experience and AI answers, in roughly the order you would check them on a page.

The HTML tags that matter for SEO
A short list of tags does most of the work. Everything else on the page (divs, spans, classes) is invisible to ranking systems except as a container for the text inside it.
| Tag | What it does for SEO | Example |
|---|---|---|
| Title | Main input for the clickable headline in search results and a strong signal of the page topic. One per page, unique. | <title>Wide Trail Running Shoes | Example</title> |
| Meta description | Often used as the snippet under the title. Not a ranking factor, but it shapes clicks. | <meta name="description" content="Wide-fit trail shoes in sizes 6-14."> |
| Headings (h1 to h6) | Give the page an outline. The h1 states the topic; h2 and h3 mark sections that search engines and AI systems can match to queries. | <h1>How to Choose Wide Trail Shoes</h1> |
| Canonical | Tells search engines which URL is the preferred version when the same content exists at several URLs. | <link rel="canonical" href="https://www.example.com/shoes/"> |
| Meta robots | Controls indexing and snippets: noindex, nofollow, nosnippet, max-image-preview. Omit it for normal pages. | <meta name="robots" content="noindex"> |
| Hreflang | Points search engines to the right language or regional version of a page. | <link rel="alternate" hreflang="de" href="https://www.example.com/de/"> |
| Image alt | Describes an image for screen readers and is a main signal for image search. | <img src="shoe.webp" alt="Grey wide-fit trail shoe" width="800" height="533"> |
| Links (a href) | How crawlers discover pages and how link signals flow. Anchor text describes the target page. | <a href="/shoes/trail-wide/">Wide trail shoes</a> |
| Structured data script | JSON-LD that describes the page as an Article, Product, FAQ and so on. Makes pages eligible for rich results. | <script type="application/ld+json">{...}</script> |
| Viewport and lang | Viewport makes the mobile layout render properly for mobile-first indexing; lang declares the page language. | <meta name="viewport" content="width=device-width, initial-scale=1"> |
Tags that no longer do anything for Google: meta keywords (ignored for years), rel="next" and rel="prev" (no longer used as an indexing signal), and the choice between <b> and <strong>, which has no documented ranking effect.
An SEO-friendly HTML page structure
Semantic elements tell machines what each part of the page is: the site header, the navigation, the main content, the side content and the footer. Google's Gary Illyes has said semantic structure has little direct effect on rankings, and that is a fair way to think about it. The value is indirect. A page with one <main> and a clear outline is easier for search engines to separate from its template, easier for AI systems to extract and quote, and easier for screen readers to navigate. Here is a complete skeleton, annotated:
<!doctype html>
<html lang="en"> <!-- declares the page language -->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>How to Choose Trail Running Shoes for Wide Feet | Example</title>
<meta name="description" content="Measure your width, compare brand codes, test the fit.">
<link rel="canonical" href="https://www.example.com/guides/wide-trail-shoes/">
<!-- full annotated head in the next section -->
</head>
<body>
<header> <!-- site header: logo is a link, not an h1 -->
<a href="/"><img src="/logo.svg" alt="Example Outdoor" width="120" height="32"></a>
<nav aria-label="Main"> <!-- primary navigation: real a href links -->
<ul>
<li><a href="/shoes/">Shoes</a></li>
<li><a href="/guides/">Guides</a></li>
</ul>
</nav>
</header>
<nav aria-label="Breadcrumb"> <!-- breadcrumb trail, mirrored in BreadcrumbList schema -->
<a href="/">Home</a> / <a href="/guides/">Guides</a> / Wide trail shoes
</nav>
<main> <!-- one main per page: the unique content -->
<article> <!-- self-contained piece: guide, post, product -->
<h1>How to Choose Trail Running Shoes for Wide Feet</h1>
<p>Measure the widest part of your foot, then match it to the
brand's width code (2E or 4E for men, D or 2E for women).</p> <!-- answer first -->
<section> <!-- a thematic block with its own heading -->
<h2>Measure your foot width</h2>
<ol> <!-- steps in an ordered list -->
<li>Stand on paper and trace your foot.</li>
<li>Measure the widest point in millimeters.</li>
</ol>
</section>
<section>
<h2>Width sizes compared</h2>
<table> <!-- comparisons in a real table -->
<tr><th>Brand</th><th>Wide</th><th>Extra wide</th></tr>
<tr><td>Brand A</td><td>2E</td><td>4E</td></tr>
</table>
<h3>Half sizes and width</h3> <!-- h3 sits inside its h2 section -->
<p>…</p>
</section>
<section>
<h2>Frequently asked questions</h2>
<details> <!-- in the HTML, so it is indexed -->
<summary>Do wide shoes run long?</summary>
<p>Usually not. Width and length are sized separately.</p>
</details>
</section>
</article>
</main>
<aside> <!-- related links: outside main -->
<h2>Related guides</h2>
<a href="/guides/trail-shoe-fit/">How trail shoes should fit</a>
</aside>
<footer> <!-- site footer: contact, policies -->
<a href="/contact/">Contact</a>
</footer>
<script type="application/ld+json">{ "@context": "https://schema.org", "@type": "Article" }</script>
</body>
</html>
Landmarks. header, nav, main, aside and footer separate the main content from the template. Use exactly one <main>. Systems that extract the "main content" of a page do a better job when there is one element wrapping it.
article vs section. <article> wraps something that makes sense on its own (a guide, a post, a product). <section> groups a block of related content and should normally have a heading. Neither is a ranking factor; a <div> that holds the same text is indexed the same way. Use them for clarity, not because a plugin scores them.
Lists and tables. Steps belong in <ol>, unordered items in <ul>, comparisons in a real <table> with <th> headers. Google uses list and table markup when it builds featured snippets, and a table built from divs styled to look like one carries none of that structure.
The <head>: an annotated example
This is a complete, valid <head> for an English product page that also exists in German. Every line is something an SEO should be able to check.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- Mobile layout: without this, phones render a zoomed-out desktop page -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Title: the main input for the blue link in search results -->
<title>Trail Running Shoes for Wide Feet | Example Outdoor</title>
<!-- Description: often used as the snippet, not a ranking factor -->
<meta name="description" content="Wide-fit trail shoes in sizes 6-14, tested on wet rock and mud. Free returns within 60 days.">
<!-- Robots: omit entirely for normal pages (default is index, follow) -->
<meta name="robots" content="max-image-preview:large">
<!-- Canonical: absolute URL of the version you want indexed -->
<link rel="canonical" href="https://www.example.com/shoes/trail-wide/">
<!-- Hreflang: every language version, including this one, plus a fallback -->
<link rel="alternate" hreflang="en" href="https://www.example.com/shoes/trail-wide/">
<link rel="alternate" hreflang="de" href="https://www.example.com/de/schuhe/trail-weit/">
<link rel="alternate" hreflang="x-default" href="https://www.example.com/shoes/trail-wide/">
<!-- Open Graph: how the link looks when shared on social and in chat apps -->
<meta property="og:type" content="product">
<meta property="og:title" content="Trail Running Shoes for Wide Feet">
<meta property="og:description" content="Wide-fit trail shoes in sizes 6-14.">
<meta property="og:url" content="https://www.example.com/shoes/trail-wide/">
<meta property="og:image" content="https://www.example.com/img/trail-wide-1200x630.jpg">
<meta name="twitter:card" content="summary_large_image">
<!-- Performance: preload the hero image and the main font -->
<link rel="preload" as="image" href="/img/trail-wide-hero.webp" fetchpriority="high">
<link rel="preload" as="font" href="/fonts/inter-var.woff2" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/css/main.css">
<!-- Structured data: JSON-LD can live in the head or the body -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Trail Running Shoes for Wide Feet",
"image": "https://www.example.com/img/trail-wide-1200x630.jpg",
"offers": {
"@type": "Offer",
"price": "129.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
}
}
</script>
</head>
Head elements that matter, and the rules for each
Title. One per page, unique, describing that page. Google does not publish a character limit; it truncates by pixel width in results, so put the words that matter first. Google can rewrite titles that are vague, stuffed or identical across many pages, and the most common trigger is a title that does not match the page's main heading.
Meta description. Not a ranking factor, but Google often uses it as the snippet when it matches the query. Write a specific summary per page. Duplicated descriptions across thousands of pages are usually ignored.
Meta robots. The default is index, follow, so you only need the tag to change something. Common values: noindex (keep out of results), nofollow (do not follow links on this page), nosnippet, max-snippet:[n] and max-image-preview:large. The same directives can be sent as an X-Robots-Tag HTTP header, which is how PDFs and other non-HTML files get a noindex. A noindex only works if the page can be crawled: if robots.txt blocks the URL, Google never sees the tag.
Canonical. Use an absolute URL and one canonical per page. Google treats it as a strong hint, not a command, and weighs it against redirects, internal links and sitemaps. A canonical in the <body> is ignored. The wider picture is in avoiding duplicate content.
Hreflang. Each language version lists all versions including itself, and every version must link back (return links), or Google may ignore the set. Codes are ISO 639-1 language, optionally with an ISO 3166-1 region: en-GB, not en-UK. Add x-default for the fallback page. Hreflang can also go in the XML sitemap, which is easier to maintain on large sites; see international SEO.
Viewport. Google indexes the mobile version of pages, so a page without width=device-width renders as a shrunken desktop layout on the device Google uses to evaluate it.
Open Graph and Twitter cards. No effect on Google rankings. They control the title, description and image when the URL is shared on LinkedIn, Facebook, X, Slack and messaging apps. A 1200×630 image is the widely used size.
Only title, meta, link, script, style, base, noscript and template belong in the head. The usual culprit is a third-party script that injects an <iframe> or <img> tracking pixel near the top of the head. Keep your SEO tags as early in the head as possible, and check the rendered head (not just the source) for elements that should not be there.
Heading hierarchy: h1, h2, h3
Headings are the outline of the page. Search engines use them to understand what each section covers, and AI systems often use them to decide which chunk of a page answers a question. A good test: read only the headings, top to bottom. If they make sense as a table of contents, the hierarchy is right.
h1 HTML for SEO: The Tags and Structure That Matter
h2 The HTML tags that matter for SEO
h2 An SEO-friendly HTML page structure
h2 Heading hierarchy
h3 How many h1 tags?
h3 Skipping levels
h2 CSS and SEO
One h1. Use a single h1 that states the page topic and matches the title closely. Google has said multiple h1s do not cause a ranking problem, but one clear h1 is the simplest signal and the easiest to audit. The most common real error is the logo or site name wrapped in an h1 on every page.
Nest in order. h2 for main sections, h3 for subsections inside them. Skipping a level (an h4 straight after an h2) will not hurt rankings, but it breaks the outline for screen readers and makes the page harder to audit.
Headings are for structure, not font size. If a designer wants small bold text, that is CSS, not an h4. Sidebar widgets, newsletter boxes and footer columns marked up as headings bury the real outline of the page.
Write headings people search for. A heading phrased the way people ask ("How much HTML does an SEO need?") gives both search engines and AI answers a clear match for that question, as long as the paragraph under it actually answers it.
Links: href, rel attributes and anchor text
Google's documentation is direct on this: it can reliably follow only <a> elements with an href attribute that resolves to a URL.
<!-- Crawlable -->
<a href="/shoes/trail-wide/">Wide trail running shoes</a>
<a href="https://www.example.com/shoes/trail-wide/">Wide trail running shoes</a>
<!-- Not crawlable as links -->
<span onclick="location.href='/shoes/trail-wide/'">Wide trail shoes</span>
<a onclick="goTo('trail-wide')">Wide trail shoes</a>
<a href="javascript:goTo('trail-wide')">Wide trail shoes</a>
<button data-url="/shoes/trail-wide/">Wide trail shoes</button>
<!-- Qualifying outbound links -->
<a href="https://partner.example/" rel="sponsored">Partner offer</a>
<a href="https://forum-user-site.example/" rel="ugc">user's site</a>
<a href="https://unvetted.example/" rel="nofollow">source</a>
A JavaScript click handler on an <a href> is fine: frameworks like Next.js do this for client-side navigation, and the href is still there for crawlers. The problem is a link with no href at all, which is common in custom menus, "load more" buttons and faceted filters.
rel values. sponsored for paid or affiliate links, ugc for links in comments and forum posts, nofollow when you do not want to vouch for the link. Google treats all three as hints. Do not put nofollow on internal links to control crawling; it does not save crawl budget and cuts the link's signal to your own pages.
Anchor text. The clickable text tells search engines what the target page is about. "Wide trail running shoes" is useful; "click here", "read more" and bare URLs are not. Vary it naturally across internal links, and make it make sense out of context. More in internal linking.
Images: alt, dimensions, lazy loading, srcset
<!-- Hero / LCP image: never lazy-loaded, fetched with high priority -->
<img src="/img/trail-wide-800.webp"
srcset="/img/trail-wide-400.webp 400w,
/img/trail-wide-800.webp 800w,
/img/trail-wide-1600.webp 1600w"
sizes="(max-width: 600px) 100vw, 800px"
width="800" height="533"
alt="Grey wide-fit trail running shoe with a lugged rubber sole"
fetchpriority="high">
<!-- Below-the-fold image: lazy-loaded -->
<img src="/img/sole-detail.webp" width="600" height="400" loading="lazy"
alt="Close-up of the 5 mm lugs on the outsole">
<!-- Decorative image: empty alt so screen readers skip it -->
<img src="/img/divider.svg" width="40" height="4" alt="">
alt describes the image for screen readers and is one of Google's main signals for what an image shows. Write what is in the picture, specifically, without stuffing keywords. Decorative images get alt="".
width and height let the browser reserve the right space before the image downloads. Without them, text jumps down when the image arrives, which counts towards Cumulative Layout Shift (CLS). CSS like img { max-width: 100%; height: auto; } keeps the image responsive while the attributes supply the aspect ratio.
loading="lazy" defers off-screen images. Never put it on the main above-the-fold image: delaying it directly worsens Largest Contentful Paint (LCP). Use fetchpriority="high" on that image instead.
srcset and sizes let the browser download a small file on phones and a large one on desktop. Google's documentation also notes that it does not index images set as CSS backgrounds, so any image you want in Google Images must be an <img> (or <picture>) element. More in image optimization.
Structured data: where JSON-LD goes
Google recommends JSON-LD, a <script type="application/ld+json"> block like the one in the head example above. It can sit in the head or anywhere in the body; position makes no difference. Three rules matter more than placement. The markup must describe content that is visible on the page (a price in the schema must match the price on screen). It must be valid JSON: a trailing comma breaks the whole block. And it should be in the server-rendered HTML where possible; Google can read JSON-LD injected by JavaScript, but other consumers may not. Test with Google's Rich Results Test and the Schema Markup Validator, and see structured data for which types earn rich results.
CSS and SEO: the rules that matter
CSS is not a ranking signal in itself. It matters for SEO in two ways: it can hide or remove content, and it drives the page experience metrics (LCP, CLS) and the mobile layout Google evaluates. These are the CSS rules worth knowing.
1. Do not hide text to deceive. Text hidden to manipulate rankings (white on white, positioned off-screen, font-size: 0) is a spam violation under Google's policies.
2. Hiding for usability is fine, if the text is in the HTML. Text inside tabs, accordions and "read more" sections that is present in the HTML and hidden with display:none until clicked is indexed; Google has said that with mobile-first indexing it gives such content full weight. The distinction that matters is whether the text is in the HTML at all. If clicking a tab triggers a JavaScript request that fetches the content, Google will not click it, and the content is not indexed.
<!-- Indexed: content is in the HTML, CSS hides it until the summary is clicked -->
<details>
<summary>Shipping and returns</summary>
<p>Free returns within 60 days. Ships in 1-2 business days.</p>
</details>
<!-- Not indexed: content only arrives after a click -->
<button onclick="loadTab('shipping')">Shipping and returns</button>
<div id="shipping"></div>
3. Keep real text out of CSS. Text added with the content property of ::before and ::after is presentation, not page content, and Google has said it does not index it. Labels, prices and headings belong in the HTML.
4. Do not block CSS from crawlers. Google renders pages with their stylesheets to judge layout and mobile usability. A robots.txt rule that disallows /css/ or a theme's assets folder leaves Google rendering an unstyled page.
5. Cut render-blocking CSS. Stylesheets in the head block the first paint until they download. Large CSS files from a slow third-party host delay LCP. Common fixes: inline the small amount of CSS needed for the above-the-fold area, add media="print" to print-only stylesheets so they do not block, remove unused CSS, and self-host fonts and styles instead of chaining requests to other domains.
6. Reserve space to prevent layout shift. Web fonts that swap in with different metrics move text around. Use font-display: swap or optional, preload the one or two fonts used above the fold, and use the size-adjust descriptor on the fallback font so it occupies the same space. Images, ads and embeds need reserved space (width/height attributes or CSS aspect-ratio). Google's "good" threshold for CLS is 0.1 or less at the 75th percentile of page loads.
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var.woff2") format("woff2");
font-display: swap;
}
.ad-slot { min-height: 250px; } /* reserve space before the ad loads */
.video-embed { aspect-ratio: 16 / 9; width: 100%; }
7. Design for mobile first. Google indexes the mobile rendering. Content hidden on mobile with a media query (@media (max-width: 768px) { .specs { display: none; } }) is still in the HTML, but if your mobile template omits content from the HTML entirely, Google will not see it. Check that tap targets are not crammed together and that nothing forces horizontal scrolling. More in mobile optimization, and for LCP and CLS fixes, web speed optimization.
HTML that AI engines can parse
Googlebot renders JavaScript. Most AI crawlers, such as GPTBot, ClaudeBot and PerplexityBot, generally fetch the HTML without running JavaScript. So for ChatGPT, Claude and Perplexity, what is in View Source is usually what exists. A product description, price, FAQ or review block that only appears after client-side rendering may be invisible to them even if Google indexes it.
Beyond being visible, the HTML has to be easy to lift a passage from. AI answers are built from chunks of pages, so structure that makes a chunk self-contained helps:
Answer-first blocks. Put a direct one- or two-sentence answer straight under the heading that asks the question, then the detail. A paragraph that only makes sense after reading three others is hard to quote.
Real lists and tables. Steps in <ol>, options in <ul>, comparisons in <table> with header cells. These survive extraction with their meaning intact; styled divs do not.
Descriptive headings. A heading like "Width sizes compared" tells a retrieval system what the next block covers. "Overview" and "More info" do not.
Server-rendered main content. Use server-side rendering or static generation for the article body, FAQs, prices and specs. Treat JavaScript as an enhancement, not the delivery mechanism.
FAQ in the HTML. Questions and answers in <details> or plain headings and paragraphs, with matching FAQPage JSON-LD if you use it.
Keep important text in accessible page structure and check the rendered content. Markup quality and observed answer citations require separate evidence.
Our AEO scanner checks a page's raw HTML for these issues, and the AI search technical optimization guide covers crawler access, rendering and the rest of the technical side.
View Source vs DevTools: raw HTML vs rendered DOM
| Tool | What it shows | Use it to check |
|---|---|---|
| View Source (Ctrl+U, Cmd+Option+U on Mac) | The raw HTML the server sent, before any JavaScript runs | What a non-rendering crawler sees: title, canonical, robots, links and body text in the initial response |
| DevTools Elements (F12, Cmd+Option+I) | The live DOM after JavaScript has run and modified it | What a rendering crawler sees; tags injected or changed by scripts and tag managers |
| Search Console URL Inspection → View crawled page | The HTML Google actually rendered, plus HTTP headers | Whether Google saw your content, canonical and noindex as intended |
| Rich Results Test → View tested page | Rendered HTML and a screenshot, for any public URL | Rendering checks on sites you do not have Search Console access to |
The quick test: open View Source and search (Ctrl+F) for a sentence from the middle of the main content, then for rel="canonical" and robots. If the sentence is missing from the source but visible in Elements, the content depends on JavaScript. If the canonical differs between the two, a script is changing it, and Google may pick either. Search engines generally use the rendered version, but a conflict between raw and rendered signals is a risk you should remove. The JavaScript for SEO guide goes deeper on rendering.
Common HTML SEO mistakes
A leftover noindex. Staging sites are often set to noindex, and the tag ships to production with the launch. Check the live head on launch day.
Two canonicals. A CMS plugin and a theme each add one, often pointing at different URLs. Google may ignore both.
The same title on every page. Templates that output the site name, or the category name, as the title for hundreds of URLs. Google rewrites them and the pages compete with each other.
Links without href. Mega-menus, pagination and filter links built as buttons or spans hide whole sections of the site from crawlers.
Lazy-loading everything. A blanket loading="lazy" on all images, including the hero, slows LCP.
Using headings for styling. Sidebar widgets marked up as h2s and footers full of h3s bury the real outline of the page.
Div tables and div lists. Comparison grids and step lists built from styled divs lose the structure that featured snippets and AI answers rely on.
Content only in JavaScript. Fine for Google in most cases, invisible to most AI crawlers and fragile if rendering fails. Keep the main content in the server HTML.
Schema that does not match the page. A rating, price or FAQ in the JSON-LD that is not visible on the page is a structured data policy violation.
Fixing in DevTools and calling it done. Edits in the Elements panel are temporary and local. Confirm every fix in View Source on the live URL, then with URL Inspection.
HTML SEO checklist
| Check | Where to look | Pass |
|---|---|---|
| Title | View Source | One <title>, unique, matches the h1 topic, key words first |
| Meta description | View Source | Present, specific to the page, not duplicated sitewide |
| Meta robots / X-Robots-Tag | Source and response headers | No unintended noindex; header and tag agree |
| Canonical | Source and Elements | One, absolute, self-referencing on indexable pages, same in raw and rendered |
| Hreflang | Source or sitemap | Self-reference, return links, valid codes, x-default |
| Head validity | Elements, inside <head> | No img, iframe or div before your SEO tags |
| Viewport and lang | View Source | width=device-width, initial-scale=1; lang set on <html> |
| Page structure | Elements | One <main>; header, nav and footer outside it |
| Headings | Elements or a headings extension | One h1 on the topic; h2/h3 in logical order; logo not an h1 |
| Main content in raw HTML | View Source, search a sentence | Found without JavaScript |
| Lists and tables | Elements | Steps in ol, comparisons in table with th |
| Links | Elements, inspect menus and pagination | Every navigational link is <a href>; descriptive anchors |
| Images | Elements | alt on content images; width/height set; LCP image not lazy |
| Structured data | Rich Results Test | Valid, matches visible content |
| CSS access | robots.txt, URL Inspection | Stylesheets not blocked; rendered screenshot looks right |
| CLS | PageSpeed Insights | 0.1 or less; no shifts from fonts, ads, images |
| Hidden content | Source vs page | Tab and accordion text present in HTML; nothing hidden to deceive |
To run these checks across many URLs instead of one at a time, the title, canonical, robots and H1 extractor in Python for SEO or the on-page script in Apps Script for SEO covers the first rows of this table.