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 takesdomainand 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,domainsandsocialMedia(as an array of{provider, value}). Two extras are declared explicitly:id, an alias fornodeNameused on create, anddomain, the argument ofget_by_domain. - On create, the node name is resolved from
nodeName, thenid, then a slugifiedname, and only then a uuid. A create with justname: "QA Site"producesqa-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 exposescreate_folderandmovefrom its base class. Moving a site into a folder makes it invisible tolistand 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