SEO: SEF URLs, Redirects and Schema.org

This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production.

This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages.

Search engine optimisation in Joomla is mostly a matter of five things the core already provides and that this page walks through in turn: readable URLs (the SEF router, URL rewriting and the System - SEF plugin), metadata (titles, descriptions and robots directives per site, menu item and article), redirects (the Redirects component and its 404 collector), structured data (the Schema.org system plugin and its schemaorg plugin group) and crawler files (robots.txt and sitemaps, the latter provided by extensions, not by core). It is written for site builders and administrators, with the developer-facing internals (router rules, plugin events) marked as such.

This page was written against Joomla 6.2 (the current release; 5.4 is in its security-only period and behaves the same for everything below unless stated), PHP 8.3 or later, and checked on 2026-10-30. The running example is the Tallpine Hiking Club site at https://www.tallpine.example/, with trail reports under the nested categories Trails › Alpine and Trails › Valley, a News category, English as the default language and German as the second language. The menu structure behind these URLs is described in Menus and Menu Items, the content model in Articles and Categories and Tags, and the global settings screen in Global Configuration.

How a Joomla URL is built and parsed

A Joomla page is always identified internally by query parameters: option (the component), view, layout, id, catid, Itemid (the menu item), lang, task, tmpl and format. Without SEF URLs the address is the raw query, for example index.php?option=com_content&view=article&id=23&catid=66&Itemid=42. The routing layer converts between that internal form and a readable path in both directions.

  • Building starts when code calls Route::_('index.php?option=...') (or Route::link() to target another application such as the administrator or the API). With SEF enabled, the site router asks the component’s router to preprocess the query (which may choose the best Itemid), lets the component router build its part of the path, and prepends the path of the menu item.

  • Parsing splits the incoming path on / and matches the segments against menu item aliases from the site root downwards. The last matching menu item becomes the active menu item, which sets Itemid and the component. The remaining segments are handed to that component’s router, which turns them into view, id and so on.

Both directions are implemented by Joomla\CMS\Router\Router and, for the public site, Joomla\CMS\Router\SiteRouter. Rules are callbacks attached with Router::attachBuildRule() and Router::attachParseRule(); a plugin may attach its own rules, but only before parsing begins, typically in the onAfterInitialise event, which is what the language filter plugin does for the language prefix. During parsing every path segment must be consumed: if characters remain after all rules ran, Joomla assumes nothing recognised them and answers with a 404. Component routers are covered in Component Site Views and Routing.

flowchart LR R[Request /trails/alpine/lake-tarn-circuit.html] --> W{Web server<br/>rewrite} W -- real file or folder --> F[Serve static file] W -- no file --> I[index.php] I --> S[SiteRouter parse] S --> M[Menu item alias match:<br/>Trails > Alpine] M --> C[com_content router:<br/>article alias] C --> Q[option=com_content<br/>view=article id=23 Itemid=42] Q --> P[System - SEF plugin<br/>canonical checks] P --> O[Rendered article]

Tainted URLs and the single 301 (5.3 and later)

Since Joomla 4.0 an unrecognised leftover path produces a 404. Since Joomla 5.3 the router can also mark a parse as tainted with Router::setTainted() (read it with Router::isTainted()): the URL could be resolved, but it does not match the canonical format, for instance because the .html suffix is missing, the alias in the path is wrong while the numeric id is right, or the URL is a duplicate form that strict routing rejects. The redirect is deliberately not issued by the router. The System - SEF plugin checks the flag after routing and sends one 301 to a freshly built canonical URL, so several corrections (domain, suffix, trailing slash, alias) are combined into a single hop rather than a chain.

Global Configuration: the SEO settings

The settings live in System → Global Configuration → Site, under SEO Settings (the screen links to the System - SEF plugin for further options).

Setting Effect Notes

Search Engine Friendly URLs

Turns the SEF router on. Links become index.php/trails/alpine/lake-tarn-circuit instead of the raw query.

Required for everything below. Works with no server configuration because the path follows index.php.

Use URL Rewriting

Removes index.php/ from URLs.

Needs the web server to send unknown paths to index.php: rename htaccess.txt to .htaccess (Apache and LiteSpeed), web.config.txt to web.config (IIS) or add the nginx rule shown below.

Add Suffix to URL

Appends an extension such as .html (or .feed for feeds) according to the content type.

