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:
parent
6ed3e2f5a6
commit
305b99c0e4
5 changed files with 24 additions and 13 deletions
|
|
@ -31,9 +31,9 @@ This command is **fundamentally different** from the short-form commands:
|
|||
phases* (fact-check sweep + persona sweep + hook-gate), NOT by the short-form
|
||||
`PreToolUse` content-gatekeeper/voice-guardian hooks (those stay short-form-only).
|
||||
- **State lives in the series folder, not the plugin.** Production state for an
|
||||
edition lives in the resolved **series root** (Step 0) — by default the
|
||||
maskinrommet series folder
|
||||
(`${LTL_SERIES_ROOT:-/Users/ktg/repos/maskinrommet/serier}/<slug>/`), per
|
||||
edition lives in the resolved **series root** (Step 0) — by default a per-slug
|
||||
folder under your series base
|
||||
(`${LTL_SERIES_ROOT:-$HOME/linkedin-series}/<slug>/`), per
|
||||
decision G, but any explicit path works (Step 0 resolution order). The plugin
|
||||
ships the *schema* (`config/edition-state.template.json`) and this command;
|
||||
the edition's actual state + drafts live with the series.
|
||||
|
|
@ -135,13 +135,14 @@ the edition left off before doing anything.
|
|||
<path-to-serie>`), use it verbatim. This is how the edition is produced for
|
||||
any repo, a throwaway fixture, or a non-default location.
|
||||
- Otherwise derive it from the series slug under the **default series base**,
|
||||
`${LTL_SERIES_ROOT:-/Users/ktg/repos/maskinrommet/serier}/<slug>/`. The
|
||||
`LTL_SERIES_ROOT` env-var overrides the base without editing this command;
|
||||
the maskinrommet path is the default, not the only path.
|
||||
`${LTL_SERIES_ROOT:-$HOME/linkedin-series}/<slug>/`. The
|
||||
`LTL_SERIES_ROOT` env-var overrides the base without editing this command
|
||||
(and `LTL_BRAND` re-brands the rendered output — empty by default); the
|
||||
default base is a default, not the only path.
|
||||
- If neither a path nor a resolvable slug is available, ask once which series
|
||||
(or series-root path) this edition belongs to.
|
||||
All later steps treat `<serie>` as this resolved series root; nothing below
|
||||
re-hardcodes the maskinrommet path.
|
||||
re-hardcodes a specific series path.
|
||||
2. **Read edition-state** (`<serie>/linkedin/edition-state.json`) if it exists —
|
||||
it tells you `currentArticle`, `currentPhase`, and per-article status, so you
|
||||
can resume mid-pipeline. The schema is documented in
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_doc": {
|
||||
"purpose": "Schema for edition-state.json — deterministic resumption state for a newsletter edition in production. Holds the current phase + per-article status so /linkedin:newsletter (Step 0) can resume exactly where a prior session stopped.",
|
||||
"decision": "G — production state lives in the serie-mappe (e.g. /Users/ktg/repos/maskinrommet/serier/<slug>/linkedin/edition-state.json), NOT in the plugin. This file is the schema-defining TEMPLATE only; copy + fill it in the serie-mappe when producing an edition.",
|
||||
"decision": "G — production state lives in the series folder (e.g. $LTL_SERIES_ROOT/<slug>/linkedin/edition-state.json, default $HOME/linkedin-series/<slug>/...), NOT in the plugin. This file is the schema-defining TEMPLATE only; copy + fill it in the series folder when producing an edition.",
|
||||
"complements": "edition-config.json (static: calendar, freshness, captions) and <serie>/STATE.md (human-readable narrative state, overwritten each phase per the ONE-system continuity rule — there is no edition-HANDOVER.md). edition-state.json is the machine-readable companion: deterministic resumption + the durable fact-check log, immutable rules, and persona verdicts that the old HANDOVER §4/§5 used to carry.",
|
||||
"lifecycle": "/linkedin:newsletter reads this in Step 0 and rewrites it at every phase transition. Article keys mirror edition-config.json (zero-padded strings: \"01\", \"02\", ..., or \"samle\").",
|
||||
"phases": [
|
||||
|
|
|
|||
|
|
@ -59,5 +59,5 @@ som *også* legger en feed-cover trenger likevel en rad over.
|
|||
|
||||
## Samle-post
|
||||
|
||||
Ev. Maskinrommet-/serie-badge (egen asset) → ingen credit. Lenken til serien ligger i
|
||||
Ev. serie-badge (egen asset) → ingen credit. Lenken til serien ligger i
|
||||
første kommentar, ikke i bildet.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue