Developers · Configuration

There is nothing to configure under config:/modules/h1-sites. A site's configuration is content — properties on a node of the h1-sites workspace — and the rest is YAML in h1-sites-light-module. This page is the reference for both, plus the rules the service applies on top of them.

The site content type

h1-site is defined in h1-sites-light-module/contentTypes/h1-site.yaml over the h1-sites workspace, with autoCreate: true (the workspace is created on first start) and nodeType: mgnl:content.

Fields

The form is h1-sites-light-module/apps/h1-sites-app.yaml, generated from the content type with !content-type:h1-site and then completed field by field. Everything except socialMedia lands as a flat property on the site node.

Property Field type i18n Notes
name textField ✓ Required. Translations are stored as name_<locale>; the default language lives in the bare property.
rootWebsite pageLinkField + JcrNodeToPathConverter Required. Stores the absolute path of the page in website, e.g. /h1. showOptions: true. This is the value the brand convention is built on: the brand is rootWebsite without the leading slash.
description textField (rows: 5, maxLength: 200) ✓ The length limit is enforced by the form only — a write through REST or MCP is not truncated.
logo damLinkField Stores jcr:<uuid>, not a bare uuid. The jcr: prefix is part of the stored value.
favicon damLinkField Same storage format as logo.
defaultOgImage damLinkField Same storage format. Consumed by h1-seo as the fallback og:image.
organizationType comboBoxField defaultValue: Organization. Options: Organization, Corporation, LocalBusiness, ProfessionalService, OnlineBusiness, EducationalOrganization, NGO. Free text is not restricted on a write through REST or MCP.
phone textField
baseUrl textField Not normalised, not validated. Stored exactly as typed.
domains multiValueField of textField Required. Stored as a multi-valued String property. Each value is a pattern — see Domain matching.
socialMedia jcrMultiField of compositeField (jcrChildNodeProvider) The one field that is not flat: it is a socialMedia container node with children 0…n, each holding provider and value. provider is a combo of instagram, facebook, tiktok, linkedin, whatsapp, youtube, twitter, pinterest.

The content type declares one sub-model, socialMediaItem (provider, value), which is what makes socialMedia a list of child nodes rather than a property.

Where sites are stored

JcrSiteRepository.findAll() overrides the base implementation on purpose: instead of an SQL2 query with an undefined order, it iterates the direct children of the workspace root and keeps the ones of type mgnl:content. That gives the sibling order shown — and drag-reordered — in AdminCentral, which is what makes "first match wins" a decision an editor controls.

The consequence is worth stating plainly: it does not descend into folders. mgnl:folder extends nt:folder, not mgnl:content, so a folder is skipped and its children are never visited. A site stored inside a folder is invisible to findAll(), and therefore to GET /v1/sites, getSite and the MCP list action. Only ?marca= reaches it.

Resolution

getSite precedence

domain is tried first, rootWebsite second. The second is a fallback, not a filter: it is used when domain was not sent, or when it was sent and matched nothing.

Request Resolved result
?domain=www.habito1.com (a site claims it) That site.
?domain=unknown.com (no rootWebsite) 404 — No site matches domain 'unknown.com' or rootWebsite 'null'.
?domain=unknown.com&rootWebsite=/h1 The site whose rootWebsite is /h1; 404 if none.
?rootWebsite=h1 The site whose rootWebsite is /h1 — the value is normalised before comparing.
?rootWebsite=/h1/ Same site. The trailing slash is stripped.
neither parameter 400 — domain or rootWebsite parameter is required.
both, domain matches The domain's site. rootWebsite is not consulted.

rootWebsite is normalised on both sides before comparison: trimmed, given a leading /, stripped of a trailing /. The comparison is then exact — it is not a prefix match, so /h1 never resolves a site rooted at /h1-shop.

Domain matching

Each value of domains is treated as a regular expression, after one rewrite: a * that is not preceded by a . becomes .*. That single rule is what lets a glob and a regex both work.

Stored pattern Compiled to Notes
habito1.com habito1.com Exact, except that . is still the regex any-character.
*.habito1.com .*.habito1.com The leading * is rewritten; the bare domain does not match.
www.* www.* The * follows a ., so it is left alone and works as the regex quantifier.
* .* Catch-all.

