diff --git a/bin/check-no-li-in-prod b/bin/check-no-li-in-prod new file mode 100755 index 000000000..44906bc75 --- /dev/null +++ b/bin/check-no-li-in-prod @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Report-only check: fails if a production Hugo build contains any +# /linkedin/ review-board output. Standalone, wires into nothing. +# +# Usage: +# bin/check-no-li-in-prod # builds prod, then inspects it +# bin/check-no-li-in-prod --self-test # asserts the checker itself works +# OUTPUT_DIR=_dest/public-prod bin/check-no-li-in-prod --skip-build # inspect an existing build + +set -euo pipefail + +OUTPUT_DIR=${OUTPUT_DIR:-_dest/public-prod-check} +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +check_dir() { + local dir="$1" + # /social/linkedin/... is a pre-existing, unrelated content page and is + # NOT the review-board section - only /linkedin/ (root) is ours. + find "$dir" -path '*/linkedin/*' -not -path '*/social/linkedin/*' 2>/dev/null +} + +if [ "${1:-}" = "--self-test" ]; then + tmp=$(mktemp -d) + mkdir -p "$tmp/linkedin/icp-validation" + echo "leak" > "$tmp/linkedin/icp-validation/index.html" + if [ -z "$(check_dir "$tmp")" ]; then + echo "❌ self-test FAILED: checker did not detect a planted /linkedin/ leak" + rm -rf "$tmp" + exit 1 + fi + rm -rf "$tmp" + echo "✅ self-test passed: checker detects /linkedin/ leaks" + exit 0 +fi + +if [ "${1:-}" != "--skip-build" ]; then + echo "Building production ($OUTPUT_DIR)..." + (cd "$SCRIPT_DIR/.." && ENVIRONMENT=production OUTPUT_DIR="$OUTPUT_DIR" ./bin/hugo-build >/dev/null) +fi + +FOUND=$(check_dir "$SCRIPT_DIR/../$OUTPUT_DIR") + +if [ -n "$FOUND" ]; then + echo "❌ FAIL: /linkedin/ review-board output found in production build:" + echo "$FOUND" + exit 1 +fi + +echo "✅ PASS: no /linkedin/ review-board output in $OUTPUT_DIR" +exit 0 diff --git a/config/development/hugo.toml b/config/development/hugo.toml index 033121be5..505e468a0 100644 --- a/config/development/hugo.toml +++ b/config/development/hugo.toml @@ -17,6 +17,41 @@ disableKinds = ["RSS", "sitemap", "robotsTXT", "taxonomy", "term"] # Skip expensive processing in development ignoreFiles = ["\\.min\\.css$", "\\.min\\.js$"] +# DEV-ONLY: mount linkedin-posts/ as a content section for local pre-review. +# Declaring [[module.mounts]] replaces Hugo's default mount set, so the +# default mounts (content, static, layouts, data, assets, archetypes) must be +# re-declared here or the rest of the dev site breaks. Production and +# _default configs have NO [[module.mounts]] block, so this section never +# exists outside `hugo server`/`hugo --environment development` builds. +[module] + [[module.mounts]] + source = "content" + target = "content" + [[module.mounts]] + source = "static" + target = "static" + [[module.mounts]] + source = "layouts" + target = "layouts" + [[module.mounts]] + source = "data" + target = "data" + [[module.mounts]] + source = "assets" + target = "assets" + [[module.mounts]] + source = "archetypes" + target = "archetypes" + [[module.mounts]] + source = "linkedin-posts" + target = "content/linkedin" + [[module.mounts]] + source = "linkedin-posts/icp-validation/assets" + target = "static/linkedin-assets/icp-validation" + [[module.mounts]] + source = "linkedin-posts/course-promo/assets" + target = "static/linkedin-assets/course-promo" + # Build optimizations for development speed [build] # Skip Hugo stats generation (not needed for PurgeCSS in dev) diff --git a/layouts/linkedin/list.html b/layouts/linkedin/list.html new file mode 100644 index 000000000..d8204d452 --- /dev/null +++ b/layouts/linkedin/list.html @@ -0,0 +1,47 @@ + + + + + LinkedIn Review Board (dev only) + + + + +