Optional. The User Guide notes that search engines do not appear to care whether URLs end in .html. Decide once: changing it later changes every URL.

Unicode Aliases

No: aliases with non-Latin-1 characters are transliterated with the rules of the content language (for en-GB, über becomes ueber). Yes: the alias keeps the characters as they are.

Matters for the German content of the Tallpine site and for any non-Latin script. Aliases are computed when an item is saved.

Site Name in Page Titles

Adds the site name before or after the page title (<title>).

Keep titles short (about 60 characters is a common recommendation).

Site Meta Description

Default <meta name="description"> for pages that have none.

Overridden by menu item and article values.

Robots

Default robots meta tag: index, follow, noindex, follow, index, nofollow or noindex, nofollow.

Setting a site-wide noindex on a staging server is common; forgetting to remove it on go-live is one of the most frequent SEO accidents.

The three URL shapes the User Guide documents for the same category blog are worth knowing, since they are what you see while diagnosing a misconfigured site:

  • SEF off: index.php?option=com_content&view=category&layout=blog&id=16&Itemid=125

  • SEF on, rewriting off: index.php/sample-layouts/articles

  • SEF on, rewriting on, suffix on: sample-layouts/articles.html

Enable the options one at a time and test the site after each save. If a change breaks the site, the usual recovery is to turn the last option off again from the administrator, or, if that is no longer possible, to set $sef_rewrite = false (or $sef = false) in configuration.php, as described in configuration.php and .htaccess.

URL rewriting on Apache

Joomla ships a htaccess.txt template in the site root. Activating it is a rename, and the Global Configuration switch is then safe to turn on.

cd /var/www/tallpine/public_html
cp htaccess.txt .htaccess

The core SEF block of the template (since Joomla 4 it covers two applications) sends every request that is not a real file or folder to the right front controller:

RewriteEngine On

## Begin - Joomla! core SEF Section.
# PHP FastCGI fix for HTTP Authorization, required for the API application
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

# SEF URLs for the API application
RewriteCond %{REQUEST_URI} ^/api/
RewriteCond %{REQUEST_URI} !^/api/index\.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* api/index.php [L]

# SEF URLs for the public frontend application
RewriteCond %{REQUEST_URI} !^/index\.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* index.php [L]
## End - Joomla! core SEF Section.

The template also begins with request filters that reject a few classic exploit query strings (base64_encode, script tags, GLOBALS, _REQUEST injection) with a 403, and with a commented # RewriteBase / line. Uncomment RewriteBase only when the URL space does not map directly to the file system (a Joomla installed in a sub-folder, for example RewriteBase /tallpine). Custom redirects such as a non-www to www rule belong in the Custom redirects section of the template, before the core SEF block, with the [R=301,L] flags.

Common Apache errors:

  • 500 Internal Server Error after the rename. mod_rewrite is not loaded (LoadModule rewrite_module modules/mod_rewrite.so is commented out) or the template uses a directive the host forbids (Options +FollowSymlinks needs AllowOverride Options). Fix the module or turn URL Rewriting off; the renamed file can stay.

  • .htaccess ignored. The virtual host needs AllowOverride All (or at least FileInfo, Options, Limit and AuthConfig) for the Joomla directory.

  • Images and assets 404 in a sub-folder install. RewriteBase is missing or wrong.

  • Redirect loops. A custom www rule duplicated by a rule in the virtual host or by a reverse proxy that terminates TLS; see Web Server, Reverse Proxy and HTTPS.

A virtual-host version that avoids .htaccess altogether (faster, because Apache does not look for override files on every request) is the recommended form on a server you control:

<VirtualHost *:443>
    ServerName www.tallpine.example
    DocumentRoot /var/www/tallpine/public_html

    <Directory /var/www/tallpine/public_html>
        AllowOverride None
        Require all granted
        Options -Indexes +FollowSymlinks

        RewriteEngine On
        RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

        RewriteCond %{REQUEST_URI} ^/api/
        RewriteCond %{REQUEST_URI} !^/api/index\.php
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule .* api/index.php [L]

        RewriteCond %{REQUEST_URI} !^/index\.php
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule .* index.php [L]
    </Directory>
</VirtualHost>

When the document root is the public folder of a Joomla 6 installation, the template paths are relative to that folder and the same rules apply; the layout is described in Installation and the public Folder.

URL rewriting on nginx

