@jahia/agentic 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/antigravity/.agents/rules/jahia.md +51 -0
  3. package/dist/antigravity/.agents/skills/jahia-cnd-author/SKILL.md +94 -0
  4. package/dist/antigravity/.agents/skills/jahia-cnd-author/references/cnd-area-types.md +55 -0
  5. package/dist/antigravity/.agents/skills/jahia-cnd-author/references/cnd-authoring-experience.md +92 -0
  6. package/dist/antigravity/.agents/skills/jahia-cnd-author/references/cnd-child-nodes.md +74 -0
  7. package/dist/antigravity/.agents/skills/jahia-cnd-author/references/cnd-jahia-mixins.md +510 -0
  8. package/dist/antigravity/.agents/skills/jahia-cnd-author/references/cnd-modeling-decisions.md +87 -0
  9. package/dist/antigravity/.agents/skills/jahia-cnd-author/references/cnd-numbers-dates.md +92 -0
  10. package/dist/antigravity/.agents/skills/jahia-cnd-author/references/cnd-string-selectors.md +133 -0
  11. package/dist/antigravity/.agents/skills/jahia-cnd-author/references/cnd-syntax.md +119 -0
  12. package/dist/antigravity/.agents/skills/jahia-cnd-author/references/types-ts-mapping.md +73 -0
  13. package/dist/antigravity/.agents/skills/jahia-dev-accessibility/SKILL.md +11 -0
  14. package/dist/antigravity/.agents/skills/jahia-dev-build-component/SKILL.md +133 -0
  15. package/dist/antigravity/.agents/skills/jahia-dev-create-page-template/SKILL.md +341 -0
  16. package/dist/antigravity/.agents/skills/jahia-dev-create-template-set/SKILL.md +205 -0
  17. package/dist/antigravity/.agents/skills/jahia-dev-create-view/SKILL.md +896 -0
  18. package/dist/antigravity/.agents/skills/jahia-dev-debug/SKILL.md +176 -0
  19. package/dist/antigravity/.agents/skills/jahia-dev-import-from/SKILL.md +244 -0
  20. package/dist/antigravity/.agents/skills/jahia-dev-jexperience/SKILL.md +269 -0
  21. package/dist/antigravity/.agents/skills/jahia-dev-ops/SKILL.md +50 -0
  22. package/dist/antigravity/.agents/skills/jahia-dev-ops/references/docker.md +151 -0
  23. package/dist/antigravity/.agents/skills/jahia-dev-ops/references/monitoring.md +195 -0
  24. package/dist/antigravity/.agents/skills/jahia-dev-ops/references/provisioning.md +269 -0
  25. package/dist/antigravity/.agents/skills/jahia-dev-properties/SKILL.md +147 -0
  26. package/dist/antigravity/.agents/skills/jahia-dev-properties/references/all-properties.md +231 -0
  27. package/dist/antigravity/.agents/skills/jahia-dev-query-content/SKILL.md +204 -0
  28. package/dist/antigravity/.agents/skills/jahia-dev-review/SKILL.md +228 -0
  29. package/dist/antigravity/.agents/skills/jahia-dev-review-cnd/SKILL.md +79 -0
  30. package/dist/antigravity/.agents/skills/jahia-dev-review-cnd/scripts/check-cnd.d.mts +13 -0
  31. package/dist/antigravity/.agents/skills/jahia-dev-review-cnd/scripts/check-cnd.mjs +198 -0
  32. package/dist/antigravity/.agents/skills/jahia-dev-screenshot/SKILL.md +177 -0
  33. package/dist/antigravity/.agents/skills/jahia-dev-site-review/SKILL.md +70 -0
  34. package/dist/antigravity/.agents/skills/jahia-dev-site-review/scripts/review-pages.mjs +85 -0
  35. package/dist/antigravity/.agents/skills/jahia-dev-start-local/SKILL.md +121 -0
  36. package/dist/antigravity/.agents/skills/jahia-jcr-sql2/SKILL.md +258 -0
  37. package/dist/antigravity/AGENTS.md +62 -0
  38. package/dist/claude/.mcp.json +11 -0
  39. package/dist/cursor/.cursor/mcp.json +11 -0
  40. package/dist/gemini/.gemini/settings.json +10 -0
  41. package/dist/index.js +12 -0
  42. package/dist/kiro/.kiro/settings/mcp.json +10 -0
  43. package/dist/kiro/.kiro/skills/jahia-cnd-author/SKILL.md +94 -0
  44. package/dist/kiro/.kiro/skills/jahia-cnd-author/references/cnd-area-types.md +55 -0
  45. package/dist/kiro/.kiro/skills/jahia-cnd-author/references/cnd-authoring-experience.md +92 -0
  46. package/dist/kiro/.kiro/skills/jahia-cnd-author/references/cnd-child-nodes.md +74 -0
  47. package/dist/kiro/.kiro/skills/jahia-cnd-author/references/cnd-jahia-mixins.md +510 -0
  48. package/dist/kiro/.kiro/skills/jahia-cnd-author/references/cnd-modeling-decisions.md +87 -0
  49. package/dist/kiro/.kiro/skills/jahia-cnd-author/references/cnd-numbers-dates.md +92 -0
  50. package/dist/kiro/.kiro/skills/jahia-cnd-author/references/cnd-string-selectors.md +133 -0
  51. package/dist/kiro/.kiro/skills/jahia-cnd-author/references/cnd-syntax.md +119 -0
  52. package/dist/kiro/.kiro/skills/jahia-cnd-author/references/types-ts-mapping.md +73 -0
  53. package/dist/kiro/.kiro/skills/jahia-dev-accessibility/SKILL.md +11 -0
  54. package/dist/kiro/.kiro/skills/jahia-dev-build-component/SKILL.md +133 -0
  55. package/dist/kiro/.kiro/skills/jahia-dev-create-page-template/SKILL.md +341 -0
  56. package/dist/kiro/.kiro/skills/jahia-dev-create-template-set/SKILL.md +205 -0
  57. package/dist/kiro/.kiro/skills/jahia-dev-create-view/SKILL.md +896 -0
  58. package/dist/kiro/.kiro/skills/jahia-dev-debug/SKILL.md +176 -0
  59. package/dist/kiro/.kiro/skills/jahia-dev-import-from/SKILL.md +244 -0
  60. package/dist/kiro/.kiro/skills/jahia-dev-jexperience/SKILL.md +269 -0
  61. package/dist/kiro/.kiro/skills/jahia-dev-ops/SKILL.md +50 -0
  62. package/dist/kiro/.kiro/skills/jahia-dev-ops/references/docker.md +151 -0
  63. package/dist/kiro/.kiro/skills/jahia-dev-ops/references/monitoring.md +195 -0
  64. package/dist/kiro/.kiro/skills/jahia-dev-ops/references/provisioning.md +269 -0
  65. package/dist/kiro/.kiro/skills/jahia-dev-properties/SKILL.md +147 -0
  66. package/dist/kiro/.kiro/skills/jahia-dev-properties/references/all-properties.md +231 -0
  67. package/dist/kiro/.kiro/skills/jahia-dev-query-content/SKILL.md +204 -0
  68. package/dist/kiro/.kiro/skills/jahia-dev-review/SKILL.md +228 -0
  69. package/dist/kiro/.kiro/skills/jahia-dev-review-cnd/SKILL.md +79 -0
  70. package/dist/kiro/.kiro/skills/jahia-dev-review-cnd/scripts/check-cnd.d.mts +13 -0
  71. package/dist/kiro/.kiro/skills/jahia-dev-review-cnd/scripts/check-cnd.mjs +198 -0
  72. package/dist/kiro/.kiro/skills/jahia-dev-screenshot/SKILL.md +177 -0
  73. package/dist/kiro/.kiro/skills/jahia-dev-site-review/SKILL.md +70 -0
  74. package/dist/kiro/.kiro/skills/jahia-dev-site-review/scripts/review-pages.mjs +85 -0
  75. package/dist/kiro/.kiro/skills/jahia-dev-start-local/SKILL.md +121 -0
  76. package/dist/kiro/.kiro/skills/jahia-jcr-sql2/SKILL.md +258 -0
  77. package/dist/kiro/.kiro/steering/jahia.md +55 -0
  78. package/dist/kiro/AGENTS.md +62 -0
  79. package/dist/opencode/opencode.json +12 -0
  80. package/package.json +1 -1
