Skip to content

SEO Management (Admin)

Flow ID: AD-14 Module(s): seo Complexity: Medium Last Updated: 2026-06-04

Business Context

SEO management in Ecommercen covers four interconnected areas: default meta tags (site-wide fallbacks per page type), custom per-URL meta tag overrides, XML sitemap generation, and structured data (JSON-LD). The admin panel provides direct CRUD for the first two; sitemap generation and structured data are automated via jobs and helper functions respectively. Per-entity SEO fields (products, categories, promos, blog articles, CMS pages) are managed in their respective admin screens, gated by the metatags permission.


1. Default Meta Tags

Controller: ecommercen/seo/controllers/Adv_metatags.phpModel: ecommercen/seo/models/Adv_metatags_model.phpDB Table: defaultmetatags

Manages site-wide fallback meta tags per language and page type. These are the base tags that Adv_seo_lib merges with page-specific tags using a cascading override pattern.

Operations

MethodDescription
index()List all default meta tags, ordered by order column. Drag-and-drop sortable.
add()Create with fields: meta_keywords, meta_title, meta_description, for, lang
edit($id)Update existing record
delete($id)Remove record
updateorder()AJAX drag-and-drop reorder via listItem POST array

Page Type Groups (for field)

ValueScope
1Frontpage (homepage)
2Any page except frontpage

Admin Menu & Auth

  • Menu: Marketing > SEO > Default Meta Tags (seo/metatags)
  • Roles: AUTH_ROLE_ADVISABLE only
  • Menu group: MARKETING

Meta Tag Resolution (Storefront)

The Adv_seo_lib library (ecommercen/seo/libraries/Adv_seo_lib.php) is loaded globally in Adv_front_controller and provides two resolution methods:

create_metatags_front($arr) -- Used by the homepage (Adv_home). Loads the for=1 (frontpage) defaults for the current language. If page-specific tags exist, they are prepended with a | separator.

create_metatags($arr) -- Used by all other pages (products, categories, vendors, blog, CMS). Loads the for=2 defaults. Implements a three-tier fallback:

  1. If page has its own meta tag, use it (for title: append | default_title)
  2. If page has no meta tag but default exists, use distinct_name, default_value
  3. If neither exists, use distinct_name | site_name (from item name config)

Callers: Adv_home::index(), Adv_products::view(), Adv_product_categories, Adv_vendors, Adv_blog, Adv_category, Adv_product_tags, Adv_reels

Caching

  • Results cached via pscache with TTL from cache_l2_seo_expires (default 864000s / 10 days)
  • Dedicated SEO cache clear: admin menu Cache > SEO (advisable/clearcache/seo)

2. Custom Meta Tags (Per-URL Overrides)

Controller: ecommercen/seo/controllers/Adv_custom_metatags.phpModel: ecommercen/seo/models/Adv_custom_metatags_model.phpDB Table: custom_meta_tags

Allows administrators to override meta tags for any specific URL on the storefront. These take highest priority, applied after all other meta tag resolution.

Operations

MethodDescription
index($offset)Paginated list with search (by URL prefix or keyword). Search stored in session.
add()Create with: url (unique), page_title, meta_description, meta_keywords
edit($id)Update. 404 if record missing.
delete($id)Remove record
export_csv()Export all records (filtered by active search) as semicolon-delimited CSV with BOM

Validation Rules

FieldRules
urlRequired, URL-decoded, unique in custom_meta_tags.url (respects update exclusion)
page_titleOptional; if provided, max 65 characters
meta_descriptionOptional; if provided, max 150 characters
meta_keywordsOptional, trimmed

Admin Menu & Auth

  • Menu: Marketing > SEO > Custom Meta Tags (seo/custom_metatags)
  • Roles: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_MARKETING, AUTH_ROLE_DEVELOPER
  • RBAC enforced in constructor via allowRole() check

Storefront Application

Adv_front_controller::customMetaTags() runs on every front page render. It matches the current REQUEST_URI (URL-decoded) against custom_meta_tags.url. If a match is found, it overrides page_title, meta_description, and meta_keywords in the render array. This executes after the standard seo_lib resolution, giving custom tags absolute priority.

Timestamps

Records have created_at and updated_at fields, set automatically on insert/update.


3. Per-Entity SEO Fields

SEO metadata is embedded directly in entity admin screens rather than managed centrally. The metatags permission gates access to these fields.

