Dashboard

What does a content performance dashboard show in Metabase?

A content performance dashboard joins what you published to what it earned: publishing cadence from the CMS, organic clicks and sessions per post from Google Search Console and GA4, top and declining content, engagement, and the conversions content actually drove. The CMS side can come from WordPress, Ghost, Strapi, or Webflow — it is the centerpiece of website analytics in the warehouse.

For: content and growth teams proving what content works. Grain: one row per content URL per day, from CMS post tables joined to search and analytics rollups on a normalized URL.

Which cards belong on a content performance dashboard?

  • Posts published by week, by author or category (bar)
  • Organic clicks per post, trailing 28 days (table)
  • Sessions per post from site analytics (table)
  • Top content by clicks and conversions (table)
  • Declining content — clicks down vs. the prior period, refresh candidates (table)
  • Engagement — comments per post, newsletter clicks per send (table)
  • Conversions or member signups attributed to content (bar)
  • Clicks by time since publish — cohort curves per publish month (line)

What data does the dashboard need?

  • A CMS content table: wp_posts (WordPress, direct MySQL), webflow_cms_items (Data API sync), or Ghost's posts table — id, title, publish date, status, and slug/URL.
  • gsc_performance_daily at page grain, and a sessions-per-page daily rollup from GA4 or Plausible.
  • Engagement and conversion sources as available: wp_comments, Ghost newsletter stats (ghost_email_stats) and member attribution, or form submissions.
The join key is a normalized URL — lowercase, no protocol, no trailing slash — built identically on every side. Get that one expression right, exclude revisions and drafts, and the rest of the dashboard is straightforward joins.

How do you build it?

  1. Connect the CMS: point Metabase at the WordPress/Ghost/Strapi database directly, or schedule a sync for hosted platforms like Webflow.
  2. Sync Search Console and analytics rollups to the same warehouse, and build the shared url_key normalization view over all three sources.
  3. Model per-post daily performance plus a time-since-publish column, and period-over-period deltas for the declining-content table.
  4. Assemble the cards with author, category, and publish-cohort filters, and a drill-through from each post to its daily trend.

Example card SQL

Organic clicks per published post, trailing 28 daysPostgreSQL
WITH content AS (
  SELECT
    ID AS post_id,
    post_title,
    post_date::date AS published_on,
    lower('yourdomain.com/blog/' || post_name) AS url_key
  FROM wp_posts
  WHERE post_type = 'post'
    AND post_status = 'publish'
),
search AS (
  SELECT
    lower(
      regexp_replace(page, '^https?://|/+$', '', 'g')
    ) AS url_key,
    SUM(clicks) AS organic_clicks_28d
  FROM gsc_performance_daily
  WHERE stat_date >= CURRENT_DATE - 28
  GROUP BY 1
)
SELECT
  c.post_title,
  c.published_on,
  COALESCE(s.organic_clicks_28d, 0) AS organic_clicks_28d
FROM content c
LEFT JOIN search s ON s.url_key = c.url_key
ORDER BY organic_clicks_28d DESC
LIMIT 25;
Publishing cadence by weekPostgreSQL
SELECT
  date_trunc('week', post_date) AS week,
  COUNT(*) AS posts_published
FROM wp_posts
WHERE post_type = 'post'
  AND post_status = 'publish'
GROUP BY 1
ORDER BY 1;

Metrics

Integrations

Dashboards

FAQ

How do CMS posts get joined to search and analytics data?
On a normalized URL — it is the only key all three systems share. Build one url_key expression and apply it everywhere: lowercase, strip the protocol and trailing slash, and drop query strings and fragments (UTM-tagged rows otherwise become phantom pages). The CMS side comes from the post slug or permalink, the search side from Search Console page URLs, the analytics side from GA4 or Plausible page paths. Put the normalization in one warehouse view — three slightly different regexes is the classic way this dashboard silently loses rows.
Why do last month's posts look like they underperform?
Because publish date isn't traffic date: organic traffic compounds for months after publishing, so a four-week-old post losing to a two-year-old one is the expected shape, not a verdict. For fair comparisons, chart clicks by time since publish — clicks in each post's first 30/60/90 days — so cohorts compete on equal footing. The all-time top-content table still belongs on the dashboard; just don't use it to judge recent work.
Which CMS rows should be excluded?
More than you'd think. WordPress's wp_posts holds revisions, drafts, autosaves, attachments, and pages alongside published posts — filter to post_type = 'post' AND post_status = 'publish' or every count on the dashboard inflates. The equivalents elsewhere: is_draft and is_archived flags on Webflow CMS items, post status in Ghost, and published_at IS NOT NULL in Strapi (drafts have no publish date).
Does this work the same for every CMS?
The join pattern is identical; only the content table changes. Self-hosted WordPress, Ghost, and Strapi are the easy cases — Metabase connects straight to their MySQL or Postgres database, so posts are queryable with zero ETL. Hosted platforms like Webflow (CMS items via the Data API) or WordPress.com need a small scheduled sync instead. The websites & CMS overview compares the routes per platform.
How are conversions attributed to content?
Decide the model before building the card, because the warehouse can support several: last-touch (the session's landing page gets the signup), first-touch, or any-touch (every content URL in the converting path gets credit). Landing-page attribution from GA4 or Plausible rollups is the simplest and usually enough to rank content by conversion rate. Membership platforms are more direct — Ghost stores an attribution source per member, so signups per post is a plain query. Label the card with whichever model you chose.