@localess/react 3.0.1-dev.20260408170501 → 3.0.1-dev.20260408183448

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.
package/README.md CHANGED
@@ -249,46 +249,186 @@ const Image = ({ data }) => (
249
249
 
250
250
  ---
251
251
 
252
+ ## `useLocaless` Hook
253
+
254
+ `useLocaless<T>` fetches content by slug in a Client Component and automatically subscribes to Visual Editor live updates when `enableSync` is active.
255
+
256
+ ```tsx
257
+ 'use client';
258
+
259
+ import { useLocaless, LocalessComponent } from "@localess/react";
260
+ import type { Page } from "./.localess/localess";
261
+
262
+ export function PageView({ slug }: { slug: string }) {
263
+ const content = useLocaless<Page>(slug, { locale: 'en' });
264
+
265
+ if (!content) return <div>Loading…</div>;
266
+
267
+ return (
268
+ <main>
269
+ {content.data.body.map(item => (
270
+ <LocalessComponent key={item._id} data={item} links={content.links} />
271
+ ))}
272
+ </main>
273
+ );
274
+ }
275
+ ```
276
+
277
+ ### Parameters
278
+
279
+ | Parameter | Type | Required | Description |
280
+ |-----------|------|----------|-------------|
281
+ | `slug` | `string \| string[]` | ✅ | Content slug. Arrays are joined with `/` — e.g. `['blog', 'post']` → `'blog/post'` |
282
+ | `options` | `ContentFetchParams` | ❌ | Same fetch options as `getContentBySlug` (locale, version, resolveReference, resolveLink) |
283
+
284
+ Returns `Content<T> | undefined` — `undefined` while the initial fetch is in progress.
285
+
286
+ When `enableSync` is active and the page is rendered inside the Localess Visual Editor iframe, the hook automatically subscribes to `input` / `change` events and updates the returned content in place.
287
+
288
+ ---
289
+
290
+ ## Link Utilities
291
+
292
+ ### `findLink(links, link)`
293
+
294
+ Resolves a `ContentLink` field to a URL string. Use it to build `href` values from Localess content links.
295
+
296
+ ```tsx
297
+ import { findLink } from "@localess/react";
298
+
299
+ // type: 'content' → '/' + fullSlug, or '/not-found' if not in map
300
+ // type: 'url' → raw URI unchanged
301
+ const href = findLink(content.links, data.ctaLink);
302
+
303
+ const NavLink = ({ data, links }) => (
304
+ <a href={findLink(links, data.link)}>{data.label}</a>
305
+ );
306
+ ```
307
+
308
+ ---
309
+
252
310
  ## Visual Editor Events
253
311
 
254
- When your application is opened inside the Localess Visual Editor, subscribe to live-editing events via `window.localess`.
312
+ ### With `useLocaless` Hook
313
+
314
+ When `enableSync: true` is set in `localessInit`, the `useLocaless` hook handles the full cycle automatically — initial fetch and live sync updates — with no extra wiring needed.
315
+
316
+ ```tsx
317
+ 'use client';
318
+
319
+ import { useLocaless, LocalessComponent, localessEditable } from "@localess/react";
320
+ import type { Page } from "./.localess/localess";
321
+
322
+ export function PageView({ slug, locale }: { slug: string; locale?: string }) {
323
+ const content = useLocaless<Page>(slug, { locale });
324
+
325
+ if (!content) return null;
326
+
327
+ return (
328
+ <main {...localessEditable(content.data)}>
329
+ {content.data?.body.map(item => (
330
+ <LocalessComponent key={item._id} data={item} links={content.links} references={content.references} />
331
+ ))}
332
+ </main>
333
+ );
334
+ }
335
+ ```
336
+
337
+ ### With `LocalessDocument` Component
338
+
339
+ `LocalessDocument` is a component alternative to the hook. Pass it server-fetched content data and it handles live sync updates internally, delegating rendering to `LocalessComponent`.
340
+
341
+ ```tsx
342
+ // app/[locale]/page.tsx (Server Component — fetches data)
343
+ import { getLocalessClient, LocalessDocument } from "@localess/react";
344
+ import type { Page } from "./.localess/localess";
345
+
346
+ export default async function HomePage({ params }: { params: Promise<{ locale?: string }> }) {
347
+ const { locale } = await params;
348
+ const client = getLocalessClient();
349
+ const content = await client.getContentBySlug<Page>('home', { locale });
350
+
351
+ return (
352
+ <LocalessDocument
353
+ data={content.data}
354
+ links={content.links}
355
+ references={content.references}
356
+ />
357
+ );
358
+ }
359
+ ```
360
+
361
+ **Props:**
362
+
363
+ | Prop | Type | Required | Description |
364
+ |------|------|----------|-------------|
365
+ | `data` | `ContentData` | ✅ | Initial content data (typically server-fetched) |
366
+ | `links` | `Links` | ❌ | Resolved links map, forwarded to the inner `LocalessComponent` |
367
+ | `references` | `References` | ❌ | Resolved references map, forwarded to the inner `LocalessComponent` |
368
+ | `ref` | `React.Ref<HTMLElement>` | ❌ | Forwarded to the rendered root element |
369
+ | `...rest` | `any` | ❌ | Any additional props are forwarded |
370
+
371
+ > `LocalessDocument` subscribes to `input` / `change` editor events automatically when `enableSync` is active. It is a Client Component internally — no `'use client'` directive needed at the call site in Server Components.
372
+
373
+ ### Manual Integration
374
+
375
+ If you manage content state yourself without `useLocaless` or `LocalessDocument`, subscribe to editor events directly via `window.localess`:
255
376
 
256
377
  ```tsx
257
378
  'use client';
258
379
 
259
380
  import { useEffect, useState } from "react";
260
- import { getLocalessClient } from "@localess/react";
261
- import type { Content } from "@localess/react";
381
+ import { LocalessComponent, localessEditable, isSyncEnabled, isBrowser } from "@localess/react";
382
+ import type { Content, Page } from "./.localess/localess";
262
383
 
263
- export function PageClient({ initialData }: { initialData: Content<Page> }) {
264
- const [pageData, setPageData] = useState(initialData.data);
384
+ export function PageClient({ initialContent }: { initialContent: Content<Page> }) {
385
+ const [pageData, setPageData] = useState(initialContent.data);
265
386
 
266
387
  useEffect(() => {
267
- if (window.localess) {
388
+ if (isSyncEnabled() && isBrowser() && window.localess) {
268
389
  window.localess.on(['input', 'change'], (event) => {
269
390
  if (event.type === 'input' || event.type === 'change') {
270
391
  setPageData(event.data);
271
392
  }
272
393
  });
273
394
  }
395
+ // No cleanup needed: window.localess has no .off() method
274
396
  }, []);
275
397
 
276
398
  return (
277
399
  <main {...localessEditable(pageData)}>
278
- {pageData.body.map(item => (
279
- <LocalessComponent key={item._id} data={item} />
400
+ {pageData?.body.map(item => (
401
+ <LocalessComponent key={item._id} data={item} links={initialContent.links} references={initialContent.references} />
280
402
  ))}
281
403
  </main>
282
404
  );
283
405
  }
284
406
  ```
285
407
 
408
+ **Available events via `window.localess.on()`:**
409
+
410
+ | Event | When |
411
+ |-------|------|
412
+ | `input` | User is typing in a field (real-time preview) |
413
+ | `change` | Field value confirmed |
414
+ | `save` | Content saved |
415
+ | `publish` | Content published |
416
+ | `pong` | Editor heartbeat response |
417
+ | `enterSchema` | Editor cursor enters a schema block |
418
+ | `hoverSchema` | Editor cursor hovers over a schema block |
419
+
420
+ > `window.localess` only exposes `.on()` and `.onChange()` — there is no `.off()` method.
421
+
286
422
  ---
287
423
 
288
- ## Full Example (Next.js 15 App Router)
424
+ ## Full Example (Next.js 16.2 App Router)
425
+
426
+ The recommended Next.js pattern is to **preload data server-side** and pass it to the Client Component. This avoids a loading flash — the page renders immediately with server data, then Visual Editor sync kicks in if active.
427
+
428
+ ### Setup — `app/layout.tsx`
289
429
 
290
430
  ```tsx
291
- // app/layout.tsx (Server Component — safe to use API token here)
431
+ // Server Component — safe to use API token here
292
432
  import { localessInit } from "@localess/react";
293
433
  import { Page, Header, Teaser, Footer } from "@/components";
294
434
 
@@ -296,38 +436,132 @@ localessInit({
296
436
  origin: process.env.LOCALESS_ORIGIN!,
297
437
  spaceId: process.env.LOCALESS_SPACE_ID!,
298
438
  token: process.env.LOCALESS_TOKEN!,
299
- enableSync: process.env.NODE_ENV === 'development',
439
+ enableSync: process.env.NODE_ENV !== 'production',
300
440
  components: { Page, Header, Teaser, Footer },
301
441
  });
302
442
 
303
- export default function RootLayout({ children }) {
443
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
304
444
  return <html><body>{children}</body></html>;
305
445
  }
306
446
  ```
307
447
 
448
+ ### Server Component — `app/[locale]/page.tsx`
449
+
450
+ Fetches content during SSR and passes it as a prop. The client component receives it already populated — no loading state needed.
451
+
308
452
  ```tsx
309
- // app/page.tsx (Server Component)
310
- import { getLocalessClient, LocalessComponent, localessEditable } from "@localess/react";
311
- import type { Page } from "./.localess/localess";
453
+ import { getLocalessClient } from "@localess/react";
454
+ import type { Content, Page } from "./.localess/localess";
455
+
456
+ // Choose one of the three client components below
457
+ import { PageClientHook } from "./page-client-hook";
312
458
 
313
459
  export default async function HomePage({
314
- searchParams,
460
+ params,
315
461
  }: {
316
- searchParams: Promise<{ locale?: string }>;
462
+ params: Promise<{ locale?: string }>;
317
463
  }) {
318
- const { locale } = await searchParams;
319
- const client = getLocalessClient();
320
- const content = await client.getContentBySlug<Page>('home', { locale });
464
+ const { locale } = await params;
465
+ const content = await getLocalessClient().getContentBySlug<Page>('home', { locale });
466
+
467
+ return <PageClientHook initialContent={content} locale={locale} />;
468
+ }
469
+ ```
470
+
471
+ ### Client Component — Option A: `useLocaless` Hook
472
+
473
+ The hook re-fetches on the client and falls back to the server-preloaded data until it resolves. Live sync is wired automatically.
474
+
475
+ ```tsx
476
+ // app/[locale]/page-client-hook.tsx
477
+ 'use client';
478
+
479
+ import { useLocaless, LocalessComponent, localessEditable } from "@localess/react";
480
+ import type { Content, Page } from "./.localess/localess";
481
+
482
+ export function PageClientHook({
483
+ initialContent,
484
+ locale,
485
+ }: {
486
+ initialContent: Content<Page>;
487
+ locale?: string;
488
+ }) {
489
+ // ?? initialContent: renders with server data immediately, switches to hook result once ready
490
+ const content = useLocaless<Page>('home', { locale }) ?? initialContent;
321
491
 
322
492
  return (
323
493
  <main {...localessEditable(content.data)}>
324
494
  {content.data?.body.map(item => (
325
- <LocalessComponent
326
- key={item._id}
327
- data={item}
328
- links={content.links}
329
- references={content.references}
330
- />
495
+ <LocalessComponent key={item._id} data={item} links={content.links} references={content.references} />
496
+ ))}
497
+ </main>
498
+ );
499
+ }
500
+ ```
501
+
502
+ ### Client Component — Option B: `LocalessDocument` Component
503
+
504
+ Skips client re-fetch entirely — uses server-preloaded data and only subscribes to live sync events. Simpler when you don't need client-side refetching.
505
+
506
+ ```tsx
507
+ // app/[locale]/page.tsx (Server Component — no separate client file needed)
508
+ import { getLocalessClient, LocalessDocument } from "@localess/react";
509
+ import type { Page } from "./.localess/localess";
510
+
511
+ export default async function HomePage({
512
+ params,
513
+ }: {
514
+ params: Promise<{ locale?: string }>;
515
+ }) {
516
+ const { locale } = await params;
517
+ const content = await getLocalessClient().getContentBySlug<Page>('home', { locale });
518
+
519
+ // LocalessDocument handles sync internally — no 'use client' wrapper needed here
520
+ return (
521
+ <LocalessDocument
522
+ data={content.data}
523
+ links={content.links}
524
+ references={content.references}
525
+ />
526
+ );
527
+ }
528
+ ```
529
+
530
+ ### Client Component — Option C: Manual
531
+
532
+ Full control over state and sync subscription. Use when you need custom logic around live updates.
533
+
534
+ ```tsx
535
+ // app/[locale]/page-client-manual.tsx
536
+ 'use client';
537
+
538
+ import { useEffect, useState } from "react";
539
+ import { LocalessComponent, localessEditable, isSyncEnabled, isBrowser } from "@localess/react";
540
+ import type { Content, Page } from "./.localess/localess";
541
+
542
+ export function PageClientManual({
543
+ initialContent,
544
+ }: {
545
+ initialContent: Content<Page>;
546
+ }) {
547
+ // Initialize with server-preloaded data — no loading state needed
548
+ const [pageData, setPageData] = useState(initialContent.data);
549
+
550
+ useEffect(() => {
551
+ if (isSyncEnabled() && isBrowser() && window.localess) {
552
+ window.localess.on(['input', 'change'], (event) => {
553
+ if (event.type === 'input' || event.type === 'change') {
554
+ setPageData(event.data);
555
+ }
556
+ });
557
+ }
558
+ // No cleanup needed: window.localess has no .off() method
559
+ }, []);
560
+
561
+ return (
562
+ <main {...localessEditable(pageData)}>
563
+ {pageData?.body.map(item => (
564
+ <LocalessComponent key={item._id} data={item} links={initialContent.links} references={initialContent.references} />
331
565
  ))}
332
566
  </main>
333
567
  );
@@ -342,7 +576,9 @@ The following are re-exported for convenience so you only need to import from `@
342
576
 
343
577
  **Types:** `Content`, `ContentData`, `ContentMetadata`, `ContentDataSchema`, `ContentDataField`, `ContentAsset`, `ContentRichText`, `ContentLink`, `ContentReference`, `Links`, `References`, `Translations`, `LocalessClient`, `LocalessSync`, `EventToApp`, `EventCallback`, `EventToAppType`
344
578
 
345
- **Functions:** `localessEditable`, `localessEditableField`, `llEditable` *(deprecated)*, `llEditableField` *(deprecated)*, `isBrowser`, `isServer`, `isIframe`
579
+ **Functions:** `localessEditable`, `localessEditableField`, `llEditable` *(deprecated)*, `llEditableField` *(deprecated)*, `isBrowser`, `isServer`, `isIframe`, `resolveAsset`, `findLink`, `useLocaless`, `renderRichTextToReact`, `localessInit`, `getLocalessClient`, `registerComponent`, `unregisterComponent`, `setComponents`, `getComponent`, `setFallbackComponent`, `getFallbackComponent`, `isSyncEnabled`
580
+
581
+ **Components:** `LocalessComponent`, `LocalessDocument`
346
582
 
347
583
  ---
348
584
 
package/SKILL.md CHANGED
@@ -174,22 +174,85 @@ localessInit({
174
174
 
175
175
  > Never enable sync in production — the script is only meaningful inside the Localess Visual Editor iframe.
176
176
 
177
- ### Receiving Real-time Editor Events
177
+ ### With `useLocaless` Hook
178
178
 
179
- Once the sync script is loaded, `window.localess` becomes available inside the iframe. Subscribe to it in a Client Component to apply live updates:
179
+ The `useLocaless` hook handles the full cycle automatically initial fetch and live sync updates with no extra wiring needed.
180
+
181
+ ```tsx
182
+ 'use client';
183
+
184
+ import { useLocaless, LocalessComponent, localessEditable } from "@localess/react";
185
+ import type { Content, Page } from "./.localess/localess";
186
+
187
+ export function PageClient({ initialContent, locale }: { initialContent: Content<Page>; locale?: string }) {
188
+ const content = useLocaless<Page>('home', { locale }) ?? initialContent;
189
+
190
+ return (
191
+ <main {...localessEditable(content.data)}>
192
+ {content.data?.body?.map(item => (
193
+ <LocalessComponent
194
+ key={item._id}
195
+ data={item}
196
+ links={content.links}
197
+ references={content.references}
198
+ />
199
+ ))}
200
+ </main>
201
+ );
202
+ }
203
+ ```
204
+
205
+ ### With `LocalessDocument` Component
206
+
207
+ `LocalessDocument` is a component alternative to the hook. It accepts server-fetched `data` and manages live sync updates internally, delegating rendering to `LocalessComponent`. Useful when you prefer a component-based approach over hooks.
208
+
209
+ ```tsx
210
+ // Server Component — pass fetched data directly to LocalessDocument
211
+ import { getLocalessClient, LocalessDocument } from "@localess/react";
212
+ import type { Page } from "./.localess/localess";
213
+
214
+ export default async function HomePage({ params }: { params: Promise<{ locale?: string }> }) {
215
+ const { locale } = await params;
216
+ const client = getLocalessClient();
217
+ const content = await client.getContentBySlug<Page>('home', { locale });
218
+
219
+ return (
220
+ <LocalessDocument
221
+ data={content.data}
222
+ links={content.links}
223
+ references={content.references}
224
+ />
225
+ );
226
+ }
227
+ ```
228
+
229
+ **Props** (same shape as `LocalessComponent`):
230
+
231
+ | Prop | Type | Required | Description |
232
+ |------|------|----------|-------------|
233
+ | `data` | `ContentData` | ✅ | Initial content data |
234
+ | `links` | `Links` | ❌ | Resolved links map |
235
+ | `references` | `References` | ❌ | Resolved references map |
236
+ | `ref` | `React.Ref<HTMLElement>` | ❌ | Forwarded to the rendered root element |
237
+
238
+ > Subscribes to `input` / `change` events automatically when `enableSync` is active. Unlike `useLocaless`, it does not fetch content — it only handles live sync for data passed in as props.
239
+
240
+ ### Manual Integration
241
+
242
+ If you manage content state yourself without `useLocaless` or `LocalessDocument`, subscribe to editor events directly via `window.localess`:
180
243
 
181
244
  ```tsx
182
245
  'use client';
183
246
 
184
247
  import { useEffect, useState } from "react";
185
- import { LocalessComponent, localessEditable } from "@localess/react";
248
+ import { LocalessComponent, localessEditable, isSyncEnabled, isBrowser } from "@localess/react";
186
249
  import type { Content, Page } from "./.localess/localess";
187
250
 
188
251
  export function PageClient({ initialContent }: { initialContent: Content<Page> }) {
189
252
  const [pageData, setPageData] = useState(initialContent.data);
190
253
 
191
254
  useEffect(() => {
192
- if (window.localess) {
255
+ if (isSyncEnabled() && isBrowser() && window.localess) {
193
256
  window.localess.on(['input', 'change'], (event) => {
194
257
  if (event.type === 'input' || event.type === 'change') {
195
258
  setPageData(event.data);
@@ -230,57 +293,97 @@ export function PageClient({ initialContent }: { initialContent: Content<Page> }
230
293
 
231
294
  ### Pattern: Split Server/Client Components (Next.js App Router)
232
295
 
233
- The recommended pattern keeps data fetching server-side while the Client Component handles live sync:
296
+ **Preload data server-side** and pass it to the Client Component. The page renders immediately with server data — no loading flash — and Visual Editor sync kicks in on top.
297
+
298
+ **Server Component** (same for all three options below):
234
299
 
235
300
  ```tsx
236
- // app/[locale]/page.tsx — Server Component: fetches data, no sync logic
301
+ // app/[locale]/page.tsx
237
302
  import { getLocalessClient } from "@localess/react";
238
- import { PageClient } from "./page-client";
239
- import type { Page } from "./.localess/localess";
303
+ import type { Content, Page } from "./.localess/localess";
240
304
 
241
305
  export default async function HomePage({
242
- searchParams,
306
+ params,
243
307
  }: {
244
- searchParams: Promise<{ locale?: string }>;
308
+ params: Promise<{ locale?: string }>;
245
309
  }) {
246
- const { locale } = await searchParams;
247
- const client = getLocalessClient();
248
- const content = await client.getContentBySlug<Page>('home', { locale });
310
+ const { locale } = await params;
311
+ // Data fetched during SSR — preloaded into the client component as a prop
312
+ const content = await getLocalessClient().getContentBySlug<Page>('home', { locale });
313
+
314
+ return <PageClient initialContent={content} locale={locale} />;
315
+ }
316
+ ```
317
+
318
+ **Option A — `useLocaless` hook**: re-fetches on client, falls back to server data until resolved, auto-syncs with editor.
319
+
320
+ ```tsx
321
+ // app/[locale]/page-client.tsx
322
+ 'use client';
323
+
324
+ import { useLocaless, LocalessComponent, localessEditable } from "@localess/react";
325
+ import type { Content, Page } from "./.localess/localess";
326
+
327
+ export function PageClient({ initialContent, locale }: { initialContent: Content<Page>; locale?: string }) {
328
+ // ?? initialContent: renders server data immediately, switches to hook result once ready
329
+ const content = useLocaless<Page>('home', { locale }) ?? initialContent;
330
+
331
+ return (
332
+ <main {...localessEditable(content.data)}>
333
+ {content.data?.body?.map(item => (
334
+ <LocalessComponent key={item._id} data={item} links={content.links} references={content.references} />
335
+ ))}
336
+ </main>
337
+ );
338
+ }
339
+ ```
340
+
341
+ **Option B — `LocalessDocument` component**: no client re-fetch, uses server data directly, auto-syncs with editor.
342
+
343
+ ```tsx
344
+ // app/[locale]/page.tsx — no separate client file needed
345
+ import { getLocalessClient, LocalessDocument } from "@localess/react";
346
+ import type { Page } from "./.localess/localess";
347
+
348
+ export default async function HomePage({ params }: { params: Promise<{ locale?: string }> }) {
349
+ const { locale } = await params;
350
+ const content = await getLocalessClient().getContentBySlug<Page>('home', { locale });
249
351
 
250
- return <PageClient initialContent={content} />;
352
+ return (
353
+ <LocalessDocument data={content.data} links={content.links} references={content.references} />
354
+ );
251
355
  }
252
356
  ```
253
357
 
358
+ **Option C — Manual**: full control, initialise state with server-preloaded data, subscribe to sync yourself.
359
+
254
360
  ```tsx
255
- // app/[locale]/page-client.tsx — Client Component: renders + handles live edits
361
+ // app/[locale]/page-client.tsx
256
362
  'use client';
257
363
 
258
364
  import { useEffect, useState } from "react";
259
- import { LocalessComponent, localessEditable } from "@localess/react";
365
+ import { LocalessComponent, localessEditable, isSyncEnabled, isBrowser } from "@localess/react";
260
366
  import type { Content, Page } from "./.localess/localess";
261
367
 
262
368
  export function PageClient({ initialContent }: { initialContent: Content<Page> }) {
369
+ // Server-preloaded data used as initial state — no loading flash
263
370
  const [pageData, setPageData] = useState(initialContent.data);
264
371
 
265
372
  useEffect(() => {
266
- if (window.localess) {
373
+ if (isSyncEnabled() && isBrowser() && window.localess) {
267
374
  window.localess.on(['input', 'change'], (event) => {
268
375
  if (event.type === 'input' || event.type === 'change') {
269
376
  setPageData(event.data);
270
377
  }
271
378
  });
272
379
  }
380
+ // No cleanup needed: window.localess has no .off() method
273
381
  }, []);
274
382
 
275
383
  return (
276
384
  <main {...localessEditable(pageData)}>
277
385
  {pageData?.body?.map(item => (
278
- <LocalessComponent
279
- key={item._id}
280
- data={item}
281
- links={initialContent.links}
282
- references={initialContent.references}
283
- />
386
+ <LocalessComponent key={item._id} data={item} links={initialContent.links} references={initialContent.references} />
284
387
  ))}
285
388
  </main>
286
389
  );
@@ -289,6 +392,67 @@ export function PageClient({ initialContent }: { initialContent: Content<Page> }
289
392
 
290
393
  ---
291
394
 
395
+ ## `useLocaless` Hook
396
+
397
+ `useLocaless<T>` fetches content by slug on the client side and automatically wires up Visual Editor live updates when `enableSync` is active.
398
+
399
+ ```tsx
400
+ 'use client';
401
+
402
+ import { useLocaless } from "@localess/react";
403
+ import type { Page } from "./.localess/localess";
404
+
405
+ export function PageView({ slug }: { slug: string }) {
406
+ const content = useLocaless<Page>(slug, { locale: 'en' });
407
+
408
+ if (!content) return <div>Loading…</div>;
409
+
410
+ return (
411
+ <main>
412
+ {content.data.body.map(item => (
413
+ <LocalessComponent key={item._id} data={item} links={content.links} />
414
+ ))}
415
+ </main>
416
+ );
417
+ }
418
+ ```
419
+
420
+ ### Signature
421
+
422
+ ```typescript
423
+ useLocaless<T extends ContentData = ContentData>(
424
+ slug: string | string[], // string[] is joined with '/' — e.g. ['blog', 'post'] → 'blog/post'
425
+ options?: ContentFetchParams
426
+ ): Content<T> | undefined
427
+ ```
428
+
429
+ - Returns `undefined` while the initial fetch is in flight.
430
+ - When `enableSync` is active and the page is inside the Localess Visual Editor, automatically subscribes to `input` / `change` events and updates the returned value in place.
431
+
432
+ ---
433
+
434
+ ## Link Utilities
435
+
436
+ ### `findLink(links, link)`
437
+
438
+ Resolves a `ContentLink` to a URL string. Use this to convert link fields from Localess content into href values.
439
+
440
+ ```typescript
441
+ import { findLink } from "@localess/react";
442
+
443
+ const href = findLink(content.links, data.ctaLink);
444
+ // type: 'content' → '/' + fullSlug (e.g. '/blog/my-post'), or '/not-found' if not in links map
445
+ // type: 'url' → raw URI (e.g. 'https://example.com')
446
+ ```
447
+
448
+ ```tsx
449
+ const NavLink = ({ data, links }) => (
450
+ <a href={findLink(links, data.link)}>{data.label}</a>
451
+ );
452
+ ```
453
+
454
+ ---
455
+
292
456
  ## Rich Text Rendering
293
457
 
294
458
  Converts Localess `ContentRichText` (Tiptap JSON) to a React node tree.
@@ -361,7 +525,7 @@ const [content, translations] = await Promise.all([
361
525
 
362
526
  ---
363
527
 
364
- ## Full Next.js 15 App Router Setup
528
+ ## Full Next.js 16.2 App Router Setup
365
529
 
366
530
  ```typescript
367
531
  // app/layout.tsx (Server Component)
@@ -437,9 +601,16 @@ export { setFallbackComponent, getFallbackComponent, isSyncEnabled }
437
601
 
438
602
  // Rendering
439
603
  export { LocalessComponent } // Dynamic schema-to-component renderer
604
+ export { LocalessDocument } // Schema renderer + built-in Visual Editor sync
440
605
  export { renderRichTextToReact } // Rich text → React nodes
441
606
  export { resolveAsset } // ContentAsset → full URL
442
607
 
608
+ // Hooks
609
+ export { useLocaless } // Client-side content fetching with sync support
610
+
611
+ // Utilities
612
+ export { findLink } // ContentLink → URL string
613
+
443
614
  // Visual editor (re-exported from @localess/client)
444
615
  export { localessEditable, localessEditableField }
445
616
  export { llEditable, llEditableField } // Deprecated
package/dist/index.d.mts CHANGED
@@ -10,6 +10,13 @@ type LocalessComponentProps<T extends ContentData = ContentData> = {
10
10
  };
11
11
  declare const LocalessComponent: React.ForwardRefExoticComponent<LocalessComponentProps<ContentData> & React.RefAttributes<HTMLElement>>;
12
12
 
13
+ type LocalessDocumentProps<T extends ContentData = ContentData> = {
14
+ data: T;
15
+ links?: Links;
16
+ references?: References;
17
+ };
18
+ declare const LocalessDocument: React.ForwardRefExoticComponent<LocalessDocumentProps<ContentData> & React.RefAttributes<HTMLElement>>;
19
+
13
20
  type LocalessOptions = LocalessClientOptions & {
14
21
  /**
15
22
  * Components mapping for Localess Component integration
@@ -91,4 +98,4 @@ declare function isSyncEnabled(): boolean;
91
98
  */
92
99
  declare function resolveAsset(asset: ContentAsset): string;
93
100
 
94
- export { LocalessComponent, type LocalessComponentProps, type LocalessOptions, type UseLocalessOptions, findLink, getComponent, getFallbackComponent, getLocalessClient, isSyncEnabled, localessInit, registerComponent, renderRichTextToReact, resolveAsset, setComponents, setFallbackComponent, unregisterComponent, useLocaless };
101
+ export { LocalessComponent, type LocalessComponentProps, LocalessDocument, type LocalessDocumentProps, type LocalessOptions, type UseLocalessOptions, findLink, getComponent, getFallbackComponent, getLocalessClient, isSyncEnabled, localessInit, registerComponent, renderRichTextToReact, resolveAsset, setComponents, setFallbackComponent, unregisterComponent, useLocaless };
package/dist/index.d.ts CHANGED
@@ -10,6 +10,13 @@ type LocalessComponentProps<T extends ContentData = ContentData> = {
10
10
  };
11
11
  declare const LocalessComponent: React.ForwardRefExoticComponent<LocalessComponentProps<ContentData> & React.RefAttributes<HTMLElement>>;
12
12
 
13
+ type LocalessDocumentProps<T extends ContentData = ContentData> = {
14
+ data: T;
15
+ links?: Links;
16
+ references?: References;
17
+ };
18
+ declare const LocalessDocument: React.ForwardRefExoticComponent<LocalessDocumentProps<ContentData> & React.RefAttributes<HTMLElement>>;
19
+
13
20
  type LocalessOptions = LocalessClientOptions & {
14
21
  /**
15
22
  * Components mapping for Localess Component integration
@@ -91,4 +98,4 @@ declare function isSyncEnabled(): boolean;
91
98
  */
92
99
  declare function resolveAsset(asset: ContentAsset): string;
93
100
 
94
- export { LocalessComponent, type LocalessComponentProps, type LocalessOptions, type UseLocalessOptions, findLink, getComponent, getFallbackComponent, getLocalessClient, isSyncEnabled, localessInit, registerComponent, renderRichTextToReact, resolveAsset, setComponents, setFallbackComponent, unregisterComponent, useLocaless };
101
+ export { LocalessComponent, type LocalessComponentProps, LocalessDocument, type LocalessDocumentProps, type LocalessOptions, type UseLocalessOptions, findLink, getComponent, getFallbackComponent, getLocalessClient, isSyncEnabled, localessInit, registerComponent, renderRichTextToReact, resolveAsset, setComponents, setFallbackComponent, unregisterComponent, useLocaless };
package/dist/index.js CHANGED
@@ -21,16 +21,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  LocalessComponent: () => LocalessComponent,
24
+ LocalessDocument: () => LocalessDocument,
24
25
  findLink: () => findLink,
25
26
  getComponent: () => getComponent,
26
27
  getFallbackComponent: () => getFallbackComponent,
27
28
  getLocalessClient: () => getLocalessClient,
28
- isBrowser: () => import_client4.isBrowser,
29
- isIframe: () => import_client4.isIframe,
30
- isServer: () => import_client4.isServer,
29
+ isBrowser: () => import_client5.isBrowser,
30
+ isIframe: () => import_client5.isIframe,
31
+ isServer: () => import_client5.isServer,
31
32
  isSyncEnabled: () => isSyncEnabled,
32
- localessEditable: () => import_client4.localessEditable,
33
- localessEditableField: () => import_client4.localessEditableField,
33
+ localessEditable: () => import_client5.localessEditable,
34
+ localessEditableField: () => import_client5.localessEditableField,
34
35
  localessInit: () => localessInit,
35
36
  registerComponent: () => registerComponent,
36
37
  renderRichTextToReact: () => renderRichTextToReact,
@@ -41,7 +42,7 @@ __export(index_exports, {
41
42
  useLocaless: () => useLocaless
42
43
  });
43
44
  module.exports = __toCommonJS(index_exports);
44
- var import_client4 = require("@localess/client");
45
+ var import_client5 = require("@localess/client");
45
46
 
46
47
  // src/components/localess-component.tsx
47
48
  var import_react = require("react");
@@ -137,11 +138,30 @@ var LocalessComponent = (0, import_react.forwardRef)(({ data, links, references,
137
138
  ] });
138
139
  });
139
140
 
140
- // src/hooks/use-localess.ts
141
+ // src/components/localess-document.tsx
141
142
  var import_react2 = require("react");
142
143
  var import_client3 = require("@localess/client");
144
+ var import_jsx_runtime2 = require("react/jsx-runtime");
145
+ var LocalessDocument = (0, import_react2.forwardRef)(({ data, links, references, ...restProps }, ref) => {
146
+ const [contentData, setContentData] = (0, import_react2.useState)(data);
147
+ (0, import_react2.useEffect)(() => {
148
+ if (isSyncEnabled() && (0, import_client3.isBrowser)()) {
149
+ window.localess?.on(["input", "change"], (event) => {
150
+ console.log("Localess:event", event);
151
+ if (event.type === "change" || event.type === "input") {
152
+ setContentData(event.data);
153
+ }
154
+ });
155
+ }
156
+ });
157
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LocalessComponent, { data: contentData, links, references, ...restProps });
158
+ });
159
+
160
+ // src/hooks/use-localess.ts
161
+ var import_react3 = require("react");
162
+ var import_client4 = require("@localess/client");
143
163
  var useLocaless = (slug, options = {}) => {
144
- const [document, setDocument] = (0, import_react2.useState)();
164
+ const [document, setDocument] = (0, import_react3.useState)();
145
165
  const client = getLocalessClient();
146
166
  let normalizedSlug;
147
167
  if (Array.isArray(slug)) {
@@ -149,11 +169,11 @@ var useLocaless = (slug, options = {}) => {
149
169
  } else {
150
170
  normalizedSlug = slug;
151
171
  }
152
- (0, import_react2.useEffect)(() => {
172
+ (0, import_react3.useEffect)(() => {
153
173
  async function loadDocument() {
154
174
  const document2 = await client.getContentBySlug(normalizedSlug, options);
155
175
  setDocument(document2);
156
- if (isSyncEnabled() && (0, import_client3.isBrowser)()) {
176
+ if (isSyncEnabled() && (0, import_client4.isBrowser)()) {
157
177
  window.localess?.on(["input", "change"], (event) => {
158
178
  if (event.type === "change" || event.type === "input") {
159
179
  setDocument({ ...document2, data: event.data });
@@ -188,7 +208,7 @@ function findLink(links, link) {
188
208
  }
189
209
 
190
210
  // src/richtext.ts
191
- var import_react3 = require("@tiptap/static-renderer/pm/react");
211
+ var import_react4 = require("@tiptap/static-renderer/pm/react");
192
212
  var import_extension_document = require("@tiptap/extension-document");
193
213
  var import_extension_text = require("@tiptap/extension-text");
194
214
  var import_extension_paragraph = require("@tiptap/extension-paragraph");
@@ -205,7 +225,7 @@ var import_extension_code = require("@tiptap/extension-code");
205
225
  var import_extension_code_block_lowlight = require("@tiptap/extension-code-block-lowlight");
206
226
  var import_extension_link = require("@tiptap/extension-link");
207
227
  function renderRichTextToReact(content) {
208
- return (0, import_react3.renderToReactElement)({
228
+ return (0, import_react4.renderToReactElement)({
209
229
  content,
210
230
  extensions: [
211
231
  import_extension_document.Document,
@@ -231,6 +251,7 @@ function renderRichTextToReact(content) {
231
251
  // Annotate the CommonJS export names for ESM import in node:
232
252
  0 && (module.exports = {
233
253
  LocalessComponent,
254
+ LocalessDocument,
234
255
  findLink,
235
256
  getComponent,
236
257
  getFallbackComponent,
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { localessEditable as localessEditable2, localessEditableField, isBrowser as isBrowser2, isServer, isIframe } from "@localess/client";
2
+ import { localessEditable as localessEditable2, localessEditableField, isBrowser as isBrowser3, isServer, isIframe } from "@localess/client";
3
3
 
4
4
  // src/components/localess-component.tsx
5
5
  import { forwardRef } from "react";
@@ -95,11 +95,30 @@ var LocalessComponent = forwardRef(({ data, links, references, ...restProps }, r
95
95
  ] });
96
96
  });
97
97
 
98
- // src/hooks/use-localess.ts
99
- import { useEffect, useState } from "react";
98
+ // src/components/localess-document.tsx
99
+ import { forwardRef as forwardRef2, useEffect, useState } from "react";
100
100
  import { isBrowser } from "@localess/client";
101
+ import { jsx as jsx2 } from "react/jsx-runtime";
102
+ var LocalessDocument = forwardRef2(({ data, links, references, ...restProps }, ref) => {
103
+ const [contentData, setContentData] = useState(data);
104
+ useEffect(() => {
105
+ if (isSyncEnabled() && isBrowser()) {
106
+ window.localess?.on(["input", "change"], (event) => {
107
+ console.log("Localess:event", event);
108
+ if (event.type === "change" || event.type === "input") {
109
+ setContentData(event.data);
110
+ }
111
+ });
112
+ }
113
+ });
114
+ return /* @__PURE__ */ jsx2(LocalessComponent, { data: contentData, links, references, ...restProps });
115
+ });
116
+
117
+ // src/hooks/use-localess.ts
118
+ import { useEffect as useEffect2, useState as useState2 } from "react";
119
+ import { isBrowser as isBrowser2 } from "@localess/client";
101
120
  var useLocaless = (slug, options = {}) => {
102
- const [document, setDocument] = useState();
121
+ const [document, setDocument] = useState2();
103
122
  const client = getLocalessClient();
104
123
  let normalizedSlug;
105
124
  if (Array.isArray(slug)) {
@@ -107,11 +126,11 @@ var useLocaless = (slug, options = {}) => {
107
126
  } else {
108
127
  normalizedSlug = slug;
109
128
  }
110
- useEffect(() => {
129
+ useEffect2(() => {
111
130
  async function loadDocument() {
112
131
  const document2 = await client.getContentBySlug(normalizedSlug, options);
113
132
  setDocument(document2);
114
- if (isSyncEnabled() && isBrowser()) {
133
+ if (isSyncEnabled() && isBrowser2()) {
115
134
  window.localess?.on(["input", "change"], (event) => {
116
135
  if (event.type === "change" || event.type === "input") {
117
136
  setDocument({ ...document2, data: event.data });
@@ -188,11 +207,12 @@ function renderRichTextToReact(content) {
188
207
  }
189
208
  export {
190
209
  LocalessComponent,
210
+ LocalessDocument,
191
211
  findLink,
192
212
  getComponent,
193
213
  getFallbackComponent,
194
214
  getLocalessClient,
195
- isBrowser2 as isBrowser,
215
+ isBrowser3 as isBrowser,
196
216
  isIframe,
197
217
  isServer,
198
218
  isSyncEnabled,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@localess/react",
3
- "version": "3.0.1-dev.20260408170501",
3
+ "version": "3.0.1-dev.20260408183448",
4
4
  "description": "ReactJS JavaScript/TypeScript SDK for Localess's API.",
5
5
  "keywords": [
6
6
  "localess",
@@ -46,7 +46,7 @@
46
46
  "react-dom": "^17 || ^18 || ^19"
47
47
  },
48
48
  "dependencies": {
49
- "@localess/client": "3.0.1-dev.20260408170501",
49
+ "@localess/client": "3.0.1-dev.20260408183448",
50
50
  "@tiptap/static-renderer": "^3.20.1",
51
51
  "@tiptap/html": "^3.20.1",
52
52
  "@tiptap/extension-bold": "^3.20.1",