A social media report template you send as a link

Put the month on one page: the verdict, four numbers, what worked, what did not, and next steps with dates. Send it as a link, so there is no PDF export to go wrong.

A social media report template that holds up when you send it is one web page: a verdict, a few numbers, highlights and next steps. A page has no export step, so there is no PDF to reflow and no screenshot to stitch together.

The problem is familiar from exporting AI output. One public post on Threads sums it up: "save as pdf, layout shifts. always end up just screenshotting." A link skips that step.

Here is a finished report. The weekly chart is drawn from four numbers near the bottom of the code, so you can change them and watch the page update.

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>Social media report - September 2026</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 16px; background: #f1f3f6; font-family: system-ui, sans-serif; color: #1d2330; line-height: 1.5; }
  .page { max-width: 720px; margin: 0 auto; background: #fff; border-radius: 14px; padding: 22px 22px 26px; box-shadow: 0 2px 12px rgba(0,0,0,.08); }
  .eyebrow { font-size: 12px; letter-spacing: .06em; text-transform: uppercase; color: #6b7280; margin: 0; }
  h1 { font-size: 24px; line-height: 1.2; margin: 4px 0 6px; }
  .meta { font-size: 13px; color: #6b7280; margin: 0 0 16px; }
  .lead { background: #eef6ff; border-left: 4px solid #2563eb; border-radius: 6px; padding: 10px 14px; margin: 0 0 18px; font-size: 16px; }
  .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 20px; }
  .stat { border: 1px solid #e1e4ea; border-radius: 10px; padding: 10px 12px; break-inside: avoid; }
  .stat b { display: block; font-size: 22px; line-height: 1.2; }
  .stat span { font-size: 12px; color: #6b7280; }
  .up { color: #15803d; font-weight: 600; }
  .down { color: #b45309; font-weight: 600; }
  h2 { font-size: 16px; margin: 22px 0 8px; }
  .chart { border: 1px solid #e1e4ea; border-radius: 10px; padding: 10px 12px 6px; break-inside: avoid; }
  .chart svg { width: 100%; height: auto; display: block; }
  ul { margin: 0; padding-left: 20px; }
  li { margin: 0 0 6px; }
  table { width: 100%; border-collapse: collapse; font-size: 14px; }
  td:last-child, th:last-child { white-space: nowrap; }
  th, td { text-align: left; padding: 8px 8px; border-bottom: 1px solid #e1e4ea; vertical-align: top; }
  th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: #6b7280; }
  @media (max-width: 560px) { .stats { grid-template-columns: repeat(2, 1fr); } h1 { font-size: 21px; } }
</style>
</head>
<body>
<div class="page">
  <p class="eyebrow">Social media report</p>
  <h1>Juniper Coffee Roasters: September 2026</h1>
  <p class="meta">Instagram and TikTok. Prepared by Studio Nine. Compared with August.</p>

  <p class="lead"><b>Reach grew 18%</b>, helped by two Reels. Follower growth slowed slightly, so October focuses on conversion.</p>

  <div class="stats">
    <div class="stat"><b id="reach">48,200</b><span>Reach <span class="up">+18%</span></span></div>
    <div class="stat"><b>+312</b><span>New followers <span class="down">-9%</span></span></div>
    <div class="stat"><b>4.1%</b><span>Engagement <span class="up">+0.3 pt</span></span></div>
    <div class="stat"><b>640</b><span>Link clicks <span class="up">+12%</span></span></div>
  </div>

  <div class="chart">
    <b style="font-size:14px">Reach by week</b>
    <svg id="bars" viewBox="0 0 400 140" role="img" aria-label="Reach by week"></svg>
  </div>

  <h2>Highlights</h2>
  <ul>
    <li>The "roast day" Reel on 12 Sep had the most reach of the month.</li>
    <li>Stories with a poll got more replies than stories without one.</li>
    <li>Link clicks rose after the bio link was changed to the new menu page.</li>
  </ul>

  <h2>What did not work</h2>
  <ul>
    <li>Static product photos reached fewer people than video. Two of the four were cut from the plan.</li>
  </ul>

  <h2>Next steps</h2>
  <table>
    <tr><th>Action</th><th>Who</th><th>By</th></tr>
    <tr><td>Post two more behind-the-scenes Reels</td><td>Studio Nine</td><td>10 Oct</td></tr>
    <tr><td>Approve the October offer wording</td><td>Juniper</td><td>5 Oct</td></tr>
    <tr><td>Add a "shop now" sticker to Stories</td><td>Studio Nine</td><td>8 Oct</td></tr>
  </table>
</div>

<script>
  // Edit these four numbers: the chart and the total update.
  const weeks = [
    { label: 'Week 1', value: 9800 },
    { label: 'Week 2', value: 11400 },
    { label: 'Week 3', value: 14900 },
    { label: 'Week 4', value: 12100 }
  ];

  const svg = document.getElementById('bars');
  const max = Math.max(...weeks.map(w => w.value));
  const NS = 'http://www.w3.org/2000/svg';
  weeks.forEach((w, i) => {
    const h = Math.round((w.value / max) * 80);
    const x = 20 + i * 95;
    const bar = document.createElementNS(NS, 'rect');
    bar.setAttribute('x', x); bar.setAttribute('y', 100 - h);
    bar.setAttribute('width', 70); bar.setAttribute('height', h);
    bar.setAttribute('rx', 5); bar.setAttribute('fill', '#2563eb');
    const val = document.createElementNS(NS, 'text');
    val.setAttribute('x', x + 35); val.setAttribute('y', 94 - h);
    val.setAttribute('text-anchor', 'middle'); val.setAttribute('font-size', '12');
    val.setAttribute('fill', '#1d2330'); val.textContent = w.value.toLocaleString('en-US');
    const lab = document.createElementNS(NS, 'text');
    lab.setAttribute('x', x + 35); lab.setAttribute('y', 124);
    lab.setAttribute('text-anchor', 'middle'); lab.setAttribute('font-size', '12');
    lab.setAttribute('fill', '#6b7280'); lab.textContent = w.label;
    svg.append(bar, val, lab);
  });
  document.getElementById('reach').textContent =
    weeks.reduce((s, w) => s + w.value, 0).toLocaleString('en-US');
</script>
</body>
</html>
A one-page monthly report. Edit the four weekly numbers in the code and the chart and total change.

What goes on the page

The top carries the conclusion, so a quick look is enough. Everything under it supports it.

The four blocks of the page, top to bottom.
The four blocks of the page, top to bottom.
  1. Verdict and headline numbers. One sentence on how the month went, then four figures, each with its change from last month.
  2. Highlights. Two or three things that worked, with the date or post.
  3. What did not work. Say it plainly and say what changes.
  4. Next steps. An action, who does it, and a date.

For the thinking behind this order, see how to send a monthly report a client reads.

Make it with AI

You do not need to write the HTML. Give an AI chat your figures and ask for the page. The AI does not know your numbers, so paste them in.

Make one self-contained HTML page: a social media report for
"Juniper Coffee Roasters", September 2026, prepared by Studio Nine.
Top: a one-sentence verdict, then four stat cards (reach, new followers,
engagement rate, link clicks), each with its change vs August.
Below: a bar chart of reach by week drawn as inline SVG,
a Highlights list, a "What did not work" list, and a Next steps table
with Action, Who and By columns.
Use only these numbers: [paste your figures here].
No external images, fonts or scripts. Add a viewport meta tag.
Make the cards stack two per row on a phone.

Before you use the result, check three things.

  • Every number matches yours. AI can round or invent a figure. Compare each one, and recalculate any percentage.
  • The verdict says what the numbers say. If followers fell, the first sentence should not call the month a success.
  • It fits a phone. Open it at a narrow width. Cards should stack and nothing should run off the edge.
Paste your figures into the AI chat, check the numbers, paste the HTML into NOS and share the link.
Paste your figures into the AI chat, check the numbers, paste the HTML into NOS and share the link.

Paste the HTML into a NOS document and choose Create share link. HTML to link walks through it. The page renders as written, so charts drawn with scripts or SVG show up as you saw them.

Anyone with the link can open it in a browser, phone included, without an account. They cannot edit it. Paste the address into an email, a message or the project chat like any other link.

If a figure was wrong, edit the code and the same link shows the new version. There is nothing to resend.

PDF Screenshot Link to a page
What the client gets A file A picture An address
Page breaks Content is split into pages None, but it is one fixed picture None, it is one scrolling page
Fixing a figure after sending Export and send again Capture and send again Edit the page, same link
Numbers can be copied as text Depends on the export No, it is a picture Yes

Screenshot vs live page goes deeper on why a picture ages badly.

A report goes out every month. Rather than a new link each time, keep one page per client and put the newest month on top. The client keeps one address.

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>Juniper Coffee Roasters: reports by month</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 16px; background: #f1f3f6; font-family: system-ui, sans-serif; color: #1d2330; line-height: 1.5; }
  .page { max-width: 640px; margin: 0 auto; background: #fff; border-radius: 14px; padding: 20px 20px 24px; box-shadow: 0 2px 12px rgba(0,0,0,.08); }
  h1 { font-size: 20px; margin: 0 0 4px; }
  .sub { margin: 0 0 14px; font-size: 13px; color: #6b7280; }
  .tabs { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
  .tabs button { font: inherit; font-size: 14px; padding: 8px 14px; border-radius: 999px; border: 1px solid #cfd5df; background: #fff; cursor: pointer; }
  .tabs button[aria-pressed="true"] { background: #1d4ed8; border-color: #1d4ed8; color: #fff; }
  .big { font-size: 34px; font-weight: 700; line-height: 1.1; margin: 0; }
  .chg { font-size: 14px; font-weight: 600; }
  .up { color: #15803d; } .down { color: #b45309; } .flat { color: #6b7280; }
  .verdict { margin: 10px 0 16px; font-size: 16px; }
  h2 { font-size: 15px; margin: 0 0 8px; }
  .steps { list-style: none; margin: 0; padding: 0; }
  .steps li { display: flex; gap: 10px; align-items: flex-start; padding: 8px 0; border-bottom: 1px solid #e1e4ea; }
  .steps input { width: 20px; height: 20px; margin-top: 2px; flex: none; }
  .steps label { flex: 1; }
  .steps small { display: block; color: #6b7280; font-size: 12px; }
</style>
</head>
<body>
<div class="page">
  <h1>Juniper Coffee Roasters</h1>
  <p class="sub">Social media results. Newest month first, one link for all of them.</p>

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

  <p class="big" id="reach"></p>
  <p class="chg" id="change"></p>
  <p class="verdict" id="verdict"></p>

  <h2>Next steps</h2>
  <ul class="steps" id="steps"></ul>
</div>

<script>
  // One entry per month. Add a new object at the top each month.
  const months = [
    { name: 'September', reach: 48200, prev: 40800,
      verdict: 'Reach is up. Two Reels did most of the work.',
      steps: [['Post two more behind-the-scenes Reels', 'Studio Nine, by 10 Oct'],
              ['Approve the October offer wording', 'Juniper, by 5 Oct']] },
    { name: 'August', reach: 40800, prev: 36000,
      verdict: 'Steady growth. Stories with polls got the most replies.',
      steps: [['Test one poll Story per week', 'Studio Nine, done'],
              ['Send new product photos', 'Juniper, done']] },
    { name: 'July', reach: 36000, prev: null,
      verdict: 'First month of tracking. This is the starting point.',
      steps: [['Set up weekly reach tracking', 'Studio Nine, done']] }
  ];

  const tabs = document.getElementById('tabs');
  const steps = document.getElementById('steps');

  function show(i) {
    const m = months[i];
    [...tabs.children].forEach((b, j) => b.setAttribute('aria-pressed', j === i));
    document.getElementById('reach').textContent = m.reach.toLocaleString('en-US') + ' reach';
    const c = document.getElementById('change');
    if (m.prev === null) {
      c.textContent = 'No earlier month to compare'; c.className = 'chg flat';
    } else {
      const pct = Math.round((m.reach - m.prev) / m.prev * 100);
      c.textContent = (pct >= 0 ? '+' : '') + pct + '% vs the month before';
      c.className = 'chg ' + (pct >= 0 ? 'up' : 'down');
    }
    document.getElementById('verdict').textContent = m.verdict;
    steps.innerHTML = '';
    m.steps.forEach(([text, who], k) => {
      const li = document.createElement('li');
      const id = 's' + i + '-' + k;
      li.innerHTML = '<input type="checkbox" id="' + id + '"><label for="' + id + '"></label>';
      li.querySelector('label').append(text);
      const small = document.createElement('small');
      small.textContent = who;
      li.querySelector('label').append(small);
      steps.append(li);
    });
  }

  months.forEach((m, i) => {
    const b = document.createElement('button');
    b.textContent = m.name;
    b.addEventListener('click', () => show(i));
    tabs.append(b);
  });
  show(0);
</script>
</body>
</html>
One page, three months. Choose a month to see its numbers and next steps. Add a new month by adding one entry in the code.

Next steps are a checklist here. When you open your own page in View mode and tick an item, that change is saved. A visitor's ticks are not saved, so use the list to show status, not to collect answers.

If a client still wants a PDF

If a client wants a PDF, you can still send the link and offer a PDF as well. When you print the page, a page break can fall in the middle of a card.

MDN documents a line that asks the browser to keep a block in one piece when it breaks pages:

.stat, .chart { break-inside: avoid; }

The demo above already has it. For more on printing, see HTML to PDF without losing formatting.

A PDF or screenshot can split a block across pages and stays as sent; a link is one scroll and shows a fixed figure.
A PDF or screenshot can split a block across pages and stays as sent; a link is one scroll and shows a fixed figure.

When it does not work

What you see Cause Fix
A logo or chart image is missing It is an image loaded from another site, and those are blocked Ask for an inline SVG, or use text instead of a logo
Numbers differ from yours The AI rounded or made up a figure Paste the figures again and say to use only those
The page is tiny or too wide on a phone The viewport meta tag is missing Ask for it; see the viewport meta tag
The client's ticks are gone on reload A visitor's changes are not saved Tick items yourself in View mode, or ask for a reply
A reply form on the page sends nothing Sending a form to a server is blocked Ask the client to reply by email or message instead

Questions people ask

What should a social media report include?

A one-sentence verdict, a few headline numbers with the change from last month, two or three highlights, what did not work, and next steps with an owner and a date. Anything beyond that can sit lower on the page for people who want it.

How long should a monthly social media report be?

Short enough that the verdict and numbers are visible without scrolling on a phone. Detail can follow below. A client who only reads the top still gets the point.

Do I need a separate page for each client?

Yes, one page per client. Each client gets their own link, so nobody sees another client's numbers. Within a page, stack the months with the newest first.

Can the client change the numbers on the page?

No. A share link gives a page anyone with the link can open and cannot edit. You edit the code in your own document, and the same link shows the new version.

Will I see who opened the report?

No. NOS does not show who opened a page. If you need to know it was read, ask for a reply, for example by making approval of a next step one of the actions.

Keep reading