Developers · Installation

What you get

One JAR — com.habito1:h1-seo — containing:

  • 5 light modules: h1-seo-metadata, h1-seo-sitemap, h1-seo-robots, h1-seo-llms, h1-seo-redirect. The JAR also carries a sixth resource folder, h1-seo/, which belongs to the Java module itself (no module.yaml): it holds the shared [H1] SEO tab decoration, the AdminCentral launcher group and the module's own i18n bundle.
  • 9 singleton components registered in the module descriptor: SitemapService, RobotsService, MetadataService, LlmsService, RedirectService, plus the redirect administration stack (RedirectRepository, RedirectMapper, RedirectCrudService, RedirectCsvImportHandler).
  • 6 REST endpoint definitions exposing 13 routes under /.rest/v1/seo/*.
  • 4 virtual URI mappings: three regex mappings (/sitemap.xml, /robots.txt, /llms.txt, each also matching /{brand}/…) and one class-based resolver for the redirects.
  • 3 page templates with their dialogs (sitemap, robots, llms), 1 content app (h1-seo-redirects-app), 1 content type (h1-seo-redirect) and 1 JCR workspace (h1-seo-redirects).
  • 1 command (importRedirectsFromCsv in the h1-seo catalog) and the CSV import dialog.
  • 15 decoration files and 6 i18n bundles (default + _en + _es).
  • 5 MCP tools, active only when h1-mcp-api is present.

Maven coordinates

<!-- your-webapp/pom.xml -->
<dependency>
  <groupId>com.habito1</groupId>
  <artifactId>h1-seo</artifactId>
  <version>1.0.2</version>
</dependency>

h1-helpers travels with the module as a compile dependency, so you do not declare it — although declaring it explicitly in the WAR is the way to pin the deployed version. h1-mcp-api is compile + optional: it is not pulled in transitively, and without it the h1McpTools/ folder is simply never read.

Add the H1 repositories to your settings.xml (or pom.xml):

<repository>
  <id>h1.magnolia.releases</id>
  <url>https://nexus.habito1.com/repository/h1-magnolia-releases/</url>
  <releases><enabled>true</enabled></releases>
  <snapshots><enabled>false</enabled></snapshots>
</repository>
<repository>
  <id>h1.magnolia.snapshots</id>
  <url>https://nexus.habito1.com/repository/h1-magnolia-snapshots/</url>
  <releases><enabled>false</enabled></releases>
  <snapshots><enabled>true</enabled></snapshots>
</repository>

Credentials are required to access these repositories. Contact us to get access.

Note — the current source tree is at 1.0.2-SNAPSHOT. Until 1.0.2 is released, take it from the snapshot repository and use 1.0.2-SNAPSHOT as the version.

Deploy

  1. Add the dependency to your webapp POM.
  2. Build and deploy the WAR as usual.
  3. Start the instance. On first start the module installs itself: the h1-seo-redirects workspace is created on demand by the content type (autoCreate: true), and the redirect CSV import command is bootstrapped into the h1-seo command catalog at config:/modules/h1-seo/commands/h1-seo/importRedirectsFromCsv.
  4. Check Tools › Modules for h1-seo and confirm the version.
  5. Confirm the app: AdminCentral shows an [H1] SEO group in the launcher with the Redirects app in it.

Note — the module descriptor declares dependencies on core, rest-integration and rest-services. A webapp without the REST modules will not start this one. That only happens on custom, minimal Magnolia webapps; the standard bundles include them.

Wiring you have to do

The module ships the fields, the templates, the services and the endpoints. It cannot guess where in your project they belong.

Out of the box the decorations target the H1 platform's site light module, frontend-md3, and its page dialogs (Base, FrontPage, CaseStudyDetail). A decoration names its target module in its own path, so on an instance without a light module called frontend-md3 those decorations simply never apply — no error, no log line, no tab. Treat them as the reference implementation and reproduce them against your own dialogs and site, in your own light module.

Four steps, plus the pages listed further down.

1. The [H1] SEO tab in page dialogs

The tab itself is created once by decorating your base page dialog; each feature then hangs its own composite inside it. Create the tab first:

# <your-light-module>/decorations/<your-light-module>/dialogs/pages/Base/Base.yaml
form:
  properties:
    h1-seo:                          # (1)
      label: h1-seo.tab.h1-seo.label
      $type: compositeField
      layout:
        $type: stacked
      itemProvider:
        $type: currentItemProvider   # (2)
      properties:
  layout:
    tabs:
      h1-seo:
        label: h1-seo.tab.h1-seo.label
        fields:
          - name: h1-seo
  1. The outer composite must be named h1-seo: every feature decoration adds itself inside a composite with that name.
  2. Mandatory at every level of the chain. currentItemProvider keeps the sub-fields as flat properties of the page node, which is how the services read them. Without it the values are stored in a child node and nothing reads them — and nothing complains.

Then add the features you want, in a decoration per feature:

# <your-light-module>/decorations/<your-light-module>/dialogs/pages/Base/Base.yaml
form:
  properties:
    h1-seo:
      properties:
        h1-seo-metadata:
          label: h1-seo.tab.h1-seo-metadata.label
          $type: compositeField
          layout:
            $type: stacked
          itemProvider:
            $type: currentItemProvider
          properties: !include:/h1-seo-metadata/includes/fields.yaml   # (1)
        h1-seo-sitemap:
          label: h1-seo.tab.h1-seo-sitemap.label
          $type: compositeField
          layout:
            $type: stacked
          itemProvider:
            $type: currentItemProvider
          properties: !include:/h1-seo-sitemap/includes/fields.yaml    # (2)
  1. The complete metadata field set. Include it, do not copy it — that is how an upgrade reaches you.
  2. The sitemap fields only need to be on the templates whose entries you want to control. In the shipped decorations, Base and FrontPage get both panels, and CaseStudyDetail gets the sitemap panel only, because it is a dynamic template: its own address is never published, only one address per item.

2. The [H1] SEO tab in content apps

Content apps have no base dialog to decorate, so the tab is provided as an include:

# <your-light-module>/decorations/<the-app's-light-module>/apps/<app-name>.yaml
subApps:
  detail:
    form:
      properties:
        h1-seo:
          label: h1-seo.tab.h1-seo.label
          $type: compositeField
          layout:
            $type: stacked
          itemProvider:
            $type: currentItemProvider
          properties:
            h1-seo-metadata:
              label: h1-seo.tab.h1-seo-metadata.label
              $type: compositeField
              layout:
                $type: stacked
              itemProvider:
                $type: currentItemProvider
              properties: !include:/h1-seo-metadata/includes/fields.yaml
      layout:
        tabs: !include:/h1-seo-metadata/includes/tabs.yaml   # (1)
  1. Adds the h1-seo tab pointing at the outer composite. Adding another SEO feature to the app is then one more composite inside, with no change to the tab.

The module ships this decoration for two apps: h1-ecommerce-products-app and h1-case-studies-app.

Warning — the decoration file name must match the name of the target app, not its folder and not its label. If it does not match, the decoration is silently ignored: no tab, no error, no log line. h1-case-studies-app.yaml is named after name: h1-case-studies-app, not after the h1-case-studies directory.

3. Template availability

The sitemap, robots and llms page templates must be available in your site before an author can create those pages. Either declare them explicitly in your site definition:

# <your-light-module>/decorations/<site-light-module>/sites/<site>.yaml
templates:
  availability:
    templates:
      h1-seo-sitemap:
        id: 'h1-seo-sitemap:pages/sitemap'
      h1-seo-robots:
        id: 'h1-seo-robots:pages/robots'
      h1-seo-llms:
        id: 'h1-seo-llms:pages/llms'

…or reuse the include each feature publishes, one decoration file per feature:

# <your-light-module>/decorations/<site-light-module>/sites/<site>.templates.availability.templates.yaml
!include:/h1-seo-sitemap/includes/pageTemplates.yaml

Note — one !include: per file. Magnolia merges decorations that come from different light modules onto the same target, which is why the module ships one such file inside each feature light module (h1-seo-sitemap, h1-seo-robots, h1-seo-llms) rather than one file with three includes. All three target the fallback site of frontend-md3. If you want the three ids in a single file of your own, write them out as in the first example.

4. Server-side <head> (server-rendered channels only)

Skip this if your front end is headless — it builds the <head> from /.rest/v1/seo/metadata. For a Freemarker channel, include the ready-made partial in your page template's <head>:

[#include "/h1-seo-metadata/includes/seo-metadata.ftl"]

It reads the h1seo* properties of the current page and emits the title, meta description, Open Graph, Twitter card, the robots meta tag, the canonical link and the JSON-LD — plus a separate FAQPage block when there are FAQ entries. It resolves the DAM asset of the social image and the site logo to absolute URLs, so the server-rendered channel needs anonymous read on dam (see Permissions).

For a detail page rendering an item of a content type, pass the item's node explicitly before the include — the item lives in another workspace, so the page node is not where the metadata is:

[#assign seoNode = cmsfn.contentById(itemUuid, "h1-ecommerce-products")!]
[#include "/h1-seo-metadata/includes/seo-metadata.ftl"]

seoNode is optional and backwards compatible: without it the include falls back to cmsfn.page(content). The canonical URL and the site globals are always derived from the page, never from the item — a content item's JCR path is not a navigable URL.

Permissions

The public channels — the REST endpoints and the three regex virtual URIs — run in the request context, which on a public instance means as the anonymous user. Grant anonymous read access to:

Workspace Needed for
website Everything except the redirects: sitemap, robots, llms.txt, page metadata. Without it all of them come back empty or 404.
Each source workspace of a dynamic page The per-item entries of the sitemap and of llms.txt (the workspace named in h1sitemapSourceWorkspace, e.g. h1-case-studies, h1-ecommerce-products). Without it the collection contributes no entries at all, silently.
h1-sites Optional. The site globals: organization name and type, logo, social profiles, default social image. Without it the metadata is returned without the publisher block and without the image fallback.
dam Only for the server-rendered seo-metadata.ftl include, which resolves the social image and the logo to absolute URLs. The headless channel returns the raw DAM reference and needs nothing.

h1-seo-redirects is not in the table, and that is deliberate: RedirectService reads it in the system context, so neither the public redirect endpoint nor the virtual URI depends on an anonymous ACL. The workspace is administered from AdminCentral and only active rules ever leave it.

Warning — a missing ACL does not surface as an error. The sitemap comes back without the collection's items, the metadata comes back without the publisher block, and a cross-workspace reference comes back as a raw path. If something is silently empty in production but fine on author, suspect the ACL before suspecting the code.

Author pages to create

Page node Template Serves
/sitemap h1-seo-sitemap:pages/sitemap /sitemap.xml
/robots h1-seo-robots:pages/robots /robots.txt
/llms h1-seo-llms:pages/llms /llms.txt

The node names are fixed, for two different reasons: the virtual URI mappings forward by name (forward:$1/sitemap), and RobotsService looks up a child page literally called robots. The extension never belongs in the node name — the node is sitemap, never sitemap.xml.

For a brand, create the same three pages under /{brand}; each one then covers that subtree and is served at /{brand}/sitemap.xml and so on. The scope comes from the page's parent path, which the rendering model reads at request time, so moving the page changes its scope.

The MCP tools enforce all of this when they create the pages: a fixed path of /{brand}/<type>, no extension, and one page per type per brand — including a check for a differently named page that already uses the same template.

Verify the installation

curl -s http://localhost:8080/magnoliaAuthor/.rest/v1/seo/sitemap | head
curl -s http://localhost:8080/magnoliaAuthor/.rest/v1/seo/robots
curl -s http://localhost:8080/magnoliaAuthor/.rest/v1/seo/llms | head
curl -s http://localhost:8080/magnoliaAuthor/.rest/v1/seo/redirects
curl -s "http://localhost:8080/magnoliaAuthor/.rest/v1/seo/metadata/my-page"
curl -s http://localhost:8080/magnoliaAuthor/.rest/v1/seo/redirects-admin/csv-template

What the answers mean:

  • [] from sitemap, llms or redirects is a valid answer: the pages or the rules are not there yet. It is not a failure.
  • 404 from robots means there is no page named robots at that root. A page that exists with an empty body answers {"content":""}, which is also valid — an empty robots.txt allows everything.
  • 404 from metadata/{path} means the path does not exist or the node is not an mgnl:page. Asking for a component's path is a 404 by design.
  • The csv-template call returns text/csv with the header row only.

Upgrading

The version handler applies these deltas:

Version Delta
1.0.1-SNAPSHOT Bootstraps the redirect CSV import command into the h1-seo catalog if the node is missing. Fresh installs get it from mgnl-bootstrap anyway; this delta covers instances that were already installed when the import was added.
1.0.2 Replaces the command node. The import stopped being a class of this module and became the generic h1-helpers command configured by YAML, so an already-installed node points at a class that no longer exists. The task removes config:/modules/h1-seo/commands/h1-seo/importRedirectsFromCsv if present and re-bootstraps it.