0% прочитано

Reusable Laravel SEO Architecture for Posts, Pages and PostSeries

SEO in ICanUp started with Posts, but once Pages and PostSeries became full public entities, duplicating meta titles, descriptions, Open Graph, fallbacks, canonical URLs, and hreflang rules across modules became risky. This article explains the reusable SEO layer, localized metadata, polymorphic integration, entity-specific fallbacks, and why shared SEO persistence does not mean identical SEO behavior for every model.

22 вересня 2026 р. 12 хв читанняLaravel

When SEO in ICanUp was needed only for Posts, separate fields and logic did not look like a serious problem.

Then Pages became full public entities. Later PostSeries gained their own public pages, canonical URLs, hreflang, sitemap participation, breadcrumbs, cover images, and editable SEO metadata.

At that point the problem became clear: if every module received its own table, Admin SEO form, resolver, and fallback rules, I would eventually have several similar but increasingly different SEO systems.

I therefore started treating SEO not as a collection of Post-specific fields, but as reusable infrastructure for multiple public entities.

A reusable SEO layer does not mean Post, Page, and PostSeries have identical SEO behavior. Persistence, localization, Admin UI, and base resolution can be shared while domain-specific fallbacks and indexability remain entity responsibilities.
Reusable Laravel SEO architecture connecting Post Page and PostSeries to one shared SEO layer with localized metadata resolver canonical hreflang Open Graph and fallback rules
Post, Page, and PostSeries reuse one SEO infrastructure while each entity keeps its own URL, fallback, and public-visibility contract.

Why Separate SEO Code for Every Laravel Model Becomes a Problem

Three almost identical SEO systems
Post  SEO fields  SEO validation  SEO form  SEO fallback  OG resolver Page  SEO fields  SEO validation  SEO form  SEO fallback  OG resolver PostSeries  SEO fields

The problem is not only code volume.

The more dangerous outcome is that one implementation starts evolving faster than the others.

  • Post supports a new Open Graph fallback while Page still uses older behavior.
  • Page resolves the localized description correctly while PostSeries uses separate logic.
  • One Admin form has character guidance while another validates the same fields differently.
  • One module falls back from an empty SEO title correctly while another renders an empty <title>.
  • A social-image contract change requires changes in several independent implementations.

Duplicated SEO logic is not dangerous while every copy is identical. It becomes dangerous after the first change lands in only one copy.

Reusable SEO Layer: What Should Be Shared

Shared concern

Responsibility

Persistence

Store SEO metadata for different entities

Localization

Separate UK/EN SEO values

Admin block

One reusable metadata editor

Validation

Consistent rules for equivalent fields

Resolver

Explicit value -> fallback

Social metadata

OG/Twitter presentation

Image resolution

Entity image -> global fallback

A Polymorphic SEO Relation Avoids Per-Entity SEO Tables

At the Laravel level, one natural way to represent this architecture is a polymorphic relation.

An SEO record does not belong only to Post or only to Page. It belongs to any entity participating in the shared SEO contract.

PHP - Simplified polymorphic relation
// Simplified architecture example.// Exact class and relation names may differ in the project. public function seo(): MorphOne{    return $this->morphOne(        SeoMetadata::class,        'seoable',    );}
This code demonstrates the architectural shape rather than claiming the exact current ICanUp SEO model, relation, or database column names.

Polymorphic Persistence Should Not Depend on PHP Class Names Forever

If a polymorphic type is persisted, I do not want the database contract tied permanently to a full PHP namespace.

Renaming or moving a model should not require migrating every polymorphic record merely because the PHP namespace changed.

PHP - Stable aliases for polymorphic relations
Relation::enforceMorphMap([    'post' => Post::class,    'page' => Page::class,    'post_series' => PostSeries::class,]);

Localized SEO Must Keep UK and EN Metadata Separate

For a multilingual site, one SEO record is not enough when titles and descriptions are localized.

The Ukrainian page needs Ukrainian metadata, while the English page needs English metadata.

Shared entity relation with locale-specific SEO values
Post / Page / PostSeries        |        vshared SEO record        |        +--> uk metadata        |        +--> en metadata

This matters especially for meta_title, meta_description, Open Graph title and description, and localized social-image alt text when it belongs to the shared SEO contract.

A Reusable Admin SEO Block Removes Form Duplication

Persistence is only half of the problem.

If each Admin form independently renders equivalent SEO fields, character guidance, validation, locale switching, and error behavior quickly begin to drift.

PostSeries therefore did not need a Series-specific SEO editor. It could reuse the standard SEO block already used by other public entities.

One Admin contract for equivalent SEO fields
Post form -------+                 |Page form -------+--> Shared SEO block                 |Series form -----+

Shared Persistence Does Not Mean Identical SEO Fallbacks

This is one of the most important boundaries in the architecture.

