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 (nomodule.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 (
importRedirectsFromCsvin theh1-seocatalog) and the CSV import dialog. - 15 decoration files and 6 i18n bundles (default +
_en+_es). - 5 MCP tools, active only when
h1-mcp-apiis 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. Until1.0.2is released, take it from the snapshot repository and use1.0.2-SNAPSHOTas the version.
Deploy¶
- Add the dependency to your webapp POM.
- Build and deploy the WAR as usual.
- Start the instance. On first start the module installs itself: the
h1-seo-redirectsworkspace is created on demand by the content type (autoCreate: true), and the redirect CSV import command is bootstrapped into theh1-seocommand catalog atconfig:/modules/h1-seo/commands/h1-seo/importRedirectsFromCsv. - Check Tools › Modules for
h1-seoand confirm the version. - 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-integrationandrest-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
- The outer composite must be named
h1-seo: every feature decoration adds itself inside a composite with that name. - Mandatory at every level of the chain.
currentItemProviderkeeps 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)
- The complete metadata field set. Include it, do not copy it — that is how an upgrade reaches you.
- The sitemap fields only need to be on the templates whose entries you want to control. In the
shipped decorations,
BaseandFrontPageget both panels, andCaseStudyDetailgets 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)
- Adds the
h1-seotab 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
nameof 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.yamlis named aftername: h1-case-studies-app, not after theh1-case-studiesdirectory.
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 thefallbacksite offrontend-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:
[]fromsitemap,llmsorredirectsis a valid answer: the pages or the rules are not there yet. It is not a failure.404fromrobotsmeans there is no page namedrobotsat that root. A page that exists with an empty body answers{"content":""}, which is also valid — an emptyrobots.txtallows everything.404frommetadata/{path}means the path does not exist or the node is not anmgnl:page. Asking for a component's path is a 404 by design.- The
csv-templatecall returnstext/csvwith 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. |