SEO Report Template: The Sections Clients Read, Built as One HTML File With AI

HTMLvault Team·October 22, 2026·11 min read

Explore with AI

Open this article in an AI assistant, with a prompt that asks it for a summary and the takeaways.

On the first Monday of every month, Liz Lemmon rebuilds Synergetics' monthly SEO report from six CSV exports, a folder of screenshots and last month's deck. She usually finishes on Wednesday. Last month's ran to 31 slides. The only reply came four minutes after she sent it, asking a question that slide two answered.

A good SEO report template follows the order a client reads in: the answer first, then the evidence, then the plan. Most SEO reports for clients do the opposite. They open on a keyword table and bury the paragraph the client actually wanted. This guide gives seven sections and what to leave out of each. Then it gives rules and a prompt that get Claude, ChatGPT or Gemini to build the report as one self-contained HTML file. Built this way, next month's report is a patch to the same page at the same URL, not a rebuild.

The SEO report template, in the order clients read it

Assume the client reads the first screen closely, skims the charts and stops about halfway down. Anything they need to decide on goes above that point. Each section gets a fixed id that next month's update will use.

  1. Summary and next actions (summary). Three to five sentences on what changed and why, then what you need from the client. Leave out methodology.
  2. Organic traffic and conversions (traffic). Organic sessions and the conversions the client counts, compared with last month and the same month last year, in one chart. Leave out total site traffic that mixes in other channels.
  3. Rankings that moved (rankings). Tracked queries that gained or lost three or more positions, each with the page that ranks for it. Fifteen at most.
  4. Pages won and lost (pages). The five biggest gains and five biggest losses in organic clicks, each with a one-line reason.
  5. Technical issues (technical). New and fixed issues, each with what it costs, such as pages dropped from the index. Leave out the crawler's raw list of warnings.
  6. Links (links). Referring domains gained or lost that actually matter. Don't lead with total backlink counts or authority scores.
  7. Next month's plan (plan). Three to five actions, each with an owner and a date.

Two things never go in. The first is a vanity keyword list: hundreds of rankings that didn't move. It shows effort and hides the change the client is paying for. The second is raw exports. Keep them in your working files and send them separately if the client asks.

Liz cut the 400-row keyword table down to the eleven queries that moved. Within the hour, Pennyman asked where "Synergetics worldwide solutions" had gone. It had ranked first every month for three years. Liz pointed out that it ranks first because everyone who has ever searched for it works on his floor.

Rules for building it as one HTML file

Ask an AI tool for "an SEO report" and the layout, ids and charts come out different every month. Give it rules instead. Each rule below is plain enough for a model to follow, and its reason comes right after it.

  1. Put all CSS in one <style> block in the <head>. HTMLvault serves the page exactly as written and adds no stylesheet or fonts, so the page has to bring its own.
  2. Put every number in one JSON block at the top of <body>: <script type="application/json" id="report-data">. Each value then appears once, so next month you replace one block instead of fifty table cells.
  3. Fill the page from that block with one script at the end of <body>, and have it draw each chart as inline <svg>. Scripts run on published pages, and script text is stored exactly as written, so the data block is easy to patch.
  4. Use no chart images, and keep every <script> and <style> outside any <svg>. An <img src="data:..."> is removed when you publish. Inside an SVG, a script is read as markup and can be refused.
  5. Keep each section's fixed id, and put its commentary in one <p> with its own id (summary-text, traffic-text). A patch has to find text that appears exactly once, and the id makes sure it does. HTML comments can't do that job, because publishing removes them.
  6. Close every <p>, <li>, <td> and <tr>, and write hidden="hidden", not a bare hidden. Malformed HTML is refused, not repaired. Missing end tags can push the page past the limit of 1,000 nested elements. And an attribute with an empty value is removed, unless it's alt.
  7. Keep the month out of the <title>. The title becomes the link's preview card in Slack and email, and a title without a month never needs updating.

