DocumentationDownload

Quality engine: SEO, accessibility, performance

Nucleo 0.2+Free~29 min readUpdated October 31, 2026

The quality engine checks every document and reports SEO, accessibility and performance issues, blocking publication when needed.

On this page

01Overview

The quality engine checks a Nucleo document and returns a report with scores, issues and a blocking flag.

The engine composes the document as a page and builds a profile from it: headings, images, links, color pairs, landmarks, anchors, buttons, forms and styles. The rules read this profile, not the HTML.

The check runs on every save and blocks publishing if the report is blocking. An acknowledgement (acknowledge) makes a single issue non-blocking.

The SEO part prints the title, meta description, Open Graph and JSON-LD on single pages, but only when neither Yoast SEO nor Rank Math is active.

All functions in this chapter are Free.

02Running the check

When the check runs

What it does: runs the check on the document in these cases:

  • on every save, create, patch, publish and import of a document, including in dryRun;
  • when the document is recompiled after a reusable section changes;
  • with the ability nucleo/quality-check;
  • with the ability nucleo/doc-validate, if the document is valid;
  • in the site scan (nucleo/quality-scan), for the SEO score of each page.

How to use it: no explicit action is needed when saving. To check without saving, use nucleo/quality-check with document or nucleo/doc-validate.

Parameters: None.

Permissions: those of the operation that starts it.

Edition: Free.

Known limits:

  • The engine does not run at runtime on the public site.
  • The check composes one document only. Header, footer and layout applied to the page are not part of the check. Reusable sections (nc/section-ref) are included, because the document arrives already expanded.
  • The document must pass schema validation. If it does not, nucleo/quality-check returns invalid_document without a report.

Page profile

What it does: walks the root and the children of the document and collects the data for the rules. It uses the semantics section of each block manifest (heading, image, link, landmark) and some fixed fields of the node.

How to use it: internal. The quality engine uses it.

Parameters:

Data collectedSource in the nodeUsed by
Headingssemantics.heading.levelFrom (integer 1–6) and textFromseo.h1.*, seo.heading.*, threshold of a11y.contrast
Imagessemantics.image.srcFrom, altFrom, decorativeFromseo.image.alt.missing, perf.lcp.lazy
Linkssemantics.link.hrefFrom, textFrom; content.ariaLabel or settings.ariaLabela11y.link.empty
Landmarkssemantics.landmark.tagFrom, or the fixed tag with the optional ifFroma11y.landmark.missing
Color pairsstyle.color and style.background (per breakpoint)a11y.contrast
Conditionsvisibility.when[].isvisibility.logged_in
Anchorssettings.anchor (no spaces at the edges)a11y.anchor.duplicate
Fixed positionstyle.position equal to fixed in at least one breakpointlayout.fixed.z_index
Buttonsnc/button nodes: content.text, settings.hrefcontent.button.inert
Stylesevery string value of style, including per breakpointstyle.token.literal
Formsnc/native-form nodes with external actionsform.*

Permissions: None.

Edition: Free.

Known limits:

  • The estimated LCP image is the first non-decorative image. The profile marks it with lcp: true and lazy: false. All other images have lazy: true.
  • The visible field of headings always stays empty: the profile does not compute visibility per breakpoint.
  • An nc/button with bind on settings.href is not part of the button list.

Severity and publish blocking

What it does: each issue has a severity: error, warning or info. An issue can also have publishBlocking. The report is blocking if there is at least one open issue with severity error or with publishBlocking.

How to use it: read quality.blocking in the response.

SeverityDraftPublishing, or saving an already published postScore
errorsavesfails with quality_blocking, unless acknowledge−20
warning with blocksPublishsavesfails with quality_blocking, unless acknowledge−5
warningsavessucceeds−5
infosavessucceeds0

Parameters: None.

Permissions: None.

Edition: Free.

Known limits:

  • Saving a post with status publish, future or private counts as publishing: the blocking check applies even without publish.
  • A draft Nucleo page with a blocking report does not go online from the WordPress editor either: block editor, classic editor, page panel or scheduling. The REST API responds quality_blocking. The classic editor keeps the previous status.
  • When the document is recompiled after a section changes, a blocking report on a published post keeps the previous version online. Nucleo keeps the issues on the post and returns quality_blocking. The kept issues are cleared at the next successful save.
  • The error message is "Blocking quality issues: fix them or acknowledge them.".

