feat(linkedin): generalize build-linkedin to read edition-config.json (S2)

This commit is contained in:
Kjell Tore Guttormsen 2026-05-26 23:15:26 +02:00
commit 6e6ed7dd4e
3 changed files with 173 additions and 53 deletions

View file

@ -16,7 +16,8 @@
// Kilder:
// - brødtekst + felt: front matter + body i utkast/NN-...md
// - delingstekst/hashtags/første kommentar: linkedin/edition-delingstekst.md (parses)
// - kalender + ferskvare + cover-credit/caption: konstanter nedenfor (HANDOVER §13 + image-credit-caption.md)
// - kalender + ferskvare + cover-credit/caption: linkedin/edition-config.json i serie-mappa
// (faller tilbake til tomme standarder hvis fila mangler — HANDOVER §13 + image-credit-caption.md)
// Ingen npm-avhengigheter, ingen nett. meta.md røres IKKE (håndholdt; har ekte pulse-URL).
import fs from "node:fs";
@ -29,32 +30,36 @@ const OUT_ROOT = path.join(process.cwd(), "linkedin");
const DELINGSTEKST_FILE = path.join(OUT_ROOT, "edition-delingstekst.md");
// ---------------------------------------------------------------------------
// LÅSTE KONSTANTER (HANDOVER §13 / DREIEBOK / image-credit-caption.md)
// EDITION-KONFIG (HANDOVER §13 / DREIEBOK / image-credit-caption.md)
// Per-serie verdier (kalender, ferskvare, cover-credit, captions) leses fra
// linkedin/edition-config.json i serie-mappa — ikke lenger hardkodet. Q1: JSON
// (deterministisk parsing). Mangler fila, faller vi tilbake til tomme
// standarder slik at byggingen degraderer pent i stedet for å kaste.
// ---------------------------------------------------------------------------
const CALENDAR = {
"01": { dag: "Tirsdag 26.05.2026", klokke: "08:00" },
"02": { dag: "Onsdag 27.05.2026", klokke: "08:00" },
"03": { dag: "Torsdag 28.05.2026", klokke: "08:00" },
"04": { dag: "Fredag 29.05.2026", klokke: "08:00" },
"05": { dag: "Lørdag 30.05.2026", klokke: "08:00" },
"06": { dag: "Søndag 31.05.2026", klokke: "08:00" },
samle: { dag: "Mandag 01.06.2026", klokke: "08:00" },
};
const CONFIG_FILE = path.join(OUT_ROOT, "edition-config.json");
const FRESHNESS = {
"01": "OpenAI-verdsettelse (~850 mrd USD «i mars») + «Anthropic forhandler akkurat nå om en runde» — er runden lukket? Oppdater tall/tempus FØR planlegging.",
"02": "SWE-bench Verified (77,6 %) for Mistral Medium 3.5 — fortsatt korrekt? Vurder avrunding FØR planlegging.",
};
const EMPTY_CONFIG = { calendar: {}, freshness: {}, coverCredit: "", captions: {} };
const COVER_CREDIT = "Illustrasjon generert med Google Gemini (Nano Banana Pro)";
const CAPTIONS = {
"01": "Noen lover vekst — men hvem tjener på at ledergruppen tror på pitchen?",
"02": "KI på maskiner vi styrer selv — der ingen data forlater huset.",
"03": "Regningen kommer hver måned — og en voksende del går ut av landet.",
"04": "Samme talent, ulik tilgang — det er der gapet begynner.",
"05": "Å forvalte var jobben. Nå er den å lede omstilling — om igjen, hvert år.",
"06": "Tolv grep en leder kan ta selv — de to første er allerede krysset av.",
};
// Les edition-config.json fra rootDir (serie-mappas linkedin/). Normaliser alle
// felt til kjente former; manglende/ugyldig fil → tomme standarder (graceful).
export function loadEditionConfig(rootDir = OUT_ROOT) {
let cfg;
try {
cfg = JSON.parse(fs.readFileSync(path.join(rootDir, "edition-config.json"), "utf8"));
} catch {
return { ...EMPTY_CONFIG };
}
if (!cfg || typeof cfg !== "object") return { ...EMPTY_CONFIG };
return {
calendar: cfg.calendar && typeof cfg.calendar === "object" ? cfg.calendar : {},
freshness: cfg.freshness && typeof cfg.freshness === "object" ? cfg.freshness : {},
coverCredit: typeof cfg.coverCredit === "string" ? cfg.coverCredit : "",
captions: cfg.captions && typeof cfg.captions === "object" ? cfg.captions : {},
};
}
// CAROUSEL-settet er ikke en del av S2-konfig-scope (kun CALENDAR/FRESHNESS/
// CAPTIONS/COVER_CREDIT generaliseres) — beholdes hardkodet inntil videre.
const CAROUSEL = new Set(["03", "06"]);
// ---------------------------------------------------------------------------
@ -250,12 +255,12 @@ ${inner}
// ---------------------------------------------------------------------------
// Edition-POST.html (Del 16)
// ---------------------------------------------------------------------------
function editionPost(nn, meta, body, share) {
const cal = CALENDAR[nn] || { dag: "—", klokke: "08:00" };
export function editionPost(nn, meta, body, share, config = EMPTY_CONFIG) {
const cal = config.calendar[nn] || { dag: "—", klokke: "08:00" };
const sTitle = seoTitle(meta.title);
const sDesc = seoDescription(meta.subtitle);
const seoWarn = sTitle.length > 60 ? ` <span class="warn">⚠️ ${sTitle.length} tegn (over SEO-anbefaling 60)</span>` : ` (${sTitle.length} tegn)`;
const fresh = FRESHNESS[nn];
const fresh = config.freshness[nn];
const blocks = markdownToBlocks(body);
const subtitle = meta.subtitle ? `<p><strong>${inline(meta.subtitle)}</strong></p>` : "";
const copyZone = [subtitle, ...blocks].filter(Boolean).join("\n ");
@ -287,8 +292,8 @@ function editionPost(nn, meta, body, share) {
<div class="fld">
<h2>2 · Cover (1920×1080)</h2>
<div class="label">Fil</div><div class="val"><code>linkedin/${nn}/cover.png</code></div>
<div class="label">Credit (Add credit and caption)</div><div class="val">${esc(COVER_CREDIT)}</div>
<div class="label">Caption</div><div class="val">${esc(CAPTIONS[nn] || "")}</div>
<div class="label">Credit (Add credit and caption)</div><div class="val">${esc(config.coverCredit || "")}</div>
<div class="label">Caption</div><div class="val">${esc(config.captions[nn] || "")}</div>
</div>
<div class="fld">
@ -315,8 +320,8 @@ function editionPost(nn, meta, body, share) {
// ---------------------------------------------------------------------------
// Samle-POST.html (frittstående native post)
// ---------------------------------------------------------------------------
function samlePost(share) {
const cal = CALENDAR.samle;
export function samlePost(share, config = EMPTY_CONFIG) {
const cal = config.calendar.samle || { dag: "—", klokke: "08:00" };
const shareField = share ? `${share.share}\n\n${share.hashtags}` : "—";
const inner = `
<h1 class="sheet">Samle-post oversikt over hele serien</h1>
@ -339,26 +344,34 @@ function samlePost(share) {
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
const files = process.argv.slice(2);
const shareMap = parseDelingstekst();
export function main(files = process.argv.slice(2)) {
const config = loadEditionConfig(OUT_ROOT);
const shareMap = parseDelingstekst();
for (const f of files) {
const abs = path.isAbsolute(f) ? f : path.join(process.cwd(), f);
const raw = fs.readFileSync(abs, "utf8");
const { meta, body } = parseFrontMatter(raw);
const base = path.basename(abs).replace(/\.md$/, "");
const nn = (base.match(/^(\d{2})/) || [, base])[1];
if (!/^\d{2}$/.test(nn)) { console.warn(`↷ hopper over ${f} (ikke NN-prefiks)`); continue; }
const dir = path.join(OUT_ROOT, nn);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, "POST.html"), editionPost(nn, meta, body, shareMap[nn]));
console.log(`✓ linkedin/${nn}/POST.html (${meta.title || base})`);
for (const f of files) {
const abs = path.isAbsolute(f) ? f : path.join(process.cwd(), f);
const raw = fs.readFileSync(abs, "utf8");
const { meta, body } = parseFrontMatter(raw);
const base = path.basename(abs).replace(/\.md$/, "");
const nn = (base.match(/^(\d{2})/) || [, base])[1];
if (!/^\d{2}$/.test(nn)) { console.warn(`↷ hopper over ${f} (ikke NN-prefiks)`); continue; }
const dir = path.join(OUT_ROOT, nn);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, "POST.html"), editionPost(nn, meta, body, shareMap[nn], config));
console.log(`✓ linkedin/${nn}/POST.html (${meta.title || base})`);
}
// Samle bygges alltid (innhold er uavhengig av utkast-filene)
if (shareMap.samle) {
const dir = path.join(OUT_ROOT, "samle");
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, "POST.html"), samlePost(shareMap.samle, config));
console.log("✓ linkedin/samle/POST.html (samle-post)");
}
}
// Samle bygges alltid (innhold er uavhengig av utkast-filene)
if (shareMap.samle) {
const dir = path.join(OUT_ROOT, "samle");
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, "POST.html"), samlePost(shareMap.samle));
console.log("✓ linkedin/samle/POST.html (samle-post)");
// CLI-guard (S2 korreksjon #4): kjør main kun når scriptet kjøres direkte,
// slik at modulen kan importeres i tester uten side-effekter.
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}