DEV Community

Cover image for Stop Counting Page Views: A Developer's Guide to Tracking B2B Content ROI to Closed Deals
Michael
Michael

Posted on Originally published at getmichaelai.com

Stop Counting Page Views: A Developer's Guide to Tracking B2B Content ROI to Closed Deals

Page views are a vanity metric. So are likes, shares, and time-on-page. None of them pay the bills.

If you run content for a B2B company, the CFO doesn't care that your latest post hit 12,000 views. They care whether that post influenced a deal that closed. The gap between those two facts is an attribution problem - and attribution is fundamentally an engineering problem.

This is a walkthrough of how to build the plumbing that connects a click to a contract.

Why most content analytics are broken

The default stack - Google Analytics plus your CMS dashboard - measures traffic, not revenue. It answers "what got read?" It cannot answer "what got bought?"

The reason is simple: your analytics tool and your CRM live in separate worlds. GA knows about anonymous sessions. Your CRM knows about named accounts and dollar amounts. Nobody stitched them together.

B2B makes this worse. Sales cycles run 3-9 months. Buying committees have 6-10 people. A single deal might touch 15 pieces of content across four humans before anyone talks to sales. Last-click attribution throws all of that away.

Step 1: Capture identity early and persist it

You can't attribute what you can't identify. The move is to capture a utm and a first-touch source the moment someone lands, then carry it all the way to the form fill.

// Run on first page load, persist across the whole session
function captureAttribution() {
  const params = new URLSearchParams(window.location.search);
  const existing = JSON.parse(localStorage.getItem('mkt_attribution') || 'null');

  const touch = {
    utm_source: params.get('utm_source'),
    utm_medium: params.get('utm_medium'),
    utm_campaign: params.get('utm_campaign'),
    landing_page: window.location.pathname,
    referrer: document.referrer || 'direct',
    timestamp: new Date().toISOString(),
  };

  // First touch is sacred - never overwrite it
  if (!existing) {
    localStorage.setItem('mkt_attribution', JSON.stringify({
      first_touch: touch,
      touches: [touch],
    }));
  } else {
    existing.touches.push(touch);
    localStorage.setItem('mkt_attribution', JSON.stringify(existing));
  }
}

captureAttribution();
Enter fullscreen mode Exit fullscreen mode

When the prospect fills a form, ship the whole mkt_attribution blob into your CRM as hidden fields. Now every lead arrives with its content history attached.

Step 2: Pick an attribution model on purpose

There is no "correct" model. There's the model that matches how you actually sell.

  • First-touch: credits the content that created awareness. Good for measuring top-of-funnel.
  • Last-touch: credits the final content before conversion. Good for measuring closers.
  • Linear: splits credit evenly across every touch. Fair, but treats a whitepaper the same as a pricing page.
  • Time-decay: weights recent touches heavier. Best for long, multi-touch B2B cycles.

For most B2B pipelines, run multi-touch with time-decay. Here's the credit split in practice:

from datetime import datetime

def time_decay_credit(touches, close_date, half_life_days=30):
    """Distribute deal credit across content touches, weighting recency."""
    close = datetime.fromisoformat(close_date)
    weights = []

    for t in touches:
        days_before = (close - datetime.fromisoformat(t['timestamp'])).days
        weight = 0.5 ** (days_before / half_life_days)
        weights.append((t['landing_page'], weight))

    total = sum(w for _, w in weights)
    return {page: round(w / total, 3) for page, w in weights}

touches = [
    {'landing_page': '/blog/ai-agents-guide', 'timestamp': '2024-01-10T09:00:00'},
    {'landing_page': '/blog/n8n-vs-zapier', 'timestamp': '2024-02-15T14:00:00'},
    {'landing_page': '/pricing', 'timestamp': '2024-03-01T11:00:00'},
]

print(time_decay_credit(touches, '2024-03-05T00:00:00'))
# {'/blog/ai-agents-guide': 0.09, '/blog/n8n-vs-zapier': 0.33, '/pricing': 0.58}
Enter fullscreen mode Exit fullscreen mode

Now when a $40K deal closes, you can distribute that revenue across the content that touched it. The guide gets $3,600, the comparison post $13,200, pricing $23,200.

Step 3: Measure the KPIs that map to money

Once content is tied to deals, track the metrics that actually move a business:

  • Content-influenced pipeline: total value of open deals that touched a piece.
  • Content-attributed revenue: closed-won dollars, distributed by your model.
  • Cost per influenced opportunity: content spend divided by opportunities touched.
  • Velocity impact: do deals that engage with content close faster?

A post with 2,000 views that touched three $50K deals beats a post with 40,000 views that touched zero. Views were never the point.

Step 4: Automate the loop

The manual version of this - exporting CRM data, matching it to analytics, building spreadsheets - dies within a month. Nobody keeps it up.

Build a pipeline instead. A scheduled job pulls closed and open deals from your CRM, joins them against the attribution payload stored on each contact, runs the credit distribution, and writes results to a warehouse table your dashboard reads. Tools like n8n or a small serverless function make this a weekend build, not a quarter-long project.

The payoff: a live view of which content drives pipeline, refreshed daily, with zero spreadsheet surgery.

The bottom line

Content ROI isn't a reporting problem you solve at quarter-end. It's a data pipeline you build once. Capture identity on the first click, carry it into the CRM, pick a model that matches your sales motion, and automate the attribution math.

Do that, and the next time someone asks "is content working?" you won't reach for page views. You'll point at closed revenue.


Originally published at getmichaelai.com

Top comments (0)