nginx has no .htaccess, and the Global Configuration screen says so ("NginX: you must configure your server"). The rule that replaces it is try_files: serve a real file or directory if one exists, otherwise hand the request to index.php with the original query arguments.

server {
    listen 443 ssl;
    server_name www.tallpine.example;
    root /var/www/tallpine/public_html;
    index index.php;

    # Joomla! SEF URLs for the public site
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # SEF URLs for the Web Services API application
    location /api/ {
        try_files $uri $uri/ /api/index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTP_AUTHORIZATION $http_authorization;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    # Never serve the template files
    location ~ ^/(htaccess\.txt|configuration\.php-dist|web\.config\.txt)$ {
        deny all;
    }
}

The User Guide documents the location / block; the /api/ location, the HTTP_AUTHORIZATION parameter (needed so the API application sees the bearer token) and the deny block are the equivalent of the extra sections of htaccess.txt and are an addition for sites that expose the Web Services API (see Web Services API). The expires 1d directive that appears in one of the guide’s examples applies to everything matched by location /, including dynamic pages; leave it out unless caching of every response for a day is intended. After editing, run nginx -t and reload. Note that the label of the Global Configuration option still reads "Use URL Rewriting"; on nginx it is the try_files rule that does the rewriting and the option only tells Joomla to generate URLs without index.php.

Suffixes, trailing slashes and canonical URLs: the System - SEF plugin

The System - SEF plugin (plg_system_sef, enabled by default) does two jobs. It adds SEF support to links inside the document HTML, which means links written by hand in article text are rewritten without any special tag, and it enforces one canonical form of every URL by redirecting. Its options are found under System → Plugins → System - SEF.

Option (XML name) Behaviour

Site Domain (domain)

The preferred domain. https://tallpine.example and https://www.tallpine.example are different domains for a search engine; the plugin uses the entered domain as the preferred (canonical) one. Leave it empty if the web server or proxy already redirects.

Enforce a suffix by redirect (enforcesuffix)

With Add Suffix to URL enabled, URLs without the suffix redirect to the one with it. The language string states that from version 6.0 this becomes the standard behaviour and not an option any more, while the 6.2 plugin manifest still declares the field.

Strict handling of index.php (indexphp)

With Use URL Rewriting enabled, removes index.php from URLs that still contain it and redirects incoming requests that have it to the version without.

Trailing slash for URLs (trailingslash)

No change (default), enforce URLs without a trailing slash, or enforce URLs with one, using redirects. Applied only when Add Suffix to URL is disabled.

Strict Routing (strictrouting)

Stops the router from accepting many URLs it considers duplicates and redirects them with a 301 to the correct one.

Recommended starting point for the Tallpine site, on a fresh site (existing sites change these with care, see the pitfalls below):

In the plugin form enter: Site Domain https://www.tallpine.example, Strict handling of index.php on, Trailing slash set to Enforce URLs without trailing slash (no suffix), Strict Routing on.

Pitfalls to avoid:

  • Changing the suffix, trailing-slash or rewriting setting on a live site changes every URL on the site. Without matching redirects, search engines see a site-wide wave of 404s. The SEF plugin’s redirects soften this, but only for the forms it recognises.

  • Several URLs for one article. An article is reachable under its category menu item, under a Featured menu item and under a raw index.php?... address. The recommended handling is to pick one as canonical, keep menu structure consistent, enable Strict Routing and the redirects above, and keep the old forms out of internal links.

  • Domain redirect loops. A Site Domain that differs from the one a reverse proxy forwards produces an endless redirect; configure forwarded headers first (see Web Server, Reverse Proxy and HTTPS).

  • Letting a CDN or full-page cache ignore the redirect rules. Cache purging after changing SEF options is covered in Caching and Performance.

Menu-based URL design

In Joomla the URL path is mostly the menu. Each menu item has an Alias (lower-case, hyphenated, unique among its siblings) and a parent; the item’s path is the chain of aliases from the root. A component’s own segments are added after the path of the menu item. For the Tallpine site:

Menu structure Link target Resulting URL

Trails (alias trails) → Alpine (alias alpine), category blog of Trails › Alpine

Category Blog, catid=66

/trails/alpine

An article Lake Tarn Circuit, alias lake-tarn-circuit, in that category

Article

/trails/alpine/lake-tarn-circuit (plus .html if the suffix is enabled)

News (alias news), category list of News

Category List

/news

An article with no suitable menu item

Article, Itemid falls back to the best match (home)

/component/content/article/lake-tarn-circuit or similar

Design rules that follow from this:

  • Create a menu item for every important category and give it a good alias. An article whose category has no menu item gets a long /component/content/... address or lands on the home item. This is the main structural SEO fix on most Joomla sites.

  • Keep the menu tree shallow. Each nesting level adds a segment; two or three levels are enough.

  • Hidden menu items are legitimate. A menu item in a menu that is not shown by any module still defines a URL path; use one (a Hidden menu) to give a section a clean address without exposing it in the navigation.

  • Aliases are history. Renaming an alias changes the URL of every item below it. Add a redirect when you rename.

  • Multilingual sites add a language code prefix (/en/, /de/) through the language filter and keep one menu tree per language, linked by associations; the interaction with SEF is covered in Multilingual Sites.

  • IDs in URLs. Joomla 4 and later use the modern router for core components, which can resolve articles by alias alone. In Articles → Options → Integration, the Remove IDs from URLs setting (sef_ids, default No) drops the numeric id from article and category URLs (/news/trail-reopened instead of /news/12-trail-reopened). Turn it on only when aliases are unique within a category, and before the site goes live, because it changes every URL.

Where a custom component such as com_tallpine needs its own URL segments (/trails/alpine/lake-tarn-circuit), it implements a RouterView with MenuRules, StandardRules and NomenuRules; see Component Site Views and Routing.

Metadata per site, menu item and article

Every page needs a distinct <title> and <meta name="description">. Joomla resolves them from the most specific source available.

Where Fields Becomes

Menu item, tab Page Display

Browser Page Title

The <title> of the page (the site name may be added by Global Configuration)

Menu item, tab Metadata

Meta Description, Keywords, Robots

<meta> tags for pages reached through the item

Article, tab Publishing

Meta Description, Keywords, Robots, Author, Content Rights

<meta> tags on the article page; override the menu item values

Global Configuration, Site

Site Meta Description, Robots

Defaults for pages with none of the above

Notes for writing good metadata:

  • Title. About 60 characters, unique per page, with the topic first. The article title is rendered as the page H1, so the article body should start at H2 and nest H3 to H5 below it; do not use headings for visual styling.

  • Description. A readable sentence of roughly 155 characters. Search engines sometimes show it under the title, but may substitute their own text.

  • Keywords. The meta keywords field still exists. Search engines no longer use it for ranking; older books and tutorials that recommend keyword density or keyword stuffing give outdated advice, and the field can stay empty. Tags (see Categories and Tags) are a navigation feature, not a ranking one.

  • Robots. Per page noindex is the right tool for thank-you pages, internal search results and members-only summaries; it does not protect content (see the security note on robots.txt below).

  • Images. Use descriptive file names with hyphens, meaningful alt text, and modern formats; responsive images and WebP generation come from the Media Manager and extensions (see Media Manager).

  • Canonical tags. Core does not output a rel="canonical" link for articles; a canonical URL in the head comes from the SEF redirects described above or from a third-party extension. The User Guide describes one such plugin and notes that it needs a code change for Joomla 4 and 5, so check any extension against your version before relying on it.

A template can set head data from code, which is occasionally useful for an extension view:

<?php

\defined('_JEXEC') or die;

use Joomla\CMS\Factory;

// In a component HtmlView::display(), after loading the trail item
$document = Factory::getApplication()->getDocument();

$document->setTitle($this->item->title . ' - Tallpine Hiking Club');
$document->setDescription(
    'Distance ' . $this->item->distance_km . ' km, elevation gain '
    . $this->item->elevation_gain . ' m, difficulty ' . $this->item->difficulty . '.'
);

if ($this->item->noindex) {
    $document->setMetaData('robots', 'noindex, follow');
}

Redirects: the Redirects component and 404 collection

Moving or renaming content leaves old addresses behind. The Redirects component (Components → Redirects, com_redirect) holds a table of Expired URL → New URL pairs; the System - Redirect plugin (plg_system_redirect) applies it. Both must be enabled; the component shows a message when the plugin is disabled.

How it works internally

  1. The plugin listens for the onError event on the site application and acts only on errors with HTTP code 404 (the administrator is ignored).

  2. It looks up the requested URL (with and without domain, and with the query string) in the redirect table.

  3. If a published entry exists with a new URL, the visitor receives a redirect. By default the status is 301. Only if Advanced mode is enabled in the component options (mode) is the per-entry Redirect Status Code used; a code outside 300-399 is raised as the error status instead (for example a 410 Gone).

  4. If no entry exists and Collect URLs is enabled on the plugin (collect_urls, enabled by default), the 404 URL is stored as a new, unpublished entry so the administrator can later give it a destination. Repeated 404s increment the Hits counter of an existing entry.

  5. If an entry exists but is unpublished, the hit counter is incremented and the visitor still gets the 404 page.

Plugin options: Collect URLs, Include Domain Name in Expired URL (store absolute or relative expired URLs) and Exclude URLs (terms or regular expressions never stored, for example /wp-login.php or xmlrpc).

Working with the list

  • Open the list: the Link Referrer column tells you who linked to the broken URL, so you can fix the source.

  • Edit an entry, set New URL (a relative path such as /trails/alpine/lake-tarn-circuit is fine), choose the status code if Advanced mode is on, add a Comment and publish it.

  • Batch: the Bulk process to add new URLs toolbar action accepts one old-url|new-url pair per line (the separator is configurable in the options), and Batch Update Selected URL(s) assigns one new URL to many entries.

  • Use 301 for permanent moves (the default and the usual choice), 302/307 only for temporary ones, and 410 for content that is gone for good.

An import for the Tallpine site after a migration from an old site, using the batch field:

/index.php/weg-zum-see|/de/wanderwege/talweg/weg-zum-see
/trails/valley/old-lake-path|/trails/valley/lake-path
/news/2024/spring-opening|/news/spring-opening

Pitfalls

  • The table grows without bound. Bots probing for /wp-login.php fill it with junk. Add Exclude URLs patterns, or disable Collect URLs once the list has been triaged, and purge unused entries.

  • Redirect chains. A redirect target must be a final URL. Redirecting to a URL that the SEF plugin then redirects again costs a second hop.

  • Server-level redirects are faster. A permanent move of a whole directory is better done in the web server (RewriteRule, nginx return 301) than by thousands of database rows.

  • Redirects only fire for 404s. An old URL that still resolves to something (for example a menu alias that now points to different content) is never redirected; fix it by changing the alias or by adding a server rule.

  • A custom 404 page is an error page of the template (error.php); see Error, Offline and Component Pages.

A server-level equivalent for a whole renamed section:

# Old German paths moved under /de/wanderwege
location ~ ^/touren/(.*)$ {
    return 301 /de/wanderwege/$1;
}

Schema.org structured data

Structured data is a standard way for a page to tell search engines what it is about: not just text, but "this is an event, with this start date and place". Joomla produces it as JSON-LD in the page head. The feature arrived in Joomla 5.0 (it grew out of a Google Summer of Code 2022 project) and is unchanged in concept in 6.2. It is split into one system plugin and a group of type plugins.

The plugins

  • System - Schema.org (plg_system_schemaorg) holds the site-wide data, adds a Schema tab to supported edit forms, stores the values in the #__schemaorg table and prints the JSON-LD into the head.

  • Plugins in the schemaorg group each define one type and its form: Article (5.1), BlogPosting, Book, Event, JobPosting, Organization, Person, Recipe and Custom. Only enabled plugins appear in the Schema Type list. Third-party types exist in the Joomla Extensions Directory (category Schema Plugins).

All of them are in System → Plugins; filter by the group schemaorg or by the word schema.

Configuring the site-wide data

Open System - Schema.org and fill in:

Field (XML name) Meaning

Base Type (baseType)

Organization (clubs, companies, NGOs) or Person (a personal site). Default Organization. For Tallpine: Organization.

Name (name)

Shown for an organization, or for a person who is not an existing user. When empty the site name from Global Configuration is used. For a person you can instead select a Joomla user (user).

Image (image)

The logo, taken from the Media Manager. Joomla makes the URL absolute and records width and height.

Social Media Accounts (socialmedia)

A repeatable list of profile URLs, output as sameAs.

The page stays empty of structured data until the plugin has been saved with a base type at least once: the editor shows "you have to configure the plugin first". Then enable the type plugins you need (for the Tallpine site: Article for trail reports, Event for the guided hikes, Organization) and, optionally, restrict each plugin to the extensions it may serve with its Allowed list (allowedlist, a list of components that implement the schema.org service interface).

Adding data to an article

In Content → Articles, edit an article and open the Schema tab. Pick the Schema Type that matches it. The form then shows the type’s fields; the Event type, for instance, offers the featured image, event name, summary or description, start date, location details (name and postal address), organizer details (a person or an organization, with name, URL, logo, e-mail and address) and an optional aggregate rating. A Generic Field section lets you add a custom property that the default form lacks. The data is saved with the article and removed when the article is deleted.

Article fields can also be fed to schema data; the User Guide page on accessing Schema.org custom fields explains the tab. Custom fields in general are covered in Custom Fields. Today only the com_content.article context is registered by core; no other core component exposes a Schema tab, and a custom component can opt in by implementing SchemaorgServiceInterface (with SchemaorgServiceTrait) and returning its contexts from getSchemaorgContexts(). A form field, schemaorgcomponentsections, lists the enabled components that implement the interface, and is what the type plugins use for their Allowed option.

What is generated

On a public article page whose context is supported, the system plugin builds a single JSON-LD document with an @graph, in the onBeforeCompileHead event:

  • the base entity (Organization or Person), with name, url, logo and sameAs links;

  • a WebSite node, with a SearchAction that points at the Smart Search page when a Smart Search module (mod_finder) is published (see Smart Search);

  • a WebPage node with the current URL, title, meta description, language and a link to breadcrumbs when the breadcrumbs module produced schema data;

  • the item node from the Schema tab, for example an Event, linked to the page with isPartOf.

Empty values are removed, and the output is escaped with JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS and JSON_HEX_QUOT so editor-supplied text cannot close the script element. It is delivered as an inline script of type application/ld+json, pretty-printed only when debug mode is on. For a guided hike on the Tallpine site the result looks like this (shortened):

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://www.tallpine.example/#/schema/Organization/base",
      "name": "Tallpine Hiking Club",
      "url": "https://www.tallpine.example/",
      "sameAs": ["https://social.example/tallpine"]
    },
    {
      "@type": "WebSite",
      "@id": "https://www.tallpine.example/#/schema/WebSite/base",
      "url": "https://www.tallpine.example/",
      "name": "Tallpine Hiking Club",
      "publisher": {"@id": "https://www.tallpine.example/#/schema/Organization/base"}
    },
    {
      "@type": "Event",
      "@id": "https://www.tallpine.example/#/schema/com_content/article/23",
      "name": "Lake Tarn guided circuit",
      "startDate": "2026-11-14T08:30",
      "location": {
        "@type": "Place",
        "name": "Lake Tarn car park",
        "address": {"@type": "PostalAddress", "addressLocality": "Tarn Valley"}
      }
    }
  ]
}

