feat: body links, outbound rules and image attributes in the post body
- renderJournalBody runs the UI-SPEC L7 order on the sanitized body: h1 demotion, the CMS phrase auto-links, the outbound rules and lazy, async images, with vue-seo-layer's autolink, applyOutbound and imageAttrs; PostBody feeds it the page's resolve answer (autolinks, autolink_max, outbound, url) and the seo.links.newTabHint option - the sanitizer keeps a link rel of only nofollow, sponsored and ugc (the Blog Editor's link form, as the Journal plugin's policy does); target is never kept, the outbound rules add it - editor-corpus.json copied again from sm-journal-plugin (link-rel and image-decorative entries) - smoke:ssr script; expectations for auto-links, both new-tab modes and the hint switched off
This commit is contained in:
5
testing/expect-acme-hint-off.txt
Normal file
5
testing/expect-acme-hint-off.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# SSR expectations with the SEO-layer option seo.links.newTabHint off
|
||||
# (NUXT_PUBLIC_SEO_LINKS_NEW_TAB_HINT=false): outbound links still open in a
|
||||
# new tab with noopener, without the visually hidden hint (UI-SPEC L7).
|
||||
200 /blog/hello-acme contains=<a href="https://partner.test/review" rel="ugc nofollow noopener" target="_blank">the review</a> notcontains=(opens in a new tab)
|
||||
200 /blog/hello-acme contains=<a href="https://shop.ads.test/deal" target="_blank" rel="sponsored noopener">a deal</a>
|
||||
@@ -75,3 +75,15 @@
|
||||
# The header box renders its no-JS form to the search route on every page.
|
||||
200 /blog contains=action="/search"
|
||||
200 /pl/blog contains=action="/pl/search"
|
||||
|
||||
# Body links, outbound rules and images (UI-SPEC L7). hello-acme: new tab on
|
||||
# with the default hint; auto-links in document order up to autolink_max 2,
|
||||
# never in a heading, never to the page itself.
|
||||
200 /blog/hello-acme contains=<a href="/blog/acme-note-07">anvils</a> contains=<a href="/blog/acme-note-05">rockets</a> notcontains=<a href="/blog/acme-note-03"> notcontains=<a href="/blog/acme-note-06"> notcontains=<a href="https://acme.test/blog/hello-acme"
|
||||
200 /blog/hello-acme contains=<a href="https://partner.test/review" rel="ugc nofollow noopener" target="_blank">the review<span class="seo-visually-hidden"> (opens in a new tab)</span></a>
|
||||
200 /blog/hello-acme contains=<a href="https://shop.ads.test/deal" target="_blank" rel="sponsored noopener">a deal<span class="seo-visually-hidden"> (opens in a new tab)</span></a>
|
||||
200 /blog/hello-acme contains=<a href="/blog/acme-note-04">an older note</a>
|
||||
200 /blog/hello-acme contains=<img src="/storage/app/uploads/public/acme/anvil.png" alt="" width="640" height="480" loading="lazy" decoding="async" />
|
||||
# heading-off: new tab off keeps the editor rel and the domain-list nofollow,
|
||||
# without target, noopener or the hint.
|
||||
200 /blog/heading-off contains=<a href="https://partner.test/x" rel="ugc nofollow">the partner</a> contains=<a href="https://other.test/y">another site</a> notcontains=target="_blank" notcontains=noopener notcontains=(opens in a new tab)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"slug": "hello-acme",
|
||||
"excerpt": "Acme says hello and introduces its blog.",
|
||||
"content": "## Welcome to Acme\n\nAcme builds anvils, rockets and very fast roller skates.\n",
|
||||
"content_html": "<h2>Welcome to Acme</h2>\n<p>Acme builds anvils, rockets and very fast roller skates.</p>\n<figure data-embed=\"youtube\" data-id=\"dQw4w9WgXcQ\"></figure>\n",
|
||||
"content_html": "<h2>Welcome to Acme</h2>\n<p>Acme builds anvils, rockets and very fast roller skates.</p>\n<figure data-embed=\"youtube\" data-id=\"dQw4w9WgXcQ\"></figure>\n<p>Read <a href=\"https://partner.test/review\" rel=\"ugc\">the review</a>, <a href=\"https://shop.ads.test/deal\">a deal</a> and <a href=\"/blog/acme-note-04\">an older note</a>.</p>\n<p><img src=\"/storage/app/uploads/public/acme/anvil.png\" alt=\"\" width=\"640\" height=\"480\"></p>\n",
|
||||
"reading_time": 1,
|
||||
"published": true,
|
||||
"published_at": "2026-01-14T09:00:00+00:00",
|
||||
@@ -505,7 +505,7 @@
|
||||
"slug": "heading-off",
|
||||
"excerpt": "Heading Off Sample shows the Journal H1 setting off.",
|
||||
"content": null,
|
||||
"content_html": "<h1>Body heading one</h1>\n<p>The body opens with its own top heading.</p>\n<h2>Body heading two</h2>\n<p>And continues below it.</p>\n",
|
||||
"content_html": "<h1>Body heading one</h1>\n<p>The body opens with its own top heading.</p>\n<h2>Body heading two</h2>\n<p>And continues below it.</p>\n<p>See <a href=\"https://partner.test/x\" rel=\"ugc\">the partner</a> and <a href=\"https://other.test/y\">another site</a>.</p>\n",
|
||||
"reading_time": 1,
|
||||
"published": true,
|
||||
"published_at": "2025-12-19T09:00:00+00:00",
|
||||
|
||||
@@ -71,7 +71,39 @@
|
||||
"url": "https://acme.test/blog/hello-acme",
|
||||
"path": "/blog/hello-acme"
|
||||
}
|
||||
]
|
||||
],
|
||||
"autolinks": [
|
||||
{
|
||||
"phrase": "Acme",
|
||||
"url": "https://acme.test/blog/hello-acme"
|
||||
},
|
||||
{
|
||||
"phrase": "Welcome",
|
||||
"url": "/blog/acme-note-06"
|
||||
},
|
||||
{
|
||||
"phrase": "roller skates",
|
||||
"url": "/blog/acme-note-03"
|
||||
},
|
||||
{
|
||||
"phrase": "rockets",
|
||||
"url": "/blog/acme-note-05"
|
||||
},
|
||||
{
|
||||
"phrase": "anvils",
|
||||
"url": "/blog/acme-note-07"
|
||||
}
|
||||
],
|
||||
"autolink_max": 2,
|
||||
"outbound": {
|
||||
"new_tab": true,
|
||||
"nofollow_domains": [
|
||||
"partner.test"
|
||||
],
|
||||
"sponsored_domains": [
|
||||
"ads.test"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/blog/heading-on": {
|
||||
"action": "ok",
|
||||
@@ -219,7 +251,14 @@
|
||||
"url": "https://acme.test/blog/heading-off",
|
||||
"path": "/blog/heading-off"
|
||||
}
|
||||
]
|
||||
],
|
||||
"outbound": {
|
||||
"new_tab": false,
|
||||
"nofollow_domains": [
|
||||
"partner.test"
|
||||
],
|
||||
"sponsored_domains": []
|
||||
}
|
||||
},
|
||||
"/blog": {
|
||||
"action": "ok",
|
||||
|
||||
Reference in New Issue
Block a user