A minimal skeleton that follows all seven rules:

<!DOCTYPE html>
<html lang="en">
<head>
<title>[Client]: monthly SEO report</title>
<style>body { font-family: system-ui, sans-serif; max-width: 760px; margin: 0 auto; }</style>
</head>
<body>
<script type="application/json" id="report-data">{"sessions":18420,"sessions_prev":16910,"conv":212,"conv_prev":187}</script>

<section id="summary">
  <h2>Summary and next actions</h2>
  <p id="summary-text">Organic conversions rose 13% on two new guides.</p>
</section>

<section id="traffic">
  <h2>Organic traffic and conversions</h2>
  <p>Organic sessions: <span data-field="sessions"></span></p>
  <svg id="traffic-chart" viewBox="0 0 240 176" role="img" aria-label="Organic sessions, last month and this month"></svg>
  <p id="traffic-text">Both new guides reached page one.</p>
</section>

<script>
const d = JSON.parse(document.getElementById("report-data").textContent);
document.querySelectorAll("[data-field]").forEach(el => {
  el.textContent = d[el.dataset.field].toLocaleString();
});
const v = [d.sessions_prev, d.sessions], max = Math.max(...v);
document.getElementById("traffic-chart").innerHTML = v.map((n, i) =>
  `<rect x="${40 + i * 100}" y="${150 - n / max * 130}" width="60" height="${n / max * 130}"></rect>` +
  `<text x="${70 + i * 100}" y="168" text-anchor="middle" font-size="12">${i ? "This month" : "Last month"}</text>`
).join("");
</script>
</body>
</html>

From top to bottom, the file is a style block, a data block and the seven sections. Each month, only the data block and the seven commentary paragraphs change.

Layout of a one-file SEO report: style block, data block and seven sections with ids Top of file: a style block holding all CSS, then a data block with id report-data holding every number as JSON. Then seven sections in reading order, with lowercase ids: summary, traffic, rankings, pages, technical, links, plan. The report file, top to bottom TOP OF FILE Style block: all CSS, in one place <STYLE> Data block: every number, written once as JSON #REPORT-DATA SECTIONS, IN THE ORDER THE CLIENT READS THEM 1. Summary and next actions #SUMMARY 2. Organic traffic and conversions #TRAFFIC 3. Rankings that moved #RANKINGS 4. Pages won and lost #PAGES 5. Technical issues #TECHNICAL 6. Links #LINKS 7. Next month's plan #PLAN
Each id is the fixed address next month's patch looks for, so this SEO report template keeps every id the same even when all the numbers change.

SEO report example: month one, then a month-two patch

Month one. The analyst runs the prompt at the end of this guide with March's exports attached. Through the HTMLvault MCP server, Claude publishes the page with create_link and returns a private link. A script can do the same through the REST API. Once the client has the link, its per-link analytics show scroll depth and time on page. If readers stop at the first screen every month, the summary is effectively the whole report.

Month two. April's exports arrive, and the analyst asks the agent to update the existing link. The agent calls get_link to read the stored HTML. Then it sends one patch_link call with eight patches: one for the data block and one for each commentary paragraph. A script can send the same patches with PATCH /api/v1/links/{slug}. Here are two of them:

Patch 1
  find:    {"sessions":18420,"sessions_prev":16910,"conv":212,"conv_prev":187}
  replace: {"sessions":19876,"sessions_prev":18420,"conv":214,"conv_prev":212}

Patch 2
  find:    <p id="summary-text">Organic conversions rose 13% on two new guides.</p>
  replace: <p id="summary-text">Conversions held at 214; the pricing page lost two positions.</p>

Each find string covers either a whole paragraph, including its id, or the whole data block, so it appears exactly once in the stored page. The call is all or nothing. If any find string is missing or appears twice, nothing changes and the March version stays as it was. Otherwise the script redraws the chart from April's numbers, and the link the client already has now shows April. Replacing the whole page and using the web editor are covered in Edit Published HTML Without Breaking Your Links.

