Different Hunger

Canonical Components (v18)

L1 · 01 Content Distribution

The Canonical Content OS (5-stage) · Content Templates · Distribution Channels.

L1 · 01 Content Distribution

Purpose: Create content and attract attention.

SlugNameKind
l1.content-distribution.content-poolOne Content Pool & Modalprinciple
l1.content-distribution.content-osCanonical Content OS (5-stage)process
l1.content-distribution.templatesContent Templatesasset
l1.content-distribution.channelsDistribution Channelsconfig
l1.content-distribution.schedulingPubler Scheduling & Content Bucketsconfig
l1.content-distribution.metricsCanonical Distribution Metrics (ATTRACT scoreboard)measure

l1.content-distribution.content-pool — One Content Pool & Modal

It's all content. Ads, organic, content distribution — the same thing, viewed the same way.

There is one content pool per brand and one modal to author, view, and approve it (the creative grid + + Add Concept). "Ad" vs "organic" is not a different system — it's just where a given piece goes: Meta (paid) and/or Publer (organic FB · IG · LinkedIn · …). Build once, distribute everywhere.

  • One pool: engine.components["ad-creative-set"].review_copy.concepts[] — every concept has stage, headline, body, cta, url, media, status.
  • One modal: the HIFI Content Distribution card and the Ads card render through the same surface (surfaceFor → "ad") and read the same pool (content-distribution aliases ad-creative-set). Approve in bulk, once.
  • Channels are destinations: a piece approved in the modal is eligible for paid (Meta) and organic (Publer) — the brand's connected_accounts decide where.

Do not build separate "ads", "organic", and "content" systems. One pool, one modal, many destinations.

Where content lives — Drive (bytes) · Supabase (index)

Storage is two layers; don't pay twice.

  • Bytes → Google Drive (already subscribed, effectively unlimited). Masters, raws, finished cuts, clips, and transcript files live here. Unlike YouTube, code gets the real, uncompressed master back via the Drive API (files.get?alt=media) — fetch → upload to the destination. Requirement: the OAuth account must have download rights (capabilities.canDownload === true); some Shared Drives disable download for non-managers, which blocks the fetch.
  • Index → Supabase (the DB). Each content record holds the Drive file id, the transcript with timecodes, links to derived clips, plus status · bucket · objective · performance. This is the repurpose brain: search transcript → timecode → ffmpeg cuts the clip from the Drive master → the clip becomes a new concept in the pool. (Small ad-creative screenshots already sit in the Supabase creatives bucket — fine to leave; the 50MB project cap is why video does NOT go to Supabase Storage.)
  • Never hotlink Drive (interstitials, no CDN) — always fetch-via-API → upload, which is what Publer/Meta need anyway.
  • YouTube = publish + SEO destination, never the master store (re-encode + no file pull).

The scheduler resolves a concept's master from drive_file_id first (Drive bytes), then a public URL fallback — apps/site/app/api/content/schedule.


l1.content-distribution.content-os — Canonical Content OS

5-stage locked flow, replaces TOFU/MOFU/BOFU:

Ideate → Analyze → Create → Promote → Optimize

1 case study/week → 17 assets produced. N8N draft generator + briefs + templates all already live ([[project_canonical_content_os]] LOCKED 2026-05-11).

Do not propose alternative content matrices. This is the locked process.


l1.content-distribution.templates

Locked per-channel templates:

  • Long-form post
  • Short post
  • Email broadcast
  • Video script
  • Long-form video

All seeded from the Canonical Content OS Create stage.


l1.content-distribution.channels

Channel mix per brand:

  • Outbound — Lemlist programmatic via n8n
  • Inbound · Referral — Share to Unlock + referral payouts
  • Inbound · Organic — SEO Arch + content distribution
  • Inbound · Ads — Meta + Google Ads via Ad Creative Set

l1.content-distribution.scheduling — Publer Scheduling & Content Buckets

The Promote stage runs through Publer for every brand. No manual posting, ever. This is how the 5-stage OS distributes — not a new content matrix, the scheduling layer underneath the locked one.

Content buckets = the Content OS Category field. Each bucket maps to a recurring Publer slot and recycles evergreen assets, so the queue is never empty. This is the "never run out of content to post, ever" guarantee — the 17 assets/week from the Create stage flow into buckets and auto-fill the calendar.

Caption rule — locked: every scheduled post = one sentence + one CTA, selected by the asset's Post Objective from the Social Content Brief:

Post ObjectiveCaption shape
Engageone hook sentence → soft CTA (follow / comment keyword)
Consumeone value sentence → mid CTA (watch / DM keyword)
Clickthroughone offer sentence → hard CTA (book / visit domain)