Permission System

Defined in ecommercen/helpers/admin_helper.php:

php
canAccess('metatags', $sessionRoles) // Allowed: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN
canAccess('urlFields', $sessionRoles) // Allowed: AUTH_ROLE_ADVISABLE only

Entities with SEO Fields

EntityMUI FieldsExtra FieldsController
Productsmeta_title, meta_keywords, meta_description per languageslug, url, redirect_url per languageAdv_products_admin.php
Product Categoriesmeta_title, meta_keywords, meta_description per languageslug, url per languageAdv_product_categories_admin.php
Promotionsmeta_title, meta_keywords, meta_description per languagedo_follow (boolean)Adv_promo_admin.php
Blog Articlesmeta_title, meta_keywords, meta_description per languageslugAdv_blog_admin.php
Blog Categoriesmeta_title, meta_keywords, meta_description per languageslugAdv_blog_category_admin.php
Blog Tagsmeta_title, meta_keywords, meta_description per languageslugAdv_blog_tags_admin.php
Blog Authorsmeta_title, meta_keywords, meta_description per languageslugAdv_blog_author_admin.php
CMS Pagesmeta_title, meta_keywords, meta_description per languageslug, urlAdv_category_admin.php
Vendor Linesmeta_title, meta_keywords, meta_description per languageslugAdv_lines_admin.php

Product Cloning Behavior

When a product is cloned via Adv_products_admin.php, meta_title, meta_keywords, meta_description, and redirect_url are explicitly set to null to prevent duplicate meta content.


4. Canonical URLs & nofollow

Canonical URLs

Controlled by $config['useCanonical'] in application/config/app.php (default true). When enabled, Adv_front_controller::setCanonicalUrl() sets canonicalUrl to the current URL (decoded). The main layout (application/views/main.php) renders:

html
<link rel="canonical" href="..." />
<meta property="og:url" content="..." />

nofollow / noindex Rules

Set in application/views/main.php, line 15:

  • Development/testing environments: Always noindex,nofollow
  • Maintenance mode (SiteMode not Online): Always noindex,nofollow
  • dofollow === false: Explicitly set by controllers for specific conditions

Adv_front_controller::setNoFollow() sets dofollow = false when the request has query string parameters (to prevent indexing of filtered/sorted pages).

Adv_vendors sets dofollow = false when tag query strings are present (vendor pages filtered by tags).

Adv_promo_admin stores a do_follow flag per promotion, controlled by the metatags permission.

Faceted-Navigation Filter SEO

Faceted-navigation pages (tag, variation, and attribute filter combinations) receive two layers of SEO suppression:

robots.txt disallow rules (public/robots.txt:25-38): A User-agent: * block disallows six URL patterns covering the three filter query-string parameters in both Greek (percent-encoded) and English variants. The parameter names are sourced from application/config/languages.php:32-45 (tag_filter_query_string, variation_filter_query_string, attribute_filter_query_string). The same block disallows /search and /*/search. Asset Allow directives at public/robots.txt:14-23 ensure Googlebot can still fetch CSS/JS needed to render pages.

rel="nofollow" on filter anchors: Added to the filter link components across all three filter types:

assets/main/vue/tag/filters/Components/EntryFilter.vue:16assets/main/vue/variations/filters/Components/EntryFilter.vue:19assets/main/vue/attributes/filters/Components/EntryFilter.vue:19

These compile into public/ui/main/js/all-listing-filters.js; the compiled bundle was rebuilt at the 4.104.0 release build.

Vendor tag-filter noindex (ecommercen/eshop/controllers/Adv_vendors.php): renderVendorListingSeo() at :1265-1273 takes a $hasTagQueryString bool and sets dofollow = false only when tag filters are active. It is called exclusively on the vendors() products path at :1121. Variation- and attribute-filtered vendor pages do not trigger noindex via this code path.


5. Structured Data (JSON-LD)

Rendered via application/views/production/ld_json.php, included in the storefront layout. All functions defined in ecommercen/helpers/theme_helper.php.

Schema.org Types

TypeFunctionWhen Rendered
WebSitesiteLdJson()Every page. Includes sameAs (social links) and SearchAction potential action.
BreadcrumbListbreadcrumbsLdJson($breadcrumbs)Pages with breadcrumbs ($jsonBreadcrumbs set). Microdata also in Breadcrumb library.
ProductproductLdJson(...)Product detail pages. Includes brand, images, AggregateRating, AggregateOffer (InStock/OutOfStock).
ItemList of ProductproductsLdJson(...)Category, vendor, and line listing pages. Each product is a ListItem with SKU, images, offers.
Article + WebPageblogArticleLdJson(...)Blog article pages. Includes author, publisher, datePublished, wordCount, keywords, thumbnailUrl.

application/libraries/Breadcrumb.php renders HTML breadcrumbs with schema.org/BreadcrumbList microdata (inline itemscope/itemprop attributes) alongside the JSON-LD version.


6. XML Sitemap Generation

Job: ecommercen/job/libraries/AdvGenerateSitemaps.phpClient override: application/modules/job/libraries/GenerateSitemaps.php (extends AdvGenerateSitemaps) Schedule: Daily at 01:30 ('30 1 * * *'), configured in application/config/jobs.php

Process

  1. Collects URLs from 5 content types (each independently toggleable via Registry)
  2. Lists every file under storage('sitemap')->listContents('sitemap') and deletes them — wipes the previous run
  3. Chunks the URL set into batches of 500 and writes each to sitemap/sitemap-{n}.xml
  4. Writes the index sitemap/sitemap_index.xml referencing all chunk URLs

All output is on the dedicated storage('sitemap') disk (localpublic/sitemap/, or s3 for K8s tenants). The index is a static file — no PHP request path. Local-disk tenants are served directly by nginx; K8s tenants by a CDN edge that mounts the S3 bucket at /sitemap/, so the registered URL /sitemap/sitemap_index.xml resolves identically in both environments. See SY-05 Sitemap Generation for full detail.

Content Type Toggles (Registry XML_FEEDS group)

Registry KeyContentPriority Logic
IS_ENABLED_SITEMAP_CATEGORIESPublished categories (all 4 depth levels)All at 0.9
IS_ENABLED_SITEMAP_PRODUCTSNon-trashed productsFirst 500: 1.0; inactive: 0.1; out-of-stock: 0.5; rest: 0.7
IS_ENABLED_SITEMAP_VENDORSVendors + lines + vendor-category combosTop 15%: 1.0; lines of top vendors: 0.8; others: 0.6-0.8
IS_ENABLED_SITEMAP_BLOGPublished blog posts (date <= now)All at 0.7
IS_ENABLED_SITEMAP_STATIC_PAGESCMS static pagesAll at 0.3

Sitemap Admin Settings

Toggles configurable in Settings > XML Feeds (application/views/admin/settings/xml_feeds_settings.php). Includes direct link to the live sitemap/sitemap_index.xml.

XML Format

Helper functions in ecommercen/helpers/xml_helper.php:

  • siteMapIndex($links) -- generates <sitemapindex> with <sitemap><loc> entries
  • siteMap($urls) -- generates <urlset> with <url> containing <loc>, <changefreq>, <priority>

7. Open Graph & Social Meta Tags

Rendered in application/views/main.php (lines 33-122). Not admin-configurable per se, but derived from entity SEO fields and page context.

Per-Page Type OG Tags

Page Typeog:typeImage Source
HomepagewebsiteSite logo from GLOBAL.LOGO registry
Productproduct.itemMain product image (640px)
Categoryproduct.groupCategory banner image
Vendorproduct.groupVendor banner or logo
Blog ArticlearticleBlog banner image (640px)

Twitter Cards

All product, blog, and general pages emit twitter:card = summary_large_image with twitter:title, twitter:description, twitter:image, and twitter:site (extracted from the SOCIAL_LINKS.TWITTER registry value via getTwitterHandle()).


8. URL Redirects

Controller: application/controllers/Redirect.php

Handles legacy URL redirects for 3 entity types, all using 301 (permanent) redirects:

Route ConstantEntityResolution
ROUTE_CATCMS CategoriesResolves url or falls back to /category/{slug}
ROUTE_PRODUCTProductsResolves url or falls back to /{slug}
ROUTE_PRODUCT_CATProduct CategoriesResolves url or falls back to /{slug}
SPECIALGenericRedirects to base_url($category_id)

Language-aware: redirects prepend the language abbreviation unless it is the default language with lang_ignore enabled.

The special() method handles arbitrary path redirects by joining all URL segments and issuing a 301 to the resolved path.


Modern Domain Layer (REST API)

Default Meta Tags have a full modern domain + REST layer with CRUD support.

Domain Layer

Namespace: Advisable\Domains\Seo\DI Config: src/Domains/Seo/container.php

EntityTableFields
DefaultMetaTagdefaultmetatagsid, meta_keywords, meta_title, meta_description, lang, for, order

The entity has: Repository, RepositoryConfigurator (no relations), Service (read), WriteRepository, WriteService (CRUD), WriteData (value object), Validator (currently no-op), ListRequest (filters + sorts).

REST Endpoints

DI Config: src/Rest/Seo/container.php

EndpointMethodsController
/rest/seo/default-meta-tagGET (index, item), POST (store), POST/:id (update), DELETE/:id (destroy)DefaultMetaTag

Supports language-prefixed routes (/{lang}/rest/seo/...).

Filters (DefaultMetaTag): id (exact), lang (exact), for (exact), metaTitle (partial), metaKeywords (partial) Sorts (DefaultMetaTag): id, lang, for, order

OpenAPI annotations on all endpoints; spec generated via php cli.php job/GenerateOpenApiJson.


Database Tables

TablePurposeKey Columns
defaultmetatagsSite-wide fallback meta tagsid, meta_keywords, meta_title, meta_description, for (1=front, 2=other), lang, order
custom_meta_tagsPer-URL meta tag overridesid, url (unique), page_title, meta_description, meta_keywords, created_at, updated_at

Key Files

FilePurpose
ecommercen/seo/controllers/Adv_metatags.phpDefault meta tags admin CRUD
ecommercen/seo/controllers/Adv_custom_metatags.phpCustom per-URL meta tags admin CRUD + CSV export
ecommercen/seo/libraries/Adv_seo_lib.phpMeta tag resolution library (create_metatags, create_metatags_front)
ecommercen/seo/models/Adv_metatags_model.phpdefaultmetatags table model
ecommercen/seo/models/Adv_custom_metatags_model.phpcustom_meta_tags table model
ecommercen/job/libraries/AdvGenerateSitemaps.phpSitemap XML generation job
ecommercen/helpers/xml_helper.phpsiteMap() and siteMapIndex() XML builders
ecommercen/helpers/theme_helper.phpJSON-LD structured data functions
ecommercen/helpers/admin_helper.phpcanAccess('metatags') permission check
application/views/production/ld_json.phpStructured data template (renders all JSON-LD blocks)
application/views/main.phpStorefront layout with OG/Twitter/canonical/robots meta rendering
application/controllers/Redirect.phpLegacy URL 301 redirect handler
application/config/cache.phpSEO cache TTL (cache_l2_seo_expires, default 10 days)
application/config/app.phpuseCanonical toggle
application/config/admin_menu.phpSEO admin menu structure
src/Domains/Seo/Modern domain layer (DefaultMetaTag entity)
src/Rest/Seo/REST API controllers and resources

Known Issues & Security Gaps

No known issues at this time. The previously-empty DefaultMetaTag REST Validator (closed as #45) now enforces:

  • for is required on create and must be 1 (front) or 2 (other).
  • order is required on create and must be ≥ 0.
  • lang, when provided, must be exactly 2 characters (DB constraint).
  • metaTitle ≤ 255 chars, metaKeywords ≤ 1000 chars, metaDescription ≤ 500 chars.
  • validateForUpdate allows partial payloads but applies the same shape rules to whatever fields are present.

Test coverage: tests/Unit/Domains/Seo/DefaultMetaTag/ValidatorTest.php (23 tests).

1. Faceted-nav noindex incomplete for category and vendor variation/attribute pages (post-revert gap): The PHP-side noindex,nofollow for filtered category pages and variation/attribute-filtered vendor pages was removed by commit f1aecc6a9. Current suppression signals are: robots.txt disallow rules covering all three filter types at crawl level (public/robots.txt:25-38), rel="nofollow" link hints on all three filter components deployed in 4.104.0, and setNoFollow()'s generic any-querystring dofollow=false for category view() (Adv_product_categories.php:572-573) and vendor vendors() (Adv_vendors.php:1338-1339). Vendor lines pages and the dedicated vendor tag check (renderVendorListingSeo()) remain tag-only. The changelog description of a shared hasActiveListingFilters() helper and defaultRender() chokepoints describes reverted code not present in the codebase.