The shared SEO layer can know how to read explicit metadata. It should not blindly guess which domain field is the correct fallback for every entity.

Entity

Example fallback

Post

SEO title -> localized Post title

Page

SEO title -> localized Page title

PostSeries

SEO title -> localized Series title

PostSeries description

SEO description -> localized Series description

SEO Resolution Works Better as Explicit Value -> Domain Fallback -> Site Fallback

Predictable fallback chain
meta title explicit localized SEO title        |        ventity localized title        |        vsite fallback if needed  social image SEO/entity image        |        v

This sequence provides two useful properties.

An editor can explicitly override metadata where necessary, while a new page does not lose its title or description merely because the SEO block has not been filled manually yet.

PostSeries Cover Joined the Same Social Image Fallback Chain

When PostSeries gained an optional cover through the shared Media architecture, SEO did not require another dedicated "series social image" field.

The Series cover can participate in the social-image fallback chain, while the global image remains available when no cover exists.

This is a useful example of two reusable layers - Media and SEO - working together without another Series-specific subsystem.

Canonical URLs Should Not Be Arbitrary Stored SEO Fields

In my architecture, canonical URLs are tightly coupled to public routing and the active locale.

Post, Page, and PostSeries each have their own public routes. Canonical URLs are therefore safer when derived from the actual routing infrastructure instead of being arbitrary editor-entered URLs that may later drift from the route.

Canonical comes from routing
entity + locale      |      vlocalized public route      |      vabsolute canonical URL

I covered this multilingual routing contract in more detail in the article about canonical and hreflang URLs in Laravel.

hreflang Also Comes from the Routing Contract, Not Just the SEO Form

The same principle applies to hreflang.

When an entity is available in Ukrainian and English, the SEO layer needs the correct localized public URLs rather than stored copies of those URLs inside metadata.

URL generation remains centralized
UK entity URL    |    +--> hreflang="uk" EN entity URL    |    +--> hreflang="en" default locale URL    |    +--> hreflang="x-default"

I covered the x-default and default-locale case separately in the x-default hreflang article.

Open Graph Type Cannot Be Identical for Post and Page

Another boundary between shared infrastructure and domain semantics is og:type.

A shared renderer can output Open Graph tags, but the type value depends on the page.

Shared renderer with different entity semantics
Post:og:type = article Page:og:type = website PostSeries:og:type = website

SSR Makes the Reusable SEO Layer Visible Without JavaScript

In an SPA-like architecture, generating metadata only after client-side JavaScript is not enough.

The SEO resolver needs to feed the rendering path that creates the initial server HTML.

This is why I verify metadata through the raw HTTP response, not only in DevTools after hydration.

I covered that failure separately in the article about meta tags that existed in the browser but not in server HTML.

SEO Metadata and Indexability Are Different Layers

The existence of an SEO record does not automatically make an entity public or indexable.

A Draft Post may already have a complete meta title. That does not mean it belongs in the sitemap or should become accessible to crawlers.

An inactive PostSeries can also retain metadata while public eligibility remains a separate domain rule.

Metadata does not replace the publication contract
SEO metadata    |    X does not decide publication  Entity public contract    |    +--> route accessibility    +--> robots behavior    +--> sitemap eligibility    +--> indexing notifications

robots noindex Must Not Be the Only Protection for Draft Content

noindex controls crawler indexing behavior, but it is not access control.

If a Draft is not public, an ordinary user must be denied by the public visibility contract regardless of which robots tag the SEO layer generates.

Sitemap Generation Should Not Read Every SEO-Enabled Record

Another possible mistake is building sitemap inclusion around the mere existence of SEO metadata.

The safer direction is the opposite: an entity first passes its public and indexable contract, and only then does SEO infrastructure generate the appropriate URL and metadata for the sitemap.

JSON-LD Should Not Be Forced into One Universal Schema Either

Reusable SEO infrastructure can provide a shared mechanism for attaching JSON-LD to a page.

The Schema.org structure itself still depends on the entity.

A Post can use article-specific structured data, while Page or PostSeries may need different page semantics. BreadcrumbList can meanwhile be generated through a separate reusable breadcrumb contract.

I covered the practical BreadcrumbList implementation separately in the article about Laravel BreadcrumbList JSON-LD.

Reusable Does Not Mean a Generic Bag of Hundreds of Nullable Fields

There is another extreme: trying to predict every possible future SEO field in one universal record.

The result is a large collection of nullable columns used only by one unusual page type.

I prefer a stable shared core and extend the contract only when a capability genuinely becomes reusable.

PostSeries Became a Good Test of the Reusable SEO Architecture

PostSeries became a particularly useful test case for me.

The public Series SEO integration already existed: localized routes, canonical URLs, hreflang, sitemap participation, and breadcrumbs.