Extending the output (developers)

The system plugin dispatches three events to plugins of the schemaorg group, each wrapped in an event class: onSchemaPrepareData and onSchemaPrepareForm (while an article form is prepared), onSchemaPrepareSave (after an article is saved) and onSchemaBeforeCompileHead (just before the JSON-LD is printed, with the whole graph in getSchema()). The core types are final classes that extend CMSPlugin, implement SubscriberInterface and use SchemaorgPluginTrait plus helper traits for dates and images; a new type is a normal plugin of the schemaorg group with a forms/schemaorg.xml and the same structure. A smaller job, adjusting the output of other types, only needs the last event:

<?php

namespace Tallpine\Plugin\Schemaorg\Tallpine\Extension;

use Joomla\CMS\Event\Plugin\System\Schemaorg\BeforeCompileHeadEvent;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;

\defined('_JEXEC') or die;

final class Tallpine extends CMSPlugin implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return ['onSchemaBeforeCompileHead' => 'onSchemaBeforeCompileHead'];
    }

    public function onSchemaBeforeCompileHead(BeforeCompileHeadEvent $event): void
    {
        $schema = $event->getSchema();
        $graph  = $schema->get('@graph');

        foreach ($graph as $key => $node) {
            if (($node['@type'] ?? '') === 'Event') {
                // Every guided hike is free for members and public
                $graph[$key]['isAccessibleForFree'] = true;
            }
        }

        $schema->set('@graph', $graph);
    }
}

