feat(linkedin-studio): parameterize series path + de-brand render output [skip-docs]

Wave 3 / Step 8 of the remediation plan (Phase 1 — usable by a non-author).

The flagship long-form engine shipped bespoke-as-general: a maintainer-private
absolute series path and a hardcoded 'Maskinrommet' brand in the renderers. Both
are now generalized so a non-author can run the pipeline without inheriting the
author's filesystem or publication identity.

- commands/newsletter.md + config/edition-state.template.json: series-root default
  /Users/ktg/repos/maskinrommet/serier -> $HOME/linkedin-series; reconciled the
  prose that called the maskinrommet folder 'the default' so it no longer
  contradicts the new neutral default. LTL_SERIES_ROOT override contract preserved.
- render/build-linkedin.mjs + render/build-carousel.mjs: brand is now an LTL_BRAND
  env-var (empty default -> generic). Samle-post title, carousel footer brand-span,
  and cover-eyebrow fallback are de-branded; empty brand renders clean chrome. The
  operator sets LTL_BRAND=Maskinrommet in their own env to re-brand (same pattern
  as LTL_SERIES_ROOT).
- config/image-credit-caption.template.md: 'Maskinrommet-/serie-badge' -> generic
  'serie-badge'.

Out of scope (Step 9): the residual 'Maskinrommet skrivekontrakt §C2/§A'
references in newsletter.md are the writing-contract generalization, handled next.

[skip-docs]: three-doc + version reconciliation is Step 21 (pre-review-gate, per
plan: 'LAST so it captures everything'). These intermediate Wave commits are NOT
pushed before the /trekreview gate, so the three-doc obligation (which governs
pushed changes) is satisfied at Step 21, not per local checkpoint commit.

Verify: grep -rIn '/Users/ktg' config/ commands/ render/ (excl .local) -> no
matches; grep -rn 'Maskinrommet' render/ -> no matches (de-branded); node --check
on both render scripts -> OK; LTL_SERIES_ROOT still present in newsletter.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-30 00:46:05 +02:00
commit 305b99c0e4
5 changed files with 24 additions and 13 deletions

View file

@ -4,7 +4,7 @@
// Hver "## SLIDE N — ..." blir én portrett-side (1080×1350, 4:5) i PDF-en.
// Designet typografisk deck — speiler avis-identiteten (Newsreader/Inter, off-white,
// oxblood). Cover (slide 1) + CTA (siste slide) = oxblood-bokstøtter; de øvrige lyse
// med bolk-kicker + footer (Maskinrommet + teller). Ingen per-slide AI-foto.
// med bolk-kicker + footer (valgfri brand + teller). Ingen per-slide AI-foto.
// Krever: weasyprint på PATH. Ingen npm-avhengigheter.
import fs from "node:fs";
@ -14,6 +14,11 @@ import { execFileSync } from "node:child_process";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Valgfri brand i footer + cover-eyebrow-fallback. Tom som standard (generisk);
// sett LTL_BRAND til serie-/publikasjonsnavnet for å re-brande (samme mønster
// som LTL_SERIES_ROOT). Tom brand → footer uten brand-span, tom eyebrow-fallback.
const BRAND = process.env.LTL_BRAND || "";
// ---------------------------------------------------------------------------
// weasyprint graceful degradation (S1, correction #3)
// Detekterer weasyprint på PATH. Returnerer et skip-signal (kaster ALDRI) når
@ -229,7 +234,7 @@ function slideHtml(slide, idx, total, eyebrows) {
}
const counter = `${idx + 1} / ${total}`;
const footer = `<div class="footer"><span class="brand">Maskinrommet</span><span class="count">${counter}</span></div>`;
const footer = `<div class="footer">${BRAND ? `<span class="brand">${esc(BRAND)}</span>` : ""}<span class="count">${counter}</span></div>`;
return `<section class="${cls}">
${head}
@ -279,7 +284,7 @@ function main() {
continue;
}
const eyebrows = {
cover: meta.cover_eyebrow || "Maskinrommet",
cover: meta.cover_eyebrow || BRAND,
cta: meta.cta_eyebrow || "Kom i gang",
};
const dir = path.dirname(inPath);

View file

@ -29,6 +29,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUT_ROOT = path.join(process.cwd(), "linkedin");
const DELINGSTEKST_FILE = path.join(OUT_ROOT, "edition-delingstekst.md");
// Valgfri brand på samle-postens tittel. Tom som standard (generisk plugin);
// sett LTL_BRAND til serie-/publikasjonsnavnet for å re-brande (samme mønster
// som LTL_SERIES_ROOT). Tom brand → ren «Samle-post»-tittel.
const BRAND = process.env.LTL_BRAND || "";
// ---------------------------------------------------------------------------
// EDITION-KONFIG (HANDOVER §13 / DREIEBOK / image-credit-caption.md)
// Per-serie verdier (kalender, ferskvare, cover-credit, captions) leses fra
@ -347,7 +352,7 @@ export function samlePost(share, config = EMPTY_CONFIG) {
<div class="copybox">${esc(share ? share.kommentar : "—")}</div>
<div class="label" style="margin-top:10px">[LENKE] = index/kanonisk hjem (fromaitochitta.com hvis live) ELLER Del 1-editionen som inngang. Velg det som faktisk er publisert.</div>
</div>`;
return shell("Samle-post · Maskinrommet", inner);
return shell(BRAND ? `Samle-post · ${BRAND}` : "Samle-post", inner);
}
// ---------------------------------------------------------------------------