News: redaktionell eingefuegtes FAQPage-JSON als echtes Schema ausgeben
17 SEO-Artikel tragen ein rohes FAQPage-JSON-Objekt in einem abschliessenden Absatz ihres Bodytexts - das Frontend renderte es als sichtbaren Text, Google sah es nie als strukturierte Daten. Neue Klasse Seo\EmbeddedJsonLd trennt solche schema.org-Bloecke aus dem RTE-HTML (fail-safe: was nicht sauber dekodiert, bleibt stehen): - NewsJsonRenderer: Detail-bodytext und Listen-Exzerpt ohne die Bloecke - PageJsonLdRenderer: die dekodierten Nodes wandern in den @graph der Seite (FAQPage landet damit als <script type=application/ld+json> im Head, den das Frontend bereits injiziert) - VitecMetaHandler: Description-Fallback aus dem Bodytext ohne JSON-Muell Verifiziert an 3 Artikeln: @graph mit NewsArticle + FAQPage (8-9 Fragen), kein FAQPage-Rest im Textkoerper, kein sichtbarer JSON-Absatz im HTML. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c9b00e6f10
commit
5c640d02f7
4 files changed
+71
-2
No files matched your search
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Seo;
|
||||
|
||||
/**
|
||||
* Splits editor-pasted schema.org JSON blocks out of RTE HTML.
|
||||
*
|
||||
* The SEO-maintained news articles carry a raw FAQPage JSON object in a
|
||||
* trailing <p>…</p> of their bodytext (found 2026-11-10: 17 records). Left
|
||||
* in place, the frontend renders the JSON as visible text while Google
|
||||
* never sees it as structured data. This helper removes such paragraphs
|
||||
* from the HTML and returns the decoded nodes, ready to join the page's
|
||||
* JSON-LD @graph (their own @context is dropped - the graph carries one).
|
||||
*
|
||||
* Fail-safe: a paragraph that merely looks like JSON but does not decode
|
||||
* to a schema.org node stays in the HTML untouched.
|
||||
*/
|
||||
final class EmbeddedJsonLd
|
||||
{
|
||||
/**
|
||||
* @return array{html: string, nodes: list<array<string,mixed>>}
|
||||
*/
|
||||
public static function extract(string $html): array
|
||||
{
|
||||
if ($html === '' || !str_contains($html, 'schema.org')) {
|
||||
return ['html' => $html, 'nodes' => []];
|
||||
}
|
||||
|
||||
$nodes = [];
|
||||
$clean = preg_replace_callback(
|
||||
'#<p[^>]*>\s*(\{.+?\})\s*</p>#s',
|
||||
static function (array $match) use (&$nodes): string {
|
||||
$candidate = $match[1];
|
||||
if (!str_contains($candidate, 'schema.org')) {
|
||||
return $match[0];
|
||||
}
|
||||
$decoded = json_decode($candidate, true);
|
||||
if (!is_array($decoded)) {
|
||||
// RTE processing may have entity-encoded the quotes.
|
||||
$decoded = json_decode(html_entity_decode($candidate, ENT_QUOTES | ENT_HTML5), true);
|
||||
}
|
||||
if (!is_array($decoded) || empty($decoded['@type'])) {
|
||||
return $match[0];
|
||||
}
|
||||
unset($decoded['@context']);
|
||||
$nodes[] = $decoded;
|
||||
return '';
|
||||
},
|
||||
$html
|
||||
) ?? $html;
|
||||
|
||||
return ['html' => trim($clean), 'nodes' => $nodes];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user