Skip to main content
WireKit
Copy for LLM

Structured Data

Emit safe, valid JSON-LD (<script type="application/ld+json">) from a plain PHP array. Solves the Blade tokenizer footgun where hand-writing JSON-LD with @json([...]) across multiple lines breaks at { / }, and bakes in the JSON_HEX_TAG flag so user-controlled string values cannot break out of the script block.

Basic Usage

<x-wirekit::structured-data> renders nothing visible. It injects a <script type="application/ld+json"> block at the spot you mount it — typically inside the layout's <head>. The previews on this page intentionally pair the Blade input with the resulting <script> output so you can see exactly what the component emits.

WebSite schema — rendered demonstration

Blade input:

<x-wirekit::structured-data :data="[
    '@context' => 'https://schema.org',
    '@type' => 'WebSite',
    'url' => 'https://wirekit.app',
    'name' => 'WireKit',
]" />

Rendered output (JSON-LD <script> block):

<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","url":"https://wirekit.app","name":"WireKit"}</script>

Schema Examples

Article schema

<x-wirekit::structured-data :data="[
    '@context' => 'https://schema.org',
    '@type' => 'Article',
    'headline' => 'WireKit — Developer polish',
    'author' => [
        '@type' => 'Organization',
        'name' => 'Pushery',
    ],
    'datePublished' => '2026-04-25',
    'image' => 'https://wirekit.app/og.png',
]" />

Rendered output:

<script type="application/ld+json">{"@context":"https://schema.org","@type":"Article","headline":"WireKit — Developer polish","author":{"@type":"Organization","name":"Pushery"},"datePublished":"2026-04-25","image":"https://wirekit.app/og.png"}</script>

Organization schema with social profiles

<x-wirekit::structured-data :data="[
    '@context' => 'https://schema.org',
    '@type' => 'Organization',
    'name' => 'WireKit',
    'url' => 'https://wirekit.app',
    'logo' => 'https://wirekit.app/logo.svg',
    'sameAs' => [
        'https://github.com/pushery/wirekit',
        'https://packagist.org/packages/pushery/wirekit',
    ],
]" />

Rendered output:

<script type="application/ld+json">{"@context":"https://schema.org","@type":"Organization","name":"WireKit","url":"https://wirekit.app","logo":"https://wirekit.app/logo.svg","sameAs":["https://github.com/pushery/wirekit","https://packagist.org/packages/pushery/wirekit"]}</script>

Security

Every string value is encoded with JSON_HEX_TAG, which converts < and > into < and >. This means a user-controlled value containing </script> cannot break out of the JSON-LD block — a real XSS vector that hand-rolled @json() does NOT defend against.

The component also passes JSON_UNESCAPED_SLASHES (URLs stay readable: https://example.com instead of https:\/\/example.com) and JSON_UNESCAPED_UNICODE (native characters: Café instead of Café). Slashes are safe because JSON_HEX_TAG already neutralizes the < that would be needed for tag injection.

Pairing with Page Layouts

Mount one or more structured-data blocks in your layout's <head> — typically alongside the <title> and meta tags:

<head>
    <title>{{ $title }}</title>
    <meta name="description" content="{{ $description }}">

    <x-wirekit::structured-data :data="$websiteSchema" />
    <x-wirekit::structured-data :data="$breadcrumbSchema" />
</head>

Validation

Test your output with Google's Rich Results Test or Schema.org Validator — paste the rendered URL and confirm the JSON-LD is detected with no errors.

Typed builders

Schema is a set of typed JSON-LD builders for your app — you call them directly, wherever the data lives. (One of them, faqPage, can additionally be fed by the faq component; that is a convenience on top, not what the builders are for.)

Hand-writing schema.org arrays is where rich results go to die: a price with a currency symbol, an availability written as a bare word, a rating with no count. The Schema builders encode those rules once.

use Pushery\WireKit\Schema\Schema;
{{-- 1. Fragments nest — the context is stamped once, at the top --}}
<x-wirekit::structured-data :data="Schema::product(
    name: $product->name,
    description: $product->summary,
    image: $product->image_url,
    brand: 'Acme',
    offers: Schema::offer(
        price: $product->price,
        priceCurrency: 'EUR',
        availability: 'InStock',
    ),
    aggregateRating: Schema::aggregateRating(
        ratingValue: $product->rating_avg,
        reviewCount: $product->rating_count,
    ),
)" />
Builder Emits
Schema::product(...) Product — nests offers, aggregateRating, review
Schema::offer(...) Offer — price as a bare string, availability expanded to its URL
Schema::aggregateRating(...) AggregateRatingreviewCount is required for a rich result
Schema::review(...) Review — nests the author and the rating
Schema::faqPage(...) FAQPage — from a question => answer map or a list of rows
Schema::article(...) Article
Schema::breadcrumbList(...) BreadcrumbList — positions numbered for you
Schema::webSite(...) WebSite — the site itself
Schema::organization(...) Organization — who is behind it, with sameAs profiles
Schema::softwareApplication(...) SoftwareApplication — a library, app or SaaS product
Schema::node(...) Any schema.org type, unmodeled
Schema::graph(...) Combines several nodes into one @graph

Every builder takes an extra: array for properties it does not model — and those override the modeled ones, so you are never boxed in.

Describing the site itself

A marketing page usually describes more than one thing: the site, the organization behind it, and the product it offers. graph() combines them into one connected document instead of a pile of separate scripts, and @id lets the nodes reference each other:

<x-wirekit::structured-data :data="Schema::graph([
    Schema::webSite(
        name: 'Acme',
        url: 'https://acme.test',
        id: 'https://acme.test/#site',
        publisher: ['@id' => 'https://acme.test/#org'],
    ),
    Schema::organization(
        name: 'Acme Inc',
        url: 'https://acme.test',
        id: 'https://acme.test/#org',
        logo: 'https://acme.test/logo.png',
        sameAs: ['https://github.com/acme'],
    ),
    Schema::softwareApplication(
        name: 'Acme CLI',
        applicationCategory: 'DeveloperApplication',
        operatingSystem: 'Any',
        isAccessibleForFree: true,
    ),
])" />

The context is stamped once at the top of the @graph, never on the individual nodes.

Any other type

node() is the escape hatch: it builds any schema.org type with the same null-pruning and the same @type-first shape, so you never have to wait for a builder to exist.

<x-wirekit::structured-data :data="Schema::node('Event', [
    'name' => 'Acme Conf',
    'startDate' => '2026-09-01T09:00:00+02:00',
    'location' => Schema::node('Place', ['name' => 'Berlin']),
])" />

What the builders fix for you

  • Price is a string, with no symbol and no separator. price: 49.9 emits "49.9""€49,90" is the single most common reason a Product rich result is rejected.
  • Availability is a URL, not a word. availability: 'InStock' emits https://schema.org/InStock. Passing the full URL also works.
  • Null properties are dropped. An empty key is worse than a missing one.
  • The context is stamped exactly once, at the top level — so nesting an Offer inside a Product needs no context-stripping.

Only describe what the page actually shows. Structured data that claims content the visitor cannot see is a Google policy violation and gets the rich result withdrawn — permanently, for the whole site in bad cases. Emit an FAQPage only where the questions are really rendered, an AggregateRating only where the rating really is on screen.

Props

Prop Type Default Description
data array [] Associative array serialized to JSON-LD. Nested arrays and objects are supported.

Keyboard Interaction

This component is purely presentational and does not respond to keyboard input.

See Also