LinkedIn Review Board dev only — never appears in production

+ {{ $posts := where .RegularPagesRecursive "Params.status" "!=" nil }} +
+ {{ range slice "now" "next" "future" }} + {{ $stageName := . }} + {{ $stagePosts := where $posts "Params.stage" $stageName }} +
+

{{ $stageName }} ({{ len $stagePosts }})

+ +
+ {{ end }} +
+ + diff --git a/layouts/linkedin/single.html b/layouts/linkedin/single.html new file mode 100644 index 000000000..9205ff92a --- /dev/null +++ b/layouts/linkedin/single.html @@ -0,0 +1,47 @@ + + + + + {{ or .Title (humanize .File.BaseFileName) }} — LinkedIn preview (dev only) + + + + + ← board +

{{ or .Title (humanize .File.BaseFileName) }}

+

{{ or .Params.lane .Params.campaign "—" }} · {{ .Params.status }} · stage: {{ .Params.stage }}

+ + {{ $parts := split .File.Path "/" }} + {{ $laneDir := "" }} + {{ if ge (len $parts) 2 }}{{ $laneDir = index $parts 1 }}{{ end }} + {{ $visualRaw := "" }} + {{ with .Params.visual }}{{ $visualRaw = index (split . " ") 0 }}{{ end }} + {{ if not $visualRaw }}{{ $visualRaw = .Params.design_file }}{{ end }} + +
+ {{ if $visualRaw }} + post visual + {{ end }} + {{ .Content }} +
+ + {{ with .Params.first_comment }} +

First comment

