VOOZH about

URL: https://dev.to/feidou/content-marketing-roi-measuring-what-matters-for-saas-growth-2k0h

⇱ Content Marketing ROI: Measuring What Matters for SaaS Growth - DEV Community


Most SaaS companies cannot answer the question: "Is our content marketing working?" They track vanity metrics (traffic, social shares) instead of revenue metrics (signups from content, content-assisted conversions, LTV of content-sourced users). This guide covers a systematic approach to content ROI measurement — from first-touch attribution through multi-touch models — using the analytics infrastructure at tanstackship.com.


The Vanity Metrics Trap

Vanity Metric Tells You Doesn't Tell You
Pageviews Someone loaded a page Whether they converted
Time on page They read (or walked away) Whether it influenced a decision
Social shares People liked it Whether it drove any signups
Email subscribers People want updates Whether they will ever pay
SEO rankings Your position in search Whether anyone clicks
Revenue Metric Tells You How to Measure
Content-sourced signups Direct attribution UTM parameters + first-touch model
Content-assisted conversions Influenced pipeline Multi-touch attribution
LTV by content source Long-term value Cohort analysis by source
Content CAC Cost per acquisition Total content spend / attr. conversions

Attribution Models

First-Touch Attribution

// First touch: the first piece of content a user encountered
export const getFirstTouchAttribution = createServerFn({ method: "GET" }).handler(
 async ({ data, context }: { data: { period: string } }) => {
 const result = await context.env.DB.prepare(`
 SELECT
 first_utm_source as source,
 first_utm_campaign as campaign,
 first_utm_content as content,
 COUNT(*) as signups,
 SUM(CASE WHEN subscription_status = 'active' THEN 1 ELSE 0 END) as conversions
 FROM users
 WHERE created_at > datetime('now', ?)
 GROUP BY first_utm_source, first_utm_campaign
 ORDER BY signups DESC
 `).bind(data.period).all()

 return result.results
 }
)

Last-Touch Attribution

// Last touch: the content that drove the final conversion
export const getLastTouchAttribution = createServerFn({ method: "GET" }).handler(
 async ({ data, context }: { data: { period: string } }) => {
 const result = await context.env.DB.prepare(`
 SELECT
 source,
 campaign,
 COUNT(*) as conversions
 FROM utm_clicks
 WHERE user_id IN (
 SELECT id FROM users
 WHERE created_at > datetime('now', ?)
 AND subscription_status = 'active'
 )
 ORDER BY created_at DESC
 LIMIT 1
 `).bind(data.period).all()

 return result.results
 }
)

Content Performance by Funnel Stage

Funnel Stage Content Type Primary Metric Secondary Metric
Top of Funnel (Awareness) Blog posts, guides, comparison pages Organic traffic Time on page, scroll depth
Middle of Funnel (Consideration) Case studies, benchmarks, tutorials Email signups Content downloads, CTR
Bottom of Funnel (Conversion) Pricing comparisons, feature deep-dives Free trial signups Demo requests, purchase
Retention Best practices, updates, templates Active usage Feature adoption, upsells

Cost Tracking

// Track content production costs
export const trackContentCost = createServerFn({ method: "POST" }).handler(
 async ({ data }: { data: {
 contentId: string
 hoursSpent: number
 hourlyRate: number
 distributionCost: number
 }}) => {
 const totalCost = data.hoursSpent * data.hourlyRate + data.distributionCost

 await contentDB.prepare(`
 INSERT INTO content_costs
 (id, content_id, hours_spent, hourly_rate,
 distribution_cost, total_cost, created_at)
 VALUES (?, ?, ?, ?, ?, ?, ?)
 `).bind(
 crypto.randomUUID(),
 data.contentId,
 data.hoursSpent,
 data.hourlyRate,
 data.distributionCost,
 totalCost,
 Date.now()
 ).run()

 return { totalCost }
 }
)

Cost Per Acquisition by Content Type

export const getContentCac = createServerFn({ method: "GET" }).handler(
 async ({}, { context }) => {
 const results = await context.env.DB.prepare(`
 SELECT
 c.content_type,
 SUM(cc.total_cost) as total_cost,
 COUNT(DISTINCT u.id) as attributed_signups,
 SUM(cc.total_cost) / COUNT(DISTINCT u.id) as cac
 FROM content_costs cc
 JOIN content c ON c.id = cc.content_id
 LEFT JOIN users u ON u.first_utm_content = c.slug
 GROUP BY c.content_type
 ORDER BY cac ASC
 `).all()

 return results.results
 }
)

LTV Analysis by Content Source

export const getLtvByContentSource = createServerFn({ method: "GET" }).handler(
 async ({}, { context }) => {
 const results = await context.env.DB.prepare(`
 SELECT
 first_utm_source as source,
 COUNT(*) as users,
 AVG(CASE WHEN status = 'active' THEN mrr ELSE 0 END) as avg_mrr,
 SUM(CASE WHEN status = 'active' THEN mrr ELSE 0 END) as total_mrr
 FROM users u
 LEFT JOIN subscriptions s ON s.user_id = u.id
 WHERE u.created_at > datetime('now', '-180 days')
 GROUP BY first_utm_source
 HAVING users > 5
 ORDER BY total_mrr DESC
 `).all()

 return results.results
 }
)

Content ROI Dashboard

export const getContentROIDashboard = createServerFn({ method: "GET" }).handler(
 async ({}, { context }) => {
 const [performance, cac, ltv] = await Promise.all([
 // Content performance overview
 context.env.DB.prepare(`
 SELECT
 c.title,
 c.slug,
 c.published_at,
 c.views,
 c.avg_read_time,
 at.conversions
 FROM content c
 LEFT JOIN attribution at ON at.content_slug = c.slug
 ORDER BY at.conversions DESC
 LIMIT 20
 `).all(),

 // CAC by content type
 getContentCac(),

 // LTV analysis
 getLtvByContentSource(),
 ])

 return { performance, cac, ltv }
 }
)

Content ROI Benchmarks

Content Type Avg. Cost (per piece) Avg. Signups Avg. CAC Avg. 6mo LTV ROI (6mo)
Tutorial/Guide $2,000 50 $40 $360 9x
Comparison page $1,500 80 $19 $420 22x
Case study $3,000 30 $100 $600 6x
Listicle/Resource $800 20 $40 $300 7.5x
Video tutorial $2,500 40 $63 $480 7.6x
Newsletter $500/month 25/mo $20 $360 18x

Building a Content Calendar with ROI Tracking

export const createContentPlan = createServerFn({ method: "POST" }).handler(
 async ({ data, context }: { data: {
 title: string
 type: string
 targetKeyword: string
 estimatedCost: number
 expectedSignups: number
 }}) => {
 const roiProjection = {
 cac: data.estimatedCost / data.expectedSignups,
 projectedMonthlyRevenue: data.expectedSignups * 29, // $29 avg MRR
 breakEvenMonths: data.estimatedCost / (data.expectedSignups * 29),
 sixMonthReturn: (data.expectedSignups * 29 * 6) / data.estimatedCost,
 }

 // Store in database for tracking
 await context.env.DB.prepare(`
 INSERT INTO content_plan
 (title, type, target_keyword, estimated_cost,
 expected_signups, projected_cac, projected_roi, created_at)
 VALUES (?, ?, ?, ?, ?, ?, ?, ?)
 `).bind(
 data.title, data.type, data.targetKeyword,
 data.estimatedCost, data.expectedSignups,
 roiProjection.cac, roiProjection.sixMonthReturn,
 Date.now()
 ).run()

 return roiProjection
 }
)

Content ROI Measurement Checklist

  • [ ] UTM parameters on every content link
  • [ ] First-touch attribution tracked in database
  • [ ] Content production costs tracked (time + distribution)
  • [ ] Content-assisted conversions identified (multi-touch)
  • [ ] LTV calculated by content source
  • [ ] CAC monitored per content type
  • [ ] ROI dashboard built and reviewed monthly
  • [ ] Content performance compared to projections
  • [ ] Underperforming content identified and optimized
  • [ ] Best-performing content types doubled down on
  • [ ] Attribution data shared with content team

Conclusion

The difference between content marketing that works and content marketing that is a cost center comes down to measurement. SaaS companies that measure content ROI — by every content piece, by source, by channel — consistently outperform those that track only pageviews and social shares.

The measurement stack is straightforward:

  1. Track every content touchpoint with UTM parameters
  2. Store first-touch and last-touch attribution in your database
  3. Calculate production costs for every piece of content
  4. Monitor CAC and LTV by content source
  5. Build a dashboard that shows what content drives revenue

For a SaaS with built-in UTM tracking, attribution, and analytics infrastructure, see tanstackship.com.

Related Resources