The plugin needs the usual services/provider.php and manifest with group="schemaorg"; see Plugin Development and Events.

Checking and pitfalls

  • Test a published page with the search engines' rich-result tester or the Schema Markup Validator and look for errors in required properties (for events: name, start date and location).

  • No visible output. The plugin is not configured (save it once), the relevant type plugin is disabled, the article has no Schema Type selected, or the page is not an article view (category blogs and lists carry only the base graph, if anything).

  • Duplicated markup. Templates, SEO extensions or older "rich snippet" plugins may also emit JSON-LD. Keep one source of truth.

  • Markup must match what the visitor sees. Do not mark up content that is hidden or does not exist; search engines may penalise it.

  • Older material. Joomla! 4 Masterclass (2023) covers Joomla 4 and has nothing about Schema.org; third-party schema extensions written for Joomla 4 may not support the Joomla 5 and 6 event classes, so check their compatibility before upgrading. Developing Extensions for Joomla! 5 (2023) does not describe the schemaorg plugin group.

Sitemaps

Joomla core does not generate an XML sitemap. The User Guide recommends one for dynamic sites because manually maintained sitemaps do not scale, and points to extensions from the Joomla Extensions Directory (category Sitemap), which can produce both HTML sitemaps for visitors and XML sitemaps for crawlers. Such extensions typically let you choose which menu items to include, set the change frequency and priority, and optionally expand a menu item into its articles or items (for example every article of a category blog).

