Appearance
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
| Method | Description |
|---|---|
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)
| Value | Scope |
|---|---|
1 | Frontpage (homepage) |
2 | Any page except frontpage |
Admin Menu & Auth
- Menu: Marketing > SEO > Default Meta Tags (
seo/metatags) - Roles:
AUTH_ROLE_ADVISABLEonly - 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:
- If page has its own meta tag, use it (for title: append
| default_title) - If page has no meta tag but default exists, use
distinct_name, default_value - If neither exists, use
distinct_name | site_name(fromitem nameconfig)
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
pscachewith TTL fromcache_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
| Method | Description |
|---|---|
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
| Field | Rules |
|---|---|
url | Required, URL-decoded, unique in custom_meta_tags.url (respects update exclusion) |
page_title | Optional; if provided, max 65 characters |
meta_description | Optional; if provided, max 150 characters |
meta_keywords | Optional, 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 onlyEntities with SEO Fields
| Entity | MUI Fields | Extra Fields | Controller |
|---|---|---|---|
| Products | meta_title, meta_keywords, meta_description per language | slug, url, redirect_url per language | Adv_products_admin.php |
| Product Categories | meta_title, meta_keywords, meta_description per language | slug, url per language | Adv_product_categories_admin.php |
| Promotions | meta_title, meta_keywords, meta_description per language | do_follow (boolean) | Adv_promo_admin.php |
| Blog Articles | meta_title, meta_keywords, meta_description per language | slug | Adv_blog_admin.php |
| Blog Categories | meta_title, meta_keywords, meta_description per language | slug | Adv_blog_category_admin.php |
| Blog Tags | meta_title, meta_keywords, meta_description per language | slug | Adv_blog_tags_admin.php |
| Blog Authors | meta_title, meta_keywords, meta_description per language | slug | Adv_blog_author_admin.php |
| CMS Pages | meta_title, meta_keywords, meta_description per language | slug, url | Adv_category_admin.php |
| Vendor Lines | meta_title, meta_keywords, meta_description per language | slug | Adv_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 (
SiteModenot Online): Alwaysnoindex,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
| Type | Function | When Rendered |
|---|---|---|
WebSite | siteLdJson() | Every page. Includes sameAs (social links) and SearchAction potential action. |
BreadcrumbList | breadcrumbsLdJson($breadcrumbs) | Pages with breadcrumbs ($jsonBreadcrumbs set). Microdata also in Breadcrumb library. |
Product | productLdJson(...) | Product detail pages. Includes brand, images, AggregateRating, AggregateOffer (InStock/OutOfStock). |
ItemList of Product | productsLdJson(...) | Category, vendor, and line listing pages. Each product is a ListItem with SKU, images, offers. |
Article + WebPage | blogArticleLdJson(...) | Blog article pages. Includes author, publisher, datePublished, wordCount, keywords, thumbnailUrl. |
Breadcrumb Library
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
- Collects URLs from 5 content types (each independently toggleable via Registry)
- Lists every file under
storage('sitemap')->listContents('sitemap')and deletes them — wipes the previous run - Chunks the URL set into batches of 500 and writes each to
sitemap/sitemap-{n}.xml - Writes the index
sitemap/sitemap_index.xmlreferencing 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 Key | Content | Priority Logic |
|---|---|---|
IS_ENABLED_SITEMAP_CATEGORIES | Published categories (all 4 depth levels) | All at 0.9 |
IS_ENABLED_SITEMAP_PRODUCTS | Non-trashed products | First 500: 1.0; inactive: 0.1; out-of-stock: 0.5; rest: 0.7 |
IS_ENABLED_SITEMAP_VENDORS | Vendors + lines + vendor-category combos | Top 15%: 1.0; lines of top vendors: 0.8; others: 0.6-0.8 |
IS_ENABLED_SITEMAP_BLOG | Published blog posts (date <= now) | All at 0.7 |
IS_ENABLED_SITEMAP_STATIC_PAGES | CMS static pages | All 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>entriessiteMap($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 Type | og:type | Image Source |
|---|---|---|
| Homepage | website | Site logo from GLOBAL.LOGO registry |
| Product | product.item | Main product image (640px) |
| Category | product.group | Category banner image |
| Vendor | product.group | Vendor banner or logo |
| Blog Article | article | Blog 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 Constant | Entity | Resolution |
|---|---|---|
ROUTE_CAT | CMS Categories | Resolves url or falls back to /category/{slug} |
ROUTE_PRODUCT | Products | Resolves url or falls back to /{slug} |
ROUTE_PRODUCT_CAT | Product Categories | Resolves url or falls back to /{slug} |
SPECIAL | Generic | Redirects 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
| Entity | Table | Fields |
|---|---|---|
DefaultMetaTag | defaultmetatags | id, 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
| Endpoint | Methods | Controller |
|---|---|---|
/rest/seo/default-meta-tag | GET (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
| Table | Purpose | Key Columns |
|---|---|---|
defaultmetatags | Site-wide fallback meta tags | id, meta_keywords, meta_title, meta_description, for (1=front, 2=other), lang, order |
custom_meta_tags | Per-URL meta tag overrides | id, url (unique), page_title, meta_description, meta_keywords, created_at, updated_at |
Key Files
| File | Purpose |
|---|---|
ecommercen/seo/controllers/Adv_metatags.php | Default meta tags admin CRUD |
ecommercen/seo/controllers/Adv_custom_metatags.php | Custom per-URL meta tags admin CRUD + CSV export |
ecommercen/seo/libraries/Adv_seo_lib.php | Meta tag resolution library (create_metatags, create_metatags_front) |
ecommercen/seo/models/Adv_metatags_model.php | defaultmetatags table model |
ecommercen/seo/models/Adv_custom_metatags_model.php | custom_meta_tags table model |
ecommercen/job/libraries/AdvGenerateSitemaps.php | Sitemap XML generation job |
ecommercen/helpers/xml_helper.php | siteMap() and siteMapIndex() XML builders |
ecommercen/helpers/theme_helper.php | JSON-LD structured data functions |
ecommercen/helpers/admin_helper.php | canAccess('metatags') permission check |
application/views/production/ld_json.php | Structured data template (renders all JSON-LD blocks) |
application/views/main.php | Storefront layout with OG/Twitter/canonical/robots meta rendering |
application/controllers/Redirect.php | Legacy URL 301 redirect handler |
application/config/cache.php | SEO cache TTL (cache_l2_seo_expires, default 10 days) |
application/config/app.php | useCanonical toggle |
application/config/admin_menu.php | SEO 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:
foris required on create and must be1(front) or2(other).orderis 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.validateForUpdateallows 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.
Related Flows
- AD-02 Product Management -- per-product SEO fields
- AD-05 Category Management -- per-category SEO fields
- AD-07 Promotion Management -- per-promo SEO fields and
do_followtoggle - AD-12 Blog Management -- per-article SEO fields
- AD-17 Lines Management -- per-line SEO fields
- AD-19 Vendor Management -- vendor slug and storefront SEO
- AD-13 Settings -- sitemap toggles in XML Feeds settings
- AD-48 CMS Static Pages -- CMS pages with SEO routes and meta fields
- CF-01 Product Browsing -- storefront meta tag rendering