# Structured Data in 2025: Schema Markup That Actually Wins Rich Results (With Real Laravel Examples)

> Source: <https://dev.to/emongmarcc/structured-data-in-2025-schema-markup-that-actually-wins-rich-results-with-real-laravel-examples-1hj0>
> Published: 2026-09-29 01:00:05+00:00

Structured data is one of those topics that developers treat as an afterthought — slap some JSON-LD on the page, validate it in Rich Results Test, and call it done. That approach worked fine in 2019. In 2025, Google's crawlers are more selective, AI-driven search summaries are pulling from schema directly, and the gap between "valid" markup and "ranking" markup has never been wider.

This article is about closing that gap. We'll look at which schema types actually drive measurable SERP features, common implementation mistakes that silently kill eligibility, and how to generate and validate structured data programmatically in a Laravel application.

The common failure mode isn't invalid JSON-LD — validators catch that. The real problem is *incomplete* or *contextually weak* schema. Google's documentation is explicit: required properties get you eligibility; recommended properties get you prominence. Most developers stop at required.

Take `Article` schema. The minimum spec asks for `headline`, `image`, and `datePublished`. But if you want your article appearing in Top Stories or Google Discover, you also need `author` with a valid `Person` entity, `publisher` with a `logo` that meets dimension specs, and `dateModified` to signal freshness. Omitting those doesn't break validation — it just quietly removes you from contention.

Another silent killer: schema that doesn't match the visible page content. Google cross-references your markup against what users actually see. A `Product` schema with a price that doesn't appear in the rendered HTML — or a `Review` aggregate that differs from the on-page star display — will get ignored or, worse, flagged as manipulative.

`Article` / `BlogPosting`
Essential for content-heavy sites. Use `BlogPosting` (a subtype of `Article`) for blog content; it signals intent to crawlers. Key recommended properties you shouldn't skip:

```
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Your Post Title Here",
  "image": [
    "https://example.com/photos/1x1/photo.jpg",
    "https://example.com/photos/4x3/photo.jpg",
    "https://example.com/photos/16x9/photo.jpg"
  ],
  "datePublished": "2025-01-15T08:00:00+04:00",
  "dateModified": "2025-06-10T09:20:00+04:00",
  "author": {
    "@type": "Person",
    "name": "Your Name",
    "url": "https://example.com/authors/your-name"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Your Brand",
    "logo": {
      "@type": "ImageObject",
      "url": "https://example.com/logo.png",
      "width": 600,
      "height": 60
    }
  },
  "description": "A concise summary that matches your meta description."
}
```

Note the three image aspect ratios. Google recommends providing all three (1:1, 4:3, 16:9) so the crawler can select the most appropriate one per context.

`FAQPage`
FAQ schema still earns expandable rich results in desktop SERPs and is increasingly used by AI Overviews to surface direct answers. The catch: questions and answers must be *verbatim* visible on the page. Don't generate FAQ schema from a database if the content isn't rendered in HTML.

`Product` with `Offer` and `AggregateRating`
For e-commerce, completeness here is commercially significant. An `Offer` without `availability` or `priceCurrency` gets deprioritised for Shopping graph features. `AggregateRating` requires both `ratingValue` and `reviewCount` — and both must reflect actual visible content.

Hard-coding JSON-LD in Blade templates is fine for static pages but doesn't scale. Here's a clean approach using a dedicated service class.

``` php
<?php

namespace App\Services\Seo;

class SchemaBuilder
{
    public function blogPosting(array $data): string
    {
        $schema = [
            '@context' => 'https://schema.org',
            '@type'    => 'BlogPosting',
            'headline' => $data['title'],
            'image'    => $data['images'] ?? [],
            'datePublished' => $data['published_at']->toIso8601String(),
            'dateModified'  => $data['updated_at']->toIso8601String(),
            'author' => [
                '@type' => 'Person',
                'name'  => $data['author_name'],
                'url'   => $data['author_url'],
            ],
            'publisher' => [
                '@type' => 'Organization',
                'name'  => config('app.name'),
                'logo'  => [
                    '@type'  => 'ImageObject',
                    'url'    => asset('images/logo-schema.png'),
                    'width'  => 600,
                    'height' => 60,
                ],
            ],
            'description' => $data['excerpt'],
        ];

        return json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
    }

    public function faqPage(array $questions): string
    {
        $schema = [
            '@context'   => 'https://schema.org',
            '@type'      => 'FAQPage',
            'mainEntity' => array_map(fn($q) => [
                '@type'          => 'Question',
                'name'           => $q['question'],
                'acceptedAnswer' => [
                    '@type' => 'Answer',
                    'text'  => $q['answer'],
                ],
            ], $questions),
        ];

        return json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
    }
}
```

In your Blade layout:

```
@stack('schema')
```

And in specific page views:

``` php
@push('schema')
<script type="application/ld+json">
{!! $schemaBuilder->blogPosting($post->toSchemaArray()) !!}
</script>
@endpush
```

This keeps schema generation testable, reusable, and away from your view layer logic. You can write unit tests against `SchemaBuilder` methods and assert that required keys are always present before anything hits production.

The Rich Results Test tells you if your schema is *valid*. It doesn't tell you if you're *eligible*. Use it, but don't stop there.

**Search Console's Rich Results report** is the ground truth. It shows actual impressions and clicks from rich results, and — critically — it surfaces warnings that the Rich Results Test misses, like schema detected on pages with thin content or low crawl priority.

**Schema Markup Validator** (`validator.schema.org`) runs a stricter conformance check than Google's tool and often catches type mismatches.

**Screaming Frog's structured data extraction** lets you audit schema across your entire site at once — invaluable when you have hundreds of product or blog pages and need to verify consistency programmatically.

For teams building content-heavy applications, automated schema auditing should be part of your CI/CD pipeline, not a quarterly manual task. Parsing rendered HTML with something like `symfony/dom-crawler` and asserting schema presence on key page types is straightforward to implement.

One underappreciated dimension: schema isn't just about individual pages. Google builds an entity graph, and your markup contributes to it. Consistent use of `sameAs` properties — pointing author entities to verified profiles like LinkedIn or Wikipedia, and organisation entities to Wikidata or Crunchbase — strengthens entity disambiguation and can improve how your content is attributed in AI-generated summaries.

This is an area that the team at [HanzWeb](https://hanzweb.ae/blog) writes about in depth — particularly how entity signals interact with local SEO for businesses operating in competitive markets like the UAE.

Structured data done right is an investment in discoverability infrastructure — not just a checkbox. The implementation effort is low; the discipline to keep it accurate, complete, and content-matched is where most projects fall short. Start with a schema service class, write tests for required property completeness, monitor Search Console's rich results report weekly, and treat schema updates as a first-class concern whenever content models change. The SERP real estate you gain is genuinely free — but only if the crawler trusts what you've marked up.