Practical advice:

  • Include only canonical, indexable URLs; leave out noindex pages and redirected URLs.

  • In the multilingual Tallpine site, include both languages and the hreflang alternates that the language associations provide.

  • Reference the sitemap from robots.txt (Sitemap: line) and submit it in each search engine’s webmaster tool.

  • Submitting a sitemap does not guarantee indexing; it only gives crawlers hints.

  • The Smart Search index (Smart Search) is a separate, internal search feature and is not a sitemap.

robots.txt

Joomla ships a robots.txt.dist file. Copy it to robots.txt in the web root (the document root, not a sub-folder) and adjust it. The distributed file blocks the application folders from crawling:

User-agent: *
Disallow: /administrator/
Disallow: /api/
Disallow: /bin/
Disallow: /cache/
Disallow: /cli/
Disallow: /components/
Disallow: /includes/
Disallow: /installation/
Disallow: /language/
Disallow: /layouts/
Disallow: /libraries/
Disallow: /logs/
Disallow: /modules/
Disallow: /plugins/
Disallow: /tmp/

A Tallpine production file adds the sitemap and keeps the media and template assets crawlable, since search engines need CSS and JavaScript to render pages:

User-agent: *
Disallow: /administrator/
Disallow: /api/
Disallow: /cache/
Disallow: /cli/
Disallow: /logs/
Disallow: /tmp/
Disallow: /component/content/
Allow: /media/
Allow: /templates/

