AEO + SEO for SaaS

How to Add FAQ and Article Schema to Laravel Blade for AEO (Working Code)

Working Laravel Blade code for Article, FAQPage, and BreadcrumbList schema, the FAQ parser we use at Growth Pigeon, the five schema mistakes that kill citations, and the testing workflow.

How to Add FAQ and Article Schema to Laravel Blade for AEO (Working Code)
Start with the pillar
AEO for SaaS Founders: Get Cited by ChatGPT, Perplexity, and Gemini

Why schema markup matters more for AI than it ever did for Google

Google deprecated FAQ rich results in 2023. Most SaaS founders read that headline and ripped the schema out of their Blade templates. That was a mistake. HubSpot's 2026 study of 10,000 URLs across 8 AI engines found that pages with FAQ schema get +24 lift on AI Overview citations and meaningful lift across Perplexity and Gemini. The carousel went away. The citation signal did not.

FAQ schema, Article schema, and BreadcrumbList in JSON-LD are now three of the highest-leverage 30-minute changes a SaaS founder can make. This guide gives you the exact Blade code we use on every article at Growth Pigeon, the parser that pulls FAQs out of arbitrary article HTML, and the gotchas that make Rich Results Test red.

If you have not already, read the AEO pillar first for context on why citations matter and what they replace.

The three schemas every SaaS article page should emit

You need three blocks of JSON-LD in your <head>. Article schema for the post itself. FAQPage schema for any question-and-answer section. BreadcrumbList schema for the hub-and-spoke navigation. Each one signals a different thing to AI engines.

Article schema: the page identity

Article schema tells AI engines what the page is, who wrote it, when it was published, and when it was last updated. The last two fields matter most. AI-cited content is roughly 25% more recent than Google-cited content. A visible "Last updated" date plus a dateModified in the schema is the single biggest recency signal you can ship.

Here is the minimum viable Article block in a Blade template:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "{{ route('view_article', ['slug' => $article->slug]) }}"
  },
  "headline": "{{ $article->title }}",
  "description": "{{ $article->short_desc }}",
  "image": "{{ $imageUrl }}",
  "author": {
    "@type": "Person",
    "name": "Mark Ashworth",
    "url": "https://growthpigeon.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "GrowthPigeon"
  },
  "datePublished": "{{ $article->created_at }}",
  "dateModified": "{{ $article->updated_at }}"
}
</script>

Two non-obvious things. The "headline" field has a 110-character soft limit. Longer headlines do not break the schema but some validators warn. Keep titles under 100 characters when possible. The "image" field should resolve to an absolute URL with a 1.91:1 aspect ratio for OG compatibility. If your image is missing the schema is still valid, but you lose the rich result.

FAQPage schema: the citation magnet

FAQ schema is the highest-impact AEO addition you can make. Every question becomes a self-contained citable unit. AI engines pull them verbatim. The mistake people make is hardcoding the same five FAQs on every page. LLMs notice that and discount the signal. Each article needs FAQs matched to that specific article's intent.

The cleanest pattern: write FAQs as part of the article body in normal HTML (with an <h2>FAQ</h2> section), then parse them server-side and emit JSON-LD from the same source. This is what we do at Growth Pigeon. One source of truth, two outputs.

Here is the parser. It lives at the top of the Blade template:

@php
$faqPairs = [];
$rawHtml = $article->description ?? '';

if (preg_match(
    '/<h2[^>]*>\s*(?:faq|faqs|frequently\s+asked\s+questions)\s*<\/h2>(.*?)(?=<h2|\z)/is',
    $rawHtml,
    $faqBlock
)) {
    $section = $faqBlock[1];
    if (preg_match_all(
        '/<h3[^>]*>(.*?)<\/h3>\s*((?:(?!<h3)[\s\S])*?)(?=<h3|\z)/i',
        $section,
        $matches,
        PREG_SET_ORDER
    )) {
        foreach ($matches as $m) {
            $q = trim(strip_tags($m[1]));
            $aText = trim(strip_tags(preg_replace('/\s+/', ' ', $m[2])));
            if ($q !== '' && $aText !== '') {
                $faqPairs[] = ['q' => $q, 'a' => $aText];
            }
        }
    }
}
@endphp

