diff --git a/src/view/frontend/web/css/toolbar.css b/src/view/frontend/web/css/toolbar.css
index 6a99b0a4..5abb2a9e 100644
--- a/src/view/frontend/web/css/toolbar.css
+++ b/src/view/frontend/web/css/toolbar.css
@@ -14,6 +14,7 @@
@import url("toolbar/_menu.css");
@import url("toolbar/_groups.css");
@import url("toolbar/_findings.css");
+@import url("toolbar/_jsonld-viewer.css");
@import url("toolbar/_highlights.css");
@import url("toolbar/_feedback.css");
@import url("toolbar/_animations.css");
diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css
index 61481bc5..df65e73f 100644
--- a/src/view/frontend/web/css/toolbar/_groups.css
+++ b/src/view/frontend/web/css/toolbar/_groups.css
@@ -102,6 +102,11 @@
background: rgba(20, 184, 166, 0.1);
}
+.mageforge-toolbar-tab-btn[data-tab="structured-data"].mageforge-tab-active {
+ color: var(--mageforge-group-color-structured-data);
+ background: rgba(217, 70, 239, 0.1);
+}
+
/* Left-edge indicator bar on active tab */
.mageforge-toolbar-tab-btn.mageforge-tab-active::before {
@@ -144,6 +149,10 @@
color: var(--mageforge-group-color-seo);
}
+.mageforge-toolbar-tab-btn[data-tab="structured-data"] .mageforge-tab-icon {
+ color: var(--mageforge-group-color-structured-data);
+}
+
.mageforge-tab-label {
display: block;
white-space: normal;
@@ -578,3 +587,8 @@
.mageforge-toolbar-menu-icon {
color: var(--mageforge-group-color-seo);
}
+
+.mageforge-toolbar-menu-item[data-group-key="structured-data"]
+ .mageforge-toolbar-menu-icon {
+ color: var(--mageforge-group-color-structured-data);
+}
diff --git a/src/view/frontend/web/css/toolbar/_jsonld-viewer.css b/src/view/frontend/web/css/toolbar/_jsonld-viewer.css
new file mode 100644
index 00000000..26279d55
--- /dev/null
+++ b/src/view/frontend/web/css/toolbar/_jsonld-viewer.css
@@ -0,0 +1,287 @@
+/**
+ * MageForge Toolbar – JSON-LD Viewer styles
+ *
+ * @package OpenForgeProject\MageForge
+ * @license GPL-3.0
+ */
+
+.mageforge-jsonld-viewer-open {
+ padding: 0 !important;
+ border-top: none !important;
+}
+
+.mageforge-jsonld-summary {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--mageforge-border-color);
+ font-size: 11px;
+ font-weight: 500;
+}
+
+.mageforge-jsonld-count {
+ color: var(--mageforge-text-secondary, rgba(148, 163, 184, 0.8));
+}
+
+.mageforge-jsonld-errors {
+ color: var(--mageforge-color-red);
+ background: var(--mageforge-color-red-alpha-15);
+ padding: 1px 6px;
+ border-radius: 4px;
+}
+
+.mageforge-jsonld-warnings {
+ color: var(--mageforge-color-amber);
+ background: var(--mageforge-color-amber-alpha-15);
+ padding: 1px 6px;
+ border-radius: 4px;
+}
+
+.mageforge-jsonld-empty {
+ padding: 12px;
+ margin: 0;
+ font-size: 11px;
+ color: var(--mageforge-color-amber);
+ font-style: italic;
+}
+
+.mageforge-jsonld-block {
+ border-bottom: 1px solid var(--mageforge-border-color);
+}
+
+.mageforge-jsonld-block:last-child {
+ border-bottom: none;
+}
+
+.mageforge-jsonld-block--error .mageforge-jsonld-block-header {
+ color: var(--mageforge-color-red);
+}
+
+.mageforge-jsonld-block--warning .mageforge-jsonld-block-header {
+ color: var(--mageforge-color-amber);
+}
+
+/* Validation badge in block header */
+.mageforge-jsonld-val-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ padding: 1px 5px;
+ border-radius: 4px;
+ font-size: 9px;
+ font-weight: 600;
+ line-height: 1.5;
+ vertical-align: middle;
+}
+
+.mageforge-jsonld-val-badge--error {
+ background: var(--mageforge-color-red-alpha-15);
+ color: var(--mageforge-color-red);
+ border: 1px solid var(--mageforge-color-red-alpha-35);
+}
+
+.mageforge-jsonld-val-badge--warning {
+ background: var(--mageforge-color-amber-alpha-15);
+ color: var(--mageforge-color-amber);
+ border: 1px solid var(--mageforge-color-amber-alpha-35);
+}
+
+/* Validation issues list */
+.mageforge-jsonld-issues {
+ list-style: none;
+ margin: 0 0 8px;
+ padding: 6px 8px;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.mageforge-jsonld-issue {
+ display: flex;
+ align-items: flex-start;
+ gap: 5px;
+ font-size: 10px;
+ line-height: 1.4;
+}
+
+.mageforge-jsonld-issue svg {
+ flex-shrink: 0;
+ margin-top: 1px;
+}
+
+.mageforge-jsonld-issue--error {
+ color: var(--mageforge-color-red);
+}
+
+.mageforge-jsonld-issue--warning {
+ color: var(--mageforge-color-amber);
+}
+
+.mageforge-jsonld-block-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 8px 12px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ text-align: left;
+ color: var(--mageforge-text-primary, rgba(248, 250, 252, 0.9));
+ font-size: 11px;
+ font-weight: 500;
+ font-family: var(--mageforge-font-family);
+ transition: background 0.15s ease;
+}
+
+.mageforge-jsonld-block-header:hover {
+ background: var(--mageforge-surface-glass-hover);
+}
+
+.mageforge-jsonld-block-type {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.mageforge-jsonld-chevron {
+ flex-shrink: 0;
+ opacity: 0.5;
+ transition: transform 0.2s ease;
+}
+
+.mageforge-jsonld-chevron--open {
+ transform: rotate(180deg);
+ opacity: 0.9;
+}
+
+.mageforge-jsonld-block-content {
+ padding: 0 12px 10px;
+ position: relative;
+}
+
+.mageforge-jsonld-parse-error {
+ margin: 4px 0 0;
+ padding: 6px 8px;
+ background: var(--mageforge-color-red-alpha-15);
+ border: 1px solid var(--mageforge-color-red-alpha-35);
+ border-radius: 4px;
+ font-size: 10px;
+ color: var(--mageforge-color-red);
+ word-break: break-word;
+}
+
+.mageforge-jsonld-pre {
+ margin: 0;
+ padding: 8px;
+ background: rgba(0, 0, 0, 0.25);
+ border: 1px solid var(--mageforge-border-color);
+ border-radius: 4px;
+ overflow: auto;
+ max-height: 260px;
+ font-size: 10px;
+ line-height: 1.6;
+}
+
+.mageforge-jsonld-pre code {
+ font-family:
+ "JetBrains Mono", "Fira Code", "Cascadia Code", Consolas, "Courier New",
+ monospace;
+ color: var(--mageforge-text-primary, rgba(248, 250, 252, 0.9));
+ white-space: pre;
+}
+
+.mageforge-jsonld-copy-btn {
+ display: block;
+ margin-top: 6px;
+ padding: 3px 10px;
+ background: var(--mageforge-surface-glass);
+ border: 1px solid var(--mageforge-border-glass);
+ border-radius: 4px;
+ color: var(--mageforge-color-slate-400);
+ font-size: 10px;
+ font-family: var(--mageforge-font-family);
+ cursor: pointer;
+ transition:
+ background 0.15s ease,
+ color 0.15s ease;
+}
+
+.mageforge-jsonld-copy-btn:hover {
+ background: var(--mageforge-surface-glass-hover);
+ color: var(--mageforge-color-white);
+}
+
+/* ── Schema.org Viewer ──────────────────────────────────────────────────── */
+
+.mageforge-schema-badge {
+ display: inline-block;
+ padding: 1px 5px;
+ border-radius: 4px;
+ font-size: 9px;
+ font-weight: 600;
+ letter-spacing: 0.03em;
+ text-transform: uppercase;
+ background: var(--mageforge-color-fuchsia-alpha-15);
+ color: var(--mageforge-color-fuchsia-light);
+ border: 1px solid var(--mageforge-color-fuchsia-alpha-30);
+ line-height: 1.5;
+}
+
+.mageforge-schema-badge--rdfa {
+ background: rgba(251, 146, 60, 0.15);
+ color: var(--mageforge-color-orange);
+ border-color: rgba(251, 146, 60, 0.3);
+}
+
+.mageforge-schema-type-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 0 6px;
+ border-bottom: 1px solid var(--mageforge-border-color);
+ margin-bottom: 4px;
+}
+
+.mageforge-schema-type-link {
+ font-size: 10px;
+ color: var(--mageforge-color-fuchsia-light);
+ text-decoration: none;
+ word-break: break-all;
+}
+
+.mageforge-schema-type-link:hover {
+ text-decoration: underline;
+}
+
+.mageforge-schema-props {
+ margin: 4px 0 0;
+ display: grid;
+ grid-template-columns: minmax(80px, max-content) 1fr;
+ gap: 2px 10px;
+ align-items: baseline;
+}
+
+.mageforge-schema-prop-name {
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--mageforge-color-fuchsia-light);
+ white-space: nowrap;
+ padding: 2px 0;
+}
+
+.mageforge-schema-prop-value {
+ font-size: 10px;
+ color: var(--mageforge-text-primary, rgba(248, 250, 252, 0.9));
+ word-break: break-word;
+ padding: 2px 0;
+ margin: 0;
+}
+
+.mageforge-schema-prop-nested {
+ color: var(--mageforge-color-slate-400);
+ font-style: italic;
+}
diff --git a/src/view/frontend/web/css/toolbar/_variables.css b/src/view/frontend/web/css/toolbar/_variables.css
index 12d22b87..0e37e12b 100644
--- a/src/view/frontend/web/css/toolbar/_variables.css
+++ b/src/view/frontend/web/css/toolbar/_variables.css
@@ -38,6 +38,11 @@
--mageforge-group-color-html-quality: var(--mageforge-color-blue);
--mageforge-group-color-performance: var(--mageforge-color-orange);
--mageforge-group-color-seo: #14b8a6;
+ --mageforge-group-color-structured-data: #d946ef;
+ --mageforge-color-fuchsia: #d946ef;
+ --mageforge-color-fuchsia-alpha-15: rgba(217, 70, 239, 0.15);
+ --mageforge-color-fuchsia-alpha-30: rgba(217, 70, 239, 0.3);
+ --mageforge-color-fuchsia-light: #e879f9;
/* Backgrounds */
--mageforge-bg-dark: rgba(15, 23, 42, 0.98);
diff --git a/src/view/frontend/web/js/toolbar/audits/index.js b/src/view/frontend/web/js/toolbar/audits/index.js
index 989dcc2c..d759ee36 100644
--- a/src/view/frontend/web/js/toolbar/audits/index.js
+++ b/src/view/frontend/web/js/toolbar/audits/index.js
@@ -38,7 +38,9 @@ import renderBlockingScripts from "./render-blocking-scripts.js";
import seoDuplicateMeta from "./seo-duplicate-meta.js";
import seoHeadingHierarchy from "./seo-heading-hierarchy.js";
import seoMissingCanonical from "./seo-missing-canonical.js";
-import seoMissingJsonLd from "./seo-missing-json-ld.js";
+import structuredMissingJsonLd from "./seo-missing-json-ld.js";
+import structuredJsonLdViewer from "./seo-json-ld-viewer.js";
+import schemaOrgViewer from "./schema-org-viewer.js";
import seoMissingLang from "./seo-missing-lang.js";
import seoMissingMetaDescription from "./seo-missing-meta-description.js";
import seoMissingTitle from "./seo-missing-title.js";
@@ -50,6 +52,7 @@ import unsafeBlankTarget from "./unsafe-blank-target.js";
/** @type {AuditGroup[]} */
export const auditGroups = [
{ key: "seo", label: "SEO" },
+ { key: "structured-data", label: "Structured Data" },
{ key: "performance", label: "Performance" },
{ key: "html-quality", label: "HTML Quality" },
{ key: "wcag", label: "Accessibility" },
@@ -79,6 +82,8 @@ export const audits = [
{ ...seoMissingCanonical, group: "seo" },
{ ...seoMissingLang, group: "seo" },
{ ...seoHeadingHierarchy, group: "seo" },
- { ...seoMissingJsonLd, group: "seo" },
{ ...seoDuplicateMeta, group: "seo" },
+ { ...structuredMissingJsonLd, group: "structured-data" },
+ { ...structuredJsonLdViewer, group: "structured-data" },
+ { ...schemaOrgViewer, group: "structured-data" },
];
diff --git a/src/view/frontend/web/js/toolbar/audits/schema-org-viewer.js b/src/view/frontend/web/js/toolbar/audits/schema-org-viewer.js
new file mode 100644
index 00000000..ad143d8c
--- /dev/null
+++ b/src/view/frontend/web/js/toolbar/audits/schema-org-viewer.js
@@ -0,0 +1,269 @@
+/**
+ * MageForge Toolbar Audit – Schema.org Microdata Viewer
+ *
+ * Reads all schema.org microdata (itemscope / itemtype / itemprop attributes)
+ * from the current page DOM and renders a structured tree view in the findings
+ * panel. Also detects RDFa (typeof / property attributes).
+ *
+ * Page-level audit: no DOM element highlighting, findings panel shows the data.
+ */
+
+const KEY = "schema-org-viewer";
+
+/**
+ * Recursively collect microdata properties from an itemscope element.
+ *
+ * @param {Element} root
+ * @returns {{ type: string, props: Array<{name: string, value: string, nested?: object}> }}
+ */
+function collectMicrodata(root) {
+ const type = root.getAttribute("itemtype") ?? "";
+ const props = [];
+
+ root.querySelectorAll("[itemprop]").forEach((el) => {
+ // Skip deeply nested items already handled by their own itemscope
+ const closestScope = el.parentElement?.closest("[itemscope]");
+ if (closestScope && closestScope !== root) return;
+
+ const name = el.getAttribute("itemprop") ?? "";
+ let value = "";
+ let nested = null;
+
+ if (el.hasAttribute("itemscope")) {
+ nested = collectMicrodata(el);
+ } else if (el.tagName === "META") {
+ value = el.getAttribute("content") ?? "";
+ } else if (el.tagName === "LINK") {
+ value = el.getAttribute("href") ?? "";
+ } else if (el.tagName === "IMG") {
+ value = el.getAttribute("src") ?? "";
+ } else if (el.tagName === "TIME") {
+ value = el.getAttribute("datetime") ?? el.textContent.trim();
+ } else if (el.tagName === "A") {
+ value = el.getAttribute("href") ?? el.textContent.trim();
+ } else {
+ value = el.textContent.trim().slice(0, 120);
+ }
+
+ props.push({ name, value, nested });
+ });
+
+ return { type, props };
+}
+
+/**
+ * Collect RDFa-annotated root elements (typeof attribute on non-nested elements).
+ *
+ * @returns {Array<{type: string, el: Element}>}
+ */
+function collectRdfa() {
+ return Array.from(document.querySelectorAll("[typeof]"))
+ .filter((el) => !el.parentElement?.closest("[typeof]"))
+ .map((el) => ({
+ type: el.getAttribute("typeof") ?? "",
+ el,
+ }));
+}
+
+/**
+ * Build a DOM tree for a collected microdata item.
+ *
+ * @param {{ type: string, props: Array }} item
+ * @param {string} badgeLabel
+ * @returns {HTMLElement}
+ */
+function buildBlockDOM(item, badgeLabel) {
+ const typeShort = item.type.replace(/https?:\/\/schema\.org\//i, "");
+
+ const block = document.createElement("div");
+ block.className = "mageforge-jsonld-block";
+
+ const header = document.createElement("button");
+ header.type = "button";
+ header.className = "mageforge-jsonld-block-header";
+ header.setAttribute("aria-expanded", "false");
+ // Static SVG only – dynamic text set via textContent (XSS-safe)
+ header.innerHTML = `
+
+
+
+
+
+
+ `;
+ header.querySelector(".mageforge-schema-type").textContent =
+ typeShort || "Unknown Type";
+ header.querySelector(".mageforge-schema-badge").textContent = badgeLabel;
+
+ const content = document.createElement("div");
+ content.className = "mageforge-jsonld-block-content";
+ content.hidden = true;
+
+ if (item.type) {
+ const typeRow = document.createElement("div");
+ typeRow.className = "mageforge-schema-type-row";
+ const typePropName = document.createElement("span");
+ typePropName.className = "mageforge-schema-prop-name";
+ typePropName.textContent = "@type";
+ typeRow.appendChild(typePropName);
+ // Only linkify http(s) URLs to prevent javascript: injection
+ if (/^https?:\/\//i.test(item.type)) {
+ const typeLink = document.createElement("a");
+ typeLink.className = "mageforge-schema-type-link";
+ typeLink.href = item.type;
+ typeLink.target = "_blank";
+ typeLink.rel = "noopener noreferrer";
+ typeLink.textContent = item.type;
+ typeRow.appendChild(typeLink);
+ } else {
+ const typeText = document.createElement("span");
+ typeText.className = "mageforge-schema-type-link";
+ typeText.textContent = item.type;
+ typeRow.appendChild(typeText);
+ }
+ content.appendChild(typeRow);
+ }
+
+ if (item.props.length === 0) {
+ const empty = document.createElement("p");
+ empty.className = "mageforge-jsonld-empty";
+ empty.textContent = "No itemprop attributes found.";
+ content.appendChild(empty);
+ } else {
+ const table = document.createElement("dl");
+ table.className = "mageforge-schema-props";
+ item.props.forEach(({ name, value, nested }) => {
+ const dt = document.createElement("dt");
+ dt.className = "mageforge-schema-prop-name";
+ dt.textContent = name;
+ table.appendChild(dt);
+
+ const dd = document.createElement("dd");
+ dd.className = "mageforge-schema-prop-value";
+ if (nested) {
+ const nestedType = nested.type.replace(/https?:\/\/schema\.org\//i, "");
+ dd.textContent = `[${nestedType || "Nested item"} — ${nested.props.length} prop${nested.props.length !== 1 ? "s" : ""}]`;
+ dd.classList.add("mageforge-schema-prop-nested");
+ } else {
+ dd.textContent = value || "—";
+ }
+ table.appendChild(dd);
+ });
+ content.appendChild(table);
+ }
+
+ header.onclick = (e) => {
+ e.stopPropagation();
+ const isOpen = !content.hidden;
+ content.hidden = isOpen;
+ header.setAttribute("aria-expanded", String(!isOpen));
+ header
+ .querySelector(".mageforge-jsonld-chevron")
+ .classList.toggle("mageforge-jsonld-chevron--open", !isOpen);
+ };
+
+ block.appendChild(header);
+ block.appendChild(content);
+ return block;
+}
+
+export default {
+ key: KEY,
+ icon: '',
+ label: "Schema.org Viewer",
+ description:
+ "Shows all schema.org microdata (itemscope/itemprop) and RDFa blocks",
+
+ run(context, active) {
+ const auditItem = context.menu?.querySelector(`[data-audit-key="${KEY}"]`);
+ const findingsContainer = auditItem?.querySelector(
+ ".mageforge-audit-findings",
+ );
+
+ if (!active) {
+ if (findingsContainer) {
+ findingsContainer.innerHTML = "";
+ findingsContainer.classList.remove(
+ "mageforge-has-findings",
+ "mageforge-findings-open",
+ "mageforge-jsonld-viewer-open",
+ );
+ }
+ return;
+ }
+
+ // Collect microdata root elements (not nested)
+ const microdataRoots = Array.from(
+ document.querySelectorAll("[itemscope][itemtype]"),
+ ).filter((el) => !el.parentElement?.closest("[itemscope]"));
+
+ const rdfaRoots = collectRdfa();
+ const total = microdataRoots.length + rdfaRoots.length;
+
+ context.setAuditCounterBadge(
+ KEY,
+ String(total),
+ total > 0 ? "success" : "warning",
+ );
+
+ if (!findingsContainer) return;
+
+ findingsContainer.innerHTML = "";
+ findingsContainer.classList.add(
+ "mageforge-has-findings",
+ "mageforge-findings-open",
+ "mageforge-jsonld-viewer-open",
+ );
+
+ if (total === 0) {
+ const empty = document.createElement("p");
+ empty.className = "mageforge-jsonld-empty";
+ empty.textContent = "No schema.org microdata or RDFa found on this page.";
+ findingsContainer.appendChild(empty);
+ return;
+ }
+
+ const summary = document.createElement("div");
+ summary.className = "mageforge-jsonld-summary";
+ summary.innerHTML = `
+ ${microdataRoots.length > 0 ? `${microdataRoots.length} microdata item${microdataRoots.length !== 1 ? "s" : ""}` : ""}
+ ${rdfaRoots.length > 0 ? `RDFa: ${rdfaRoots.length}` : ""}
+ `;
+ findingsContainer.appendChild(summary);
+
+ microdataRoots.forEach((el) => {
+ const item = collectMicrodata(el);
+ findingsContainer.appendChild(buildBlockDOM(item, "Microdata"));
+ });
+
+ rdfaRoots.forEach(({ type, el }) => {
+ // Normalise to a full schema.org URL only when the value is already one;
+ // CURIEs (e.g. "schema:Product") or plain names are passed through as-is
+ // to avoid producing broken URLs like https://schema.org/schema:Product.
+ const normalizedType = /^https?:\/\//i.test(type)
+ ? type
+ : type.includes(":")
+ ? type // CURIE – leave untouched
+ : `https://schema.org/${type}`;
+
+ const props = Array.from(el.querySelectorAll("[property]"))
+ .filter(
+ (p) =>
+ !p.parentElement?.closest("[typeof]") ||
+ p.closest("[typeof]") === el,
+ )
+ .map((p) => ({
+ name: p.getAttribute("property") ?? "",
+ value:
+ p.getAttribute("content") ??
+ p.getAttribute("href") ??
+ p.textContent.trim().slice(0, 120),
+ nested: null,
+ }));
+
+ findingsContainer.appendChild(
+ buildBlockDOM({ type: normalizedType, props }, "RDFa"),
+ );
+ });
+ },
+};
diff --git a/src/view/frontend/web/js/toolbar/audits/seo-json-ld-viewer.js b/src/view/frontend/web/js/toolbar/audits/seo-json-ld-viewer.js
new file mode 100644
index 00000000..a4f7bf0b
--- /dev/null
+++ b/src/view/frontend/web/js/toolbar/audits/seo-json-ld-viewer.js
@@ -0,0 +1,713 @@
+/**
+ * MageForge Toolbar Audit – JSON-LD Viewer
+ *
+ * Reads all