Sitemap: https://www.tallpine.example/sitemap.xml

Things to know:

  • Blocked folders and rendering. Core front-end assets are published under /media/ and template assets under /templates/, which the distributed file leaves open. If a third-party extension loads CSS or JavaScript from a blocked folder, crawlers cannot render the page; check the search engine’s URL inspection result and add Allow rules as needed.

  • robots.txt is not an access control. It is public and advisory. Do not list secret paths in it, and protect private content with access levels (see Users, Groups, Access Levels and ACL), not with Disallow.

  • Disallow is not noindex. A blocked URL can still appear in results if it is linked from elsewhere; use the Robots metadata (noindex) on a page that crawlers are allowed to read.

  • A sub-folder install needs robots.txt at the site root of the domain with the folder name prefixed to every path (Disallow: /joomla/administrator/).

  • Staging sites should be blocked by authentication, not only by Disallow: /.

Checklist for the Tallpine launch

Step Check

1

Search Engine Friendly URLs and Use URL Rewriting on; .htaccess or the nginx try_files rule in place; a deep link such as /trails/alpine/lake-tarn-circuit opens directly.

2

A menu item exists for every important category; aliases are final; Remove IDs from URLs decided before launch.

3

System - SEF: Site Domain set, Strict handling of index.php on, trailing slash policy chosen, Strict Routing on.

4

Site Meta Description set; Robots at index, follow; each menu item has a Browser Page Title and Meta Description.

5

Redirects: System - Redirect enabled, Collect URLs on during the first weeks, old addresses imported, junk excluded.

6

Schema.org: System plugin saved with Organization data and logo; Article and Event plugins enabled; a sample page validates.

7

robots.txt in the web root with a Sitemap: line; sitemap extension installed and submitted.

8

After go-live: monitor the search console for 404s and coverage, and review the Redirects list weekly.