Mechanism:

  • One Publer workspace per agency; per-brand connected accounts (FB Page · IG Business · LinkedIn · TikTok · …) are the Distribution Checklist channels.
  • API key per agency in ~/.config/0hr/publer.env; posts are created scheduled, then a human approves in Publer's calendar before publish (Approval Workflow §10 — nothing goes live without approval).
  • Bucket → cadence → calendar is the durable plan; the calendar view is the approval surface.

Max the channels: connect every channel the brand can sustain (FB · IG · LinkedIn · TikTok · YouTube · Pinterest · X · Google Business). More connected accounts = more reach per asset at zero extra production. New connected account → add its id to the brand's connected_accounts; it auto-joins the live check and the schedule.

Reuse rule — positive signs of life → repurpose (the Optimize stage). Every post is categorized into a bucket. Publer + the weekly Optimize pass read engagement; any post showing positive signs of life (above-median engagement for its bucket) is recycled and repurposed — re-queued, cross-posted to other channels, and cut into derivative formats. Losers retire. This is what makes the queue compound instead of drain: winners never die, so the brand never runs out of proven content.

Per-brand install — ONE workspace, many brands, verified binding:

  • One Publer workspace per agency holds the social accounts for ALL brands (one subscription, not one per brand). The workspace is shared; brands are NOT.
  • Each brand binds to its own connected_accounts (a subset of the workspace). MCM → its FB Page / IG / Sam's LinkedIn; DH → DH's; never the whole workspace.
  • VERIFY-BY-NAME at install (mandatory). Before a brand's connected_accounts go live, confirm every account_id resolves to the expected brand-named account in the live Publer /accounts directory (e.g. MCM facebook id → "Medical Consulting & Management", not "Matt Kohn"). A workspace with 19 accounts means a wrong id silently cross-posts one brand's content to another's audience. Fail the install loudly on any mismatch. (Second lock: everything ships as a draft first, and Publer shows the account name before publish — but the binding must still be name-verified, never assumed.)
  • Map each Content OS Category to its bucket cadence.

The flow — approve (per atom) → fire → draft

Approval is the trigger, and it is per atom. A "content atom" = one concept/reel in the pool. The POC approves each atom individually in the wizard (a per-card Approve button) — that single click is the only human action upstream. It fires the rest automatically:

Approve atom  (the one human action, per atom)
   → categorize : drops into its bucket (objective → Engage/Consume/Clickthrough)
   → fire        : drafts to Publer (every connected channel) as state="draft"
   → fire        : queues a PAUSED Meta ad with the same atom as creative
   ← human       : publishes the Publer draft / un-pauses the ad (the only public step)

It's all content (one atom, two destinations): the same approved atom fans out to organic (Publer drafts) and paid (a paused Meta ad). Approve once, both queued. No scripts, no bulk jobs — the approve click on the atom is the wire.

Mechanics:

  1. ApprovePOST /api/content/approve {brand_slug, concept_id} sets the atom status: "approved" (clobber-safe).
  2. Categorize — same call assigns a bucket (explicit, or defaulted by objective: tofu→Engage · mofu→Consume · bofu→Clickthrough). The bucket carries the cadence + recycle.
  3. Fire — approval triggers …/content/schedule for that atom → Publer draft (+ paused Meta ad). Approved-only gate; nothing publishes without the human publish/un-pause (Approval Workflow §10).

Un-approving ({approved:false}) pulls the atom back out and removes its queued drafts.

Never create drafts/ads via an out-of-band bulk script — approval-of-the-atom is the only trigger. This keeps the client surface honest: one click per atom, fully traceable.

Distribution action — approved content → Publer drafts

The action that fills the calendar. Endpoint: apps/site/app/api/content/schedule (?brand=<slug>).

  • Approved-only gate: only concepts with status === "approved" in the one content pool are eligible. Pending content never schedules.
  • Drafts only: default is dry run (returns the plan, writes nothing). ?live=1 creates Publer drafts (state:"draft") across the brand's connected_accountsnever publishes, never schedules-live (Approval Workflow §10). The human approves each in Publer's calendar.
  • Fan-out: each approved concept × each connected channel → one draft, caption built by the locked rule (one sentence + one CTA, by Post Objective).
Verified Publer API contracts
base    https://app.publer.io/api/v1
headers Authorization: Bearer-API <key>  ·  Publer-Workspace-Id: <ws>
upload  POST /media  (multipart file=<binary>)  → {id, path, thumbnails[]}
          ⚠ the Blob MUST carry a MIME type (Content-Type video/mp4, image/jpeg…)
          or Publer 400s "File is not supported".
