Portfolio sync

Weekly automated detection of new and updated entries on portfolio.apexure.com, writing skeleton drafts into _landing-page-examples/ for editorial review.

How it runs

What it does

  1. Pulls every post from https://portfolio.apexure.com/wp-json/wp/v2/posts
  2. Diffs against state.json — a manifest of every WP post ID we’ve seen and its last-known modified timestamp
  3. For each truly-new or modified item, writes _landing-page-examples/<slug>-landing-page.md with:
  4. Updates state.json so subsequent runs only flag genuine deltas
  5. Opens a PR titled Portfolio sync — new portfolio.apexure.com items
  6. Posts to Slack (if SLACK_WEBHOOK_URL secret is configured)

Drafts are published: false, so Jekyll skips them at build time — they never appear in the lookbook listing or get a public URL until a human flips the flag.

Handoff to the “full implementor” agent

When you git checkout a sync branch in VSCode and ask Claude to fill in the drafts, this is the contract it implements per file:

Field What to do
title Replace TODO with: <Client> <Vertical> Landing Page \| CRO Breakdown (~60-70 chars)
description 1-2 sentences, ~150-200 chars, mentions vertical and the standout conversion mechanic
meta-keywords 5-10 search terms for the vertical and page type
industry Pick from the established taxonomy (SaaS, Healthcare, FinTech, etc.)
page-type Lead Generation / Sales / Click-Through / Squeeze
technology WordPress / Unbounce / Webflow / etc. — inferred from the rendered page
audience B2B / B2C / B2B2C
cro-score Integer 0-100 — see scoring rubric below
primary-cta Verbatim button text from the live portfolio page
conversion-goal Demo Request / Trial Signup / Lead Form / etc.
design-features List, drawn from established taxonomy
psychological-principles List: authority-bias, social-proof, cognitive-load, visual-hierarchy, scarcity, etc.
your-question-section 5-8 FAQ entries, each ~200-400 words, doubling as CRO commentary
social-image Generate /images/generated/lp-<slug>.svg
published Flip falsetrue once everything above is done

The <!-- DRAFT auto-generated --> block at the bottom of each file is a checklist for the agent. Delete it once the file is fully authored.

CRO scoring rubric

Score 0-20 in each of five dimensions, sum to a total out of 100:

Dimension What to look at
Hierarchy Is the value prop instantly clear? Single dominant element above the fold?
Clarity Headline-ad match, plain language, scannable structure
Proof Specific named clients, metrics, testimonials with attribution
CTA Single primary CTA, contextual, low form friction
Friction Form length, navigation distractions, trust signals near commit point

A strong B2B SaaS demo page typically scores 80-95. A page with distracting navigation, generic stock proof, and a fuzzy headline scores 50-70.

State file format

state.json:

{
  "last_run": "2026-05-02T11:02:37Z",
  "items": {
    "2823": {
      "slug": "valley-solar",
      "modified": "2026-04-28T08:16:16",
      "link": "https://portfolio.apexure.com/valley-solar/"
    }
  }
}

Keyed by WP post ID, so slug renames don’t break tracking. Committed to the repo so the cron is stateless across runs.

Backlog handling

When this system was set up, portfolio.apexure.com had 495 posts vs. 311 examples in _landing-page-examples/ — i.e. ~184 portfolio items have never been written up here. The cron does not auto-draft this backlog: it was seeded into state.json as “already known” so weekly runs only flag genuinely new items.

If you want to chip away at the backlog:

# Drop a slug from state.json and re-run sync — that one item gets drafted
ruby -rjson -e '
  s = JSON.parse(File.read("script/portfolio-sync/state.json"))
  s["items"].delete("2823")  # ← whichever WP post id
  File.write("script/portfolio-sync/state.json", JSON.pretty_generate(s) + "\n")
'
ruby script/portfolio-sync/sync.rb --max-drafts 1

Or batch through them with --max-drafts 5 once a week.

Slack setup

To enable the Slack notification:

  1. Slack: create an app at https://api.slack.com/apps (or use an existing one)
  2. App → “Incoming Webhooks” → activate → “Add New Webhook to Workspace” → pick the channel
  3. Copy the webhook URL (starts with https://hooks.slack.com/services/...)
  4. GitHub: repo → Settings → Secrets and variables → Actions → New repository secret

The workflow gracefully no-ops the Slack step if the secret isn’t set.

Operations

What you’d do How
Force a sync now Actions tab → Portfolio sync → Run workflow
Cap drafts on a one-off run Actions tab → workflow_dispatch → set max_drafts
Inspect deltas without writing ruby script/portfolio-sync/sync.rb --dry-run
Reset state from current portfolio ruby script/portfolio-sync/sync.rb --init
Disable temporarily comment out the schedule: block in the workflow
Change cadence edit the cron expression — current is 0 8 * * 1 (Mon 08:00 UTC)