+
{{ . }}
+ {{ end }} + + + + + diff --git a/linkedin-posts/course-promo/week1-fri-why-i-wrote-it.md b/linkedin-posts/course-promo/week1-fri-why-i-wrote-it.md index 17e7d786c..872536fa5 100644 --- a/linkedin-posts/course-promo/week1-fri-why-i-wrote-it.md +++ b/linkedin-posts/course-promo/week1-fri-why-i-wrote-it.md @@ -15,6 +15,7 @@ course_link_placement: first-comment utm_campaign: course_promo_module1 utm_content: week1_fri_why_i_wrote_it status: draft +stage: future first_comment: | Free, no email gate: https://jetthoughts.com/course/tech-for-non-technical-founders-2026/?utm_source=linkedin&utm_medium=social&utm_campaign=course_promo_module1&utm_content=week1_fri_why_i_wrote_it diff --git a/linkedin-posts/course-promo/week1-mon-built-before-testing.md b/linkedin-posts/course-promo/week1-mon-built-before-testing.md index 1989410ba..ca0e916e2 100644 --- a/linkedin-posts/course-promo/week1-mon-built-before-testing.md +++ b/linkedin-posts/course-promo/week1-mon-built-before-testing.md @@ -15,6 +15,7 @@ course_link_placement: first-comment utm_campaign: course_promo_module1 utm_content: week1_mon_built_before_testing status: draft +stage: future notes: | Opener archetype: history-led (Tue=conflict-led, Wed=question-led, Thu=observation-led, Fri=dialogue-led). RESOLVED 2026-07-31: synthetic wedding-photographer anecdote de-fabricated into Paul's real diff --git a/linkedin-posts/course-promo/week1-thu-price-before-product.md b/linkedin-posts/course-promo/week1-thu-price-before-product.md index 4a7b62fbc..6d7223047 100644 --- a/linkedin-posts/course-promo/week1-thu-price-before-product.md +++ b/linkedin-posts/course-promo/week1-thu-price-before-product.md @@ -15,6 +15,7 @@ course_link_placement: first-comment utm_campaign: course_promo_module1 utm_content: week1_thu_price_before_product status: draft +stage: future notes: | Opener archetype: observation-led. Plan called for Format A (marked screenshot of a smoke-test page); no real smoke-test page exists to screenshot honestly, so this uses the course's own diff --git a/linkedin-posts/course-promo/week1-thu-validate-before-build.md b/linkedin-posts/course-promo/week1-thu-validate-before-build.md index 3f7716e20..e4273020a 100644 --- a/linkedin-posts/course-promo/week1-thu-validate-before-build.md +++ b/linkedin-posts/course-promo/week1-thu-validate-before-build.md @@ -12,6 +12,7 @@ campaign: course-promo utm_campaign: course_promo_pre_validation utm_content: validate-before-build status: scheduled +stage: now scheduled_for: 2026-08-13 visual: assets/week1-thu-validate-before-build.png (from dollar-presale-flow.svg, rsvg -w 1080) first_comment: | diff --git a/linkedin-posts/course-promo/week1-tue-founding-hypothesis.md b/linkedin-posts/course-promo/week1-tue-founding-hypothesis.md index 253fee4e6..dcba6668c 100644 --- a/linkedin-posts/course-promo/week1-tue-founding-hypothesis.md +++ b/linkedin-posts/course-promo/week1-tue-founding-hypothesis.md @@ -15,6 +15,7 @@ course_link_placement: first-comment utm_campaign: course_promo_module1 utm_content: week1_tue_founding_hypothesis status: draft +stage: future notes: | REVISED 2026-08-13 to idea-first (opener: habit-led "The first thing I do..." -> idea-led belief stated flat). Opener archetype: idea-led (belief stated flat). Give-away = the five-blank Founding Hypothesis sentence from diff --git a/linkedin-posts/course-promo/week1-wed-first-move-poll.md b/linkedin-posts/course-promo/week1-wed-first-move-poll.md index d2071cc6c..f29fa4745 100644 --- a/linkedin-posts/course-promo/week1-wed-first-move-poll.md +++ b/linkedin-posts/course-promo/week1-wed-first-move-poll.md @@ -15,6 +15,7 @@ course_link_placement: first-comment utm_campaign: course_promo_module1 utm_content: week1_wed_first_move_poll status: draft +stage: future notes: | Opener archetype: idea-led (flat claim, then the poll question). Native LinkedIn poll, 1-week duration, 4 options. Deliberately different signal from the ICP-validation week-1 poll ("what makes you trust a dev diff --git a/linkedin-posts/course-promo/week2-fri-ten-interviews-recap.md b/linkedin-posts/course-promo/week2-fri-ten-interviews-recap.md index ced04995d..df2cd2e14 100644 --- a/linkedin-posts/course-promo/week2-fri-ten-interviews-recap.md +++ b/linkedin-posts/course-promo/week2-fri-ten-interviews-recap.md @@ -16,6 +16,7 @@ opener_archetype: stat-led utm_campaign: course-pilot-aug26 utm_content: week2_fri_ten_interviews_recap status: draft +stage: future notes: | Opener archetype: stat-led, per the week-2 row of docs/workflows/linkedin-course-promo-plan.md. "Ten interviews, one weekend of outreach, zero code" is the plan's own week-2 Friday hook, kept diff --git a/linkedin-posts/course-promo/week2-mon-friends-politely-lying.md b/linkedin-posts/course-promo/week2-mon-friends-politely-lying.md index ce811eb76..8ec5e331c 100644 --- a/linkedin-posts/course-promo/week2-mon-friends-politely-lying.md +++ b/linkedin-posts/course-promo/week2-mon-friends-politely-lying.md @@ -15,6 +15,7 @@ course_link_placement: first-comment utm_campaign: course-pilot-aug26 utm_content: week2_mon_friends_politely_lying status: draft +stage: future notes: | Opener archetype: observation-led (name the line every founder says, then counter it - the Cutler move), per the week-2 row of docs/workflows/linkedin-course-promo-plan.md. diff --git a/linkedin-posts/course-promo/week2-tue-mom-test-past-questions.md b/linkedin-posts/course-promo/week2-tue-mom-test-past-questions.md index 0d1701a51..e9fe43284 100644 --- a/linkedin-posts/course-promo/week2-tue-mom-test-past-questions.md +++ b/linkedin-posts/course-promo/week2-tue-mom-test-past-questions.md @@ -15,6 +15,7 @@ course_link_placement: first-comment utm_campaign: course-pilot-aug26 utm_content: week2_tue_mom_test_past_questions status: draft +stage: future notes: | Opener archetype: idea-led - opens with the flat claim that "Would you use this?" can only hurt you, then argues it. REVISED 2026-08-13 to idea-first: body already delivered the tactic in full and led with the idea; no rewrite needed, kept as-is. diff --git a/linkedin-posts/course-promo/week2-wed-interview-count-poll.md b/linkedin-posts/course-promo/week2-wed-interview-count-poll.md index b09b322b3..1272a2bc6 100644 --- a/linkedin-posts/course-promo/week2-wed-interview-count-poll.md +++ b/linkedin-posts/course-promo/week2-wed-interview-count-poll.md @@ -15,6 +15,7 @@ course_link_placement: first-comment utm_campaign: course-pilot-aug26 utm_content: week2_wed_interview_count_poll status: draft +stage: future opener_archetype: observation-led notes: | Opener archetype: observation-led. Idea-first framing (a flat claim about pre-build diff --git a/linkedin-posts/icp-validation/backlog-vibe-coding-disposable.md b/linkedin-posts/icp-validation/backlog-vibe-coding-disposable.md index 58d964e8a..d7c8ce6af 100644 --- a/linkedin-posts/icp-validation/backlog-vibe-coding-disposable.md +++ b/linkedin-posts/icp-validation/backlog-vibe-coding-disposable.md @@ -10,6 +10,7 @@ cta: Open question - "If you've been through one of these rebuilds, what did you utm_campaign: icp_validation_validation_vs_production utm_content: vibe_coding_disposable_by_design status: draft +stage: future opener_archetype: idea-led notes: | REVISED 2026-08-13 to idea-first: opener was habit-led ("For the last diff --git a/linkedin-posts/icp-validation/week1-mon-jira-not-progress.md b/linkedin-posts/icp-validation/week1-mon-jira-not-progress.md index b42e0cccd..afad8b0e3 100644 --- a/linkedin-posts/icp-validation/week1-mon-jira-not-progress.md +++ b/linkedin-posts/icp-validation/week1-mon-jira-not-progress.md @@ -9,6 +9,7 @@ opener_archetype: observation-led icp_test: does a founder already working with a dev shop recognize "the board isn't the product" and reply with their own version? visual: assets/week1-mon-jira-not-progress.png (demo-rule.svg - real demo vs Jira screenshot) status: scheduled +stage: now scheduled_for: 2026-08-18T15:30+02:00 (Tue 3:30pm CEST, WITH demo-rule image + tags) first_comment: | (rescue lane: no link - reply-CTA only) diff --git a/linkedin-posts/icp-validation/week1-tue-staging-question.md b/linkedin-posts/icp-validation/week1-tue-staging-question.md index dfedb223c..bace2f3c5 100644 --- a/linkedin-posts/icp-validation/week1-tue-staging-question.md +++ b/linkedin-posts/icp-validation/week1-tue-staging-question.md @@ -9,6 +9,7 @@ opener_archetype: observation-led icp_test: does a founder recognize that "is it on track?" is reassurance, not information, and reply with their own standup question? visual: assets/week1-tue-staging-question.png (TBD - needs a rescue-lane exhibit or skip) status: draft +stage: future first_comment: | (rescue lane: no link - reply-CTA only) notes: | diff --git a/linkedin-posts/icp-validation/week1-wed-trust-signals-poll.md b/linkedin-posts/icp-validation/week1-wed-trust-signals-poll.md index bb7fdef22..66385a0ba 100644 --- a/linkedin-posts/icp-validation/week1-wed-trust-signals-poll.md +++ b/linkedin-posts/icp-validation/week1-wed-trust-signals-poll.md @@ -12,6 +12,7 @@ utm_campaign: icp_validation_progress_visibility utm_content: trust_signals_poll opener_archetype: idea-led (claim about proof signals, not a bare question) status: draft +stage: future notes: | Poll format - shorter body than narrative posts. Opener is idea-led: it states a claim about which proof signals actually mean something before