Commit 8de4a3b6 authored by jan.koester's avatar jan.koester
Browse files

seo update

parent 5ecdaa88
Loading
Loading
Loading
Loading
+59 −0
Changes for editor/html/index.html: 59 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -131,12 +131,59 @@
        <h3 id="save-dialog-title" data-i18n="I18N_SAVE_TPL_TITLE">Dokument speichern</h3>
        <label><span data-i18n="I18N_GROUP">Gruppe</span>: <input type="text" id="save-group" data-i18n-placeholder="I18N_GROUP_PLACEHOLDER" placeholder="z.B. Vorlagen/Blog"></label>
        <label><span data-i18n="I18N_TPL_NAME_LABEL">Name</span>: <input type="text" id="save-name" data-i18n-placeholder="I18N_TPL_NAME_PLACEHOLDER" placeholder="Dokumentname"></label>
        <div class="set-field" style="display:flex;gap:0.5em;flex-wrap:wrap">
            <button id="btn-doc-seo" type="button" class="set-save-btn" style="background:var(--bg-tertiary)">&#x1F50D; <span data-i18n="I18N_SEO_SOCIAL">SEO &amp; Social&hellip;</span></button>
        </div>
        <div class="dialog-actions">
            <button id="btn-save-confirm" data-i18n="I18N_SAVE">Speichern</button>
            <button id="btn-save-cancel" data-i18n="I18N_CANCEL">Abbrechen</button>
        </div>
    </dialog>

    <!-- Document-level SEO & Social metadata, persisted into the saved
         template's <WebEditor><SeoMeta> XML (see webedit_api.cpp
         exportTreeXml/loadDocumentTreeFromXml) rather than a specific
         published page's DB row (compare #seo-meta-dialog above, which is
         per-publish-target). -->
    <dialog id="doc-seo-dialog">
        <h3><span data-i18n="I18N_SEO_SOCIAL">SEO &amp; Social&hellip;</span></h3>
        <div class="set-field">
            <label data-i18n="I18N_DOC_OG_TITLE">OG Title (Facebook/LinkedIn)</label>
            <input type="text" id="doc-seo-og-title">
        </div>
        <div class="set-field">
            <label data-i18n="I18N_DOC_OG_DESCRIPTION">OG Description</label>
            <textarea id="doc-seo-og-description"></textarea>
        </div>
        <div class="set-field">
            <label data-i18n="I18N_DOC_OG_IMAGE">OG Image</label>
            <div style="display:flex;gap:0.4em;align-items:center">
                <input type="text" id="doc-seo-og-image" readonly style="flex:1">
                <button id="btn-doc-seo-choose-image" type="button" data-i18n="I18N_CHOOSE_IMAGE">Choose&hellip;</button>
            </div>
        </div>
        <div class="set-field">
            <label data-i18n="I18N_DOC_TWITTER_CARD">Twitter Card Type</label>
            <select id="doc-seo-twitter-card">
                <option value="summary_large_image" data-i18n="I18N_DOC_TWITTER_SUMMARY_LARGE">Summary with large image</option>
                <option value="summary" data-i18n="I18N_DOC_TWITTER_SUMMARY">Summary</option>
            </select>
        </div>
        <div class="set-field">
            <label data-i18n="I18N_DOC_CANONICAL_URL">Canonical URL (override)</label>
            <input type="text" id="doc-seo-canonical-url">
        </div>
        <div class="set-field">
            <label><input type="checkbox" id="doc-seo-robots-noindex"> <span data-i18n="I18N_DOC_ROBOTS_NOINDEX">Hide from search engines (noindex)</span></label>
        </div>
        <div class="set-field">
            <label><input type="checkbox" id="doc-seo-robots-nofollow"> <span data-i18n="I18N_DOC_ROBOTS_NOFOLLOW">Don't follow links on this page (nofollow)</span></label>
        </div>
        <div class="dialog-actions">
            <button id="btn-doc-seo-close" data-i18n="Close">Schlie&szlig;en</button>
        </div>
    </dialog>

    <dialog id="template-wizard-dialog" class="template-wizard-dialog">
        <h3>CSV Vorlagen-Wizard</h3>
        <p class="hint" style="margin-bottom:0.8em">Platzhalter werden als ${spaltenname} ersetzt. Die CSV-Spalte domain wird als Ziel-Gruppe/Ordner verwendet.</p>