And the schema emitter:

@if(!empty($faqPairs))
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    @foreach($faqPairs as $i => $pair)
    {
      "@type": "Question",
      "name": {!! json_encode($pair['q']) !!},
      "acceptedAnswer": {
        "@type": "Answer",
        "text": {!! json_encode($pair['a']) !!}
      }
    }@if($i < count($faqPairs) - 1),@endif
    @endforeach
  ]
}
</script>
@endif

The reason for json_encode rather than e() or string concatenation: question text often contains quotes, apostrophes, and special characters that break JSON if you escape them with HTML entities. json_encode handles unicode and control characters correctly. e() does not.

BreadcrumbList: the cluster signal

BreadcrumbList schema tells AI engines where the article sits in your topical hierarchy. If you are running a hub-and-spoke structure (and you should be), the breadcrumb should include the hub level: Home, Articles, Hub, Article. Not just Home, Articles, Article. The extra level signals topical clustering.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://growthpigeon.com/" },
    { "@type": "ListItem", "position": 2, "name": "Articles", "item": "https://growthpigeon.com/articles" },
    { "@type": "ListItem", "position": 3, "name": {!! json_encode($clusterLabel) !!}, "item": "https://growthpigeon.com/articles#{{ $article->topic_cluster }}" },
    { "@type": "ListItem", "position": 4, "name": {!! json_encode($article->title) !!}, "item": "{{ route('view_article', ['slug' => $article->slug]) }}" }
  ]
}
</script>

Common schema mistakes that kill the citation signal

The schema is either valid or it is not. Half-right schema sometimes returns no warnings in Rich Results Test but gets ignored by AI engines anyway. Five mistakes show up over and over.

Mistake 1: FAQ schema that does not match visible content

Google's policy (which AI engines inherited) requires FAQPage schema content to match what users see on the page. If your schema lists five questions but the page shows three, the page gets flagged. Worse, AI engines learn to distrust the source. The parser-based approach above avoids this because the schema is generated from the same HTML the user reads.

Mistake 2: Inline HTML inside the answer field

JSON-LD "text" fields should be plain text. If you paste raw HTML with <p>, <a>, or <strong> tags in there, validators allow it but most AI engines strip the tags and sometimes mangle the content. The parser above uses strip_tags() before assigning to the answer field. Do not skip that.

Mistake 3: Forgetting to escape quotes

If a question contains a quote or apostrophe and you build the JSON manually with string concatenation, the resulting JSON is invalid. Validators throw "Invalid JSON" errors. AI engines drop the schema entirely. Always use json_encode in PHP, JSON.stringify in JavaScript, or your framework's equivalent. Never hand-build JSON-LD.

Mistake 4: Missing dateModified or stale dateModified

Article schema without dateModified loses the recency signal that drives AI citations. Worse: setting dateModified to a hardcoded date or to created_at means every article looks stale forever. Wire it to the updated_at column in your articles table and update that column whenever you re-edit. We re-stamp every article on at least a quarterly cadence and our citation rate improved noticeably after we started doing this.

Mistake 5: Author with no Person schema and no /about URL

"author": "John Smith" is technically valid but signals nothing. Use the Person object with a name and a URL pointing to a real author bio page. Bonus: include sameAs with social URLs. AI engines weight verifiable authorship. Anonymous content gets discounted.

How to test your schema before deploying

Three tools, in order of usefulness for AEO specifically.

Google Rich Results Test

Go to search.google.com/test/rich-results and paste your URL. This catches structural errors (missing required fields, wrong types) but it does not test how AI engines parse the content. Pass this first. It is necessary but not sufficient.

Schema.org Validator

validator.schema.org gives stricter validation than Google's tool. It will flag fields that are technically allowed but semantically wrong. Use it as a second pass.

Manual AI engine testing

After deploying, open ChatGPT or Perplexity and ask a question your article answers. Specifically ask about something covered in your FAQ. If your article gets cited within a week or two, the schema is working. If not, check the schema, check whether the article is indexed in Bing (ChatGPT pulls from Bing for browse mode), and check recency.

