Constraints
- Apply
@rules/laravel/laravel.mdcfor routing, Blade, and asset conventions. - Apply
@rules/security/frontend.md— never inject unsanitized user content into JSON-LD, meta tags, or<head>; escape every dynamic value. - Fix technical blockers before content optimization.
- One page maps to one clear primary search intent.
- Every recommendation must be page-specific and implementable. No generic "improve SEO" output.
- Mobile-first: indexing is mobile-first, so audit the mobile render.
- White-hat only: recommend nothing that violates search-engine guidelines (no cloaking, paid links, doorway pages, or scaled spam content).
Use when
- Auditing crawlability, indexability, canonicals, or redirects.
- Improving title tags, meta descriptions, or heading structure.
- Adding or validating structured data (JSON-LD).
- Improving Core Web Vitals (LCP, INP, CLS).
- Doing keyword research and mapping keywords to URLs.
- Planning internal linking, sitemap, or robots changes.
- Comparing against competitors to find content or keyword gaps.
- Assessing content quality and E-E-A-T signals.
- Defining what to measure after changes ship.
Technical SEO
Crawlability
robots.txtallows important pages and blocks low-value surfaces (e.g./admin, filtered listing permutations). In Laravel, serve a staticpublic/robots.txtor a route that renders one per environment (block everything on staging).- No important page is unintentionally
noindex. - Important pages are reachable within a shallow click depth (3 clicks from the homepage).
- Avoid redirect chains longer than two hops. Audit Laravel
Redirect::rules and web-server rewrites for stacked redirects. - Canonical tags are self-consistent and non-looping.
Indexability
- Preferred URL format is consistent (trailing slash, casing,
wwwvs apex). Enforce with a single canonical host and HTTPS redirect. - Generate canonical URLs from named routes, not hand-built strings, so they stay consistent:
route('post.show', $post). - Multilingual pages need correct
hreflangwhen locales exist. - Sitemaps reflect the intended public surface only. Build a sitemap from Laravel routes/models, or use the optional
spatie/laravel-sitemappackage to crawl and emitsitemap.xml. - No duplicate URLs compete without canonical control (e.g. pagination, query-string facets).
Site-health audit checklist
Walk these every audit; each item maps to a concrete file, route, or response to inspect — never report a category without naming where it occurs.
| Check | What to look for | Where in Laravel |
|---|---|---|
| Crawl errors | 4xx/5xx on linked URLs | server logs, route list, php artisan route:list |
| Broken links | dead internal/outbound hrefs | Blade views, content models, a crawl pass |
| Redirect chains | 2+ stacked hops | Redirect:: rules, web-server rewrites |
| Mixed content | http:// assets on an HTTPS page | Blade asset refs; force asset()/secure_url() |
| Duplicate content | same intent on multiple URLs | facets, pagination, casing/slash variants |
| Thin content | low-value near-empty pages | auto-generated tag/archive pages |
| Orphan pages | published but unlinked internally | compare sitemap/routes against internal links |
- Orphan detection: a page in the sitemap or route list with no internal
hrefpointing at it cannot be discovered by crawlers following links — surface it and add an internal link or drop it from the sitemap. - Mixed content: an HTTPS page that loads any
http://asset is downgraded and may be blocked by the browser. Derive asset URLs fromasset()/secure_url()so they inherit the request scheme.
Canonical + meta in a Blade layout
{{-- resources/views/layouts/app.blade.php (inside <head>) --}}
<title>@yield('title', config('app.name'))</title>
<meta name="description" content="@yield('meta_description', '')">
<link rel="canonical" href="{{ $canonical ?? url()->current() }}">
@hasSection('noindex')
<meta name="robots" content="noindex,nofollow">
@endif
@stack('schema')
{{-- a page view --}}
@section('title', $post->title . ' | ' . config('app.name'))
@section('meta_description', Str::limit(strip_tags($post->excerpt), 155))
@php($canonical = route('post.show', $post))
Core Web Vitals
Targets: LCP < 2.5s, INP < 200ms, CLS < 0.1.
- LCP: preload the hero image and critical fonts; ship hashed assets through Vite (
@vite) so they cache long-term. Useloading="eager"andfetchpriority="high"on the LCP image;loading="lazy"on below-the-fold images. - INP: trim heavy JS. Keep Alpine.js components small and defer non-critical Livewire polling. Avoid long synchronous work in event handlers.
- CLS: reserve layout space — set explicit
width/heighton images, and avoid injecting content above existing content. Tailwindaspect-*utilities help reserve space. - Reduce render-blocking work: let Vite split CSS/JS, defer non-critical scripts, and self-host fonts with
font-display: swap.
Structured data (JSON-LD)
Emit JSON-LD in the layout via a pushed stack so each page contributes its own schema. Match schema to content that is actually present.
- Homepage:
Organizationor local business schema where appropriate. - Editorial pages:
Article/BlogPosting. - Product pages:
ProductandOffer. - Interior pages:
BreadcrumbList. - Q&A sections:
FAQPageonly when the content truly matches.
{{-- a page view --}}
@push('schema')
<script type="application/ld+json">
{!! json_encode([
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => $post->title,
'author' => ['@type' => 'Person', 'name' => $post->author->name],
'publisher'=> ['@type' => 'Organization', 'name' => config('app.name')],
'datePublished' => $post->published_at->toIso8601String(),
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
</script>
@endpush
Build the array with json_encode (never string-concatenate user input) so values are escaped and the document stays valid. Validate the emitted markup with Google's Rich Results Test before relying on it.
On-page rules
Title tags
- Aim for roughly 50–60 characters.
- Put the primary keyword or concept near the front.
- Write for humans, not stuffed for bots.
Meta descriptions
- Aim for roughly 120–160 characters.
- Describe the page honestly and include the main topic naturally.
Headings
- Exactly one
H1per page. H2/H3reflect real content hierarchy; do not pick heading levels for visual styling.
Formulas
Title: Primary Topic - Specific Modifier | Brand
Meta: Action + topic + value proposition + one supporting detail
Keyword mapping
- Define the search intent.
- Gather realistic keyword variants.
- Prioritize by intent match, likely value, and competition.
- Map one primary keyword/theme to one URL.
- Detect and avoid cannibalization (two URLs targeting the same intent).
Competitor gap analysis
Use competitors to find concrete, page-level opportunities — never to copy.
- Keyword gaps: queries competitors rank for that the site does not. Map each gap to a new or existing URL, not a generic "write more content" note.
- Content gaps: topics or intents covered by competitors but missing from the site's coverage map.
- SERP-feature gaps: rich results competitors win (FAQ, breadcrumb, review stars) that the site's schema does not yet support.
- Site-structure gaps: shallower paths or clearer internal linking that make competitor pages easier to crawl.
- Output each gap as
[gap type] target query/topic → owning URL → concrete change, so it slots straight into the audit output below.
Content quality and E-E-A-T
Search engines reward Experience, Expertise, Authoritativeness, and Trustworthiness. Tie each signal to something on the page.
- Experience / expertise: show a real, attributed author (
Article.authorin JSON-LD plus a visible byline) and first-hand detail, not paraphrased generalities. - Authoritativeness: cite primary sources and earn relevant links naturally; do not buy or exchange links.
- Trust: keep content accurate and current, show a clear publish/updated date, and make contact / ownership discoverable.
- Helpful-content first: write for the person with the query. Consolidate or differentiate thin near-duplicate pages instead of keeping them for keyword coverage.
Internal linking
- Link from strong pages to pages you want to rank.
- Use descriptive anchor text; avoid generic anchors when a specific one fits.
- Backfill links from new pages to relevant existing ones.
Measurement and reporting
Every recommendation needs a measurable outcome; pick the metrics that match the change and capture a baseline before shipping.
- Visibility: organic traffic, impressions, average position, keyword rankings.
- Engagement / conversion: click-through rate, conversions, assisted conversions.
- Technical health: Core Web Vitals (field data), index coverage, crawl errors.
- Source field data from Search Console and Analytics, not lab numbers alone.
- State the metric, its baseline, and the expected direction with each finding so the change is verifiable after release.
Tooling
Validate audits and findings with real tools rather than asserting them:
- Search Console — index coverage, queries, impressions/CTR, Core Web Vitals field data.
- PageSpeed Insights / Lighthouse — Core Web Vitals diagnostics and lab traces.
- Rich Results Test — confirm JSON-LD is valid and eligible before relying on it.
- A crawler (e.g. Screaming Frog) — broken links, redirect chains, orphan pages, duplicate titles.
- Prefer Laravel-native checks where they suffice:
php artisan route:listfor the public surface, a sitemap build for the intended index set.
Audit output shape
[HIGH] Duplicate title tags on product pages
Location: resources/views/products/show.blade.php
Issue: @section('title') falls back to the app name for every product, weakening relevance and creating duplicate signals.
Fix: Render a unique title from the product name and primary category.
Measure: distinct title coverage in Search Console; expect duplicate-title warnings to drop.
Anti-patterns
| Anti-pattern | Fix |
|---|---|
| keyword stuffing | write for users first |
| thin near-duplicate pages | consolidate or differentiate them |
| schema for content that is not present | match schema to reality |
| advice without checking the actual page | read the real Blade view first |
| generic "improve SEO" output | tie every recommendation to a page or asset |
| hand-built canonical URLs | derive from named routes |
| buying or exchanging links | earn links through useful content |
| recommendation with no metric | state the metric and baseline to verify it |
Done when
- robots.txt, canonicals, and redirects are correct with no unintended
noindex. - The site-health checklist passes: no crawl errors, broken links, mixed content, redirect chains, orphan pages, or thin/duplicate content go unsurfaced.
- Sitemap reflects the public surface and important pages are shallow-depth.
- Each audited page has one H1, a 50–60 char title, and a 120–160 char meta description.
- Valid JSON-LD matches the page's real content and passes the Rich Results Test.
- LCP/INP/CLS targets are met or have a concrete remediation plan.
- Competitor and content gaps are captured as page-level, actionable findings.
- Every finding cites a specific file or URL, a concrete fix, and a metric to verify it.