@@ -275,6 +322,7 @@
            </div>
            <div id="publish-target-fields"></div>
            <div class="set-field" style="display:flex;gap:0.5em;flex-wrap:wrap">
                <button id="btn-publish-seo" class="set-save-btn" style="background:var(--bg-tertiary);display:none">&#x1F50D; <span data-i18n="I18N_SEO_SOCIAL">SEO &amp; Social&hellip;</span></button>
                <button id="btn-publish-preview" class="set-save-btn" style="background:var(--bg-tertiary)">&#x1F441; <span data-i18n="I18N_VIEW">Vorschau</span></button>
                <button id="btn-publish-send" class="set-save-btn" data-i18n="I18N_PUBLISH">Ver&ouml;ffentlichen</button>
            </div>
@@ -285,6 +333,17 @@
        </div>
    </dialog>

    <!-- SEO & Social Dialog: a sub-drawer over the same publish-target-select's
         params — its fields carry data-publish-key just like publish-target-fields
         and get collected together with them on Publish, see editor.js -->
    <dialog id="seo-meta-dialog">
        <h3><span data-i18n="I18N_SEO_SOCIAL">SEO &amp; Social&hellip;</span></h3>
        <div id="seo-meta-fields"></div>
        <div class="dialog-actions">
            <button id="btn-seo-meta-close" data-i18n="Close">Schlie&szlig;en</button>
        </div>
    </dialog>

    <!-- Media Browser Dialog -->
    <dialog id="media-browser-dialog">
        <h3>Media Browser</h3>