What changed on Growth Pigeon after we added all three schemas

We added FAQ schema and breadcrumb schema to every article in May 2026. Article schema was already in place. Within four weeks Perplexity started citing our positioning grader article in roughly 8% of relevant queries. Before the schema change it was 0%. ChatGPT citations are harder to track but we saw a similar pattern in browse mode. Gemini results were mixed and likely take longer because Google's indexer needs time to refresh.

The biggest single change was adding the visible "Last updated" line. We had dateModified in the schema for months but no human-readable date on the page. Adding a one-line "Last updated: 12 May 2026" under the byline correlated with a meaningful bump in Perplexity citations. The on-page date is what visitors see and what AI tools cross-reference against the schema.

FAQ

Do I need both Article schema and FAQPage schema on the same page?

Yes. They describe different things. Article schema describes the page as a whole. FAQPage describes the question-and-answer section. Both can coexist on one page and AI engines use both. The recommended approach is to use multiple JSON-LD blocks rather than trying to combine them.

Will FAQ schema break my Google rankings?

No. Google deprecated the FAQ rich result (the dropdown carousel in SERPs) in 2023. The schema itself is still parsed and used. There is no ranking penalty for having FAQ schema. The myth that schema "doesn't work anymore" comes from the lost carousel, not from any ranking change.

Does HowTo schema help for AEO?

For step-by-step tutorial content, yes. Add HowTo schema in addition to Article schema. AI engines pull cited steps from HowTo blocks for instructional queries. The implementation is similar: a totalTime field, a list of HowToStep objects, and optional HowToTool or HowToSupply objects. Use it when the article is genuinely a tutorial. Do not force HowTo schema onto opinion or analysis pieces.

How often should I update dateModified?

Whenever the article content meaningfully changes. Fixing a typo does not count. Adding new data, updating a stat, or rewriting a section does. We re-review every article on a quarterly cadence. Articles that get updated quarterly perform meaningfully better in Perplexity than articles that are static.

Should the schema URL match the canonical URL?

Yes. The mainEntityOfPage @id field in Article schema should match the canonical link tag exactly. Mismatches cause validators to warn and AI engines to deprioritize the source. Use the same Laravel route() call in both places.

What if my article has multiple FAQ sections?

Adjust the parser to find all <h2>FAQ</h2> blocks rather than just the first. Or restructure the article to have one FAQ section near the end. Single FAQ section is the convention and the easier signal for AI engines to parse.

Do I need to escape HTML entities in the JSON-LD output?

No. JSON-LD inside a script tag is parsed as JSON, not HTML. Use json_encode to handle JSON escaping. Do not use htmlspecialchars or e() on the content because that converts apostrophes and quotes to &#39; and &quot;, which then appear literally in the AI engine's citation.

What to ship this week

If your SaaS site has more than five articles and none of them have schema, you are leaving citations on the table. Three moves, in order:

  1. Add the Article schema block to your article template. Verify with Rich Results Test. That alone takes 30 minutes and pays off for years.
  2. Pick your top three articles. Add a FAQ section with five real questions each. Add the FAQPage parser and emitter. Re-deploy.
  3. Add a visible "Last updated" line under the byline. Wire it to updated_at. Re-stamp the date when you do meaningful edits.

For the wider context on AEO strategy and which content types actually get cited, read the AEO for SaaS Founders pillar. To see whether your homepage is positioned to actually convert the AEO traffic you earn, run it through the free positioning grader.

Stop Guessing Your Growth Lever

Get a 48-Hour Growth Clarity Map — a one-page teardown that finds what’s blocking your next 10 → 100 sales. Delivered in 48 hours with actionable next steps.

Get Your Growth Clarity Map → $37

Delivered in 48 hours. 100% satisfaction or your money back.

First Published:

Curious how your homepage scores? Grade it free →

Want Clear Messaging?

Get a Growth Clarity Map ($37, delivered in 48h) or a full 7-Day Growth Sprint ($249) and find the lever behind your next 10 → 100 sales.

Get the $37 Map → See the $249 Sprint