The test is Pattern.matches() — the whole value must match, not a substring. An invalid pattern is not fatal: PatternSyntaxException is caught, logged as Invalid domain pattern '…', and treated as "no match", so one bad row cannot take a site down.

Sites are checked in sibling order and the first site with any matching row wins. The domain value is whatever the caller sends; the module never reads the Host header itself.

REST reference

Base path /.rest. The resource is registered by h1-sites-light-module/restEndpoints/sites.yaml, which declares only the implementation class — no path key — so the @Path("/v1/sites") annotation on SiteController is what defines the URL. All responses are application/json.

Endpoint Parameters Returns
GET /v1/sites marca — optional; restricts the listing to the /{marca} folder Array of sites, in sibling order. [] when there are none. Without marca, top-level sites only.
GET /v1/sites/id/{uuid} — One site. 404 when the uuid is unknown.
GET /v1/sites/getSite domain, rootWebsite — at least one required One site, including mdTheme. 400 with neither, 404 when nothing matches.
POST /v1/sites marca — optional; creates inside the /{marca} folder, creating the folder if needed. Body: a site 201 with the created site.
PUT /v1/sites/id/{uuid} Body: a site 200 with the updated site. 404 when the uuid is unknown.
DELETE /v1/sites/id/{uuid} — 204 with no body. 404 when the uuid is unknown.

Errors are ApiError from h1-helpers: 400 bad request, 404 not found, 500 server error.

Response shape

Field Notes
uuid, path, nodeName Identity of the stored node.
name, description Localised for the request — see Language.
logo, favicon, defaultOgImage The stored jcr:<uuid> values, not resolved URLs. The caller resolves the asset.
organizationType, phone, baseUrl, rootWebsite As stored.
socialMedia Array of {provider, value}, in stored order. [] when there are none.

Write semantics

POST and PUT take the same JSON body: nodeName, name, description, logo, favicon, defaultOgImage, organizationType, phone, baseUrl, rootWebsite, domains (array of strings) and socialMedia (array of {provider, value}).

SiteMapper.toNode uses the h1-helpers setters, and their null-handling is the contract:

Field in the body Effect
A scalar with a value Written.
A scalar absent or null Left untouched — this is what makes a partial PUT work.
A scalar as "" Written as an empty string, which is how you clear one.
domains absent, null or [] Left untouched. setMultiString skips empty lists, so the patterns cannot be cleared through this API.
socialMedia with rows Rewritten in place, matched to existing children by provider, so uuids and translations of unchanged rows survive.
socialMedia absent, null or [] The whole container is removed — every social profile is deleted.

Language

name and description are localised per request by RequestLocale: the Accept-Language header is matched against the locales configured in Magnolia, falling back to the primary subtag and then to the default locale. Translations live in name_<locale> / description_<locale>; the default language is the bare property, so a language with no translation falls back to it rather than returning empty.

Everything else in the response is language-independent.

MCP tool

Present only when h1-mcp-api is installed. h1-sites-light-module/h1McpTools/site.yaml declares the class, and the McpToolRegistry discovers it by watching the h1McpTools/ folder of every module — dropping the JAR in is enough.

Tool Class Scope
site com.habito1.site.mcp.SiteTool Full CRUD over the h1-site content type in h1-sites, plus get_by_domain. Sites are created flat at /.
  • Standard actions come from AbstractContentTypeTool: list, get, create, update, delete, publish, unpublish, create_folder, move, rename, rename_folder, delete_folder, reorder, remove_property.
  • Own action: get_by_domain, which takes domain and runs the same matcher the REST resolution uses.
  • Writable fields are derived from the form, not hard-coded — name, description, logo, favicon, defaultOgImage, organizationType, phone, baseUrl, rootWebsite, domains and socialMedia (as an array of {provider, value}). Two extras are declared explicitly: id, an alias for nodeName used on create, and domain, the argument of get_by_domain.
  • On create, the node name is resolved from nodeName, then id, then a slugified name, and only then a uuid. A create with just name: "QA Site" produces qa-site.
  • On update, the payload is merged onto the stored item, so unmentioned fields keep their values — including socialMedia.

Warning — the tool creates sites at / but exposes create_folder and move from its base class. Moving a site into a folder makes it invisible to list and to domain resolution. Folders are for other content types; sites belong at the top level.

Module components

Registered as singletons in META-INF/magnolia/h1-sites.xml:

SiteRepository → JcrSiteRepository · SiteMapper · SiteService