+4 −4
Changes for editor/html/js/api.js: 4 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -219,12 +219,12 @@ var EditorApi = (function() {
            return request('POST', '/api/document/import-xml', payload);
        },

        saveDocument: function(name, group, overwrite) {
            return request('POST', '/api/document/save', { name: name, group: group || '', overwrite: !!overwrite });
        saveDocument: function(name, group, overwrite, seo) {
            return request('POST', '/api/document/save', { name: name, group: group || '', overwrite: !!overwrite, seo: seo || {} });
        },

        saveDocumentAs: function(name, group, overwrite) {
            return request('POST', '/api/document/save-as', { name: name, group: group || '', overwrite: !!overwrite });
        saveDocumentAs: function(name, group, overwrite, seo) {
            return request('POST', '/api/document/save-as', { name: name, group: group || '', overwrite: !!overwrite, seo: seo || {} });
        },

        listTemplates: function() {
+148 −50
Changes for editor/html/js/editor.js: 148 added lines, 50 removed lines.
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@
    var plugins = [];
    var currentDocId = '';
    var currentDocGroup = '';
    var currentDocSeo = {};
    var saveDialogMode = 'save';
    var wizardTemplates = [];
    var wizardCsvRows = [];
@@ -178,6 +179,7 @@
        EditorApi.getDocument().then(function(resp) {
            document.getElementById('doc-name').textContent = resp.name || 'Untitled';
            currentDocId = resp.id || '';
            currentDocSeo = resp.seo || {};
            Canvas.setTreeData(resp.tree || []);
            DocumentTree.render(resp.tree || []);
            Preview.refresh();
@@ -208,7 +210,7 @@
                openSaveDialog('save', '', currentDocGroup);
            } else {
                saveWithConflictConfirm(function(overwrite) {
                    return EditorApi.saveDocument(name, currentDocGroup, overwrite);
                    return EditorApi.saveDocument(name, currentDocGroup, overwrite, currentDocSeo);
                }, function(resp) {
                    document.getElementById('doc-name').textContent = resp.name;
                    currentDocGroup = resp.group || '';
@@ -367,6 +369,36 @@
        document.getElementById('save-dialog').showModal();
    }

    // Fills the document-level SEO & Social dialog from currentDocSeo (see
    // DocumentState::seo / <WebEditor><SeoMeta> in webedit_api.cpp) — this
    // metadata travels with the saved template's XML, unlike the per-publish
    // "SEO & Social" fields collected by renderPublishDialogFields().
    function populateDocSeoDialog() {
        var seo = currentDocSeo || {};
        document.getElementById('doc-seo-og-title').value = seo.og_title || '';
        document.getElementById('doc-seo-og-description').value = seo.og_description || '';
        document.getElementById('doc-seo-og-image').value = seo.og_image || '';
        document.getElementById('doc-seo-twitter-card').value = seo.twitter_card || 'summary_large_image';
        document.getElementById('doc-seo-canonical-url').value = seo.canonical_url || '';
        var robots = seo.robots || '';
        document.getElementById('doc-seo-robots-noindex').checked = robots.indexOf('noindex') !== -1;
        document.getElementById('doc-seo-robots-nofollow').checked = robots.indexOf('nofollow') !== -1;
    }

    function collectDocSeoFromDialog() {
        var robotsParts = [];
        if (document.getElementById('doc-seo-robots-noindex').checked) robotsParts.push('noindex');
        if (document.getElementById('doc-seo-robots-nofollow').checked) robotsParts.push('nofollow');
        currentDocSeo = {
            og_title: document.getElementById('doc-seo-og-title').value.trim(),
            og_description: document.getElementById('doc-seo-og-description').value.trim(),
            og_image: document.getElementById('doc-seo-og-image').value.trim(),
            twitter_card: document.getElementById('doc-seo-twitter-card').value,
            canonical_url: document.getElementById('doc-seo-canonical-url').value.trim(),
            robots: robotsParts.join(',')
        };
    }

    // --- Dialog bindings ---

    // Runs a save request; if the server reports a name/group conflict, asks
@@ -397,7 +429,7 @@
            var group = document.getElementById('save-group').value.trim();
            saveWithConflictConfirm(function(overwrite) {
                return saveDialogMode === 'save-as' ?
                    EditorApi.saveDocumentAs(name, group, overwrite) : EditorApi.saveDocument(name, group, overwrite);
                    EditorApi.saveDocumentAs(name, group, overwrite, currentDocSeo) : EditorApi.saveDocument(name, group, overwrite, currentDocSeo);
            }, function(resp) {
                document.getElementById('doc-name').textContent = resp.name;
                currentDocGroup = resp.group || '';
@@ -405,6 +437,23 @@
            });
        });

        document.getElementById('btn-doc-seo').addEventListener('click', function() {
            populateDocSeoDialog();
            document.getElementById('doc-seo-dialog').showModal();
        });

        document.getElementById('btn-doc-seo-close').addEventListener('click', function() {
            collectDocSeoFromDialog();
            document.getElementById('doc-seo-dialog').close();
        });

        document.getElementById('btn-doc-seo-choose-image').addEventListener('click', function() {
            if (typeof MediaBrowser === 'undefined') return;
            MediaBrowser.open(function(item) {
                if (item && item.url) document.getElementById('doc-seo-og-image').value = item.url;
            });
        });

        document.getElementById('btn-save-cancel').addEventListener('click', function() {
            document.getElementById('save-dialog').close();
        });
@@ -1762,6 +1811,62 @@
        }
    }

    // Builds the actual <select>/<textarea>/<input>/image-picker element for one
    // publish-form field descriptor (shared by the toolbar quick-publish bar,
    // the main publish dialog, and the SEO & Social sub-dialog). "image" opens
    // the existing MediaBrowser (same pattern as properties.js/survey-manager.js)
    // instead of taking free text.
    function createPublishFieldInput(field, styleCss) {
        if (field.type === 'select' && field.options) {
            var sel = document.createElement('select');
            sel.setAttribute('data-publish-key', field.key);
            if (styleCss) sel.style.cssText = styleCss;
            for (var o = 0; o < field.options.length; o++) {
                var optEl = document.createElement('option');
                optEl.value = field.options[o].value;
                optEl.textContent = field.options[o].label;
                sel.appendChild(optEl);
            }
            return sel;
        }
        if (field.type === 'textarea') {
            var ta = document.createElement('textarea');
            ta.setAttribute('data-publish-key', field.key);
            if (styleCss) ta.style.cssText = styleCss;
            if (field.placeholder) ta.placeholder = field.placeholder;
            return ta;
        }
        if (field.type === 'image') {
            var wrap = document.createElement('div');
            wrap.style.cssText = 'display:flex;gap:0.4em;align-items:center;flex:1;';
            var imgInp = document.createElement('input');
            imgInp.type = 'text';
            imgInp.readOnly = true;
            imgInp.setAttribute('data-publish-key', field.key);
            imgInp.style.cssText = (styleCss || '') + 'flex:1;';
            if (field.placeholder) imgInp.placeholder = field.placeholder;
            var chooseBtn = document.createElement('button');
            chooseBtn.type = 'button';
            chooseBtn.textContent = I18n.t('I18N_CHOOSE_IMAGE', 'Wählen…');
            chooseBtn.addEventListener('click', function() {
                if (typeof MediaBrowser === 'undefined') return;
                MediaBrowser.open(function(item) {
                    if (item && item.url) imgInp.value = item.url;
                });
            });
            wrap.appendChild(imgInp);
            wrap.appendChild(chooseBtn);
            return wrap;
        }
        var inp = document.createElement('input');
        inp.type = field.type || 'text';
        inp.setAttribute('data-publish-key', field.key);
        if (styleCss) inp.style.cssText = styleCss;
        if (field.placeholder) inp.placeholder = field.placeholder;
        if (field.required) inp.required = true;
        return inp;
    }

    function renderPreviewPublishFields(targetName) {
        var container = document.getElementById('preview-publish-fields');
        container.innerHTML = '';
@@ -1772,7 +1877,10 @@
                break;
            }
        }
        if (!target || !target.fields || target.fields.length === 0) {
        // SEO & Social fields are only edited via the full publish dialog's
        // sub-dialog, keeping this compact one-click bar uncluttered.
        var fields = (target && target.fields) ? target.fields.filter(function(f) { return f.group !== 'seo'; }) : [];
        if (fields.length === 0) {
            container.style.display = 'none';
            return;
        }
@@ -1780,7 +1888,7 @@
        container.style.flexWrap = 'wrap';
        container.style.gap = '0.4em';
        container.style.alignItems = 'center';
        var fields = target.fields;
        var fieldStyle = 'padding:0.15em 0.3em;border-radius:3px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-size:0.75rem;width:8em;';
        for (var f = 0; f < fields.length; f++) {
            var field = fields[f];
            var label = document.createElement('label');
@@ -1788,26 +1896,7 @@
            label.style.fontSize = '0.75rem';
            label.style.color = 'var(--text-secondary)';
            container.appendChild(label);

            if (field.type === 'select' && field.options) {
                var sel = document.createElement('select');
                sel.setAttribute('data-publish-key', field.key);
                sel.style.cssText = 'padding:0.15em 0.3em;border-radius:3px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-size:0.75rem;';
                for (var o = 0; o < field.options.length; o++) {
                    var optEl = document.createElement('option');
                    optEl.value = field.options[o].value;
                    optEl.textContent = field.options[o].label;
                    sel.appendChild(optEl);
                }
                container.appendChild(sel);
            } else {
                var inp = document.createElement('input');
                inp.type = field.type || 'text';
                inp.setAttribute('data-publish-key', field.key);
                inp.style.cssText = 'padding:0.15em 0.3em;border-radius:3px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-size:0.75rem;width:8em;';
                if (field.placeholder) inp.placeholder = field.placeholder;
                container.appendChild(inp);
            }
            container.appendChild(createPublishFieldInput(field, fieldStyle));
        }
    }

@@ -2036,9 +2125,22 @@
        renderPublishDialogFields(targets[0].name);
    }

    function appendFormFieldRow(container, field) {
        var div = document.createElement('div');
        div.className = 'set-field';
        var label = document.createElement('label');
        label.textContent = field.label || field.key;
        div.appendChild(label);
        div.appendChild(createPublishFieldInput(field));
        container.appendChild(div);
    }

    function renderPublishDialogFields(targetName) {
        var container = document.getElementById('publish-target-fields');
        var seoContainer = document.getElementById('seo-meta-fields');
        var seoBtn = document.getElementById('btn-publish-seo');
        container.innerHTML = '';
        seoContainer.innerHTML = '';
        var target = null;
        for (var i = 0; i < currentPublishTargets.length; i++) {
            if (currentPublishTargets[i].name === targetName) {
@@ -2046,36 +2148,22 @@
                break;
            }
        }
        if (!target || !target.fields) return;
        if (!target || !target.fields) {
            seoBtn.style.display = 'none';
            return;
        }
        var fields = target.fields;
        var hasSeoFields = false;
        for (var f = 0; f < fields.length; f++) {
            var field = fields[f];
            var div = document.createElement('div');
            div.className = 'set-field';
            var label = document.createElement('label');
            label.textContent = field.label || field.key;
            div.appendChild(label);

            if (field.type === 'select' && field.options) {
                var sel = document.createElement('select');
                sel.setAttribute('data-publish-key', field.key);
                for (var o = 0; o < field.options.length; o++) {
                    var optEl = document.createElement('option');
                    optEl.value = field.options[o].value;
                    optEl.textContent = field.options[o].label;
                    sel.appendChild(optEl);
                }
                div.appendChild(sel);
            if (field.group === 'seo') {
                hasSeoFields = true;
                appendFormFieldRow(seoContainer, field);
            } else {
                var inp = document.createElement('input');
                inp.type = field.type || 'text';
                inp.setAttribute('data-publish-key', field.key);
                if (field.placeholder) inp.placeholder = field.placeholder;
                if (field.required) inp.required = true;
                div.appendChild(inp);
                appendFormFieldRow(container, field);
            }
            container.appendChild(div);
        }
        seoBtn.style.display = hasSeoFields ? '' : 'none';
    }

    function bindPublishDialog() {
@@ -2096,6 +2184,14 @@
            Preview.refresh();
        });

        document.getElementById('btn-publish-seo').addEventListener('click', function() {
            document.getElementById('seo-meta-dialog').showModal();
        });

        document.getElementById('btn-seo-meta-close').addEventListener('click', function() {
            document.getElementById('seo-meta-dialog').close();
        });

        document.getElementById('publish-conn-select').addEventListener('change', function() {
            autoConnectPublish(this.value);
        });
@@ -2110,9 +2206,11 @@
            var target = document.getElementById('publish-target-select').value;
            if (!target) return;

            // Collect dynamic fields
            // Collect dynamic fields — the SEO & Social sub-dialog holds its own
            // real input elements (not a copy), so it's included here too rather
            // than requiring a separate save step.
            var params = {};
            var inputs = document.getElementById('publish-target-fields').querySelectorAll('[data-publish-key]');
            var inputs = document.querySelectorAll('#publish-target-fields [data-publish-key], #seo-meta-fields [data-publish-key]');
            for (var i = 0; i < inputs.length; i++) {
                var key = inputs[i].getAttribute('data-publish-key');
                if (inputs[i].type === 'checkbox') {
+82 −3

File changed.

Preview size limit exceeded, changes collapsed.

+1 −0
Changes for editor/src/webedit_api.h: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -117,6 +117,7 @@ namespace webedit {
            std::string currentDocId;
            std::string currentDocName;
            std::string currentDocGroup;
            blogi::SeoMeta seo; // round-tripped via the <WebEditor> root's seo_* attributes
            std::vector<std::unique_ptr<blogi::webedit::EditPlugin,
                                        blogi::webedit::EditPluginDeleter>> ownedElements;
            std::mutex mtx;
Loading