Score

What it does: computes three scores from 0 to 100: seo, a11y and perf. It starts from 100 and subtracts 20 for each open error and 5 for each open warning. info issues are worth 0. The minimum is 0.

How to use it: read quality.score.

Parameters: None.

Permissions: None.

Edition: Free.

Known limits:

  • The category depends on the prefix of the rule code: seo., a11y. or perf.. Rules with other prefixes (content., form., style., layout., visibility., links.) do not affect any score.
  • Issues acknowledged with acknowledge do not count.

Report format

What it does: returns the quality object of the response.

How to use it:

{
  "quality": {
    "score": { "seo": 75, "a11y": 100, "perf": 100 },
    "blocking": true,
    "issues": [
      {
        "rule": "seo.h1.multiple",
        "severity": "error",
        "nodes": [ { "doc": "page:120", "id": "hero" }, { "doc": "page:120", "id": "sub" } ],
        "message": "The composed page contains 2 h1: hero in page:120, sub in page:120.",
        "fix": [ { "op": "set", "doc": "page:120", "id": "sub", "path": "settings.level", "value": 2 } ]
      }
    ]
  }
}

Parameters:

NameTypeAllowed valuesDefaultNotes
rulestringrule code—Use it in acknowledge.
severitystringerror, warning, info—
nodesarray{doc, id}[]Empty for page rules (seo.h1.missing, a11y.landmark.missing) and for post checks, which use {doc: "page:<id>", id: "meta"}.
messagestring——Translated, except where the rules say otherwise.
fixarraypatch operations with an extra doc key[]Today only seo.h1.multiple fills it.
guidestringR1.2, R1.5, R2.3, R4.4, R4.5absentPresent only for rules linked to the phase guide.
blocksPublishbooleantrueabsentPresent only for warning issues with publishBlocking.

Permissions: None.

Edition: Free.

Known limits: quality.issues contains only open issues. Acknowledged issues do not appear in the response.

Acknowledgement (acknowledge)

What it does: makes an issue non-blocking. The acknowledgement states the rule, the node and the reason. The engine accepts it if rule matches, node is one of the id values in the issue's nodes, and reason is not empty after trimming the edge spaces.

How to use it: pass acknowledge to the write abilities (nucleo/doc-publish, nucleo/doc-patch, nucleo/doc-create, nucleo/site-import) or to nucleo/quality-check. From the local CLI, use nucleo push --acknowledge FILE.

{ "acknowledge": [ { "rule": "form.action.consequential", "node": "f1", "reason": "The user authorized the webhook on October 30" } ] }

Parameters:

NameTypeAllowed valuesDefaultNotes
rulestringrule code—Required.
nodestringid of a node of the issue—Required. An acknowledgement on the nc/section-ref node applies to the expanded nodes of the section.
reasonstringnot empty—Required.

Permissions: those of the ability that receives it.

Edition: Free.

Known limits:

  • Nucleo keeps the acknowledgements used on the post, with a fingerprint (SHA-1) of the node without children. They apply to later saves, imports and recompilations.
  • An acknowledgement expires if the node changes (type, content, settings, style), if the node disappears, or if the issue no longer exists.
  • For the same rule and node pair, the most recent acknowledgement wins.
  • Issues without nodes (seo.h1.missing, a11y.landmark.missing) cannot be acknowledged: they stay blocking until fixed.
  • The rules listed in nonForcible (option nucleo_quality, settable with nucleo/settings-set in the quality group, up to 50 ids) cannot be acknowledged: they stay blocking. The list is empty by default, so every rule is forcible.
  • Acknowledgements are not written to the activity log.

Rules by document type

What it does: removes some rules from the report when the document is not a page.

How to use it: automatic, based on kind.

kindRules removed
layout, popupseo.h1.missing, a11y.landmark.missing, seo.content.thin
sectionthe three above plus content.button.inert, form.action.consequential

Parameters: None.

Permissions: None.

Edition: Free.

Known limits: nucleo/quality-check adds the post checks only with postId and only if kind is not layout, section or popup.

Third-party rules

What it does: the nucleo/quality/rules filter receives the list of rules and can add or remove some. Nucleo discards the items that do not implement the Nucleo\Quality\Rule interface.

How to use it:

add_filter('nucleo/quality/rules', function (array $rules): array {
    $rules[] = new MyRule(); // implements id(), severity(), check(ComposedPage $page): array
    return $rules;
});

Parameters: the Rule interface has id(): string, severity(): Severity and check(ComposedPage $page): array, that is, a list of issues.

Permissions: None.

Edition: Free.

Known limits: the engine is built once per request. The filter must be added before the first check.

03Rules

Summary table. "Point" shows where the issue comes from: engine (on every save), sections (on every save, for reusable sections), post (only in nucleo/quality-check with postId and in the scan), site (only in the scan).

CodeSeverityCategoryBlocksPointWhat it checks
seo.h1.missingerrorseoyesengineNo h1
seo.h1.multipleerrorseoyesengineMore than one h1
seo.heading.skipwarningseonoengineLevel jump between headings
seo.heading.emptyerrorseoyesengineHeading without text
seo.image.alt.missingwarningseonoengineNon-decorative image without alt
a11y.contrasterrora11yyesengineContrast below 4.5:1 or 3:1
a11y.link.emptyerrora11yyesengineLink without accessible text
a11y.anchor.duplicatewarninga11ynoengineSame anchor on several nodes
a11y.landmark.missingwarninga11ynoengineMissing main landmark
perf.lcp.lazyerrorperfyesengineLCP image with loading="lazy"
visibility.logged_ininfo—noengineContent visible only to logged-in users
layout.fixed.z_indexwarning—noengineposition: fixed without zIndex
content.button.inertwarning + blocksPublish—yesengineButton without link or action
form.action.consequentialwarning + blocksPublish—yesengineForm with external actions
form.recipient.invalidwarning—noengineInvalid email recipient of the form
a11y.button.as-linkwarninga11ynoengineLink without a real destination (empty href, # or javascript:) or <a> without href in the texts
a11y.focus.hiddenerrora11yyesengineFocus style that hides the focus indicator (outline none, transparent or alpha 0)
a11y.target.sizewarninga11ynoengineButton or icon smaller than 24 px at the base breakpoint
a11y.lang.missingwarninga11ynoengineEnglish text on a site in another language, without settings.lang. Threshold: at least 8 words, 3 distinct stopwords, at least 25%
a11y.video.autoplaywarninga11ynoengineVideo that starts by itself without being muted (mute=1 missing)
seo.link.text.genericwarningseonoengineGeneric link text ("click here", "read more", "here"...) without aria-label
seo.image.filenameinfoseonoengineGeneric (for example img, DSC) or hexadecimal image file name
seo.heading.style-onlyinfoseonoengineShort text with large font (24 px or 1.5 rem) and no heading, or a heading longer than 150 characters
seo.hidden.contentwarningseonoengineHeading or text (at least 20 words) hidden on some breakpoints and visible on others
perf.html.blockinfoperfnoenginenc/html or nc/shortcode block: free code that the theme does not optimise
perf.dom.sizewarningperfnoengineEstimated DOM elements of the page above 1500 (one issue per page)
perf.css.budgetwarningperfnoengineCSS served to the page above 75 KB
perf.js.budgetwarningperfnoengineJS served to the page above 100 KB
perf.font.countwarningperfnoengineMore than 2 font families or more than 4 variants on the site (one issue per site)
perf.image.oversizewarningperfnoengineImage more than twice as wide as it is shown, without srcset
perf.embed.no-facadewarningperfnoengineiframe above the fold without a facade
ds.layout.conflictwarning—noengineTwo or more layouts with the same area and the same conditions
style.token.literalerror—yesengineStyle value equal to a token
content.link.missingwarning—nosectionsReference to content missing or not published
content.link.pathinfo—nosectionsInternal link written as a path
content.section.missingwarning—nosectionsReusable section missing or in the trash
seo.title.lengthwarningseonopostTitle outside 30–60 characters
seo.description.missingwarningseonopostMeta description missing
seo.description.lengthinfoseonopostMeta description outside 70–160 characters
seo.content.thinwarningseonopostFewer than 300 words
seo.noindexwarningseonopostPage excluded from search engines
seo.canonical.externalwarningseonopostCanonical pointing to another host
seo.links.internal.noneinfoseonopostNo internal link in the content
seo.title.missingwarningseonopostSEO title missing
seo.h1.title-mismatchinfoseonopostFirst h1 different from the SEO title
seo.noindex.importanterrorseoyespostPage marked noindex but present in the main menu
seo.canonical.conflictwarningseonopostCanonical pointing to another page of the site
seo.title.duplicatewarningseonositeSame title on several pages
seo.h1.duplicatewarningseonositeSame h1 text on several pages
links.orphanwarning—nositePage that no other document links to
seo.hreflang.missingwarningseonositePages missing a translation in at least one language
seo.schema.duplicateerrorseonositeThe SEO plugin and Nucleo both print a BreadcrumbList on the same page
seo.duplicate.sectionwarningseonositeSection repeated on the page, shown only on small screens and only on large ones: content is duplicated

seo.h1.missing

Code: seo.h1.missing. Severity: error. Category: seo.

What it checks: no level 1 heading in the composed document.

Thresholds: at least one h1.

How to fix it: set the level of a heading to 1 (settings.level for nc/heading).

Known limits: the issue has no nodes, so it cannot be acknowledged. It does not apply to layout, section and popup.

seo.h1.multiple

Code: seo.h1.multiple. Severity: error. Category: seo.

What it checks: more than one level 1 heading. The issue lists all the h1 in nodes.

Thresholds: at most one h1.

How to fix it: apply the fix. The fix keeps the first h1 of a page: document (or the first one overall) and sets settings.level to 2 on the others. Alternatively, acknowledge the issue on one of the nodes.

Known limits: if an h1 is inside a reusable section, the report removes the fix and adds "(inside a reusable section)" to the message.

seo.heading.skip

Code: seo.heading.skip. Severity: warning. Category: seo.

What it checks: two consecutive headings in document order where the level goes up by more than one (for example h2 followed by h4). A level going down does not count.

Thresholds: maximum step +1.

How to fix it: correct the level of the second heading.

Known limits: the message is fixed in Italian, not translated ("Salto di livello: da hX a hY.").

seo.heading.empty

Code: seo.heading.empty. Severity: error. Category: seo.

What it checks: heading with empty text after removing the tags and the spaces at the edges.

Thresholds: none.

How to fix it: write the heading text or remove the node.

Known limits: none known.

seo.image.alt.missing

Code: seo.image.alt.missing. Severity: warning. Category: seo.

What it checks: non-decorative image with a missing alt, or an alt made only of spaces.

Thresholds: none.

How to fix it: write the alt, or mark the image as decorative in the field indicated by semantics.image.decorativeFrom of the block.

Known limits: none known.

a11y.contrast

Code: a11y.contrast. Severity: error. Category: a11y.

What it checks: WCAG 2.x contrast ratio between the style.color of the node and the background. The background is the style.background of the node or of the nearest ancestor. Without a declared background, the token $color.background applies.

Thresholds:

  • 4.5:1 for text;
  • 3:1 for level 1 and 2 headings;
  • 3:1 for nc/icon with a label (content.label not empty or bound with bind), according to WCAG 1.4.11.

How to fix it: change the color or the background. The message gives the two colors, the ratio and the minimum.

Known limits:

  • The check creates one pair for every breakpoint where the color or the background changes. A breakpoint without a value inherits the base value.
  • It accepts only #rgb or #rrggbb colors, also as resolved tokens. Values such as rgba(), var() or gradients produce no issues.
  • It does not check blocks without text: nc/icon without label, nc/image, nc/divider, nc/spacer, nc/video, nc/post-image.
  • For nc/container and nc/section the check looks at the children. A child with its own color has its own pair.
  • The message is fixed in Italian, not translated.

Code: a11y.link.empty. Severity: error. Category: a11y.

What it checks: node with link semantics, without visible text and without ariaLabel in content or settings.

Thresholds: none.

How to fix it: write the link text or add content.ariaLabel.

Known limits: none known.

a11y.anchor.duplicate

Code: a11y.anchor.duplicate. Severity: warning. Category: a11y.

What it checks: the same value of settings.anchor on two or more nodes. A typical case is a section with an anchor used twice.

Thresholds: one node per anchor.

How to fix it: make each anchor unique.

Known limits: none known.

a11y.landmark.missing

Code: a11y.landmark.missing. Severity: warning. Category: a11y.

What it checks: the composed page, with its layouts, has no main, nav or footer landmark.

Thresholds: at least one main, one nav and one footer.

How to fix it: set the main tag on the main container, in the field indicated by semantics.landmark.tagFrom. An nc/menu with a menu is a nav. The footer layout usually gives the footer.

Known limits: the issue has no nodes. It does not apply to layout, section and popup.

perf.lcp.lazy

Code: perf.lcp.lazy. Severity: error. Category: perf.

What it checks: image marked as LCP and as lazy.

Thresholds: none.

How to fix it: no change to the document is needed with the current profile.

Known limits: the profile always marks the LCP image with lazy: false. With the Nucleo engine the rule produces no issues. It fires only with a profile built in another way, as in the tests.

visibility.logged_in

Code: visibility.logged_in. Severity: info. Category: none (does not affect the scores).

What it checks: node with visibility.when that contains is: "logged_in" or is: "role:<role>". The content stays hidden from anonymous visitors and search engines.

Thresholds: none.

How to fix it: it is information. Check that the content is not needed for indexing.

Known limits: the message is fixed in Italian, not translated.

layout.fixed.z_index

Code: layout.fixed.z_index. Severity: warning. Category: none.

What it checks: style.position equal to fixed in at least one breakpoint, without the style.zIndex key.

Thresholds: none.

How to fix it: add style.zIndex.

Known limits: the presence of the zIndex key is enough, whatever its value.

content.button.inert

Code: content.button.inert. Severity: warning with blocksPublish. Category: none. Guide: R4.5.

What it checks: nc/button with settings.href empty, equal to # or discarded by the renderer (for example javascript:).

Thresholds: none.

How to fix it: add an href (for example page:<slug> or home) or use the right interactive block. The message suggests the block based on whole words in the text, in this order:

Suggested blockWords (stems, case insensitive)
nc/slidertext made only of arrows (← → ‹ › « » < >)
nc/collectionfiltr…, filter(s), ordina…, sort, azzera…, reset, clear, carica altr…, load more, mostra altr…, show more
nc/tabstab(s), scheda, schede
nc/accordionfaq, domand…, espandi, expand, open
nc/sliderprev, previous, next, precedent…, successiv…
nc/searchcerc…, search
nc/native-forminvia…, send, iscriv…, subscribe, contatta…, contact, prenota…, book

Known limits:

  • An href bound with bind excludes the button from the check.
  • It does not apply to section documents.
  • A button handled by a script through attributes is not recognized. In that case acknowledge is needed.

form.action.consequential

Code: form.action.consequential. Severity: warning with blocksPublish. Category: none. Guide: R4.4.

What it checks: nc/native-form with at least one external action in settings.actions: webhook, create-post, component, or email with to not empty and not admin.

Thresholds: none.

How to fix it: ask the user for consent. Then publish with acknowledge on the form node and write the consent in reason. The message also asks for a test submission.

Known limits: it does not apply to section documents. The acknowledgement expires when the form node changes.

form.recipient.invalid

Code: form.recipient.invalid. Severity: warning. Category: none.

What it checks: recipient of an email action of nc/native-form that is not admin, not a valid email address, and not field:<name>.

Thresholds: the <name> of field: follows ^[a-z][a-z0-9_]{0,31}$.

How to fix it: use admin, a valid address or field:<name> of an email field.

Known limits: it does not block publishing.

style.token.literal

Code: style.token.literal. Severity: error. Category: none. Guide: R1.2.

What it checks: string value of style equal to the value of a token of the property group (colors, spacing, radii, shadows, fonts). The comparison is on the whole value.

Thresholds: one issue per node, property and value, even if the value repeats across breakpoints.

How to fix it: write the token reference shown in the message (for example $color.primary). From the local CLI, nucleo push --use-tokens replaces the values in the files.

Known limits: the rule is active only if the engine receives the site tokens. Nucleo always passes them.

Codes and severity: content.link.missing (warning, guide R2.3), content.link.path (info, guide R2.3), content.section.missing (warning, guide R1.5). Category: none.

What they check:

  • content.link.missing: an internal reference (for example page:contact) points to content that is missing or not published. The link is rendered as # with aria-disabled.
  • content.link.path: an internal link written as a path (/contact) has an equivalent reference.
  • content.section.missing: an nc/section-ref cites a section that is missing or in the trash. The block renders nothing.

Thresholds: none.

How to fix them: publish the content or correct the reference; write the suggested reference shown in the message; create or restore the section.

Known limits: they do not come from the engine rules and do not pass through the nucleo/quality/rules filter. Nucleo adds them on every save, in nucleo/quality-check and in nucleo/doc-validate.

seo.title.length

Code: seo.title.length. Severity: warning. Category: seo.

What it checks: length in characters of the SEO title read by Nucleo (see Reading the SEO meta).

Thresholds: from 30 to 60 characters.

How to fix it: without an SEO plugin, write meta.seo.title of the document. With Yoast SEO or Rank Math, edit the title in the plugin.

Known limits: without an SEO plugin and without meta.seo.title, the post title is used. The issue uses the meta node of the document page:<id>.

seo.description.missing, seo.description.length

Codes: seo.description.missing (severity warning), seo.description.length (severity info). Category: seo.

What they check: meta description missing; otherwise, length outside the threshold.

Thresholds: from 70 to 160 characters.

How to fix them: write meta.seo.description, or the description in the active SEO plugin.

Known limits: the document schema accepts meta.seo.description up to 170 characters, beyond the recommended threshold.

seo.content.thin

Code: seo.content.thin. Severity: warning. Category: seo.

What it checks: number of words in the page content (compiled HTML) without tags, with entities decoded.

Thresholds: at least 300 words. Fixed value.

How to fix it: add text.

Known limits: the threshold does not change by content type. It does not apply to layout, section and popup.

seo.noindex

Code: seo.noindex. Severity: warning. Category: seo.

What it checks: the noindex value read by Nucleo (see Reading the SEO meta).

Thresholds: none.

How to fix it: remove the noindex in the SEO plugin, or in the WordPress reading settings.

Known limits: without an SEO plugin, noindex is true if the option blog_public is '0' (a site that discourages search engines). meta.seo.noindex of the document does not activate the rule. With Yoast SEO the rule reads the Yoast noindex; with Rank Math it reads the Rank Math robots setting.

seo.canonical.external

Code: seo.canonical.external. Severity: warning. Category: seo.

What it checks: host of the canonical different from the host of the site, without distinction between uppercase and lowercase.

Thresholds: none.

How to fix it: correct the canonical in the SEO plugin.

Known limits: without an SEO plugin the canonical read is always empty: the rule fires only with Yoast SEO or Rank Math.

Code: seo.links.internal.none. Severity: info. Category: seo (worth 0 in the score).

What it checks: no internal link in the page content. Internal means same host as the site, or an href without host and without scheme.

Thresholds: at least one internal link.

How to fix it: add a link to another page of the site.

Known limits: an href #anchor or a relative one counts as internal.

seo.title.duplicate, seo.h1.duplicate

Codes: seo.title.duplicate, seo.h1.duplicate. Severity: warning. Category: seo.

What they check: same SEO title, or same h1 text, on two or more published documents. The comparison is made after lowercasing and collapsing spaces.

Thresholds: two pages.

How to fix them: make titles and h1 unique.

Known limits: only in the site scan. The h1 is read from the HTML of the content. The issue lists the pages involved (list of IDs) instead of the nodes.

Code: links.orphan. Severity: warning. Category: none.

What it checks: published document that no other published document links to. The static home page is excluded.

Thresholds: at least one incoming link.

How to fix it: add a link to the page from another document.

Known limits: it reads only the links in the HTML of published Nucleo documents. Menu, header and footer do not count. It accepts only hrefs that start with / or with the home address; it ignores hrefs that start with //.

seo.hreflang.missing

Code: seo.hreflang.missing. Severity: warning. Category: seo.

What it checks: published documents without a translation in at least one language of the site. One issue lists all the pages in posts.

Thresholds: it applies with at least 2 languages.

How to fix it: create the missing translations in the multilingual plugin.

Known limits: the languages come from the supported multilingual plugin and from the nucleo/languages and nucleo/translations filters.

04Site scan

Batch scan

What it does: reads all posts with status publish and a Nucleo document, of every content type, in ID order. It computes the site rules on all documents on each call. It computes the scores only for the pages of the requested batch.

How to use it: ability nucleo/quality-scan with page. Repeat the call with page set to the value of next until next is null. Result:

{
  "site": [ { "rule": "links.orphan", "severity": "warning", "message": "Page without internal links from other documents.", "posts": [42] } ],
  "pages": [ { "postId": 42, "score": 85, "a11y": 100, "perf": 100 } ],
  "next": 2
}

Parameters:

NameTypeAllowed valuesDefaultNotes
pageinteger≥ 11Batch number.

Permissions: edit_posts. OAuth scope nucleo:read.

Edition: Free.

Known limits:

  • The batch has 50 pages. The batch size parameter is not exposed by the ability.
  • Each page reports the seo, a11y and perf scores. It sums the engine rules and the post checks. It does not apply acknowledge or the limit by document type.
  • With page: 1 the ability stores for one day, in the transient nucleo_quality_badge, the number of pages involved in site issues with severity error. Site issues are warning, except seo.schema.duplicate, which is error.

Checking a document (nucleo/quality-check)

What it does: returns the report of a saved document (postId) or of a document passed as input (document), without writing.

How to use it: see the ability nucleo/quality-check.

Parameters: postId (integer ≥ 1) or document (object), never both; acknowledge (list of {rule, node, reason}).

Permissions: with postId, edit_post on that post; without it, edit_posts. OAuth scope nucleo:read.

Edition: Free.

Known limits:

  • Acknowledgements saved on the post do not apply: only those passed in the call count.
  • The post checks run only with postId.
  • Errors: invalid_input (neither or both parameters), not_found, invalid_document.

Quality and SEO admin page

What it does: the page runs the batch scan, shows the site issues, the page scores and the detail of a page with nucleo/quality-check.

How to use it: menu Nucleo AI → Quality and SEO.

Parameters: tabs issues, pages, overrides, rules.

Permissions: page edit_posts; starting the scan edit_others_posts. Tabs issues and overrides: edit_others_posts. Tab rules: edit_theme_options. The counter in the menu appears only with edit_others_posts.

Edition: Free.

Known limits:

  • The overrides tab shows "Not available yet".
  • The Accessibility and Performance columns of the pages tab show the a11y and perf scores from the scan. A missing value stays "—".
  • With the scan, the rules tab shows the real severity of each rule. Without the scan it shows a fixed list of ten rules with a "typical" severity, which for four does not match the code: seo.image.alt.missing (error instead of warning), perf.lcp.lazy (warning instead of error), seo.noindex (info instead of warning), seo.links.internal.none (warning instead of info).

05SEO on the page

Reading the SEO meta

What it does: returns title, description, source, noindex and canonical of a post. With Yoast SEO active it reads Yoast; with Rank Math active it reads Rank Math; otherwise it reads meta.seo of the document.

How to use it: internal. The post checks and the site scan use it. The filter nucleo/seo/meta can change the result.

Parameters: None.

Permissions: None.

Edition: Free.

Known limits: without an SEO plugin, canonical is always empty and noindex follows the WordPress setting that discourages search engines (blog_public).

Title, meta description, Open Graph, robots

What it does: on single pages with a Nucleo document and a non-empty meta.seo, it replaces the title of the HTML document and prints these tags in wp_head, with priority 1:

TagValueWhen
<meta name="description">meta.seo.descriptionif not empty
og:titlemeta.seo.title, otherwise the post titlealways
og:descriptionmeta.seo.descriptionif not empty
og:urlpermalinkalways
og:typewebsitealways
og:imagemeta.seo.imageif not empty
<meta name="robots" content="noindex">—if meta.seo.noindex is true

How to use it: fill in meta.seo in the document.

NameTypeAllowed valuesDefaultNotes
meta.seo.titlestringmax 70 characterspost title
meta.seo.descriptionstringmax 170 characters—
meta.seo.imagestringURI—
meta.seo.noindexbooleanfalseAlso excludes from the sitemap.

Permissions: None (frontend).

Edition: Free.

Known limits:

  • It turns off if Yoast SEO or Rank Math is active.
  • The canonical stays with WordPress: Nucleo does not print it.
  • No Twitter Card tag. og:type is always website.
  • Archives and the blog home do not receive these tags: they apply only to single pages.

Page JSON-LD

What it does: prints in the page footer a single application/ld+json script with @context and @graph. The graph contains, in order:

  1. the nodes of meta.seo.schema, with url equal to the permalink if missing; the value "@business", at any depth, becomes {"@id": "<home>/#business"};
  2. the company node, on the static home page or if a node uses "@business";
  3. the nodes of the ld+json scripts present in the content, removed from the HTML; a script with @graph adds each node of its own graph;
  4. an automatic BreadcrumbList.

How to use it: fill in meta.seo.schema and, if needed, meta.seo.breadcrumbs.

NameTypeAllowed valuesDefaultNotes
meta.seo.schemaarray of objectsmax 10 nodes; @type required with pattern ^[A-Z][A-Za-z]{2,60}$[]
meta.seo.breadcrumbsbooleanfalse—false removes the automatic breadcrumb.

Permissions: None (frontend).

Edition: Free.

Known limits:

  • It turns off if Yoast SEO or Rank Math is active.
  • It runs on every single page, even without meta.seo: the graph may contain only the breadcrumb.
  • Breadcrumb: site name, ancestors of the post, page. It is missing on the static home page and if the content already contains a BreadcrumbList.
  • Nucleo merges the nodes: it removes identical nodes and merges all the FAQPage nodes into one.
  • Scripts with invalid JSON stay in the content.
  • The output escapes < and >: a </script> in the texts does not close the script.

JSON-LD consolidation at compile time

What it does: when the document is compiled, it removes the valid ld+json scripts from the HTML and adds one single script with @graph at the end. It merges the FAQPage nodes (for example two nc/accordion) and removes identical nodes.

How to use it: automatic, at compile time. The filter nucleo/schema/skip_types receives the list of @type values to exclude.

Parameters: None.

Permissions: None.

Edition: Free.

Known limits:

  • With Yoast SEO or Rank Math active, the default list excludes WebPage, WebSite, Organization and BreadcrumbList. Without an SEO plugin the list is empty.
  • The exclusion applies at compile time. If you activate an SEO plugin later, documents already compiled stay unchanged until the next save.
  • Scripts with invalid JSON, or with a list at the top level, stay where they are.

Company data

What it does: saves the company data in the option nucleo_business and derives from it the schema.org node for the JSON-LD. The node has @id <home>/#business, url defaulting to the home page, address with @type: PostalAddress and geo with @type: GeoCoordinates.

How to use it: key business of nucleo/site-options-set. null deletes the data.

NameTypeAllowed valuesDefaultNotes
namestring1–300 characters—Required.
@typestring^[A-Z][A-Za-z]{2,60}$LocalBusiness
legalName, vatID, taxID, description, telephone, email, priceRangestring1–300 characters—
url, logo, imagestringURL http or httpsurl = home
sameAsarrayURL http or https—A single string is not valid.
addressobjectstreetAddress, addressLocality, addressRegion, postalCode, addressCountry (strings)—Other keys rejected.
geoobjectlatitude (±90) and longitude (±180), numbers—Both required, no other key.
openingHoursarrayMo-Fr 08:00-18:00 (days Mo…Su, hours 00–23)—
foundingDatestringYYYY, YYYY-MM or YYYY-MM-DD—
areaServedstring or array of strings1–300 characters—
other keyscamelCase ^[a-z][A-Za-z]{1,40}$text, list of texts or object of scalars—

Permissions: manage_options. OAuth scope nucleo:admin.

Edition: Free.

Known limits:

  • An invalid value returns invalid_input with the name of the key: "<key>" is unknown or has an invalid value..
  • The node appears only where Nucleo JSON-LD is active, so not with Yoast SEO or Rank Math.

Sitemap

What it does: Nucleo excludes from the WordPress sitemap the posts with meta.seo.noindex set to true. It updates the exclusion on every save and recompilation.

How to use it: set meta.seo.noindex: true in the document.

Parameters: None.

Permissions: None.

Edition: Free.

Known limits:

  • The exclusion applies only to public sitemap requests, and only without Yoast SEO or Rank Math.
  • Nucleo does not generate its own sitemap and does not change robots.txt. The only robots control is the noindex meta of the page.

Coexistence with Yoast SEO and Rank Math

What it does: Nucleo detects the active SEO plugin. With a plugin active:

  • Nucleo does not print title, description, Open Graph or robots;
  • it does not print the page JSON-LD and does not remove the scripts from the content;
  • in the compiled JSON-LD it excludes WebPage, WebSite, Organization and BreadcrumbList;
  • it does not apply the sitemap exclusion;
  • the post checks and the scan read title, description, noindex and canonical from the plugin.

How to use it: automatic.

Parameters: None.

Permissions: None.

Edition: Free.

Known limits: if both plugins are active, Yoast SEO wins.

Esc
↑↓ to select↵ to openEsc to closeLocal search, no data sent