@joai/sdk
The React entry point contains reusable building blocks for editable content on client websites. The components stay domain-neutral: sites provide identifiers, copy, styling hooks, and rendered fields; the SDK owns session handoff and the editing lifecycle.
Managed content editor
import { ManagedContentEditor } from '@joai/react'
<ManagedContentEditor
config={{
contentId: 'content-id',
publicSlug: 'website-content',
sessionStorageKey: 'website-content-session',
team: 'team-slug',
}}
initialContent={content}
title="Inhalte bearbeiten"
labels={{
edit: 'Bearbeiten',
save: 'Entwurf speichern',
saving: 'Wird gespeichert …',
publish: 'Veröffentlichen',
publishing: 'Wird veröffentlicht …',
cancel: 'Abbrechen',
toolbar: 'Inhaltseditor',
unsaved: 'Nicht gespeicherte Änderungen',
}}
messages={{
conflict: 'Der Inhalt wurde zwischenzeitlich geändert. Deine Eingaben bleiben erhalten. Bitte prüfe sie und versuche es erneut.',
loadError: 'Der Inhalt konnte nicht geladen werden.',
publishError: 'Die Änderungen konnten nicht veröffentlicht werden.',
published: 'Die Änderungen wurden veröffentlicht.',
saveError: 'Der Entwurf konnte nicht gespeichert werden.',
saved: 'Der Entwurf wurde gespeichert.',
sessionExpired: 'Deine Sitzung ist abgelaufen.',
unsavedNavigation: 'Du hast nicht gespeicherte Änderungen. Seite wirklich verlassen?',
}}
imagePickerLabels={{
applyCrop: 'Zuschnitt übernehmen',
back: 'Zurück',
choose: 'Bild auswählen',
close: 'Schließen',
cropDescription: 'Passe Bildausschnitt und Zoom vor dem Hochladen an.',
cropTitle: 'Bild zuschneiden',
dimensionsTooLarge: 'Die Bildabmessungen sind zu groß.',
dimensionsTooSmall: 'Die Bildabmessungen sind zu klein.',
drop: 'Bild hier ablegen',
empty: 'Noch keine Bilder vorhanden.',
field: 'Bearbeitbares Bild',
invalidType: 'Bitte wähle eine Bilddatei.',
libraryDescription: 'Wähle ein vorhandenes Bild oder lade ein neues hoch.',
libraryTitle: 'Bildmediathek',
loadError: 'Bilder konnten nicht geladen werden.',
loading: 'Bilder werden geladen …',
positionHorizontal: 'Horizontale Position',
positionVertical: 'Vertikale Position',
processing: 'Bild wird vorbereitet …',
retry: 'Erneut versuchen',
tooLarge: 'Das Bild ist zu groß.',
upload: 'Bild hochladen',
uploadError: 'Das Bild konnte nicht hochgeladen werden.',
uploading: 'Wird hochgeladen …',
zoom: 'Zoom',
}}
reauthenticateLabel="Anmeldung erneuern"
sessionExpiredMessage="Deine Sitzung ist abgelaufen. Deine lokalen Änderungen bleiben erhalten."
confirmCancel={() => window.confirm('Nicht gespeicherte Änderungen verwerfen?')}
confirmLeave={() => window.confirm('Du hast nicht gespeicherte Änderungen. Seite wirklich verlassen?')}
confirmPublish={() => window.confirm('Änderungen jetzt veröffentlichen?')}
>
{children}
</ManagedContentEditor>
Use EditableContent, EditableImage, and useContentValue from @joai/react inside the editor. Pass server-fetched public content as initialContent to avoid a client-side loading state for visitors.
Configure each image field as a reusable, domain-neutral building block. Images are compressed to WebP at up to 2560 × 2560 pixels by default, while animated GIF and SVG files retain their original format. Cropping is opt-in per field:
<EditableImage
field="heroImage"
label="Titelbild"
maxSourceBytes={40 * 1024 * 1024}
maxUploadBytes={5 * 1024 * 1024}
processing={{
crop: { aspectRatio: 16 / 9, outputWidth: 1920 },
compression: { maxWidth: 1920, maxHeight: 1080, mimeType: 'image/webp', quality: 0.84 },
dimensions: { minWidth: 1280, minHeight: 720, maxPixels: 50_000_000 },
}}
/>
Set compression: false or dimensions: false to disable only that stage, or processing={false} to upload the original image without client-side decoding, validation, cropping, or compression.
Session storage
useContentSession and createSessionTokenStore are exported for custom shells. A custom SessionTokenStore can be injected when a host needs a different persistence mechanism. The URL fragment containing the one-time handoff token is removed before it is exchanged and persisted.
The managed editor automatically adds platform=app to its reauthentication return URL so JoAi returns a one-time handoff token. Override reauthenticateUrl only when a host has a dedicated login callback.
createContentDraftStore uses sessionStorage by default. ManagedContentEditor wires it automatically so unsaved values survive refreshes and expired-session reauthentication without being shared across browser sessions.
Editing behavior
- Cancel restores the currently loaded version.
- Save is disabled when there are no local changes.
- A dirty publish saves and publishes with one request.
- Network failures keep local edits available for retry.
- Refresh, close, and link/navigation attempts warn while edits are unsaved.
- Editable text and image fields are visibly labelled in edit mode.
- The image library supports loading, empty, error, byte/dimension validation, cropping, compression, keyboard, focus, and drag-and-drop states.
- Expired authorization offers a sign-in action and restores locally persisted edits afterwards.
- Version conflicts load the latest version while preserving local edits.
- Public content cache entries are invalidated after authenticated mutations.