Skip to content

SEO Management (Admin) ​

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

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 13 (condition at :12):

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

Adv_checkout::defaultRender() (ecommercen/checkout/controllers/Adv_checkout.php:2362-2365) and Adv_customer::defaultRender() (ecommercen/eshop/controllers/Adv_customer.php:92-100) force $this->render['dofollow'] = false, so checkout / payment-return pages and customer account / login / signup / password pages render <meta name="robots" content="noindex,nofollow"/>.

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:46-57): A User-agent: * block disallows six URL patterns (:46-53) 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 (:56-57). Asset Allow directives at public/robots.txt:33-42 ensure Googlebot can still fetch CSS/JS needed to render pages.

Crawler groups (public/robots.txt:1-31): third-party SEO / unwanted-search-engine crawlers (Slurp, Baiduspider, YandexBot, MJ12bot, SemrushBot*, SiteAuditBot, DotBot, rogerbot, PetalBot, BLEXBot, DataForSeoBot, serpstatbot, barkrowler, MegaIndex.ru, Ezooms) are blocked with Disallow: / (:3-23); AhrefsBot is intentionally allowed (:2). Crawl-delay: 5 applies to the * group (:28-30). AI crawlers (GPTBot, ClaudeBot, PerplexityBot, etc.) get no group of their own and stay allowed under * (:25-26).

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 (local → public/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 31-120). 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:46-57), 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 describes reverted code not present in the codebase (no match anywhere). The defaultRender() noindex overrides that do exist are limited to checkout and customer pages (see nofollow / noindex Rules above), not category or vendor listings.