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.

Why Separate SEO Code for Every Laravel Model Becomes a Problem
Post SEO fields SEO validation SEO form SEO fallback OG resolver Page SEO fields SEO validation SEO form SEO fallback OG resolver PostSeries SEO fieldsThe 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.
// Simplified architecture example.// Exact class and relation names may differ in the project. public function seo(): MorphOne{ return $this->morphOne( SeoMetadata::class, 'seoable', );}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.
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.
Post / Page / PostSeries | vshared SEO record | +--> uk metadata | +--> en metadataThis 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.
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
meta title explicit localized SEO title | ventity localized title | vsite fallback if needed social image SEO/entity image | vThis 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.
entity + locale | vlocalized public route | vabsolute canonical URLI 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.
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.
Post:og:type = article Page:og:type = website PostSeries:og:type = websiteSSR 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.
SEO metadata | X does not decide publication Entity public contract | +--> route accessibility +--> robots behavior +--> sitemap eligibility +--> indexing notificationsrobots 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
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 testsEntity 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.
DB::transaction(function () use ($entity, $seoData) { $entity->save(); $seoService->sync( $entity, $seoData, );});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 |
|
Page/Series OG type |
|
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
noindexas 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.