A monthly SEO report built on stable ids takes one patch call to update, and the client's existing link shows the new month without anyone sending anything again.
Month two took one patch call, and Liz was done before lunch on Monday. She sent no email, because everyone already had the link. A regional director replied to the March thread anyway, asking for "the final version." Liz sent back the same link. For the first time in four years, that was an accurate answer.

Limits, caveats and delivery

  • Patch the stored page, not your draft. Publishing changes the HTML slightly. It drops the DOCTYPE and comments, puts double quotes around attribute values and re-encodes characters: &nbsp; is stored as the actual character, and a bare & is stored as &amp;. So copy find strings from get_link. To see the stored version before you publish, run scan_html with normalize: "full". Script text is never rewritten.
  • Size. A page can be up to 5 MB. For a very long report, use append mode: start with create_draft_link, send chunks of up to 200 KB with append_html, and finalize on the last one. Nothing is public until you finalize.
  • Write commentary that makes sense on its own. The numbers and charts only appear if the script runs in the reader's browser. The paragraphs show up either way.
  • Every publish and edit is checked. HTMLvault scans for secrets and personal data, and checks every outside URL against Google's unsafe-URL lists. If a publish is refused, you get the findings instead of a URL. A pasted export is the most likely cause, because form-fill data often includes contact emails.

To deliver it, put the link on a branded subdomain or a white-label custom domain so the client sees your name in the URL. Pro and Teams include one custom domain; Enterprise includes three. Add a password or an expiry if the engagement needs one. Setup is covered in White-Label Custom Domains.

SEO report prompt you can reuse

Paste this into Claude, ChatGPT or Gemini with the month's exports attached:

Build this month's SEO report for [CLIENT] from the attached exports as
one complete HTML document. Return only the HTML.

Sections, in this order, each a <section> with this exact id:
- summary, "Summary and next actions": 3-5 sentences on what changed,
  why, and what we need from the client
- traffic, "Organic traffic and conversions": vs last month and last
  year, one bar chart
- rankings, "Rankings that moved": queries that moved 3+ positions, max 15
- pages, "Pages won and lost": top 5 gains and losses in organic clicks
- technical, "Technical issues": new and fixed, each with its impact
- links, "Links": referring domains gained and lost that matter
- plan, "Next month's plan": 3-5 actions, each with owner and date
Leave out full keyword lists, raw exports and backlink totals.

Rules:
1. All CSS in one <style> in <head>; system fonts; nothing external.
2. Every number goes in one single-line JSON object in
   <script type="application/json" id="report-data">, the first
   element of <body>. Tables and charts read from it.
3. A second <script> at the end of <body> fills data-field="key"
   elements and draws each chart as labeled inline <svg>. No images,
   no data: URLs, no <script> or <style> inside <svg>.
4. Each section's commentary is one <p id="[section]-text">.
5. Close every <p>, <li>, <td>, <tr>. Write hidden="hidden",
   never a bare attribute. No HTML comments as markers.
6. <title>[CLIENT]: monthly SEO report</title>, no month.

Next month, give an AI agent connected to HTMLvault this prompt instead:

Update the SEO report at [LINK] from the attached exports, using my
notes below for the commentary. Call get_link for the stored HTML, then
make one patch_link call that replaces the report-data JSON and each
<p id="...-text"> paragraph in full, copying every find string
exactly from the stored HTML. Change nothing else.

[NOTES]

That second prompt is all month two takes. The layout, the charts, the ids and the link the client bookmarked stay where they were. The analyst spends the first Monday writing the notes on what the numbers mean, the one part no template can write.

HTMLvault

Publish your AI creations. See who reads them.

Proposals, dashboards and reports from Claude, ChatGPT or any AI tool become private links on your own domain. Update them after you send, and see who read what, down to the scroll.

Create a link

Related posts