Writing Pages That Answer: Heading Structure, the First Paragraph, Code Samples, Images and Bilingual Content
A page that answers gives the answer to its title's question in the first paragraph, has a single clear h1, and shows code as text with its real output; heading order is not a ranking factor for Google but matters for readers and accessibility. We cover heading structure, the first paragraph, versions and output in technical content, alt text and file names for images, TR/EN localisation and AI-assisted writing, and build a page checker with .NET 10 and AngleSharp.
Version note: The code in this post was built and run with .NET 10 and AngleSharp 1.10.3; the post is based on the November 2026 state of Google's SEO Starter Guide and its title link, image and generative AI content documentation.
Guess First
The body of the "Book Picks" page on the Bookstore blog starts like this:
<h1>Book Picks</h1>
<h1>Our favourite books of the year</h1>
<p>Every year our team gets together at the end of December, and after a long discussion over coffee, and a few disagreements about whether a book about databases can ever be called exciting, we finally agree on a list that we think is worth sharing with everyone who reads this blog, so here it is, and we hope you enjoy reading it as much as we enjoyed writing it.</p>
<h4>Domain-Driven Design</h4>
<img src="/img/image1.png">
<h2>Code sample</h2>
<img src="/img/code-screenshot.png" alt="code">
A teammate says: "We jumped from h2 to h4 and there are two h1s; Google will penalise the page for that." Someone else says: "The first paragraph is friendly, readers will like it." Do you think Google cares about heading order? What is the real problem on this page for readers and for Google: the headings, the first paragraph or the images?
Heading Structure: H1 and the Heading Hierarchy
Let's answer the first claim with Google's own words. The SEO Starter Guide says headings in semantic order are very helpful for readers who use screen readers, but it leaves no doubt about Google Search: "it doesn't matter if you're using them out of order" (Google, SEO Starter Guide). So jumping from h2 to h4 does not bring a ranking penalty. There is no ideal number of headings either.
That does not mean heading structure is unimportant. Headings are the reader's main tool for scanning a page: a developer arriving with an error message looks at the headings first and jumps to the relevant section. Screen reader users also navigate a page by its headings; skipping levels breaks the page's structure for them. The Accessibility (a11y) series being written in this round covers that side in detail.
The main title is a separate matter, because it is one of the places Google looks at when choosing a title link. The title link documentation recommends making the main title the most prominent text on the page, for example the first visible h1; if several headings carry the same visual weight, Google may not be sure which one is the main title. The two h1 elements on the sample page create exactly that ambiguity. We covered how title links are chosen in detail in the SEO Fundamentals post "Appearing in Results".
The practical rule for a technical blog is simple: a single h1 that carries the page's question or topic; h2 elements for the sub-questions a reader might ask; h3 elements for the steps inside an h2. Use headings to describe structure, not to make text bigger.
The First Paragraph and a Scannable Structure
The real problem of the sample page is the first paragraph. A 69-word sentence does not tell readers what the page is about; the book list is still out of sight. Readers looking for book recommendations skip the paragraph, and readers who arrive with an error message close the page. On a page that answers, the first paragraph gives the answer to the question in the title in a sentence or two; details, story and reasoning come afterwards.
This rule is also discussed in terms of citations by AI answer engines. We examined what the research really shows and which claims are overstated in the GEO series post "Citable Content: What the Research Actually Shows"; but from the reader's point of view the reason is strong enough: the person looking for the answer should see it on the first screen. On mertozen.com every post's summary is shown right below the cover image, before the body, and under this series' rule the first sentence of the summary is the clear answer to the question. So readers see the answer the moment they enter the page.
There are three tools for a scannable structure, and each has its place. A table is for comparisons: when two options need to be shown side by side against the same criteria. A list is for steps whose order matters or for short, independent items. A code block is for everything the reader will copy: commands, configuration, code. Apart from these, short paragraphs, each with a single idea. Breaking a fluent explanation of a concept into bullet points just to make it look scannable makes it harder to read.
Technical Content: Working Code, Real Output and Versions
Readers test whether a technical page answers in one way or another: they copy the code and run it. That is why quality in technical content starts with working code and real output. In the first post of the series we saw this as the "How" question, the experience signal; here let's look at it from the angle of writing the page:
Version and date. The framework, SDK and package versions the code was tried with should be at the top of the page. The "Version note" line on mertozen.com is there for this. Readers whose environment differs see it in the first line.
Real output. Compiler warnings, log lines, HTTP responses and test results should come from a real run. If a reader runs the same command and sees different output, their trust in the page is gone.
Code as text. Putting code in as a screenshot stops readers from copying it and makes the content unreadable as text. Google's page on AI features also recommends providing important information in text form. Code should always sit inside pre and code, as text.
Dependencies. The packages and versions an example needs should be given together with the install command; write dotnet add package AngleSharp --version 1.10.3 rather than "add AngleSharp".
Images: Alt Text, File Names and Size
According to Google's image documentation, alt text is the most important metadata you can provide for an image. It is also the only description for readers who use screen readers or cannot load the image. Good alt text is a short, specific sentence that describes how the image relates to the content; a single word is better than nothing but not enough, while stuffing it with keywords wears readers out and can be treated as spam. That is why alt="code" on the sample page falls short.
The file name is a signal too. Google recommends short, descriptive names instead of generic ones such as image1.jpg, and suggests translating file names if you localise images. The way figures on mertozen.com are produced in two languages, as 04-content-architecture-tr.png and -en.png, matches this recommendation. Placing an image near the relevant text also helps Google understand what it is about.
On size, the width and height attributes prevent the layout from shifting while the page loads; this is a Core Web Vitals topic covered in the Web Performance series. File size matters as well: large images slow the page down.
Checking the Page in Code
Most of these checks are mechanical and can be automated before publishing. Let's write a small program that checks two pages of the local Bookstore blog from the previous post with AngleSharp: the title and h1, heading levels, the length of the first content paragraph, a visible date, image alt text, sizes and file names, the number of code blocks and the hreflang alternates.
using System.Text.RegularExpressions;
using AngleSharp;
using AngleSharp.Dom;
string[] urls = args.Length > 0 ? args :
[
"http://localhost:5281/blog/ef-core-migration-rehberi",
"http://localhost:5281/blog/kitap-onerileri-2023",
];
IBrowsingContext context = BrowsingContext.New(Configuration.Default.WithDefaultLoader());
Regex genericName = new(@"^(image|img|dsc|photo)\d*\.|screenshot", RegexOptions.IgnoreCase);
foreach (string url in urls)
{
IDocument doc = await context.OpenAsync(url);
List<string> warnings = [];
Console.WriteLine(new Uri(url).AbsolutePath);
// Title: a single h1 that matches <title>
IElement[] h1 = [.. doc.QuerySelectorAll("h1")];
Console.WriteLine($" title: {doc.Title}");
Console.WriteLine($" h1: {string.Join(" | ", h1.Select(h => h.TextContent))}");
if (h1.Length != 1)
{
warnings.Add($"{h1.Length} h1 elements; the main title is ambiguous");
}
// Heading hierarchy: skipped levels (h2 -> h4)
int previous = 1;
foreach (IElement h in doc.QuerySelectorAll("h1, h2, h3, h4, h5, h6"))
{
int level = h.LocalName[1] - '0';
if (level > previous + 1)
{
warnings.Add($"heading skips a level: h{previous} -> h{level} \"{h.TextContent}\"");
}
previous = level;
}
// First content paragraph: does the answer come first?
IElement? first = doc.QuerySelectorAll("h1 ~ p").FirstOrDefault(p => p.QuerySelector("time") is null);
int words = first?.TextContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length ?? 0;
Console.WriteLine($" first paragraph: {words} words");
if (words > 60)
{
warnings.Add("first paragraph is long; does it answer the question?");
}
int dates = doc.QuerySelectorAll("time[datetime]").Length;
if (dates == 0)
{
warnings.Add("no visible date (<time datetime>)");
}
foreach (IElement img in doc.QuerySelectorAll("img"))
{
string src = img.GetAttribute("src") ?? "";
string file = src[(src.LastIndexOf('/') + 1)..];
string alt = img.GetAttribute("alt") ?? "";
if (!img.HasAttribute("alt"))
{
warnings.Add($"{file}: no alt");
}
else if (alt.Split(' ').Length < 2)
{
warnings.Add($"{file}: alt \"{alt}\" is too short to describe the image");
}
if (!img.HasAttribute("width") || !img.HasAttribute("height"))
{
warnings.Add($"{file}: no width/height");
}
if (genericName.IsMatch(file))
{
warnings.Add($"{file}: file name says nothing about the content");
}
if (file.Contains("code") || alt.Contains("code"))
{
warnings.Add($"{file}: code as an image; use <pre><code>");
}
}
string[] hreflang = [.. doc.QuerySelectorAll("link[rel=alternate][hreflang]").Select(l => l.GetAttribute("hreflang")!)];
Console.WriteLine($" code blocks: {doc.QuerySelectorAll("pre > code").Length}, dates: {dates}, hreflang: {(hreflang.Length == 0 ? "-" : string.Join(",", hreflang))}");
if (!hreflang.Contains("en"))
{
warnings.Add("no EN alternate (hreflang)");
}
warnings.ForEach(w => Console.WriteLine($" WARN {w}"));
Console.WriteLine(warnings.Count == 0 ? " OK\n" : "");
}
Output of dotnet run:
/blog/ef-core-migration-rehberi
title: EF Core Migrations Step by Step | Bookstore Blog
h1: EF Core Migrations Step by Step
first paragraph: 36 words
code blocks: 1, dates: 2, hreflang: tr,en
OK
/blog/kitap-onerileri-2023
title: Book Picks | Bookstore Blog
h1: Book Picks | Our favourite books of the year
first paragraph: 69 words
code blocks: 0, dates: 0, hreflang: tr
WARN 2 h1 elements; the main title is ambiguous
WARN heading skips a level: h1 -> h4 "Domain-Driven Design"
WARN first paragraph is long; does it answer the question?
WARN no visible date (<time datetime>)
WARN image1.png: no alt
WARN image1.png: no width/height
WARN image1.png: file name says nothing about the content
WARN code-screenshot.png: alt "code" is too short to describe the image
WARN code-screenshot.png: no width/height
WARN code-screenshot.png: file name says nothing about the content
WARN code-screenshot.png: code as an image; use <pre><code>
WARN no EN alternate (hreflang)
The migration guide passes every check: a single h1, a 36-word first paragraph that answers directly, publication and update dates, a code block as text, an image with descriptive alt text and known dimensions, and a TR/EN hreflang pair. The "Book Picks" page gets twelve warnings. It is important to separate which affect Google and which affect readers: the skipped heading level is a problem for readers and accessibility, not for Google's ranking. The two h1 elements make the title link choice ambiguous. The long first paragraph and the code as an image lose readers directly.
Know the limit of a checker like this: the "first paragraph longer than 60 words" rule does not measure whether there is an answer; it only flags suspicious cases. A person still makes the call.
Bilingual Content: Translation or Localisation?
Every post on mertozen.com has a TR and an EN version. The relationship between them can be built in two ways. Translation carries the text over sentence by sentence. Localisation rewrites the same content for the target reader: examples, terms and even titles can change according to how the reader searches.
As we saw in the second post of the series, the TR and EN pages compete for different queries. The TR page's title can be chosen for the question "ef core migration nasıl yapılır", the EN page's for a global query such as "ef core migrations". The EN page does not have to be a literal translation of the TR one; it is enough that it covers the same topic, with the same code, in the phrasing EN readers expect. On this site the code of a post is kept identical in both languages and only code comments are translated; the text, however, should read as if it had been written in that language.
The technical piece that links the two versions is hreflang. Every TR page should point to its EN counterpart and every EN page to its TR counterpart; we implemented this in the Technical SEO series' hreflang post. A note from the title link documentation matters here too: the title should be in the same language and writing system as the page's main content. A TR page with an English title is one of the reasons Google rewrites title links.
AI-Assisted Writing
We looked at Google's position in detail in the first post of the series: AI-generated content is fine if it is helpful and original; bulk production to manipulate rankings is scaled content abuse. From the angle of writing a page, the main risk is AI producing unverified claims. The generative AI content guidance reminds us that these models predict likely word sequences rather than retrieving facts from a source and can therefore produce wrong output; it recommends checking before publishing.
For technical content this has three concrete forms. Made-up APIs: a method or parameter name that does not exist shows up at the reader's first build. Made-up sources: a documentation page that does not exist or a release note with the wrong date. Made-up output: the "expected" output of code that was never run. The defence against all three is the same: run the code, open and read the source, and take the output from a real run. The guidance wants this checking to go beyond the body text and cover titles, meta descriptions, structured data and alt text as well.
In Production
Connect the checker to the publishing flow. Running these checks against the preview URL when a post moves to "ready to publish" in the CMS catches two h1 elements or an image without alt before it goes live.
Grade the warnings. Code as an image or an ambiguous main title can block publishing; the length of the first paragraph should only be a note for the editor.
Make the summary field required. Have the first sentence of the summary answer the question and show it on the page before the body; that single field spreads the first-paragraph discipline across all posts.
Set an image naming rule. If the file name, language suffix and dimensions are written down as a rule and checked in the pipeline, they no longer depend on each editor's attention.
Common Pitfalls
Filling the introduction with a story and hiding the answer. A story can bring a post to life, but it should not take the place of the answer; first the answer, then the story.
Using headings for styling. Using an h4 to make text bigger or an empty h2 as a separator describes the page's structure wrongly to readers and screen readers. Appearance is CSS's job.
Thinking heading order is an SEO penalty. Google does not look at the order; but that is no reason to ignore heading structure, because readers do.
Leaving the EN version as a machine translation. An EN page whose translation was never checked wears readers out and does not answer the same question in the phrasing EN readers expect.
A Small Experiment
★☆☆ Pick a post from your own site and read only its first paragraph. Is the answer to the question in the title there? If not, rewrite the first paragraph in two sentences.
★★☆ Add two checks to PageCheck: one that warns when title and h1 share no words, and one that compares the lang attribute of the html element with the language of the page text.
★★★ Turn PageCheck into an xUnit test: start the local Bookstore blog with WebApplicationFactory, check every page in the sitemap, and fail the test on warnings that should block publishing.
We wrote a page that answers. But a page is not at its best on the day it is published; versions change, queries change. In the last post of the series we look at content maintenance: detecting content decay with Search Console data, the update, merge or delete decision and keeping version-dependent posts alive.