A client report template you can fill with AI and send as a page

A monthly client report is the same six sections every month, and an AI can fill them from your notes. Sending a page skips the export step and lets the browser lay it out on the client's screen.

A client report template is a fixed order of sections you refill each month. An AI can write the page from your notes, and you send it as a link instead of a PDF, so the layout is not decided by an export.

That last part is where the time goes. One person, writing about exporting from Claude, put it this way: "save as pdf, layout shifts. always end up just screenshotting."

A page avoids the export step. The browser lays it out for the screen that opens it. Here is a finished monthly report. It is one column, written for a phone first.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Monthly report - September - Harbor Tea Co.</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; background: #f3f4f6; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #1f2937; line-height: 1.5; }
  .page { max-width: 640px; margin: 0 auto; background: #fff; padding: 22px 18px 30px; }
  h1 { margin: 0 0 2px; font-size: 24px; line-height: 1.2; }
  .sub { margin: 0 0 16px; font-size: 14px; color: #6b7280; }
  h2 { margin: 24px 0 8px; font-size: 18px; }
  .summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
  .stat { background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 12px; padding: 10px 12px; }
  .stat.ask { background: #fff7ed; border-color: #fed7aa; }
  .stat b { display: block; font-size: 22px; line-height: 1.2; }
  .stat span { font-size: 13px; color: #4b5563; }
  .bar { height: 10px; background: #e5e7eb; border-radius: 6px; margin: 12px 0 4px; overflow: hidden; }
  .bar i { display: block; height: 100%; background: #16a34a; }
  .barnote { font-size: 14px; color: #6b7280; margin: 0; }
  .item { display: grid; grid-template-columns: 1fr auto; gap: 2px 10px; padding: 11px 0; border-bottom: 1px solid #eef0f3; break-inside: avoid; }
  .item .name { font-size: 16px; font-weight: 600; }
  .item .note { grid-column: 1; font-size: 14px; color: #6b7280; }
  .item .hrs { grid-row: 1 / span 2; grid-column: 2; text-align: right; font-size: 15px; }
  .chip { display: inline-block; font-size: 12px; font-weight: 600; padding: 1px 8px; border-radius: 999px; margin-left: 6px; vertical-align: 2px; }
  .done { background: #dcfce7; color: #166534; }
  .wip { background: #fef3c7; color: #92400e; }
  ul { margin: 0; padding-left: 20px; }
  li { margin: 4px 0; }
  .decision { background: #fff7ed; border: 1px solid #fed7aa; border-radius: 12px; padding: 12px 14px; break-inside: avoid; }
  .fine { margin-top: 20px; font-size: 14px; color: #6b7280; }
  @media print {
    @page { size: A4; margin: 16mm; }
    body { background: #fff; }
    .page { max-width: none; padding: 0; }
    .stat, .bar i, .chip, .decision { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
  }
</style>
</head>
<body>
<div class="page">
  <h1>Monthly report: September</h1>
  <p class="sub">Prepared for Harbor Tea Co. by Dana Reyes · copywriting retainer</p>

  <div class="summary">
    <div class="stat"><b id="s-hours"></b><span>hours used</span></div>
    <div class="stat"><b id="s-done"></b><span>items finished</span></div>
    <div class="stat ask"><b id="s-ask"></b><span>decision needed</span></div>
  </div>
  <div class="bar"><i id="bar"></i></div>
  <p class="barnote" id="barnote"></p>

  <h2>What got done</h2>
  <div id="items"></div>

  <h2>Waiting on you</h2>
  <ul id="waiting"></ul>

  <h2>Next month</h2>
  <ul id="next"></ul>

  <h2>Decision needed</h2>
  <div class="decision" id="decision"></div>

  <p class="fine">Hours are from my own time log. Questions about any line: reply to the email this link came in.</p>
</div>

<script>
  // ---- Change the numbers here each month. ----
  const RETAINER_HOURS = 24;
  const ITEMS = [
    { name: 'Rewrote 6 product pages', note: 'All six are live on the shop.', hours: 9, status: 'done' },
    { name: 'Welcome email sequence', note: 'Three emails, scheduled for 3 October.', hours: 6.5, status: 'done' },
    { name: 'Homepage hero test copy', note: 'Two versions delivered for your A/B test.', hours: 3, status: 'done' },
    { name: 'Blog post: steeping guide', note: 'First draft sent; waiting for your edits.', hours: 4, status: 'wip' }
  ];
  const WAITING = ['Photos for the two new oolong pages'];
  const NEXT = ['Finish and publish the steeping guide', 'Write the October gift-set page', 'Review welcome email results with you'];
  const DECISION = 'Approve or comment on the steeping guide draft by 8 October so it can go live that week.';
  // ---------------------------------------------

  const fmt = (n) => String(Math.round(n * 10) / 10);
  const used = ITEMS.reduce((sum, it) => sum + it.hours, 0);
  const finished = ITEMS.filter((it) => it.status === 'done').length;

  document.getElementById('s-hours').textContent = fmt(used) + ' / ' + RETAINER_HOURS;
  document.getElementById('s-done').textContent = finished + ' of ' + ITEMS.length;
  document.getElementById('s-ask').textContent = '1';
  document.getElementById('bar').style.width = Math.min(100, used / RETAINER_HOURS * 100) + '%';
  document.getElementById('barnote').textContent = fmt(RETAINER_HOURS - used) + ' hours left in the retainer this month.';

  document.getElementById('items').innerHTML = ITEMS.map((it) =>
    '<div class="item"><span class="name">' + it.name +
    '<span class="chip ' + it.status + '">' + (it.status === 'done' ? 'Done' : 'In progress') + '</span></span>' +
    '<span class="note">' + it.note + '</span><span class="hrs">' + fmt(it.hours) + ' h</span></div>'
  ).join('');
  document.getElementById('waiting').innerHTML = WAITING.map((t) => '<li>' + t + '</li>').join('');
  document.getElementById('next').innerHTML = NEXT.map((t) => '<li>' + t + '</li>').join('');
  document.getElementById('decision').textContent = DECISION;
</script>
</body>
</html>
A monthly report for a fictional client. The hours, items and decision are one block at the top of the script. Edit them and it reruns.

The six sections, in the order a client reads them

The example follows one order. It is an editorial choice, not a standard, and it puts the numbers first and flags the decision in the summary.

  1. Summary – hours used, items finished, decisions needed. Three numbers.
  2. What got done – one line per item, with its status and hours.
  3. Waiting on you – what is blocked until the client acts.
  4. Next month – three or four items, no more.
  5. Decision needed – one clear ask with a date.
  6. A line about the numbers – where the hours come from.

Clients skim. The first screen should answer "is this on track?" and "do you need something from me?" without scrolling.

For the wider case, including what to put in a report and how often, see sending a monthly report to a client.

Make it with AI

Paste this prompt into ChatGPT, Claude or Gemini. Put your own facts at the bottom.

Write ONE self-contained HTML file: a monthly client report, to be read on a phone.
Rules:
- Plain HTML, CSS and JavaScript in one file. No images, no external files, no form that sends data.
- One column. Add a viewport meta tag. Text at least 16px.
- Start with three summary boxes: hours used of the retainer, items finished, decisions needed.
- Then these sections in order: What got done, Waiting on you, Next month, Decision needed.
- Put every number, item and date in one block at the top of the script, with the comment "Change the numbers here each month".
- Add a print style: A4, no grey page background, items do not split across pages.
- If I did not give you a fact, write TODO. Do not invent work, hours or results.
My facts:
Client: ...
Month: ...
Retainer hours: ...
Work done, with hours and status: ...
Waiting on the client for: ...
Next month: ...
Decision I need, and by when: ...

The block-of-numbers line matters. It turns next month's report into one edit instead of a hunt through the page.

What to check in the result

An AI is good at layout and loose with facts. Read the page the way the client will.

  • Every number. Hours, totals and the hours left. Compare them with your time log.
  • Every TODO. Replace each one, or delete the line.
  • Invented work. Items or results you did not give it.
  • The phone. Open it on your own phone before the client does.

The longer version of this check is in the HTML report template guide. If the page looks wrong, fixing AI-generated HTML covers the usual causes.

Paste the HTML into a NOS document and choose Create share link. HTML to link walks through it.

The result is a page anyone with the link can open in a browser, on a phone too, with no account. They can read it but not edit it.

Send the address in the body of your email or message, with one line saying which month it covers.

Next month: open the page in NOS, click into the code and change the figures in the block at the top of the script. Save, and the same link shows the new version.

If you want each month to stay readable, make a new page and a new link per month, and keep the old ones. The second example keeps three months on one page, with a tap to switch.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Retainer hours by month - Larkspur Bakery</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; background: #f3f4f6; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #1f2937; line-height: 1.5; }
  .page { max-width: 560px; margin: 0 auto; background: #fff; min-height: 100vh; padding: 22px 18px 28px; }
  h1 { margin: 0 0 4px; font-size: 24px; line-height: 1.2; }
  .sub { margin: 0 0 14px; font-size: 14px; color: #6b7280; }
  .tabs { display: flex; gap: 8px; margin-bottom: 14px; }
  .tabs button { flex: 1; min-height: 44px; border: 1px solid #d1d5db; border-radius: 10px; background: #fff; font-size: 16px; cursor: pointer; }
  .tabs button[aria-pressed="true"] { background: #1d4ed8; border-color: #1d4ed8; color: #fff; font-weight: 600; }
  .big { font-size: 30px; font-weight: 700; margin: 0; }
  .bar { height: 12px; background: #e5e7eb; border-radius: 6px; margin: 8px 0 4px; overflow: hidden; }
  .bar i { display: block; height: 100%; background: #1d4ed8; }
  .bar i.over { background: #c2410c; }
  .note { font-size: 14px; color: #6b7280; margin: 0 0 14px; }
  .row { display: flex; justify-content: space-between; gap: 10px; padding: 10px 0; border-bottom: 1px solid #eef0f3; font-size: 16px; }
  .row span:last-child { white-space: nowrap; }
  .fine { margin-top: 12px; font-size: 14px; color: #6b7280; }
</style>
</head>
<body>
<div class="page">
  <h1>Retainer hours</h1>
  <p class="sub">Larkspur Bakery · design support · tap a month</p>

  <div class="tabs" id="tabs"></div>

  <p class="big" id="big"></p>
  <div class="bar"><i id="bar"></i></div>
  <p class="note" id="note"></p>

  <div id="rows"></div>

  <p class="fine">Hours above the retainer are agreed with you before I start them.</p>
</div>

<script>
  // ---- Add a month here each time you send a report. ----
  const RETAINER_HOURS = 20;
  const MONTHS = [
    { name: 'July', work: [['Menu redesign', 8], ['Social templates', 6], ['Shop signage', 3]] },
    { name: 'August', work: [['Menu redesign', 4], ['Social templates', 5], ['Shop signage', 7], ['Loyalty card', 6]] },
    { name: 'September', work: [['Social templates', 4], ['Loyalty card', 9], ['Window poster', 6]] }
  ];
  // -------------------------------------------------------

  const tabs = document.getElementById('tabs');
  const big = document.getElementById('big');
  const bar = document.getElementById('bar');
  const note = document.getElementById('note');
  const rows = document.getElementById('rows');

  function show(i) {
    const m = MONTHS[i];
    const used = m.work.reduce((s, w) => s + w[1], 0);
    big.textContent = used + ' of ' + RETAINER_HOURS + ' hours';
    bar.style.width = Math.min(100, used / RETAINER_HOURS * 100) + '%';
    bar.className = used > RETAINER_HOURS ? 'over' : '';
    const gap = Math.abs(RETAINER_HOURS - used);
    const unit = gap === 1 ? ' hour' : ' hours';
    note.textContent = used > RETAINER_HOURS
      ? gap + unit + ' over the retainer in ' + m.name + '.'
      : gap + unit + ' left in ' + m.name + '.';
    rows.innerHTML = m.work.map((w) => '<div class="row"><span>' + w[0] + '</span><span>' + w[1] + ' h</span></div>').join('');
    tabs.querySelectorAll('button').forEach((b, j) => b.setAttribute('aria-pressed', j === i ? 'true' : 'false'));
  }

  MONTHS.forEach((m, i) => {
    const b = document.createElement('button');
    b.type = 'button';
    b.textContent = m.name;
    b.addEventListener('click', () => show(i));
    tabs.appendChild(b);
  });
  show(MONTHS.length - 1);
</script>
</body>
</html>
Three months of retainer hours on one page. Add a month in the block at the top of the script.
From AI chat to a link, in four steps.
From AI chat to a link, in four steps.

PDF, screenshot or page: how they compare

A PDF or screenshot attached, against a page sent as a link.
A PDF or screenshot attached, against a page sent as a link.
PDF attached Screenshot Page as a link
Layout on a phone Fixed page size Fixed picture Fits the screen
Fix after sending New file New picture Edit the page, same link
Needs an account to read No No No
Old months Pile up in the inbox Pile up One address per month

For personal Gmail accounts, Google's help page says the attachment limit is 25 MB, and Gmail adds a larger file as a Drive link instead (as of October 2026).

A one-page report is far below that, so size is rarely the issue. The issue is what happens when you fix a number after sending. See email vs link for documents.

What the client sees on a phone: the numbers first, then the rest in one column.
What the client sees on a phone: the numbers first, then the rest in one column.

If the client wants a printed copy

Some clients file reports for their own records. The first example carries a print style, so the same page prints cleanly. MDN describes the print media type as the way to apply styles to printed documents.

@media print {
  @page { size: A4; margin: 16mm; }
  .item { break-inside: avoid; }
}

break-inside: avoid asks the browser not to split an item across two pages. @page sets the page size; use letter instead of A4 for North American paper. Print from your browser, check the preview, and then decide whether to send it.

By default, a browser may adjust colors when it prints. print-color-adjust: exact asks it to keep them, and the example uses it on the colored boxes. See print stylesheets for more.

When it does not work

What you see Cause Fix
The page shows raw code in the chat You copied the chat text, not the code block Copy only the code, from the doctype line to the last html tag
A logo or chart image is missing Images from other sites are blocked Ask for no images, or a logo drawn in SVG
The numbers do not add up The AI guessed or rounded a figure Compare with your log and ask it to compute totals in the script
A contact form does nothing Forms cannot send to a server from the link Write your email address as plain text
The client sees last month They opened an old file or an old link Send only the link, never the file
The text is tiny on a phone No viewport meta tag Ask the AI to add one, and a 16px base size
The printout has no colors The browser adjusted colors for print Add print-color-adjust: exact to those boxes

Related reading. A report is one document in a run. For the ongoing view, HTML for client reports covers what to put on the page, and HTML vs PDF for clients goes deeper on the PDF question.

Questions people ask

What should a monthly client report include?

Six parts cover it: a short summary with the key numbers, what got done, hours or spend, what you are waiting on, next month, and any decision you need. This order is an editorial choice, not a standard. Put the numbers and a count of decisions in a summary at the top.

Can I use AI to write the report?

Yes, for the layout and the wording. Give it your own list of work and hours, and tell it to write TODO where it has no fact. Then check every figure against your time log, because an AI can invent a number that reads as real.

Why not just send a PDF?

You can. A PDF has a fixed page size, so on a narrow screen the client may have to pinch and zoom, and a correction means a new file. One person, writing about exporting from Claude, said "save as pdf, layout shifts." A page is laid out by the browser for the screen it opens on.

How do I update the report next month?

Open the page in NOS, change the figures in the block at the top of the script, and save. The link stays the same and shows the new version. If you want to keep each month, make a new page and a new link per month.

What if the client needs a printed copy?

Add a print style to the page so it prints cleanly, then print it from your browser. Check the preview before you send it. The first example on this page already has one.

Keep reading