From f649858fcd53b4918ef28838674bef175f25fc97 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 23 Jun 2026 12:29:17 -0500 Subject: [PATCH] feat: add SEO content scoring panel to article editor JavaScript-based SEO analysis with 7 checks (OG title, description, image, SEO title, meta description, title length, description length). Shows pass/fail dots and overall score. Closes #68 --- .../plg_content_mokoog/media/css/preview.css | 13 +++ .../plg_content_mokoog/media/js/preview.js | 79 ++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/source/packages/plg_content_mokoog/media/css/preview.css b/source/packages/plg_content_mokoog/media/css/preview.css index 7f7d915..346f8e8 100644 --- a/source/packages/plg_content_mokoog/media/css/preview.css +++ b/source/packages/plg_content_mokoog/media/css/preview.css @@ -240,3 +240,16 @@ color: #d32f2f; font-weight: 600; } + +/* SEO scoring panel */ +.mokoog-seo-score { margin: 15px 0; padding: 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #dee2e6; } +.mokoog-seo-heading { margin: 0 0 10px; font-size: 14px; color: #666; } +.mokoog-seo-list { list-style: none; padding: 0; margin: 0 0 10px; } +.mokoog-seo-item { padding: 4px 0; font-size: 13px; display: flex; align-items: center; gap: 8px; } +.mokoog-seo-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } +.mokoog-seo-pass { background: #2e7d32; } +.mokoog-seo-fail { background: #d32f2f; } +.mokoog-seo-total { font-size: 14px; font-weight: 600; padding-top: 8px; border-top: 1px solid #dee2e6; } +.mokoog-seo-total-good { color: #2e7d32; } +.mokoog-seo-total-ok { color: #f57c00; } +.mokoog-seo-total-bad { color: #d32f2f; } diff --git a/source/packages/plg_content_mokoog/media/js/preview.js b/source/packages/plg_content_mokoog/media/js/preview.js index 8a17c70..77192b2 100644 --- a/source/packages/plg_content_mokoog/media/js/preview.js +++ b/source/packages/plg_content_mokoog/media/js/preview.js @@ -367,17 +367,90 @@ document.addEventListener('DOMContentLoaded', function () { document.getElementById('mokoog-sl-domain').textContent = domain; } + // SEO scoring panel + var seoChecks = [ + { id: 'og-title', label: 'OG Title', check: function() { return fields.ogTitle && fields.ogTitle.value.length > 0; }}, + { id: 'og-desc', label: 'OG Description', check: function() { return fields.ogDesc && fields.ogDesc.value.length > 0; }}, + { id: 'og-image', label: 'OG Image', check: function() { return fields.ogImage && fields.ogImage.value.length > 0; }}, + { id: 'seo-title', label: 'SEO Title', check: function() { return fields.seoTitle && fields.seoTitle.value.length > 0; }}, + { id: 'meta-desc', label: 'Meta Description', check: function() { return fields.metaDescription && fields.metaDescription.value.length > 0; }}, + { id: 'title-length', label: 'Title Length (\u226460)', check: function() { + var t = (fields.ogTitle && fields.ogTitle.value) || (fields.articleTitle && fields.articleTitle.value) || ''; + return t.length > 0 && t.length <= 60; + }}, + { id: 'desc-length', label: 'Description Length (\u2264160)', check: function() { + var d = (fields.ogDesc && fields.ogDesc.value) || (fields.metaDesc && fields.metaDesc.value) || ''; + return d.length > 0 && d.length <= 160; + }} + ]; + + var seoPanel = document.createElement('div'); + seoPanel.className = 'mokoog-seo-score'; + + var seoHeading = document.createElement('h4'); + seoHeading.className = 'mokoog-seo-heading'; + seoHeading.textContent = 'SEO Analysis'; + seoPanel.appendChild(seoHeading); + + var seoList = document.createElement('ul'); + seoList.className = 'mokoog-seo-list'; + + var seoDots = {}; + seoChecks.forEach(function (chk) { + var li = document.createElement('li'); + li.className = 'mokoog-seo-item'; + + var dot = document.createElement('span'); + dot.className = 'mokoog-seo-dot mokoog-seo-fail'; + seoDots[chk.id] = dot; + li.appendChild(dot); + + var label = document.createElement('span'); + label.textContent = chk.label; + li.appendChild(label); + + seoList.appendChild(li); + }); + + seoPanel.appendChild(seoList); + + var seoTotal = document.createElement('div'); + seoTotal.className = 'mokoog-seo-total'; + seoPanel.appendChild(seoTotal); + + wrapper.parentNode.insertBefore(seoPanel, wrapper.nextSibling); + + function updateSeoScore() { + var passed = 0; + seoChecks.forEach(function (chk) { + var ok = chk.check(); + if (ok) passed++; + seoDots[chk.id].className = 'mokoog-seo-dot ' + (ok ? 'mokoog-seo-pass' : 'mokoog-seo-fail'); + }); + + seoTotal.textContent = passed + '/' + seoChecks.length + ' checks passed'; + + if (passed === seoChecks.length) { + seoTotal.className = 'mokoog-seo-total mokoog-seo-total-good'; + } else if (passed >= Math.ceil(seoChecks.length / 2)) { + seoTotal.className = 'mokoog-seo-total mokoog-seo-total-ok'; + } else { + seoTotal.className = 'mokoog-seo-total mokoog-seo-total-bad'; + } + } + Object.values(fields).forEach(function (el) { if (el) { - el.addEventListener('input', updatePreview); - el.addEventListener('change', updatePreview); + el.addEventListener('input', function () { updatePreview(); updateSeoScore(); }); + el.addEventListener('change', function () { updatePreview(); updateSeoScore(); }); } }); if (fields.ogImage) { - var observer = new MutationObserver(updatePreview); + var observer = new MutationObserver(function () { updatePreview(); updateSeoScore(); }); observer.observe(fields.ogImage, { attributes: true, attributeFilter: ['value'] }); } updatePreview(); + updateSeoScore(); });