@@ -0,0 +1,896 @@
1
+ ---
2
+ name: jahia-dev-create-view
3
+ description: Implements a React view for a Jahia content type. Use when asked to create or update the rendering of a component, add a new view, or add styling.
4
+ allowed-tools: Bash, Read, Write, Edit
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ A **view** tells Jahia how to render a content type. Views are React components (TypeScript/TSX) registered with the `jahiaComponent` function. They follow the **Single Directory Component (SDC)** pattern alongside the `definition.cnd`.
10
+
11
+ ---
12
+
13
+ ## File naming convention
14
+
15
+ | Filename | Meaning |
16
+ |---|---|
17
+ | `default.server.tsx` | Default server-side rendered view |
18
+ | `<name>.server.tsx` | Named view (e.g. `small.server.tsx`) |
19
+ | `<name>.client.tsx` | Client-side rendered (interactive) view |
20
+
21
+ A node type can have **multiple views**. When `name` is omitted in `jahiaComponent`, the view is the default.
22
+
23
+ ---
24
+
25
+ ## Step 1 — Create the view file
26
+
27
+ In the component folder (`src/components/<Category>/<Name>/`), create `default.server.tsx`:
28
+
29
+ ```tsx
30
+ import { jahiaComponent, buildNodeUrl, RenderChildren, RenderChild } from "@jahia/javascript-modules-library";
31
+ import type { Props } from "./types.js";
32
+ import classes from "./component.module.css";
33
+
34
+ jahiaComponent(
35
+ {
36
+ componentType: "view", // always "view" for a component (use "template" for page templates)
37
+ nodeType: "namespace:typeName",
38
+ displayName: "Human Readable Name",
39
+ // name: "small", // omit for default view; set for named views
40
+ },
41
+ ({ title, subtitle, background }: Props) => (
42
+ <section className={classes.root}>
43
+ <h2>{title}</h2>
44
+ <p>{subtitle}</p>
45
+ </section>
46
+ ),
47
+ );
48
+ ```
49
+
50
+ `jahiaComponent` **returns its second argument** (the React component). Export it to reuse the component directly in other views:
51
+
52
+ ```tsx
53
+ // small.server.tsx — registered as a named view AND exported for direct reuse
54
+ export const SmallHero = jahiaComponent(
55
+ { componentType: "view", nodeType: "ns:heroSection", name: "small" },
56
+ ({ title, background }: Props) => <header style={{ backgroundImage: `url(${buildNodeUrl(background)})` }}><h1>{title}</h1></header>,
57
+ );
58
+
59
+ // fullPage.server.tsx — reuse the component directly without going through Jahia rendering
60
+ import { SmallHero } from "../Hero/Section/small.server.jsx";
61
+ <SmallHero title={title} background={cover} />
62
+ ```
63
+
64
+ ### When implementing a view from existing HTML
65
+
66
+ When you have a source HTML fragment to translate (e.g. from `/jahia-dev-import-from`), apply only **mechanical transformations**:
67
+
68
+ - `class=` → `className=`
69
+ - Void elements: `<img>`, `<input>`, `<br>` → self-close with ` />`
70
+ - `{placeholder}` text → `{propName}` matching `Props`
71
+
72
+ **Never** remove, rearrange, or simplify elements. Every `data-*`, `aria-*`, `role`, `id`, `<noscript>`, and `<source>` must appear in the TSX output. Carousel and slider wrapper `id`s in particular must be preserved verbatim — JS libraries use them for initialization.
73
+
74
+ **Self-check before finishing:** Count the attributes on 2–3 key elements in the source HTML. If the source `<div>` has 6 attributes and your TSX has 4, you dropped something — go back.
75
+
76
+ **CSS class names:** Rename source HTML class names to CSS Module keys (`hero__title` → `classes.heroTitle`). If the component also imports a vendor CSS file as a static asset (see `jahia-dev-import-from`), those vendor classes stay as plain strings in the JSX — they are not processed by CSS Modules.
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Step 1b — Accessibility and SEO rules (apply to every view)
82
+
83
+ Build these requirements in from the start — retrofitting them later is more expensive.
84
+
85
+ ### Semantic HTML structure
86
+
87
+ | Element | Rule |
88
+ |---|---|
89
+ | `<section>`, `<article>` | Wrap every self-contained block of content |
90
+ | `<header>` / `<footer>` | Use for the page header and footer in page templates |
91
+ | `<nav>` | Wrap navigation menus; add `aria-label` when there are multiple navs |
92
+ | `<main>` | Exactly one per page, wrapping all page body content (already enforced by the Layout component for page templates) |
93
+ | Headings | Each page must have exactly one `<h1>`; section headings use `<h2>`; sub-section headings use `<h3>`. Never skip levels. |
94
+
95
+ ### Images
96
+
97
+ Every `<img>` must have an `alt` attribute. Decorative images use `alt=""`. Informational images use a descriptive string from a CND property:
98
+
99
+ ```tsx
100
+ // ❌ Missing alt
101
+ <img src={buildNodeUrl(props.image)} />
102
+
103
+ // ✅ Alt from the image node's title (no extra CND property needed)
104
+ <img src={buildNodeUrl(props.image)} alt={props.image?.getPropertyAsString("jcr:title") ?? ""} />
105
+ ```
106
+
107
+ The image node already has `jcr:title` (from `mix:title`). **Do not add `imageAlt (string) i18n`** to the CND — it forces editors to enter duplicate data.
108
+
109
+ ### Colour contrast
110
+
111
+ Use colours with a contrast ratio ≥ 4.5:1 for body text and ≥ 3:1 for large text (18px+ or bold 14px+) against the background. Avoid light grey on white. When in doubt, use near-black (`#333333` or `#1a1a1a`) for body text.
112
+
113
+ ### Link and button names
114
+
115
+ Every `<a>` and `<button>` must have an accessible name — either visible text or `aria-label`. Icon-only buttons must have `aria-label`:
116
+
117
+ ```tsx
118
+ // ❌ No accessible name
119
+ <button><svg>…</svg></button>
120
+
121
+ // ✅ Accessible name via aria-label
122
+ <button aria-label="Close menu"><svg aria-hidden="true">…</svg></button>
123
+ ```
124
+
125
+ ### Focus styles
126
+
127
+ Never suppress focus indicators globally. Use `:focus-visible` to style keyboard focus:
128
+
129
+ ```css
130
+ /* ❌ Never do this */
131
+ * { outline: none; }
132
+
133
+ /* ✅ Style keyboard focus without affecting mouse users */
134
+ :focus-visible { outline: 2px solid #0969da; outline-offset: 2px; }
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Step 2 — Import Props from types.ts
140
+
141
+ Always import `Props` from `./types.js` (not `./types.ts` — use `.js` extension at import time):
142
+
143
+ ```ts
144
+ import type { Props } from "./types.js";
145
+ ```
146
+
147
+ If `types.ts` doesn't exist yet, create it first (see `jahia-dev-define-content-type` skill).
148
+
149
+ ---
150
+
151
+ ## CMS rule — never hardcode links or URLs
152
+
153
+ > ⚠️ **This is a CMS. All links must come from contributed content — never from hardcoded strings in code.**
154
+
155
+ > 🚫 **NEVER use an external link (`j:linkType: "external"`) to point to an internal Jahia page.** Use `"internal"` with `j:linknode` instead. An external URL hardcoded to an internal path breaks on environment changes, language switches, workspace toggling (live/preview), and vanity URL rewrites.
156
+
157
+ ```tsx
158
+ // ❌ Wrong — hardcoded URL
159
+ <a href="https://www.jahia.com">Jahia</a>
160
+ <a href="/en/documentation">Documentation</a>
161
+
162
+ // ❌ Wrong — external link used for an internal page
163
+ // j:linkType: "external", j:url: "/sites/mySite/documentation.html"
164
+
165
+ // ✅ Correct — internal link to a JCR node
166
+ switch (props["j:linkType"]) {
167
+ case "internal": return <a href={buildNodeUrl(props["j:linknode"])}>{props.label}</a>;
168
+ case "external": return <a href={props["j:url"]}>{props.label}</a>; // only for truly external URLs
169
+ }
170
+
171
+ // ✅ Correct — URL resolved from a JCR node at render time
172
+ <a href={buildNodeUrl(currentNode)}>{title}</a>
173
+ ```
174
+
175
+ This applies everywhere: `href`, `src`, `action`, `data-url`. If a link needs to appear on screen, it must have a corresponding contributed field (`j:linkType`, `weakreference`, or similar). The only exception is links within the CMS UI itself (edit mode chrome).
176
+
177
+ ---
178
+
179
+ ## Step 3 — Use library helpers as needed
180
+
181
+ ### `buildNodeUrl(node)` — convert a JCR node to a URL
182
+
183
+ ```tsx
184
+ import { buildNodeUrl } from "@jahia/javascript-modules-library";
185
+
186
+ <img src={buildNodeUrl(coverNode)} alt="Descriptive alt text" />
187
+ <header style={{ backgroundImage: `url(${buildNodeUrl(background)})` }}>
188
+ ```
189
+
190
+ **Options** (second argument):
191
+
192
+ | Option | Default | Use |
193
+ |---|---|---|
194
+ | `extension` | `.html` | Change output extension, e.g. `extension: ".pdf"` |
195
+ | `language` | current language | Override language: `language: "fr"` |
196
+ | `mode` | current mode | Force workspace: `"edit"`, `"preview"`, or `"live"` |
197
+ | `parameters` | — | Append query params: `parameters: { page: "2" }` |
198
+
199
+ ```tsx
200
+ // Link to the blog page in the current language
201
+ <a href={buildNodeUrl(renderContext.getSite().getNode("blog"))}>Blog</a>
202
+
203
+ // Same link forced to French
204
+ <a href={buildNodeUrl(blogNode, { language: "fr" })}>Blog (FR)</a>
205
+ ```
206
+
207
+ > ⚠️ **Always guard optional nodes**: `buildNodeUrl(undefined)` throws `"Expected a node in buildNodeUrl, received undefined"`. If the prop is optional in the CND, guard it:
208
+ > ```tsx
209
+ > // ❌ Crashes when background is not set
210
+ > style={{ backgroundImage: `url(${buildNodeUrl(background)})` }}
211
+ >
212
+ > // ✅ Safe
213
+ > style={background ? { backgroundImage: `url(${buildNodeUrl(background)})` } : undefined}
214
+ > ```
215
+
216
+ > ⚠️ **Caching rule**: Never render properties of a **weakreference** node directly in the same view. Doing so will produce stale output because Jahia's cache is based on the referencing node, not the referenced one. Instead, render the referenced node using `<RenderChild>` (or a dedicated sub-view), or call `addCacheDependency` explicitly. Example:
217
+ >
218
+ > ```tsx
219
+ > // ❌ Don't do this — stale on referenced node change
220
+ > <img src={buildNodeUrl(background)} alt={background.getProperty('jcr:title').getString()} />
221
+ >
222
+ > // ✅ Do this — render the referenced node as its own view
223
+ > <RenderChild name="background" />
224
+ > ```
225
+
226
+ ### `RenderChildren` — render child nodes with optional pagination and filtering
227
+
228
+ ```tsx
229
+ import { RenderChildren } from "@jahia/javascript-modules-library";
230
+
231
+ // All children
232
+ <RenderChildren />
233
+
234
+ // Offset-based pagination
235
+ <RenderChildren pagination={{ count: 10, start: 0 }} />
236
+
237
+ // Page-based pagination (for paginator UI)
238
+ <RenderChildren pagination={{ count: 10, page: 0 }} />
239
+
240
+ // Filter by node type — string (single type) or function
241
+ <RenderChildren filter="ns:cardItem" />
242
+ <RenderChildren filter={(node) => node.isNodeType("ns:highlight")} />
243
+
244
+ // Combined
245
+ <RenderChildren pagination={{ count: 6, page: 0 }} filter="ns:blogPost" />
246
+ ```
247
+
248
+ ### `RenderChild` — render a specific named child node
249
+
250
+ ```tsx
251
+ import { RenderChild } from "@jahia/javascript-modules-library";
252
+
253
+ <RenderChild name="hero" /> // default view
254
+ <RenderChild name="hero" view="small" /> // named view
255
+ ```
256
+
257
+ ### `Render` — render any arbitrary JCR node or virtual node
258
+
259
+ ```tsx
260
+ import { Render } from "@jahia/javascript-modules-library";
261
+
262
+ // Render a specific node by reference (also solves the weakreference cache issue)
263
+ <Render node={cityNode} view="name" />
264
+
265
+ // Render a virtual node — no JCR storage, no editor interaction needed
266
+ // Use for components that take no parameters and need no per-page configuration
267
+ <Render content={{ nodeType: "namespace:navBar" }} />
268
+ ```
269
+
270
+ **Virtual nodes** (`content={{ nodeType }}`) render a component inline without creating a JCR node. This is the right pattern for parameterless structural components like a nav bar that is always the same on every page. Unlike `<AbsoluteArea>`, virtual nodes require zero editor interaction.
271
+
272
+ > **Why `<Render node={...} />` solves the cache issue**: When you render a weakreference node via `<Render>`, its fragment is cached separately. If the referenced node changes, its fragment is invalidated and Jahia propagates that invalidation upward to any parent fragment that included it.
273
+
274
+ ### `linkTypeInitializer` — rendering links
275
+
276
+ When a CND type uses `choicelist[linkTypeInitializer]`, the `j:linkType` property is a discriminator, NOT a URL. Use a `switch` statement:
277
+
278
+ ```tsx
279
+ import { buildNodeUrl, jahiaComponent } from "@jahia/javascript-modules-library";
280
+ import type { Props } from "./types.js";
281
+
282
+ jahiaComponent(
283
+ { componentType: "view", nodeType: "namespace:callToAction" },
284
+ (props: Props) => {
285
+ switch (props["j:linkType"]) {
286
+ case "internal":
287
+ return <a href={buildNodeUrl(props["j:linknode"])}>{props.label}</a>;
288
+ case "external":
289
+ return <a href={props["j:url"]} title={props["j:linkTitle"]}>{props.label}</a>;
290
+ default:
291
+ return <span>{props.label}</span>;
292
+ }
293
+ },
294
+ );
295
+ ```
296
+
297
+ The `Props` type must be a discriminated union (see `jahia-dev-define-content-type` skill).
298
+
299
+ ### Cache properties — controlling fragment caching
300
+
301
+ Add a `properties` key to `jahiaComponent` to tune caching:
302
+
303
+ ```tsx
304
+ jahiaComponent(
305
+ {
306
+ componentType: "view",
307
+ nodeType: "namespace:price",
308
+ properties: {
309
+ "cache.expiration": "60", // re-render at most once per minute
310
+ },
311
+ },
312
+ ({ price }: Props) => <span>{price}</span>,
313
+ );
314
+ ```
315
+
316
+ ```tsx
317
+ jahiaComponent(
318
+ {
319
+ componentType: "view",
320
+ nodeType: "namespace:greeting",
321
+ properties: {
322
+ "cache.perUser": "true", // different cache per logged-in user
323
+ },
324
+ },
325
+ (_, { renderContext }) => (
326
+ <div>Welcome, {renderContext.getUser().getUsername()}</div>
327
+ ),
328
+ );
329
+ ```
330
+
331
+ > Cache only applies in **live mode**. Edit and preview modes bypass the cache entirely.
332
+
333
+ ### `buildModuleFileUrl` — URL to a static module asset
334
+
335
+ ```tsx
336
+ import { buildModuleFileUrl, AddResources } from "@jahia/javascript-modules-library";
337
+
338
+ // Inject a vendor CSS file into the page head
339
+ <AddResources type="css" url={buildModuleFileUrl("css/vendor.min.css")} />
340
+
341
+ // Reference a bundled image
342
+ <img src={buildModuleFileUrl("images/placeholder.svg")} alt="" />
343
+ ```
344
+
345
+ Never hardcode `/modules/<name>/javascript/apps/...` paths — use `buildModuleFileUrl` so the path resolves correctly across environments.
346
+
347
+ ---
348
+
349
+ ### `getChildNodes` — iterate over child nodes in code
350
+
351
+ ```tsx
352
+ import { getChildNodes, buildNodeUrl, jahiaComponent } from "@jahia/javascript-modules-library";
353
+ import type { JCRNodeWrapper } from "org.jahia.services.content";
354
+
355
+ jahiaComponent(
356
+ { componentType: "view", nodeType: "namespace:navBar" },
357
+ (_, { renderContext, mainNode }) => {
358
+ // Get all child pages of the site root
359
+ const pages = getChildNodes(renderContext.getSite(), -1, 0,
360
+ (node: JCRNodeWrapper) => node.isNodeType("jnt:page")
361
+ );
362
+ return (
363
+ <nav>
364
+ <ul>
365
+ {pages.map(page => (
366
+ <li key={page.getPath()}>
367
+ <a
368
+ href={buildNodeUrl(page)}
369
+ aria-current={page.getPath() === mainNode.getPath() ? "page" : undefined}
370
+ >
371
+ {page.getProperty("jcr:title").getString()}
372
+ </a>
373
+ </li>
374
+ ))}
375
+ </ul>
376
+ </nav>
377
+ );
378
+ },
379
+ );
380
+ ```
381
+
382
+ `getChildNodes(node, limit, offset, filterFn)` — `limit: -1` means no limit.
383
+
384
+ Use `aria-current="page"` (not a CSS class) to mark the active page — it's the accessible standard and can be styled with `[aria-current="page"] { font-weight: bold }`. Compare against `mainNode.getPath()` since `===` identity comparison doesn't work across GraalJS polyglot contexts.
385
+
386
+ ### `useServerContext` — access rendering context
387
+
388
+ The second argument to `jahiaComponent` is the `ServerContext`. You can also call `useServerContext()` explicitly in helper functions outside the component signature.
389
+
390
+ ```tsx
391
+ jahiaComponent(
392
+ { componentType: "view", nodeType: "ns:type" },
393
+ ({ title }: Props, { renderContext, currentNode, mainNode, jcrSession, bundleKey }) => {
394
+ const isEdit = renderContext.isEditMode();
395
+ const siteKey = renderContext.getSite().getName();
396
+ return <div data-edit={isEdit}>{title}</div>;
397
+ },
398
+ );
399
+ ```
400
+
401
+ | Context field | Type | What it is |
402
+ |---|---|---|
403
+ | `renderContext` | `RenderContext` | Full rendering context (site, workspace, edit mode, user) |
404
+ | `currentNode` | `JCRNodeWrapper` | The component's own JCR node |
405
+ | `mainNode` | `JCRNodeWrapper` | The page's main resource node |
406
+ | `currentResource` | `Resource` | The render resource |
407
+ | `jcrSession` | `JCRSessionWrapper` | Current JCR session — do NOT hold across requests |
408
+ | `bundleKey` | `string` | Module bundle key (e.g. `"my-module"`) |
409
+
410
+ > Use `mainNode` to navigate to the page or site from within a sub-component. Use `jcrSession` for JCR reads that can't go through props (e.g. loading a node by path in a computed listing).
411
+
412
+ ---
413
+
414
+ ### `Java.type()` — low-level Java interop
415
+
416
+ For accessing Java classes directly from JS (use only for well-known, stable APIs):
417
+
418
+ ```js
419
+ const LogManager = Java.type("org.apache.logging.log4j.LogManager");
420
+ const logger = LogManager.getLogger("MyJSLogger");
421
+ logger.info("Hello from JS!");
422
+ ```
423
+
424
+ Only use `Java.type()` with classes from Jahia's documented core. Undocumented classes may change without notice. Prefer `useServerContext()` for officially supported objects.
425
+
426
+ **Mixing JS and Java modules** is fully supported — content types and services from one type can be used by the other. What does NOT work:
427
+ - JSP files inside a JS module
428
+ - JSX views inside a Java module
429
+
430
+ ---
431
+
432
+ ### Render filters
433
+
434
+ Register a render filter from a JS module's init script to transform rendered output:
435
+
436
+ ```js
437
+ registry.add("render-filter", "myFilter", renderFilterRef, {
438
+ target: "render:50", // phase + priority (lower = earlier)
439
+ applyOnNodeTypes: "jnt:bigText",
440
+ prepare: (renderContext, resource, chain) => { /* setup before render */ },
441
+ execute: (previousOut, renderContext, resource, chain) => {
442
+ return previousOut.replace("foo", "bar");
443
+ },
444
+ });
445
+ ```
446
+
447
+ The `target` string format is `"<phase>:<priority>"`. Filters with priority < 16 run on every request; priority > 16 only on cache miss.
448
+
449
+ ---
450
+
451
+ ### `useGQLQuery` — server-side GraphQL
452
+
453
+ Executes a GraphQL query **synchronously** using the current user's credentials. Returns the `data` portion of the response.
454
+
455
+ ```tsx
456
+ import { useGQLQuery, jahiaComponent } from "@jahia/javascript-modules-library";
457
+ import { gql } from "graphql-tag";
458
+
459
+ const QUERY = gql`
460
+ query ListNodes($path: String!) {
461
+ jcr {
462
+ nodeByPath(path: $path) {
463
+ children { nodes { name displayName path } }
464
+ }
465
+ }
466
+ }
467
+ `;
468
+
469
+ jahiaComponent(
470
+ { componentType: "view", nodeType: "ns:listing" },
471
+ (_, { renderContext }) => {
472
+ const siteKey = renderContext.getSite().getName();
473
+ const data = useGQLQuery(QUERY, { path: `/sites/${siteKey}/contents` });
474
+ const nodes = data?.jcr?.nodeByPath?.children?.nodes ?? [];
475
+ return <ul>{nodes.map((n: any) => <li key={n.path}>{n.displayName}</li>)}</ul>;
476
+ },
477
+ );
478
+ ```
479
+
480
+ Use `useGQLQuery` when you need field-level projection, joins across nodes, or complex filtering. Use `useJCRQuery` for simple node listings where you'll call Java methods on the results.
481
+
482
+ > **Edit mode pattern for interactive components**: Carousels, accordions, tabs, and sliders are hard for editors to work with in their interactive state. In edit mode, render them **flat** (all slides/tabs visible) and optionally show an editor hint:
483
+ >
484
+ > ```tsx
485
+ > ({ slides }: Props, { renderContext }) => {
486
+ > const isEdit = renderContext.isEditMode();
487
+ > return isEdit ? (
488
+ > <div className={classes.editStack}>
489
+ > {/* flat — all children visible for editing */}
490
+ > <RenderChildren />
491
+ > <p className={classes.hint}>🖊 Carousel — add or reorder slides here</p>
492
+ > </div>
493
+ > ) : (
494
+ > <div className={classes.carousel}>
495
+ > <RenderChildren />
496
+ > </div>
497
+ > );
498
+ > }
499
+ > ```
500
+
501
+ ### `readOnly` prop for shared/structural nodes
502
+
503
+ Use `readOnly` when rendering a node that editors should not edit in-place (e.g. a shared footer, a system-level navigation area):
504
+
505
+ ```tsx
506
+ <RenderChild name="footer" readOnly={true} />
507
+ ```
508
+
509
+ For `AbsoluteArea`, use `readOnly="children"` to allow editing only from the owning page:
510
+
511
+ ```tsx
512
+ // Fully read-only — editors cannot edit the footer from any page
513
+ <AbsoluteArea name="footer" parent={renderContext.getSite()} readOnly={true} />
514
+
515
+ // Read-only everywhere EXCEPT the designated "footer management" page
516
+ <AbsoluteArea name="footer" parent={renderContext.getSite()} readOnly="children" />
517
+ ```
518
+
519
+ `readOnly="children"` is the recommended pattern: the footer is manageable from one page, but other page templates just include it without showing edit handles.
520
+
521
+ ---
522
+
523
+ ## Step 4 — Add CSS with CSS Modules
524
+
525
+ Create a `component.module.css` file in the same folder:
526
+
527
+ ```css
528
+ .root {
529
+ display: flex;
530
+ flex-direction: column;
531
+ gap: 1rem;
532
+ padding: 2rem;
533
+ }
534
+ ```
535
+
536
+ Import and use in the view:
537
+
538
+ ```tsx
539
+ import classes from "./component.module.css";
540
+
541
+ <section className={classes.root}>
542
+ ```
543
+
544
+ Combine multiple classes:
545
+
546
+ ```tsx
547
+ <section className={[classes.root, classes.small].join(" ")}>
548
+ ```
549
+
550
+ ### ⚠️ CSS grid: `auto-fit` vs `auto-fill`
551
+
552
+ When using `repeat(auto-fill, ...)`, CSS creates **phantom empty tracks** for remaining grid columns, leaving gaps when there are fewer items than columns. Use **`auto-fit`** instead — it collapses empty tracks so items stretch to fill the row:
553
+
554
+ ```css
555
+ /* ❌ auto-fill — leaves gaps when items don't fill the row */
556
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
557
+
558
+ /* ✅ auto-fit — items stretch to fill the full row */
559
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
560
+ ```
561
+
562
+ ### ⚠️ Full-card clickability
563
+
564
+ When only the title of a card is a link, make the entire card clickable using the CSS stretched-link technique:
565
+
566
+ ```tsx
567
+ // In the card view
568
+ <article className={classes.card}>
569
+ <h3 className={classes.cardTitle}>
570
+ <a href={buildNodeUrl(currentNode)} className={classes.cardLink}>
571
+ {title}
572
+ </a>
573
+ </h3>
574
+ <p>{description}</p>
575
+ </article>
576
+ ```
577
+
578
+ ```css
579
+ /* In component.module.css */
580
+ .card {
581
+ position: relative; /* ← required for stretch to work */
582
+ }
583
+
584
+ .cardLink::after {
585
+ content: "";
586
+ position: absolute;
587
+ inset: 0; /* stretches to cover the entire card */
588
+ }
589
+ ```
590
+
591
+ The `::after` pseudo-element on the link covers the entire `position: relative` card, making every pixel clickable while keeping the link semantically on the title.
592
+
593
+ ---
594
+
595
+ ## Step 5 — Creating a named (non-default) view
596
+
597
+ To create a second view (e.g. a compact version), create a new file `small.server.tsx` and add `name: "small"` to the `jahiaComponent` call:
598
+
599
+ ```tsx
600
+ jahiaComponent(
601
+ {
602
+ componentType: "view",
603
+ nodeType: "namespace:typeName",
604
+ displayName: "Small View",
605
+ name: "small", // ← this registers a named view
606
+ },
607
+ ({ title }: Props) => <span className={classes.small}>{title}</span>,
608
+ );
609
+ ```
610
+
611
+ Request a named view from a parent component with `<RenderChild name="child" view="small" />`.
612
+
613
+ ---
614
+
615
+ ## Step 5b — Creating a client-side interactive component (Island Architecture)
616
+
617
+ Jahia uses the **Island Architecture**: server components render static HTML; interactive islands are hydrated in the browser. Use this when you need React state, browser events, or browser-only APIs.
618
+
619
+ ### When to use client vs server rendering
620
+
621
+ | Use `.server.tsx` for… | Use `.client.tsx` for… |
622
+ |---|---|
623
+ | Static HTML, CMS content, navigation | Buttons, toggles, counters, forms |
624
+ | Reading JCR/GQL data | `useState`, `useEffect`, browser events |
625
+ | SEO-important content | Animations, browser-only libraries |
626
+
627
+ ### Step 1 — Create the client component
628
+
629
+ Create `MyComponent.client.tsx` **in the same folder** as the server view. This is a plain React component — no `jahiaComponent` call needed:
630
+
631
+ ```tsx
632
+ // src/components/Counter/Counter.client.tsx
633
+ import { useState } from "react";
634
+ import classes from "./component.module.css";
635
+
636
+ interface Props {
637
+ label: string; // only serializable types allowed as Island props
638
+ initialCount?: number;
639
+ }
640
+
641
+ export default function Counter({ label, initialCount = 0 }: Props) {
642
+ const [count, setCount] = useState(initialCount);
643
+ return (
644
+ <div className={classes.counter}>
645
+ <button type="button" onClick={() => setCount(c => c - 1)}>−</button>
646
+ <span>{label}: {count}</span>
647
+ <button type="button" onClick={() => setCount(c => c + 1)}>+</button>
648
+ </div>
649
+ );
650
+ }
651
+ ```
652
+
653
+ > ⚠️ **Only the default export** of a `.client.tsx` file can be used as an island component. Named exports are bundled for the browser but cannot be registered as islands.
654
+
655
+ > ⚠️ **Props must be serializable**: only strings, numbers, booleans, plain objects, and arrays. You cannot pass `JCRNodeWrapper`, `renderContext`, or Java objects to a client component.
656
+
657
+ ### Step 2 — Wrap it with `<Island>` in the server view
658
+
659
+ ```tsx
660
+ // src/components/Counter/default.server.tsx
661
+ import { jahiaComponent, Island } from "@jahia/javascript-modules-library";
662
+ import Counter from "./Counter.client.jsx"; // .jsx at import time
663
+ import type { Props } from "./types.js";
664
+
665
+ jahiaComponent(
666
+ { componentType: "view", nodeType: "namespace:counter" },
667
+ ({ label, initialCount }: Props) => (
668
+ <div>
669
+ <Island component={Counter} props={{ label, initialCount }} />
670
+ </div>
671
+ ),
672
+ );
673
+ ```
674
+
675
+ The `Island` component handles SSR + hydration automatically. The server view fetches the content from JCR; only serializable values flow into the client island.
676
+
677
+ ### Step 3 — Browser-only rendering (skip SSR)
678
+
679
+ If the component cannot run on the server (e.g. uses `window`, `document`, or a browser-only library), use `clientOnly`:
680
+
681
+ ```tsx
682
+ <Island component={MapWidget} props={{ lat, lng }} clientOnly>
683
+ <p>Loading map…</p> {/* shown until the component hydrates */}
684
+ </Island>
685
+ ```
686
+
687
+ ### Step 3b — Passing children to an island (accordion pattern)
688
+
689
+ In default mode (without `clientOnly`), children passed to `<Island>` are rendered on the server and inserted as static children of the island component. Use this for accordion or collapsible containers where the shell is interactive but the content is static:
690
+
691
+ ```tsx
692
+ // Accordion.client.tsx
693
+ import type { ReactNode } from "react";
694
+ import { useState } from "react";
695
+
696
+ export default function Accordion({ children }: { children: ReactNode }) {
697
+ const [isOpen, setIsOpen] = useState(false);
698
+ return (
699
+ <div>
700
+ <button type="button" onClick={() => setIsOpen(o => !o)}>
701
+ {isOpen ? "Close" : "Open"}
702
+ </button>
703
+ <div style={{ display: isOpen ? "block" : "none" }}>
704
+ {children}
705
+ </div>
706
+ </div>
707
+ );
708
+ }
709
+ ```
710
+
711
+ ```tsx
712
+ // default.server.tsx
713
+ <Island component={Accordion}>
714
+ <p>Server-rendered static content</p>
715
+ </Island>
716
+ ```
717
+
718
+ > ⚠️ The `{children}` insertion point must always be present in the client component. If you want to hide children, use CSS — a JS condition will cause them to be omitted from the page entirely.
719
+
720
+ > ⚠️ Children are wrapped in a `<jsm-children>` custom element. Avoid using the CSS `>` child combinator to target island children. Also do not target `jsm-island` or `jsm-children` in CSS — they are implementation details and may change.
721
+
722
+ In `clientOnly` mode, children act as a **loading placeholder** shown until the island hydrates (see Step 3 above).
723
+
724
+ ### Step 4 — Dynamic import for heavy/browser-only libraries
725
+
726
+ For large libraries, import them dynamically inside `useEffect` to avoid SSR issues and reduce bundle size:
727
+
728
+ ```tsx
729
+ // Counter.client.tsx
730
+ import { useEffect, useState } from "react";
731
+
732
+ export default function Confetti() {
733
+ const [fire, setFire] = useState<(() => void) | null>(null);
734
+
735
+ useEffect(() => {
736
+ import("canvas-confetti").then(({ default: confetti }) => {
737
+ setFire(() => () => confetti({ origin: { y: 1 } }));
738
+ });
739
+ }, []);
740
+
741
+ return <button type="button" onClick={() => fire?.()} disabled={!fire}>🎉</button>;
742
+ }
743
+ ```
744
+
745
+ ### Edit mode caveat
746
+
747
+ Client components are hydrated even in Page Builder edit mode. If the interactive behaviour is disruptive in edit mode (e.g. a slider that auto-advances), guard it:
748
+
749
+ ```tsx
750
+ // Pass isEditMode from the server view as a prop
751
+ <Island component={Slider} props={{ slides, isEditMode: renderContext.isEditMode() }} />
752
+ ```
753
+
754
+ Then in the client component, skip the interactive behaviour when `isEditMode` is true.
755
+
756
+ ---
757
+
758
+ ## Step 5c — Add front-end UI labels (locales)
759
+
760
+ Any string that appears in the rendered HTML and is not a JCR property value must come from `settings/locales/`.
761
+ Do not hardcode button text, section headings, alt text templates, error messages, or form labels.
762
+
763
+ **File location:**
764
+ ```
765
+ settings/locales/en.json ← required
766
+ settings/locales/fr.json ← required minimum
767
+ ```
768
+
769
+ These files are auto-discovered by `@jahia/vite-plugin` — no registration needed.
770
+
771
+ **Usage in views:**
772
+
773
+ ```tsx
774
+ import { useTranslation } from "react-i18next";
775
+
776
+ // Works in both .server.tsx and .client.tsx
777
+ const { t } = useTranslation();
778
+
779
+ // Simple
780
+ <button>{t("hero.cta.label")}</button>
781
+
782
+ // With interpolation
783
+ <img alt={t("alt.hero", { title })} />
784
+ ```
785
+
786
+ **Add to both `en.json` and `fr.json` for every new string:**
787
+
788
+ ```json
789
+ {
790
+ "hero": {
791
+ "cta": {
792
+ "label": "Discover more"
793
+ }
794
+ },
795
+ "alt": {
796
+ "hero": "Hero image for {{title}}"
797
+ }
798
+ }
799
+ ```
800
+
801
+ **Best practices:**
802
+ - Use random/opaque keys (e.g. `"r3k2"`) or scoped semantic keys (e.g. `"hero.cta.label"`) — never bare English words as keys (`"read-more"`) which creates ambiguity across contexts and forces renaming.
803
+ - Never concatenate: always use interpolation (`{{author}}`) for dynamic data.
804
+ - For HTML inside translations, use the `<Trans>` component instead of `t()`:
805
+
806
+ ```tsx
807
+ import { Trans } from "react-i18next";
808
+ // key value: "Written by <a>{{author}}</a>"
809
+ <Trans i18nKey="article.byline" values={{ author }} components={{ a: <a href={authorUrl} /> }} />
810
+ ```
811
+
812
+ **IDE integration:** `npm init @jahia/module@latest` automatically configures the [i18n ally](https://github.com/lokalise/i18n-ally#readme) VS Code extension. When installed it shows translation values inline in the code, lets you edit them without opening JSON files, and provides an `Extract text into i18n messages` command that replaces a hardcoded string with a `t("...")` call. Install the recommended extensions in `.vscode/extensions.json` to get it.
813
+
814
+ > Front-end UI labels (`locales/*.json`) are separate from CND editor labels (`settings/resources/*.properties`). Both are required — locales for rendered UI strings, properties for the Jahia content editor.
815
+
816
+ ---
817
+
818
+ ## Step 5d — Language switcher
819
+
820
+ Use the following utilities from `@jahia/javascript-modules-library` to build a server-rendered language switcher:
821
+
822
+ ```tsx
823
+ import { getSiteLocales, buildNodeUrl, jahiaComponent } from "@jahia/javascript-modules-library";
824
+
825
+ jahiaComponent(
826
+ { componentType: "view", nodeType: "ns:languageSwitcher" },
827
+ (_, { renderContext, currentNode }) => {
828
+ const locales = getSiteLocales(renderContext.getSite());
829
+ const invalidLanguages: string[] = currentNode.getPropertyAsString("j:invalidLanguages")?.split(" ") ?? [];
830
+
831
+ return (
832
+ <ul>
833
+ {locales
834
+ .filter(locale => !invalidLanguages.includes(locale))
835
+ .filter(locale => currentNode.hasI18N(renderContext.getSite().getLocale(locale)))
836
+ .map(locale => (
837
+ <li key={locale}>
838
+ <a href={buildNodeUrl(currentNode, { language: locale })}>{locale.toUpperCase()}</a>
839
+ </li>
840
+ ))}
841
+ </ul>
842
+ );
843
+ },
844
+ );
845
+ ```
846
+
847
+ `j:invalidLanguages` is a system property Jahia sets on nodes that haven't been translated to a given language. Filtering it out prevents dead links to untranslated pages. `node.hasI18N(locale)` provides an additional check — it returns false for nodes that have no translated properties at all for the given locale, catching cases `j:invalidLanguages` may not cover.
848
+
849
+ ---
850
+
851
+ ## Step 6 — Push to Jahia
852
+
853
+ Build and deploy the module to push all changes (existing or new files):
854
+
855
+ ```bash
856
+ # Always use this — never use yarn dev from an agent (it's interactive-only)
857
+ yarn build && yarn jahia-deploy
858
+ ```
859
+
860
+ ---
861
+
862
+ ## Validation checklist
863
+ - [ ] `jahiaComponent` registered with correct `nodeType` (matches CND)
864
+ - [ ] `Props` imported from `./types.js`
865
+ - [ ] `buildNodeUrl` used for any image or node URL
866
+ - [ ] Weakreference-backed content rendered via sub-view (`RenderChild`), not inline property access
867
+ - [ ] Interactive UI (carousels, tabs) flattened in edit mode with editor hints
868
+ - [ ] Structural/shared nodes rendered with `readOnly` prop
869
+ - [ ] Semantic HTML used (`<article>`, `<section>`, `<nav>`, `<header>`, `<footer>`)
870
+ - [ ] Images have meaningful `alt` text (not empty `alt=""` unless decorative) — use `t("alt.key", {...})` for translated alt text
871
+ - [ ] No hardcoded UI strings — all button labels, headings, messages use `t("key")` from `settings/locales/`
872
+ - [ ] `settings/locales/en.json` and `fr.json` both updated with any new keys
873
+ - [ ] CSS Module created and imported
874
+ - [ ] **If client-side**: component is in `.client.tsx`, wrapped with `<Island>` in the server view
875
+ - [ ] **If client-side**: all props passed to Island are serializable (no JCR objects)
876
+ - [ ] **If client-side**: browser-only libraries use dynamic `import()` inside `useEffect`
877
+ - [ ] `yarn build && yarn jahia-deploy` run after all changes
878
+ - [ ] Component renders without errors in Page Builder
879
+
880
+ ## Troubleshooting
881
+ > https://academy.jahia.com/tutorials-get-started/front-end-developer/making-a-hero-section
882
+
883
+ ### JSX vs HTML attribute differences
884
+
885
+ | Feature | HTML | JSX |
886
+ |---|---|---|
887
+ | CSS class | `class="..."` | `className="..."` |
888
+ | Inline style | `style="color:red"` | `style={{ color: 'red' }}` |
889
+ | Event handler | `onclick="fn()"` | `onClick={fn}` |
890
+ | Comments | `<!-- -->` | `{/* */}` |
891
+ | Boolean attributes | `disabled` | `disabled={true}` or just `disabled` |
892
+
893
+ ## References
894
+
895
+ - JavaScript modules monorepo: https://github.com/Jahia/javascript-modules
896
+ - Preparing for i18n: https://academy.jahia.com/documentation/jahia-cms/jahia-8-2/developer/javascript-module-development/preparing-for-internationalization-i18n