create  POST /posts/schedule  {bulk:{state:"draft", posts:[{
          accounts:[{id, scheduled_at?}],
          networks:{<provider>:{ type:"status|photo|video", text:"<caption>",
            media:[{ id, thumbnails:[…from upload…], default_thumbnail:0 }] }}
          }]}}  → {job_id};  poll GET /job_status/<job_id> (failures:{} == success)
read    GET /posts?state=scheduled|published|draft  (returns bare array)
accts   GET /accounts → bare array [{id,name,provider,type}] (verify-by-name)
delete  NOT exposed by the API (all paths 404) — remove drafts in the Publer UI

Verified contract (2026-06-19, video proven end-to-end on MCM). Both text AND media live INSIDE networks.<provider> — NOT at the post root. Media must be uploaded to the library first (URL-attach is rejected), and the post's media object MUST carry the upload's full thumbnails array + default_thumbnail:0 — omitting it throws Publer's undefined method 'first' for nil. The upload Blob MUST set a MIME type or /media 400s. job_status with empty failures:{} = the draft was created. Code: apps/portal/app/api/ content/approve (per-atom) + apps/site/app/api/content/schedule (cycle).


l1.content-distribution.metrics — Canonical Distribution Metrics

Publer measures the engine up to the click. The click is the handoff. Distribution is the ATTRACT pillar; its job ends when a person clicks through. Everything past the click — lead, consult, client — is owned by QUALIFY → CONVERT (the CRM), joined back to the post that earned the click. Distribution metrics answer one question: is the content engine efficient? They are never business results on their own.

The canonical metric set (per brand, per period)

MetricStageSourceStatus
Posts PublishedPromotePubler /posts?state=publishedtotalLivecheckPubler, apps/site/app/api/brand-systems/route.ts
ScheduledPromotePubler /posts?state=scheduledtotalLive — queue-depth; GREEN when > 0
ReachOptimizePubler analytics/:account/chart_data (post_reach)⚠️ Extension — gated on Publer insights backfill
EngagementsOptimizePubler post_engagement⚠️ Extension — see definition below
ClicksOptimizePubler link_clicks⚠️ Extension — the handoff seam

Posts Published + Scheduled are canonical today — already wired and trustworthy. Reach · Engagements · Clicks are the locked extension to the ATTRACT scoreboard: their definitions are fixed here, but they render only once Publer insights actually populate (chart_data returns empty until accounts are gathering insights). Never ship zeros — a metric with no data is hidden, not shown as 0 (Kernel §11 ZERO FABRICATION).

Engagement — locked definition

Engagement = reactions/likes + comments + shares + saves. Excludes link clicks (that is Clicks, its own metric — never double-count) and video views (autoplay-inflated, tracked separately). Excludes follows.

Identical across every provider (FB · IG · LinkedIn · TikTok · X). Engagement = active interaction with the post itself; the click and the view each have their own lane.

The join — ATTRACT → QUALIFY → CONVERT

One row per brand, the full content-to-revenue chain across three pillars:

Posts → Reach → Engagements → Clicks │ → Leads → Converted
        └────── ATTRACT (Publer) ─────┘   QUALIFY    CONVERT
                                          (contacts) (contacts.stage)
  • Leads = QUALIFY pillar — new contacts rows attributed to content.
  • Converted = CONVERT pillar — contacts advanced to client / consult booked.

Attribution mechanism (the only bridge past the click): Publer goes blind at the click, so the link itself must carry the trace.

  1. The scheduler UTM-tags every link it posts: utm_source=publer · utm_medium=social · utm_campaign={brand} · utm_content={post_id}.
  2. Intake captures the UTM onto the contacts row at lead creation (extends the existing source capture).
  3. Join contacts.utm_content → Publer post_id — a lead traces to the exact post and brand.

Efficiency ratios — "is the engine working?"

The ratios between stages are the distribution-engine efficiency signal:

RatioFormulaReads
AmplificationReach ÷ Postshow far each asset travels
Engagement RateEngagements ÷ Reachresonance
CTRClicks ÷ Reachpull to action
Click→Lead %Leads ÷ Clicksthe seam's conversion
Lead→Converted %Converted ÷ Leadsthe business close

Not the scoreboard — diagnostic only

  • Followers — audience size, not a result. Trend it for sanity; never the headline number (the "XP" trap — real results, not vanity).
  • Video Views · Demographics (countries/ages) — targeting QA ("are we reaching MoCo for MCM?"), not a score.

Workspaces are tool-config, not a metric. One Publer workspace per agency; per-brand connected_accounts scope every metric above to the brand (account_ids[]=…). See l1.content-distribution.scheduling.


Reference rule

Min 2 / Max 4 reviewer-rated 4-5/5 library refs per sub-deliverable.