Elementor-Ausgabe, Referenzartikel, Redaktionsplan

This commit is contained in:
2026-09-20 13:14:28 +02:00
parent d90dd41cc5
commit 7dcf774896
4 changed files with 290 additions and 74 deletions

View File

@@ -68,6 +68,8 @@ KONF = node("Konfiguration", "n8n-nodes-base.set", 3.4, [-240, 190], {
{"id": nid(), "name": "wp_base", "value": "https://skrift.de", "type": "string"},
{"id": nid(), "name": "score_ziel", "value": "={{ $json.body && $json.body.score_ziel ? Number($json.body.score_ziel) : 72 }}", "type": "number"},
{"id": nid(), "name": "pause_zwischen_artikeln", "value": 5, "type": "number"},
{"id": nid(), "name": "elementor_ausgabe", "value": True, "type": "boolean"},
{"id": nid(), "name": "faq_ueberschrift", "value": "Häufige Fragen", "type": "string"},
{"id": nid(), "name": "benachrichtigung_an", "value": "privat@lucas-orth.de", "type": "string"},
]}, "options": {}})
for t in ["Manuell starten", "Webhook Start"]: link(t, KONF)
@@ -170,7 +172,19 @@ while ((m = re.exec(String(artikel.content_html || ''))) !== null) {
}
const shortcodes = (String(artikel.content_html || '').match(/\[skrift_preis[^\]]*\]/g) || []);
// NeuronWriter bewertet nur den uebergebenen Text. Kurzantwort und FAQ stehen
// aber auf der fertigen Seite, also gehoeren sie in die Bewertung. Der gespeicherte
// Artikel bleibt unveraendert, das hier ist nur der Pruefkoerper.
const faqText = (Array.isArray(artikel.faq) ? artikel.faq : [])
.map(f => `<h3>${f.frage || ''}</h3><p>${f.antwort || ''}</p>`).join('\n');
const bewertungs_html = [
artikel.kurzantwort ? `<p>${artikel.kurzantwort}</p>` : '',
artikel.content_html || '',
faqText ? `<h2>Häufige Fragen</h2>\n${faqText}` : '',
].filter(Boolean).join('\n');
return [{ json: { slug: meta.slug, artikel, brief, brief_vorhanden: !!brief,
bewertungs_html,
preis_warnung: preisTreffer.length
? `${preisTreffer.length} ausgeschriebene(r) Preis(e) im Text: ${[...new Set(preisTreffer)].slice(0,5).join(', ')}. `
+ 'Bitte durch [skrift_preis key="..."] ersetzen.'
@@ -182,7 +196,7 @@ link(BRIEF, PRUEFEN)
# ---------------------------------------------------------------- Score
EVAL = http("Score ermitteln", [1560, 420], "POST",
"={{ $('Konfiguration').first().json.nw_base }}/evaluate-content", CRED_NW,
body="={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}")
body="={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.bewertungs_html || $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}")
link(PRUEFEN, EVAL)
FEHLER_EVAL = code("Fehler Score", [1560, 640], """
@@ -220,7 +234,7 @@ NACH = code("Fehlende Begriffe ermitteln", [2220, 640], r"""
const s = $input.first().json;
const brief = s.brief;
const text = String(s.artikel.content_html || '')
const text = String(s.bewertungs_html || s.artikel.content_html || '')
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
@@ -333,7 +347,7 @@ link(NACH_COMMIT, NACH_OK, out=0)
# ================================================================ ERFOLGS-PFAD
IMPORT = http("Revision zu NeuronWriter", [2220, 240], "POST",
"={{ $('Konfiguration').first().json.nw_base }}/import-content", CRED_NW,
body="={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}")
body="={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.bewertungs_html || $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}")
link(GATE, IMPORT, out=0)
IMPORT_FEHLER = code("Fehler Revision", [2220, 60], """
@@ -422,6 +436,71 @@ const meta = {
_seopress_analysis_target_kw: a.keyword || '',
};
// --- Elementor-Struktur aus Artikel und FAQ ---
// Zerlegt den Text an den H2-Grenzen in Text-Editor-Widgets und haengt das FAQ
// als nested-accordion an, dasselbe Widget wie auf den Leistungsseiten.
const konf = $('Konfiguration').first().json;
const eid = () => Math.random().toString(16).slice(2, 9);
const textWidget = (h) => ({ id: eid(), elType: 'widget', settings: { editor: h },
elements: [], widgetType: 'text-editor' });
const behaelter = (kinder, innen) => {
const s = { content_width: 'full' };
if (innen) Object.assign(s, {
_title: 'Element',
padding: { unit: 'px', top: '8', right: '8', bottom: '8', left: '8', isLinked: true },
flex_gap: { column: '20', row: '20', isLinked: true, unit: 'px', size: 20 },
});
const c = { id: eid(), elType: 'container', settings: s, elements: kinder };
if (innen) { c.isInner = true; c.isLocked = true; }
return c;
};
function elementorBauen() {
const wurzel = [];
if (a.kurzantwort) {
wurzel.push(behaelter([textWidget(
`<div class="skrift-kurzantwort"><p>${a.kurzantwort}</p></div>`)]));
}
const html = String(a.content_html || '');
const stellen = [];
const re2 = /<h2[ >]/g;
let mm;
while ((mm = re2.exec(html)) !== null) stellen.push(mm.index);
const bloecke = [];
if (!stellen.length) {
if (html.trim()) bloecke.push(html.trim());
} else {
if (stellen[0] > 0 && html.slice(0, stellen[0]).trim()) bloecke.push(html.slice(0, stellen[0]).trim());
stellen.forEach((s, i) => {
const ende = i + 1 < stellen.length ? stellen[i + 1] : html.length;
const b = html.slice(s, ende).trim();
if (b) bloecke.push(b);
});
}
bloecke.forEach(b => wurzel.push(behaelter([textWidget(b)])));
if (meta.faq.length) {
const items = [], kinder = [];
meta.faq.forEach(f => {
items.push({ item_title: f.frage, _id: eid() });
kinder.push(behaelter([textWidget(`<p>${f.antwort || ''}</p>`)], true));
});
wurzel.push(behaelter([
textWidget(`<h2>${konf.faq_ueberschrift || 'Häufige Fragen'}</h2>`),
{ id: eid(), elType: 'widget', widgetType: 'nested-accordion',
settings: { items, faq_schema: 'yes', default_state: 'all_collapsed' },
elements: kinder },
]));
}
return wurzel;
}
if (konf.elementor_ausgabe) {
meta._elementor_data = JSON.stringify(elementorBauen());
meta._elementor_edit_mode = 'builder';
meta._elementor_template_type = 'wp-post';
}
const body = {
title: a.titel,
slug: s.slug,
@@ -483,6 +562,10 @@ return [{ json: {
+ '. Mu-Plugin skrift-ratgeber-felder.php installiert?'
: null,
faq_eintraege: faq_ok,
elementor: (p.meta || {})._elementor_edit_mode === 'builder'
? (() => { try { return JSON.parse((p.meta || {})._elementor_data || '[]').length + ' Container'; }
catch (e) { return 'ungueltig'; } })()
: 'klassischer Inhalt',
seo_titel: (p.meta || {})._seopress_titles_title || null,
preis_warnung: v.preis_warnung || null,
preis_shortcodes: v.preis_shortcodes || 0,
@@ -600,6 +683,7 @@ if (s.importiert.length) {
z.push(` ${r.slug} (Score ${r.score}, ${r.aktion})`);
z.push(` Vorschau: ${r.vorschau_url}`);
z.push(` Bearbeiten: ${r.bearbeiten_url}`);
if (r.elementor) z.push(` Layout: ${r.elementor}`);
if (r.meta_warnung) z.push(` ACHTUNG: ${r.meta_warnung}`);
if (r.preis_warnung) z.push(` PREISE: ${r.preis_warnung}`);
if (r.cache_hinweis) z.push(` Hinweis: ${r.cache_hinweis}`);