When the Admin needed a complete editable SEO block, the right direction was not another Series-specific system but integration with the SEO contract already used by Post and Page.

If adding a new entity requires rebuilding the entire SEO pipeline, the architecture is not truly reusable yet.

Adding Another SEO-Enabled Content Type Should Be Predictable

A new entity does not need a new SEO subsystem
New public entity      |      +--> connect shared SEO persistence      +--> connect localized SEO form      +--> define title/description fallback      +--> define public URL      +--> define OG type      +--> define image fallback      +--> define indexability      +--> add regression tests

Entity and SEO Metadata Should Save as One Transactional Operation

When Admin updates both entity fields and SEO metadata, I do not want a state where the PostSeries title was saved but the SEO write failed halfway through.

The create and update flow should therefore treat those changes as one operation according to the shared service pattern.

PHP - Simplified transactional save
DB::transaction(function () use ($entity, $seoData) {    $entity->save();     $seoService->sync(        $entity,        $seoData,    );});
The seoService name here is intentionally generic. The example demonstrates the transaction boundary rather than claiming the exact current service name.

SEO Tests for Post, Page and PostSeries

Scenario

Expected result

Explicit SEO title

Overrides the entity title

Empty SEO title

Entity fallback is used

UK metadata

Does not mix with EN

EN metadata

Does not mix with UK

Canonical

Matches the localized public route

hreflang

Contains the correct locale URLs

Post OG type

article

Page/Series OG type

website

Entity cover

Used when supported by the contract

No image

Global fallback is used

Draft / inactive

SEO metadata does not make the entity indexable

SSR

Meta tags exist in the initial HTML

Fallback Tests Matter More Than the Fully Populated Happy Path

An SEO form is easy to test when every field is populated.

Real regressions are more likely to appear when some metadata fields are intentionally empty.

  • SEO title is empty while description is explicit.
  • SEO description is empty while title is explicit.
  • The entity has no cover image.
  • UK has explicit SEO while EN uses a fallback.
  • Metadata exists while the entity is still non-public.
  • A changed entity title also changes the resolved fallback.

Admin SEO Preview and Real Public Indexability Must Stay Separate

Allowing an administrator to preview a Draft through the public layout is useful for checking metadata, ContentBlocks, and social presentation before publication.

That access must not change sitemap inclusion, public Search, IndexNow behavior, or ordinary guest accessibility.

Preview is a way to inspect the future page, not a way to make it indexable early.

What I Avoid in a Reusable Laravel SEO Architecture

  • Do not create a new SEO table or JSON schema for every public entity.
  • Do not duplicate equivalent SEO fields manually across Admin forms.
  • Do not store canonical URLs as arbitrary text when routing already defines them.
  • Do not persist separate hreflang URLs in metadata.
  • Do not treat an SEO record as proof that an entity is public or indexable.
  • Do not force one universal fallback that ignores domain semantics.
  • Do not create another social-image field when the entity cover already participates in shared Media architecture.
  • Do not use noindex as access control.
  • Do not force every entity into one JSON-LD schema.
  • Do not add speculative nullable SEO fields without a real reusable use case.

Checklist for a Reusable Laravel SEO Layer

  • Define a stable shared core of SEO metadata.
  • Connect different entities through a reusable relation or contract.
  • Do not tie polymorphic persistence to unstable PHP namespaces.
  • Keep localized SEO values separate per locale.
  • Reuse one Admin SEO block.
  • Reuse equivalent validation rules.
  • Use a predictable explicit -> domain -> site fallback chain.
  • Keep canonical and hreflang tied to routing infrastructure.
  • Allow each entity to define its Open Graph type.
  • Reuse entity covers through the Media contract.
  • Keep metadata persistence separate from public eligibility.
  • Generate sitemap entries only for genuinely indexable entities.
  • Verify metadata in SSR HTML.
  • Test empty explicit fields and fallback paths.
  • Save entity changes and SEO metadata transactionally.

Reusable SEO architecture should remove duplication from the technical mechanism without erasing the domain differences between pages.

Conclusion: Share the SEO Layer, Keep SEO Decisions Domain-Specific

In ICanUp, SEO gradually stopped being a Post-only feature.

Pages and PostSeries gained the same foundational needs: localized titles and descriptions, Open Graph, social-image fallbacks, canonical URLs, hreflang, sitemap participation, and server-rendered metadata.

Instead of creating parallel systems, I kept persistence, localization, the Admin SEO block, and base metadata resolution reusable. A polymorphic or otherwise extendable integration allows new public entities to join that layer without another SEO subsystem.

Reusable infrastructure should not decide everything, however. Post owns its publication semantics and article metadata. Page has its own URL and fallbacks. PostSeries has its own title, description, active state, and cover.

My main conclusion is that the SEO mechanism should be reusable without pretending that every public entity is the same domain object.