Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions bin/check-no-li-in-prod
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions config/development/hugo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 47 additions & 0 deletions layouts/linkedin/list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>LinkedIn Review Board (dev only)</title>
<meta name="robots" content="noindex,nofollow" />
<style>
body { font: 14px/1.4 -apple-system, sans-serif; margin: 0; padding: 1.5rem; background: #f4f4f6; color: #1a1a1a; }
h1 { font-size: 1.25rem; margin: 0 0 1rem; }
h1 small { font-weight: normal; color: #a00; }
.board { display: flex; gap: 1rem; align-items: flex-start; }
.col { flex: 1; min-width: 0; background: #fff; border: 1px solid #ddd; border-radius: 6px; }
.col h2 { font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.05em; margin: 0; padding: 0.6rem 0.8rem; border-bottom: 1px solid #ddd; background: #eee; }
.col ul { list-style: none; margin: 0; padding: 0.5rem; }
.card { display: block; border: 1px solid #ddd; border-radius: 4px; padding: 0.6rem 0.7rem; margin-bottom: 0.5rem; text-decoration: none; color: inherit; background: #fafafa; }
.card:hover { border-color: #888; }
.card .title { font-weight: 600; display: block; margin-bottom: 0.25rem; }
.card .meta { font-size: 0.8rem; color: #555; }
.empty { padding: 0.8rem; color: #888; font-size: 0.85rem; }
</style>
</head>
<body>
<h1>LinkedIn Review Board <small>dev only — never appears in production</small></h1>
{{ $posts := where .RegularPagesRecursive "Params.status" "!=" nil }}
<div class="board">
{{ range slice "now" "next" "future" }}
{{ $stageName := . }}
{{ $stagePosts := where $posts "Params.stage" $stageName }}
<div class="col">
<h2>{{ $stageName }} ({{ len $stagePosts }})</h2>
<ul>
{{ range $stagePosts }}
<li>
<a class="card" href="{{ .RelPermalink }}">
<span class="title">{{ or .Title (humanize .File.BaseFileName) }}</span>
<span class="meta">{{ or .Params.lane .Params.campaign "—" }} · {{ .Params.status }}</span>
</a>
</li>
{{ else }}
<li class="empty">nothing here</li>
{{ end }}
</ul>
</div>
{{ end }}
</div>
</body>
</html>
47 changes: 47 additions & 0 deletions layouts/linkedin/single.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ or .Title (humanize .File.BaseFileName) }} — LinkedIn preview (dev only)</title>
<meta name="robots" content="noindex,nofollow" />
<style>
body { font: 15px/1.5 -apple-system, sans-serif; margin: 0; padding: 1.5rem; max-width: 640px; background: #f4f4f6; color: #1a1a1a; }
a.back { font-size: 0.85rem; color: #555; }
h1 { font-size: 1.1rem; margin: 0.8rem 0 0.2rem; }
.meta { font-size: 0.8rem; color: #555; margin-bottom: 1rem; }
.post { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: 1rem; white-space: pre-wrap; }
.post img { max-width: 100%; border-radius: 4px; margin-bottom: 0.75rem; display: block; }
h2 { font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.05em; margin: 1.2rem 0 0.4rem; color: #555; }
.first-comment { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: 1rem; white-space: pre-wrap; }
button { margin-top: 1rem; padding: 0.5rem 0.9rem; border: 1px solid #888; border-radius: 4px; background: #fff; cursor: pointer; font-size: 0.85rem; }
button:active { background: #eee; }
</style>
</head>
<body>
<a class="back" href="{{ "/linkedin/" | relURL }}">&larr; board</a>
<h1>{{ or .Title (humanize .File.BaseFileName) }}</h1>
<p class="meta">{{ or .Params.lane .Params.campaign "—" }} · {{ .Params.status }} · stage: {{ .Params.stage }}</p>

{{ $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 }}

<div class="post">
{{ if $visualRaw }}
<img src="/linkedin-assets/{{ $laneDir }}/{{ path.Base $visualRaw }}" alt="post visual" onerror="this.style.display='none'" />
{{ end }}
{{ .Content }}
</div>

{{ with .Params.first_comment }}
<h2>First comment</h2>
<div class="first-comment">{{ . }}</div>
{{ end }}

<button onclick="navigator.clipboard.writeText(document.getElementById('raw-body').value)">Copy body</button>
<textarea id="raw-body" style="display:none">{{ .RawContent }}</textarea>
</body>
</html>
1 change: 1 addition & 0 deletions linkedin-posts/course-promo/week1-fri-why-i-wrote-it.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions linkedin-posts/course-promo/week1-wed-first-move-poll.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading