# Assets Localess Assets is a file manager for all media and documents used across your content. Files are organised in a folder hierarchy and served via CDN URLs that you can reference from content schemas or fetch directly via the API. ## Main Screen [#main-screen] Navigate to **Assets** from the side menu. Assets are displayed in a grid of folders and files, with folders always listed first. Assets screen ## Navigation [#navigation] The breadcrumb bar at the top of the asset grid shows your current location in the folder tree. Click any segment to navigate back up. Click a folder card to enter it. ## Toolbar [#toolbar] | Action | Description | | ----------------- | -------------------------------------------------------- | | **Upload Assets** | Select one or more files to upload to the current folder | | **Create Folder** | Create a new subfolder in the current location | | **View toggle** | Switch between grid and list view | ## Uploading Files [#uploading-files] Files can be added in three ways: * **Upload from device** — click **Upload Assets** and select one or more files from your device. * **Drag and drop** — drag files directly onto the asset grid. * **From URL** — provide a web URL and Localess fetches and stores the file for you. Multiple files can be uploaded at once. Each file shows an in-progress indicator while uploading. Once complete, the file card appears in the grid with its preview, type badge, and metadata. ## Creating Folders [#creating-folders] Click **Create Folder** and enter a name to create a new subfolder in the current location. Use folders to organise assets by project area, content type, or locale (e.g. `images/`, `icons/`, `documents/`). ## File Cards [#file-cards] Each file card displays: | Field | Description | | -------------- | --------------------------------------------------------------------------- | | **Preview** | Thumbnail for images; icon for other file types | | **Type badge** | File extension shown in the top-right corner (e.g. `.svg`, `.webp`, `.png`) | | **Name** | File name | | **Size** | File size in human-readable format (e.g. `4.49 MB`) | | **Dimensions** | Width × Height in pixels, shown for images and video (e.g. `W3749 x H1804`) | | **Duration** | Playback length for animated and video files (e.g. `Duration: 31s`) | | **Date** | Date the file was uploaded | ## File Actions [#file-actions] Hover over a file card to reveal action buttons: | Action | Description | | ----------------- | ---------------------------------------------------- | | **Edit** (pencil) | Edit the file's alt text and other editable metadata | | **Download** | Download the original file | | **Delete** | Permanently delete the file | > Deleting a file that is referenced in published content will leave those references unresolvable. Check content usage before deleting. ## Folder Actions [#folder-actions] Hover over a folder card to reveal: | Action | Description | | ----------------- | -------------------------------------- | | **Edit** (pencil) | Rename the folder | | **Delete** | Delete the folder and all its contents | ## Pagination [#pagination] The bottom of the asset grid shows the current page range and total item count. Use the **Items per page** dropdown to control how many assets are shown at once, and the arrow buttons to move between pages. ## Alt Text [#alt-text] Each file has an optional **alt** field for accessibility and SEO. Set it by clicking the edit (pencil) action on a file card. The alt text is included in the API response and can be used directly in your image components. ## Using Assets in Content [#using-assets-in-content] Reference an asset in a content schema by using a field of type `Asset`. When fetching content via the API the field resolves to a `ContentAsset` object containing the asset URI: ```typescript interface ContentAsset { kind: 'ASSET'; uri: string; // Relative asset path } ``` Resolve the URI to a full CDN URL using the SDK: ```typescript // @localess/client const url = client.assetLink(content.data.image); // @localess/react import { resolveAsset } from "@localess/react"; const url = resolveAsset(content.data.image); ``` See [Schemas](schemas/overview) for how to define asset fields in your content model. # Content Content in Localess is made up of **Documents** and **Folders**. A Document holds the actual content data, shaped by a [Schema](schemas/overview) of type ROOT. Folders let you organise documents into a hierarchy that maps directly to URL slugs. Both are available via the API and the SDKs. ## Main Screen [#main-screen] Navigate to **Content** from the side menu. The screen shows all folders and documents in the current location, with folders listed first. Content list The list has the following columns: | Column | Description | | -------------- | ----------------------------------------------------------------------------------------- | | **Status** | Publish state of the document — solid icon means published, dashed icon means unpublished | | **Name** | Display name with the slug shown beneath in muted text (e.g. `#home`) | | **Schema** | The ROOT schema this document uses. Empty for folders | | **Updated At** | Timestamp of the last saved change | | **Actions** | Three-dot menu for edit, duplicate, and delete | ## Navigation [#navigation] The breadcrumb bar at the top of the list shows your current position in the folder tree. Click any segment to navigate back up, or click a folder row to enter it. ## Toolbar [#toolbar] | Action | Description | | ---------------------------- | --------------------------------------------------- | | **+ Add Content → Document** | Create a new content document in the current folder | | **+ Add Content → Folder** | Create a new folder in the current location | ## Add Folder [#add-folder] | Field | Required | Description | | -------- | -------- | --------------------------------------------------------------------------------------------------- | | **Name** | ✅ | Display name shown in the content list | | **Slug** | ✅ | URL-safe identifier for this folder (e.g. `blog`). Combined with parent slugs to form the full path | ## Add Document [#add-document] | Field | Required | Description | | ---------- | -------- | ----------------------------------------------------------------------------------------------------- | | **Name** | ✅ | Display name shown in the content list | | **Slug** | ✅ | URL-safe identifier for this document (e.g. `home`). Combined with parent slugs to form the full path | | **Schema** | ✅ | The ROOT schema that defines the shape of this document's fields | ## Slug System [#slug-system] Every folder and document has three slug-related fields: | Field | Description | Example | | ------------ | --------------------------------------------------- | -------------- | | `slug` | The identifier for this item alone | `my-post` | | `parentSlug` | The full path of the containing folder | `blog` | | `fullSlug` | The complete path used to fetch content via the API | `blog/my-post` | Use `fullSlug` as the value passed to `getContentBySlug()` in the SDK or API. ## Document Editor [#document-editor] Click any document row to open it in the editor. The editor is split into two panels: Document editor ### Left panel — Visual Editor preview [#left-panel--visual-editor-preview] When a frontend application is connected to the Localess Visual Editor, the left panel shows a live iframe preview of the rendered page. Changes made in the form on the right are reflected in the preview in real time. See [Visual Editor](visual-editor) for setup instructions. ### Right panel — Schema form [#right-panel--schema-form] The right panel renders all fields defined in the document's ROOT schema as an editable form. Field types — text, rich text, arrays, asset pickers, nested blocks — are rendered according to the schema definition. At the top of the right panel: | Control | Description | | ------------------- | ---------------------------------------------------------------------------------------------------- | | **Locale dropdown** | Select the locale being edited. Each locale's values are stored independently | | **Save** | Save the current locale's changes as a draft. A dot indicator appears when there are unsaved changes | | **Publish** | Promote all saved changes to the published version | The form is validated in real time against the selected schema and locale. On save, all schema constraints across all locales are checked. ## Save vs Publish [#save-vs-publish] Content follows a two-state model: | State | How to access via API | | ----------------- | ------------------------------------------ | | **Saved (draft)** | Available immediately with `version=draft` | | **Published** | Available without specifying a version | This lets you author and preview content before making it available to production consumers. ## Edit Document Properties [#edit-document-properties] To rename a document or change its slug after creation, use the **Actions** menu (three-dot icon) on the content list row. Changes to a slug will update the `fullSlug` of the document and all its children. ## Fetching Content [#fetching-content] Use the [TypeScript SDK](sdk/typescript) or [REST API](api/overview) to fetch content at runtime: ```typescript // Fetch by full slug const content = await client.getContentBySlug('blog/my-post', { locale: 'en', resolveReference: true, resolveLink: true, }); // Fetch by document ID const content = await client.getContentById('abc123', { locale: 'en' }); ``` See [Schemas](schemas/overview) for how to define the fields that appear in the document editor, and [CLI](cli) for generating TypeScript types from your schemas. # Draft & Publish Localess separates **saving** from **publishing**. When an editor saves content, it is stored as a **Draft** — visible only to people previewing with `?version=draft`. Nothing changes on the live site. Only when an editor explicitly clicks **Publish** does that content become the **Published** version and reach production consumers. This workflow protects your team from accidental publishes, gives editors a safe space to work in progress, and lets reviewers check content before it goes live. *** ## The Two States [#the-two-states] | State | Who can see it | How to access via API | | ------------- | ----------------------- | -------------------------------- | | **Draft** | Editors in preview mode | `?version=draft` query parameter | | **Published** | Production visitors | No version parameter (default) | A document always has exactly one Draft state and one Published state. Saving updates the Draft. Publishing copies the current Draft into the Published slot. *** ## How It Works for Editors [#how-it-works-for-editors] ### Writing and saving [#writing-and-saving] Open a document in the Content editor, make your changes, and click **Save** (or press `⌘S` / `Ctrl+S`). A dot indicator in the toolbar appears whenever there are unsaved changes. Saving is safe — nothing goes live until you publish. You can save as many times as you need. Reviewers can preview your saved changes using draft mode in a connected frontend or the Localess Visual Editor. ### Reviewing in draft mode [#reviewing-in-draft-mode] Pass `version=draft` when fetching content via the SDK to see your saved-but-unpublished changes: ```typescript const draft = await client.getContentBySlug('home', { locale: 'en', version: 'draft', }); ``` The [Visual Editor](visual-editor) connected to your frontend automatically reflects the current Draft state in real time as you type — no explicit save required for the live preview. ### Publishing [#publishing] When the content is ready to go live, click **Publish** in the document editor toolbar. Localess promotes the current Draft to the Published state. The change becomes available to production consumers typically within 30 seconds via CDN — no build step, no deploy, no code change required. If you are not ready to publish everything at once, that is fine: only click Publish when the content is truly ready. Your draft stays in the Draft state indefinitely. *** ## Translations Follow the Same Model [#translations-follow-the-same-model] The Translations module works identically. Saved translation strings are available in draft mode. Click **Publish** in the Translations toolbar to make the entire current set of translations available to production. The toolbar shows an indicator when there are unpublished changes pending so editors always know the current state before walking away. *** ## Draft & Publish by Workflow Stage [#draft--publish-by-workflow-stage] Here is how a typical editorial team moves content through the workflow: | Stage | Action | State after | | ------------------------ | ------------------------------ | -------------------------------------------------- | | Writer starts a draft | Opens doc, adds content, saves | **Draft** — not live | | Writer shares for review | Sends a draft preview link | **Draft** — reviewer sees it with `?version=draft` | | Editor requests changes | Writer edits and saves again | **Draft** — updated | | Editor approves | Clicks **Publish** | **Published** — live on the site | No one needs to file a ticket, open a PR, or wait for a deploy. The writer and editor own the entire workflow end-to-end. *** ## Frequently Asked Questions [#frequently-asked-questions] **Can I unpublish content after publishing it?** Yes. Replace the published document with an empty state or a redirect page, or delete the document entirely. Deleting a document removes it from both the Draft and Published states. **Does publishing one document affect others?** No. Each document is published independently. You can publish a single article without touching any other content. **Can I see what changed between my current draft and what is live?** Not as a visual diff in the current UI — compare by fetching both `version=draft` and the default (published) response via the API and comparing the JSON. **Does Publish push changes immediately?** Yes. Publishing triggers Localess to convert the Firestore document into static JSON in Cloud Storage. CDN propagation typically completes within 30 seconds. **What if I want to batch publish many changes at once?** Save each document individually as you work, then publish each one when you are ready. There is no scheduled or batched publish across multiple documents in the current version. # Getting Started This guide takes you from zero to a working Localess space connected to a frontend. ## 1. Deploy Localess [#1-deploy-localess] Pick the path that matches your goal: * **Production** — [Deploy on Firebase](setup/firebase). Required for live applications. * **Local exploration** — [Run locally](setup/local). Spin up the full stack without a Google Cloud project. Once Localess is running, open `/auth/setup` to create your first user. The full URL depends on the deployment method. ## 2. Create your first space [#2-create-your-first-space] A space is the top-level container for content, translations, assets, and schemas. Sign in to the admin UI and create one. Note the **Space ID** and generate an **API token** in **Settings → Access Tokens** — you'll need both to talk to the API. ## 3. Define your content [#3-define-your-content] In the admin UI: 1. Go to **Schema** and create a ROOT schema for your first document type (e.g., `page`). See [Schema → Overview](schemas) for field types. 2. Go to **Content** and create a document using that schema. 3. Go to **Translations** and add the strings your application needs. ## 4. Pull content into your frontend [#4-pull-content-into-your-frontend] Choose the integration that fits your stack: | Tool | Use it for | | ------------------------------------ | ---------------------------------------------------------------------- | | [`@localess/client`](sdk/typescript) | Server-side TypeScript / Node — the foundation for everything below | | [`@localess/react`](sdk/react) | React, Next.js (App Router, RSC, or static export) | | [`@localess/cli`](cli) | Push/pull translations and generate TypeScript types from your schemas | | [REST API](api/overview) | Any language or framework without an official SDK | Minimal example with the JS client: ```typescript import { localessClient } from "@localess/client"; const client = localessClient({ origin: "https://my-localess.web.app", spaceId: "YOUR_SPACE_ID", token: "YOUR_API_TOKEN", // server-side only }); const home = await client.getContentBySlug("home", { locale: "en" }); const ui = await client.getTranslations("en"); ``` ## 5. Generate types (recommended) [#5-generate-types-recommended] Once you have schemas defined, use the CLI to generate TypeScript types so `getContentBySlug(...)` is fully typed: ```bash npm install @localess/cli -D npx localess login npx localess types generate ``` Details: [CLI → Type Generation](cli/types). ## What's next [#whats-next] * [Translations](translations) — manage UI strings and locales * [Content](content) — author documents and folders * [Visual Editor](visual-editor) — preview live edits in your running app * [Schemas](schemas/overview) — design content models * [API reference](api/overview) — endpoints, parameters, responses # Image Transforms Localess asset URLs support on-demand image transformation via query parameters. Append a parameter to any asset CDN URL to get back a resized, reformatted, or quality-adjusted image — without uploading multiple size variants and without a third-party image CDN. This is useful for responsive images, thumbnails, and format optimization. The transformation happens server-side and the result is cached at the CDN edge. *** ## URL Parameter Reference [#url-parameter-reference] Append parameters to any asset URL returned by `client.assetLink()` or `resolveAsset()`. | Parameter | Type | Description | Example | | ----------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | | `w` | integer > 0 | Target width in pixels | `?w=800` | | `h` | integer > 0 | Target height in pixels | `?h=600` | | `q` | integer (1–100) | Output quality (default: `85`). Applies to JPEG, WebP, AVIF. Ignored for PNG. | `?q=70` | | `f` | string | Output format: `webp`, `jpeg`, `png`, or `avif`. Converts the image to this format. | `?f=webp` | | `download` | flag | Changes `Content-Disposition` from `inline` to `form-data`, forcing a browser download. | `?download` | | `thumbnail` | flag | For animated WebP/GIF: extracts the first frame before resizing. For video: extracts a frame with FFmpeg, then resizes with Sharp. | `?thumbnail` | ### Resize Behaviour (`w` / `h`) [#resize-behaviour-w--h] | `w` | `h` | Behaviour | | --- | --- | ------------------------------------------------------------------------ | | ✓ | — | Scale to width, height auto — aspect ratio preserved, no crop | | — | ✓ | Scale to height, width auto — aspect ratio preserved, no crop | | ✓ | ✓ | **Cover crop** — resizes to fill the exact box, excess edges are cropped | | — | — | No resize — only format/quality re-encoding if `f`/`q` provided | ### Special Cases [#special-cases] * **SVG** — always passed through; `w`, `h`, and `f` are ignored. * **Animated WebP or GIF without `thumbnail`** — passed through unchanged (cannot resize animated files). * **Animated WebP or GIF with `thumbnail`** — first frame extracted, then `w`/`h`/`f` apply normally. * **Video with `w` + `thumbnail`** — frame extracted via FFmpeg, then resized with Sharp; output defaults to `image/webp`. Combine parameters with `&`: ``` https://my-space.web.app/api/v1/spaces/abc123/assets/hero.jpg?w=1200&q=80&f=webp ``` *** ## Examples [#examples] ### Resize to a fixed width [#resize-to-a-fixed-width] ``` /api/v1/spaces/abc123/assets/hero.jpg?w=800 ``` Returns the image at 800 px wide. Height scales proportionally to preserve the aspect ratio. ### Resize to a fixed height [#resize-to-a-fixed-height] ``` /api/v1/spaces/abc123/assets/hero.jpg?h=400 ``` Returns the image at 400 px tall. Width scales proportionally. ### Crop to exact dimensions [#crop-to-exact-dimensions] ``` /api/v1/spaces/abc123/assets/hero.jpg?w=800&h=600 ``` Returns a 800×600 image using cover crop — the image is scaled to fill the box and excess edges are trimmed. Use this for fixed-size slots like thumbnails or hero banners. ### Convert to WebP with quality control [#convert-to-webp-with-quality-control] ``` /api/v1/spaces/abc123/assets/product.png?f=webp&q=75 ``` Converts a PNG to WebP at 75% quality. Typical file size reduction: 25–40% compared to the original PNG. ### Responsive thumbnail [#responsive-thumbnail] ``` /api/v1/spaces/abc123/assets/blog-cover.jpg?w=400&f=webp&q=80 ``` A compact, web-optimized version of a full-size image — suitable for article cards and preview grids. *** ## Using Transforms in Code [#using-transforms-in-code] ### @localess/client [#localessclient] Build the transform URL by appending parameters to the result of `assetLink()`: ```typescript import { localessClient } from '@localess/client'; const client = localessClient({ spaceId: 'abc123', baseUrl: 'https://my-space.web.app' }); // Base CDN URL const url = client.assetLink(content.data.heroImage); // → https://my-space.web.app/api/v1/spaces/abc123/assets/hero.jpg // With transform const optimized = `${url}?w=1200&f=webp&q=80`; ``` ### @localess/react [#localessreact] ```tsx import { resolveAsset } from '@localess/react'; function HeroImage({ image }: { image: ContentAsset }) { const baseUrl = resolveAsset(image); return ( {image.alt ); } ``` ### @localess/angular — automatic with NgOptimizedImage [#localessangular--automatic-with-ngoptimizedimage] When you use `provideLocalessBrowser()`, Angular's `NgOptimizedImage` (`ngSrc`) directive automatically appends `?w=` to the asset URL based on the `width` attribute. No manual parameter construction is needed: ```html Hero image ``` *** ## URL Transforms vs Uploading Pre-Sized Assets [#url-transforms-vs-uploading-pre-sized-assets] | Approach | Best when | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | **URL transforms** | You upload once and serve at multiple sizes — responsive images, thumbnails, format variants | | **Pre-sized uploads** | You need pixel-perfect control over cropping, or you have assets that are already optimized at a fixed size (icons, logos, print-ready images) | For most editorial content (blog images, product photos, hero banners) URL transforms are the right default. Upload the highest-quality version of an image once, then let Localess handle resizing and format conversion at request time. Pre-sized uploads make sense for: * Art-directed crops (different composition at mobile vs desktop) * Images where the original quality must be preserved exactly (print, legal documents) * SVG and other vector formats that do not benefit from raster transforms *** ## Format Recommendations [#format-recommendations] | Source format | Recommended `?f=` value | Reason | | ------------------------- | ----------------------- | ---------------------------------------------------------------------- | | JPEG | `webp` | 25–35% smaller at equivalent quality | | PNG (photographic) | `webp` or `avif` | Better compression than PNG for photos | | PNG (transparency needed) | `webp` or `avif` | Both support alpha — AVIF offers better compression on modern browsers | | Already WebP | — | No conversion needed; add `?f=avif` to target Chrome/Firefox users | | Animated GIF/WebP | — | Pass through unless `?thumbnail` is set | Check browser support for your target audience before relying on WebP or AVIF exclusively. For broad compatibility, serve AVIF with a WebP and JPEG fallback using `` and ``. # Overview Localess is an open-source headless CMS with first-class translation management, a Visual Editor, and a JSON API. You define content shapes with schemas, edit content (with optional AI-assisted translation) in the admin UI, and pull it into any frontend through the REST API or one of the official SDKs. It runs on Firebase — the recommended path for production — or locally for development and exploration. ## When to reach for Localess [#when-to-reach-for-localess] * You need to manage UI strings and long-form content for an app or website that ships in multiple languages. * You want editors to update content without rebuilding the application. * You want a typed, schema-driven CMS rather than a freeform document store. ## Get started [#get-started] * New here? Follow [Getting Started](getting-started) to deploy a space and connect your first frontend. * Deploying to production? Use [Setup → Firebase](setup/firebase). * Just exploring locally? Use [Setup → Local Run](setup/local). ## What you get [#what-you-get] **Translation management.** Manage all UI strings in one place, translate with Google Translate, and ship updates without an application build. Served from the Google CDN (\~20 ms for 5,000 translations). **Schema-driven content.** Define your content shape with schemas (ROOT documents, NODE blocks, ENUM values), validate at edit time, and create hierarchical content trees. Editors get a Visual Editor that highlights live components and reflects changes instantly. **Typed API.** Pull content, translations, and assets through a JSON API or a typed SDK ([`@localess/client`](sdk/typescript), [`@localess/react`](sdk/react)). Generate TypeScript types from your schemas with the [CLI](cli). **Operations included.** Granular per-user permissions, import/export for backups and migrations, and an OpenAPI spec for any language or framework not covered by an SDK. ## Project & community [#project--community] Localess is part of the [Lessify Project](https://github.com/Lessify), an open-source effort funded entirely through sponsorship. * Star the [GitHub repo](https://github.com/Lessify/localess) * Follow updates on [Twitter](https://twitter.com/lessifyio) or [Telegram](https://t.me/lessify) * Sponsor development via [GitHub Sponsors](https://github.com/sponsors/Lessify) # Localization Localess is built for global teams that need to ship in multiple languages. It provides **two complementary localization capabilities** that solve different problems. Understanding which one to reach for — and when to use both — is the starting point for any multilingual project. | Capability | What it manages | Audience | | ----------------------- | ---------------------------------------------------------------- | --------------------------------------- | | **Translation Manager** | UI strings — button labels, error messages, navigation copy | Frontend developers and content editors | | **Content Translation** | Structured content documents — articles, pages, product listings | Editors and content managers | *** ## Translation Manager [#translation-manager] ### What it is [#what-it-is] Translation Manager is Localess's key–value string management system for web application UI copy. Think of it as a hosted, editor-friendly replacement for local i18n JSON files — the kind you would normally manage with a library like [i18next](https://www.i18next.com/), [react-intl](https://formatjs.io/docs/react-intl/), or Angular's built-in i18n module. Instead of committing JSON files to your repository, you manage strings in the Localess UI and pull them at runtime via the API. The API returns a flat key–value map that drops straight into any i18n library. ### When to use it [#when-to-use-it] Use Translation Manager when you need to translate **application UI strings** — labels, error messages, tooltips, button copy, navigation items — that live in your codebase rather than in content documents. Good candidates: * `"nav.home"` → `"Home"` / `"Accueil"` / `"Startseite"` * `"errors.required"` → `"This field is required"` / `"Ce champ est obligatoire"` * `"common.submit"` → `"Submit"` / `"Envoyer"` / `"Absenden"` Not a good fit: long-form editorial content (articles, landing pages, product descriptions). Use [Content Translation](#content-translation) for those. ### How it works [#how-it-works] 1. Navigate to **Translations** in the side menu. 2. Click **+ Add Translation** and give the entry a unique **ID** — for example, `nav.home` or `errors.required`. 3. Add the value for your default locale. 4. Enable **Auto-translate** to request machine translations for all other configured locales automatically (powered by Google Cloud Translation or DeepL). 5. Click **Publish** when you are ready to make the strings available to production consumers. Translations follow the same [Draft & Publish](draft-publish) model as content: saved changes are immediately available with `?version=draft`, and published changes are available to production without a version parameter. ### Translation Status [#translation-status] Localess tracks completeness automatically: | Status | Meaning | | ------------------------ | ---------------------------------------------- | | **Translated** | All configured locales have a value | | **Partially Translated** | Some locales are filled in, others are missing | | **Untranslated** | No locale has a value yet | Use the **Status** filter in the toolbar to find strings that need attention before your next release. ### Code example — fetching via SDK [#code-example--fetching-via-sdk] ```typescript // @localess/client const translations = await client.getTranslations('en'); // { "nav.home": "Home", "errors.required": "This field is required", ... } // Pass directly to i18next or any other i18n library i18next.addResourceBundle('en', 'translation', translations); ``` ```bash # Pull translations to a local JSON file using the CLI npx localess translations pull en --path ./locales/en.json ``` The flat JSON format is compatible with i18next, react-intl, LinguiJS, Vue I18n, Angular's `$localize`, and any other library that accepts a key–value map. *** ## Content Translation [#content-translation] ### What it is [#what-it-is-1] Content Translation is the per-field multilingual capability built into Localess content documents. Every document can store independent values for each field in every locale you have configured. Editors switch between locales inside the same document editor and fill in each language version independently. This is different from Translation Manager: you are not managing short UI strings — you are managing full structured content documents (pages, articles, product listings) where each field (title, body, image alt text, CTA label) needs a locale-specific value. ### When to use it [#when-to-use-it-1] Use Content Translation when you need to translate **structured content documents** — anything shaped by a Schema in Localess. Good candidates: * A blog article that has a different title, body, and metadata in each language * A product listing where the description, features, and pricing copy differ by locale * A landing page where headlines and body copy are fully translated rather than just substituted Not a good fit: short application UI strings that do not belong in a CMS document. Use [Translation Manager](#translation-manager) for those. ### How it works [#how-it-works-1] 1. Open a document in the Content editor. 2. In the right panel, use the **Locale dropdown** to select the locale you want to edit. 3. Fill in all fields for that locale. Fields that have not been filled in for a locale remain empty in the API response for that locale. 4. Save the locale's content, then switch to the next locale and repeat. 5. When all locales are ready, click **Publish** to promote the document to the published state for all locales at once. Each locale's values are stored independently. Saving one locale does not overwrite another. ### Fetching content for a specific locale [#fetching-content-for-a-specific-locale] Pass the `locale` parameter when fetching a document. Localess returns the field values for that locale: ```typescript // Fetch the English version const enContent = await client.getContentBySlug
('blog/my-post', { locale: 'en', }); // Fetch the French version of the same document const frContent = await client.getContentBySlug
('blog/my-post', { locale: 'fr', }); // Fetch the draft version while previewing const draft = await client.getContentBySlug
('blog/my-post', { locale: 'de', version: 'draft', }); ``` ### How locales are configured [#how-locales-are-configured] Locales are configured at the **Space** level in **Settings → Locales**. Every content document and translation entry automatically gains a slot for each configured locale. Add a locale once; it appears everywhere. *** ## Using Both Together [#using-both-together] Most multilingual applications use both capabilities at the same time: * **Translation Manager** supplies the i18n namespace for UI chrome — navigation, buttons, form labels, error messages. * **Content Translation** supplies the locale-specific body content — articles, landing pages, product descriptions. ```typescript // In a Next.js server component: load both in parallel const [translations, page] = await Promise.all([ client.getTranslations(locale), client.getContentBySlug('home', { locale }), ]); ``` This separation keeps application copy and editorial content in the right system: developers own the key structure in Translation Manager; editors own the narrative in Content documents. # Translations Localess Translations lets you manage your application's UI strings as structured key–value pairs, each supporting multiple locales. The data is served over the API as a flat JSON map, ready to drop into any i18n library. ## Main Screen [#main-screen] Navigate to **Translations** from the side menu. The screen is divided into three panels: | Panel | Purpose | | ---------- | ----------------------------------------------------------------- | | **Left** | Translation list, browsable by locale | | **Middle** | Locale editor — translate from a source locale to a target locale | | **Right** | Translation metadata — ID, status, labels, and timestamps | Translations screen ## Toolbar [#toolbar] The toolbar at the top right of the screen provides the main actions: | Action | Description | | --------------------- | -------------------------------------------- | | **+ Add Translation** | Create a new translation entry | | **Publish** | Make all saved changes available via the API | | **Refresh** | Reload the translation list | | **View toggle** | Switch between list and grid view | ## Filtering [#filtering] Four controls let you narrow down the translation list: | Control | Description | | ------------------- | ----------------------------------------------------------------------------------------- | | **Locale dropdown** | Shown at the top of the left panel — sets which locale's values are displayed in the list | | **Search** | Filter translations by keyword | | **Status** | Filter by translation completeness (see [Translation Status](#translation-status)) | | **Labels** | Filter by one or more labels | ## Add Translation [#add-translation] Click **+ Add Translation** to open the creation form: | Field | Required | Description | | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------- | | **ID** | ✅ | Unique key used in your application (e.g. `common.submit`). Cannot be changed after creation | | **Description** | — | Optional note for translators — explains the context or usage of this string | | **Value** | — | The string value for the default locale | | **Labels** | — | One or more labels for grouping and filtering | | **Auto-translate** | — | When enabled, Localess automatically requests translations for all other configured locales when the entry is created | ## Editing Translations [#editing-translations] Click any entry in the left panel to open it in the editor. ### Middle panel — locale editor [#middle-panel--locale-editor] 1. Select the **Source Locale** (the language you're translating from) in the left dropdown. 2. Select the **Target Locale** (the language you're editing) in the right dropdown. 3. The source locale value is displayed read-only in the upper text area. 4. Type the translated value in the lower text area. 5. Press **Save** or `⌘S` / `Ctrl+S` to save. The character count below the target text area shows how many characters remain within the field limit. ### Right panel — translation details [#right-panel--translation-details] | Field | Description | | --------------- | --------------------------------------------------------------------------------- | | **ID** | The unique translation key, with a copy button | | **Status** | Computed translation completeness — see [Translation Status](#translation-status) | | **Description** | The context note set at creation | | **Labels** | Labels assigned to this translation | | **Created At** | Timestamp of creation | | **Updated At** | Timestamp of the last edit | | **Updated By** | Name of the user who last edited this translation | Use the **edit** (pencil) and **delete** (trash) action buttons in the right panel header to modify or remove the entry. ## Translation Status [#translation-status] Translation status is computed automatically based on how many locales have a value for a given entry: | Status | Description | | -------------------------------- | --------------------------------------------- | | Translation Translated | All configured locales have a value | | Translation Partially Translated | Some locales have a value, others are missing | | Translation Untranslated | No locale has a value yet | Each locale within a translation also carries its own status: | Status | Description | | ------------------- | ------------------------------ | | Locale Translated | This locale has a value | | Locale Untranslated | This locale is missing a value | ## Labels [#labels] Labels are free-form tags you can assign to translations to group related strings. Use them to: * Filter the list down to a feature area (e.g. `navigation`, `errors`, `onboarding`) * Coordinate translation work across team members Labels are optional and do not affect the API output. ## Publishing [#publishing] Translations have two availability states: | State | How to access via API | | ----------------- | ------------------------------------------ | | **Saved (draft)** | Available immediately with `version=draft` | | **Published** | Available without specifying a version | Click **Publish** to promote all saved changes to the published version. The toolbar indicator shows when there are unpublished changes pending. This lets you stage translation work in progress and preview it in draft mode before making it available to production consumers. ## Fetching Translations [#fetching-translations] Use the [TypeScript SDK](sdk/typescript) or [REST API](api/overview) to fetch translations at runtime: ```typescript const translations = await client.getTranslations("en"); // { "common.submit": "Submit", "nav.home": "Home", ... } ``` Use the [CLI](cli/translations) to pull translations into local JSON files for use with file-based i18n libraries: ```bash npx localess translations pull en --path ./locales/en.json ``` # Visual Editor The Localess Visual Editor opens your frontend application inside an iframe alongside the content form. As you edit fields on the right, the changes are pushed to the preview on the left in real time. Clicking on any marked element in the preview jumps directly to the corresponding field in the form. Visual Editor ## How it works [#how-it-works] 1. Localess renders your app inside an iframe in the left panel of the document editor. 2. A small sync script loaded by your app opens a message channel to the Localess parent window. 3. When a field changes in the form, Localess sends an `input` or `change` event through the channel. 4. Your app receives the event, updates its local state, and re-renders — no page reload needed. 5. Editable elements marked with `data-ll-id` and `data-ll-schema` attributes are highlighted so editors can click them to jump to the right field in the form. ## Setup [#setup] Integrating the Visual Editor requires three steps in your frontend application. ### Step 1 — Load the sync script [#step-1--load-the-sync-script] The sync script must be loaded in the browser. Use the SDK helper or add the script tag manually. **Via `@localess/client`:** ```typescript import { loadLocalessSync } from "@localess/client"; // Call once on the client side, e.g. in your root layout or app entry point loadLocalessSync("https://my-localess.web.app"); ``` **Or manually in HTML:** ```html ``` Replace `https://my-localess.web.app` with your Localess instance origin. Framework-specific SDKs can load the script automatically. Set `enableSync: true` when registering the provider: | SDK | Option | | ------------------- | ---------------------------------------------- | | `@localess/react` | `localessInit({ enableSync: true })` | | `@localess/angular` | `provideLocalessBrowser({ enableSync: true })` | *** ### Step 2 — Mark editable elements [#step-2--mark-editable-elements] Add `data-ll-id` and `data-ll-schema` attributes to the root element of each content block so the Visual Editor can highlight and select it. Add `data-ll-field` to individual field elements for field-level selection. Use the helper functions from `@localess/client` to set these attributes: ```typescript import { localessEditable, localessEditableField } from "@localess/client"; // Marks the block root as editable // Returns: { 'data-ll-id': '...', 'data-ll-schema': '...' } localessEditable(data) // Marks a specific field as editable // Returns: { 'data-ll-field': 'title' } localessEditableField('title') ``` **React example:** ```tsx const HeroSection = ({ data, links }) => (

('title')}>{data.title}

('subtitle')}>{data.subtitle}

); ``` **Vue example:** ```vue ``` **Svelte example:** ```svelte

{data.title}

``` > `localessEditable` and `localessEditableField` are no-ops outside the Visual Editor iframe — they return empty objects when the sync script is not active, so they are safe to use in production. *** ### Step 3 — Subscribe to edit events [#step-3--subscribe-to-edit-events] Listen for `input` and `change` events from `window.localess` and update your application state. Always guard the subscription with a browser check — `window.localess` only exists when the sync script is loaded inside the Visual Editor iframe. ```typescript import { isBrowser } from "@localess/client"; if (isBrowser() && window.localess) { window.localess.on(['input', 'change'], (event) => { if (event.type === 'input' || event.type === 'change') { // Replace your server-fetched content with the live editor data setPageData(event.data); } }); } ``` The `input` event fires on every keystroke for a real-time preview. The `change` event fires when a field value is confirmed. For most use cases, subscribing to both gives the smoothest editing experience. ## Events reference [#events-reference] | Event | When it fires | Payload | | ------------- | ----------------------------------------------- | --------------------------------------------- | | `input` | While a field is being edited (every keystroke) | `{ type: 'input', data: ContentData }` | | `change` | When a field value is confirmed | `{ type: 'change', data: ContentData }` | | `save` | When the document is saved | `{ type: 'save' }` | | `publish` | When the document is published | `{ type: 'publish' }` | | `unpublish` | When the document is unpublished | `{ type: 'unpublish' }` | | `pong` | Heartbeat response from the editor | `{ type: 'pong' }` | | `enterSchema` | Editor cursor enters a schema block | `{ type: 'enterSchema', id, schema, field? }` | | `hoverSchema` | Editor cursor hovers over a schema block | `{ type: 'hoverSchema', id, schema, field? }` | | `leaveSchema` | Editor cursor leaves a schema block | `{ type: 'leaveSchema' }` | ## SDK integrations [#sdk-integrations] Official SDKs provide built-in Visual Editor support with `enableSync` and helper utilities. For frameworks without a dedicated SDK, use `@localess/client` directly following the manual setup steps above. | Framework | SDK | Guide | | ------------------ | ------------------- | -------------------------------- | | TypeScript / Node | `@localess/client` | [TypeScript SDK](sdk/typescript) | | Angular | `@localess/angular` | [Angular SDK](sdk/angular) | | React / Next.js | `@localess/react` | [React SDK](sdk/react) | | Vue / Nuxt | `@localess/client` | [Vue](sdk/vue) | | Svelte / SvelteKit | `@localess/client` | [Svelte](sdk/svelt) | | Astro | `@localess/client` | [Astro](sdk/astro) | # Webhooks Localess can send an HTTP `POST` request to any URL you configure whenever a significant event occurs — a content publish, a translation update, an asset upload. Use webhooks to keep downstream systems in sync without polling the API. Common uses: * **Trigger a build or deploy** when content is published (Vercel, Netlify, GitHub Actions) * **Invalidate ISR or CDN caches** when a content document goes live * **Send a Slack notification** when a translation batch is published * **Kick off search re-indexing** after new content is published *** ## Setting Up a Webhook [#setting-up-a-webhook] 1. Navigate to **Settings → Webhooks** in the Localess admin UI. 2. Click **+ Add Webhook**. 3. Fill in the form: | Field | Required | Description | | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------ | | **Name** | ✅ | A label for this webhook (e.g. `Vercel Production Deploy`) | | **URL** | ✅ | The HTTPS endpoint that will receive the `POST` request | | **Secret** | — | A shared secret used to sign the payload. Strongly recommended — see [Verifying Signatures](#verifying-signatures) | | **Events** | ✅ | One or more event types that trigger this webhook | 4. Click **Save**. Localess immediately stores the webhook and starts delivering events. *** ## Events [#events] Localess fires webhooks on the following events: | Event | Value | Triggered when | | ----------------------- | ----------------------- | ------------------------------------------ | | `CONTENT_PUBLISHED` | `content.published` | A content document is published | | `CONTENT_UNPUBLISHED` | `content.unpublished` | A content document is unpublished | | `CONTENT_CHANGED` | `content.changed` | A content document is updated or deleted | | `TRANSLATION_PUBLISHED` | `translation.published` | Translations are published | | `TRANSLATION_CHANGED` | `translation.changed` | A translation is added, updated or deleted | Select only the events your endpoint needs. Unneeded events are never delivered to that endpoint, which keeps your handler logic simple. *** ## Payload Format [#payload-format] Every webhook request is an HTTP `POST` with a JSON body and the content type `application/json`. ### Request headers [#request-headers] | Header | Description | | ---------------------- | ------------------------------------------------------------------------------------------------- | | `Content-Type` | `application/json` | | `X-Localess-Event` | The event name, e.g. `content.published` | | `X-Localess-Delivery` | A unique UUID for this delivery attempt | | `X-Localess-Signature` | HMAC-SHA256 signature of the raw body using your secret. Present only when a secret is configured | | `X-Localess-Timestamp` | Unix timestamp (seconds) of when the event was generated | ### Example payload — `content.published` [#example-payload--contentpublished] ```json { "event": "content.published", "deliveryId": "e4a2c891-3f10-4b2d-9c5a-1d8f63b2a471", "timestamp": 1748131200, "spaceId": "abc123", "data": { "documentId": "doc_7xKp9mNq", "name": "How to Get Started", "slug": "how-to-get-started", "fullSlug": "blog/how-to-get-started", "locale": "en", "schema": "article" } } ``` ### Example payload — `translation.published` [#example-payload--translationpublished] ```json { "event": "translation.published", "deliveryId": "b1c3d592-8e47-4f9a-bc12-2e7a94f1d830", "timestamp": 1748131440, "spaceId": "abc123", "data": { "locales": ["en", "fr", "de"] } } ``` *** ## Verifying Signatures [#verifying-signatures] When you set a secret on a webhook, Localess signs every request body with HMAC-SHA256 using that secret. Verify the signature before processing the payload to confirm the request came from Localess and was not tampered with in transit. The signature is in the `X-Localess-Signature` header in the format `sha256=`. ### Verification example — Node.js [#verification-example--nodejs] ```typescript import crypto from 'crypto'; function verifyLocalessWebhook( rawBody: string | Buffer, signature: string, secret: string ): boolean { const expected = `sha256=${crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex')}`; return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } // In a Next.js route handler: export async function POST(request: Request) { const rawBody = await request.text(); const signature = request.headers.get('X-Localess-Signature') ?? ''; if (!verifyLocalessWebhook(rawBody, signature, process.env.LOCALESS_WEBHOOK_SECRET!)) { return new Response('Unauthorized', { status: 401 }); } const payload = JSON.parse(rawBody); // handle payload... return new Response('OK', { status: 200 }); } ``` **Always use `timingSafeEqual`** — standard string comparison is vulnerable to timing attacks. *** ## Retry Logic [#retry-logic] If your endpoint does not respond with an HTTP `2xx` status within **10 seconds**, Localess marks the delivery as failed and retries with exponential backoff: | Attempt | Delay | | --------- | ---------- | | 1st retry | 30 seconds | | 2nd retry | 5 minutes | | 3rd retry | 30 minutes | | 4th retry | 2 hours | | 5th retry | 8 hours | After 5 failed attempts the delivery is abandoned. The delivery log in **Settings → Webhooks → \[webhook name] → Deliveries** records every attempt, the HTTP status received, and the response body, so you can investigate failures. Make your endpoint **idempotent** — use the `X-Localess-Delivery` header as an idempotency key so that retried deliveries do not cause duplicate actions. *** ## Integration Examples [#integration-examples] ### Trigger a Vercel deploy [#trigger-a-vercel-deploy] Vercel provides a Deploy Hook URL in your project settings. Point a Localess `content.published` webhook at it directly — no code required. ``` https://api.vercel.com/v1/integrations/deploy/prj_xxxx/yyyyyyyy ``` ### Send a Slack notification [#send-a-slack-notification] ```typescript // Slack incoming webhook handler export async function POST(request: Request) { const payload = await request.json(); if (payload.event === 'content.published') { await fetch(process.env.SLACK_WEBHOOK_URL!, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: `📝 Content published: *${payload.data.name}* (${payload.data.fullSlug})`, }), }); } return new Response('OK', { status: 200 }); } ``` ### Trigger Next.js ISR revalidation on publish [#trigger-nextjs-isr-revalidation-on-publish] ```typescript // app/api/localess-webhook/route.ts import { revalidatePath } from 'next/cache'; export async function POST(request: Request) { const rawBody = await request.text(); if (!verifyLocalessWebhook(rawBody, request.headers.get('X-Localess-Signature') ?? '', process.env.LOCALESS_WEBHOOK_SECRET!)) { return new Response('Unauthorized', { status: 401 }); } const payload = JSON.parse(rawBody); if (payload.event === 'content.published') { // Revalidate the specific page that changed revalidatePath(`/${payload.data.fullSlug}`); } if (payload.event === 'translation.published') { // Revalidate all pages that use translations revalidatePath('/', 'layout'); } return new Response('OK', { status: 200 }); } ``` ### Trigger search re-indexing [#trigger-search-re-indexing] ```typescript export async function POST(request: Request) { const payload = await request.json(); if (payload.event === 'content.published') { // Fetch the published document and push to your search index const content = await localessClient.getContentBySlug(payload.data.fullSlug, { locale: payload.data.locale, }); await searchClient.upsert({ id: payload.data.documentId, title: content.data.title, body: content.data.body, url: `/${payload.data.fullSlug}`, }); } return new Response('OK', { status: 200 }); } ``` *** ## Delivery Logs [#delivery-logs] Navigate to **Settings → Webhooks → \[webhook name] → Deliveries** to inspect the history of every event sent to that endpoint. Each delivery record shows: * The event type and delivery ID * The HTTP status code returned by your endpoint * The timestamp and duration * The full request payload and response body Use this view to debug failed deliveries or verify that your endpoint is receiving events correctly. # Get Started Package: [`@localess/cli`](https://github.com/Lessify/localess-js/blob/main/packages/cli) — v3.0.6 The `@localess/cli` package is the official command-line interface for the Localess headless CMS platform. It provides commands to authenticate with your Localess instance, synchronize translations, and generate TypeScript type definitions from your content schemas. The CLI is a standalone tool — framework-agnostic and independent of the SDK packages. Install it once per project (or globally) regardless of which framework you use on the application side. ## Requirements [#requirements] * Node.js >= 24.0.0 ## Installation [#installation] ```bash # Install as a project dev dependency (recommended) npm install @localess/cli -D # Or install globally npm install @localess/cli -g ``` ## Features [#features] * **Authentication** — Secure credential storage for CLI and CI/CD environments * **Translations** — Push and pull translation files to/from your Localess space * **Type Generation** — Generate TypeScript type definitions from your Localess content schemas for end-to-end type safety ## Stored Files [#stored-files] | File | Description | | ---------------------------- | ----------------------------------------------------------------------- | | `.localess/credentials.json` | Stored login credentials (created by `localess login`) | | `.localess/localess.ts` | Generated TypeScript definitions (created by `localess types generate`) | > `localess login` automatically adds `.localess` to `.gitignore` (creating the file if absent). ## AI Coding Agents [#ai-coding-agents] This package ships a [`SKILL.md`](https://github.com/Lessify/localess-js/blob/main/packages/cli/SKILL) file that provides AI coding agents (GitHub Copilot, Claude Code, Cursor, and others) with accurate, up-to-date APIs, patterns, and best practices. Reference it from your project's `AGENTS.md`: ``` ## Localess @node_modules/@localess/cli/SKILL.md ``` ## License [#license] [MIT](https://github.com/Lessify/localess-js/blob/main/LICENSE) # Login Authenticate with your Localess instance. Credentials are validated immediately and stored securely in `.localess/credentials.json` with restricted file permissions (`0600`). ```bash localess login --origin --space --token ``` If any option is omitted, the CLI will interactively prompt for the missing values. ## Options [#options] | Flag | Description | | ----------------------- | ----------------------------------------------------------- | | `-o, --origin ` | Localess instance URL (e.g., `https://my-localess.web.app`) | | `-s, --space ` | Space ID (found in Localess Space settings) | | `-t, --token ` | API token (input is masked for security) | ## Examples [#examples] ```bash # Interactive login (prompts for any missing values) localess login # Non-interactive login (CI/CD) localess login --origin https://my-localess.web.app --space MY_SPACE_ID --token MY_API_TOKEN ``` ## Authentication via Environment Variables [#authentication-via-environment-variables] For CI/CD pipelines, you can provide credentials through environment variables instead of running `localess login`. The CLI automatically reads these variables and skips the file-based credentials: ```bash export LOCALESS_ORIGIN=https://my-localess.web.app export LOCALESS_SPACE=MY_SPACE_ID export LOCALESS_TOKEN=MY_API_TOKEN localess translations pull en --path ./public/locales/en.json ``` | Variable | Description | | ----------------- | --------------------- | | `LOCALESS_ORIGIN` | Localess instance URL | | `LOCALESS_SPACE` | Space ID | | `LOCALESS_TOKEN` | API token | ## .gitignore [#gitignore] `localess login` automatically appends `.localess` to `.gitignore` in the current working directory (creating the file if it doesn't exist). No manual step is required — credentials are protected out of the box. # Logout Clear stored credentials from `.localess/credentials.json`. ```bash localess logout ``` > If you authenticated via environment variables, those must be unset manually — `logout` only affects file-based credentials. # Translations ## Push [#push] Push a local JSON translation file to your Localess space. Only keys present in the file are affected, based on the selected update type. ```bash localess translations push --path [options] ``` ### Arguments [#arguments] | Argument | Description | | ---------- | ---------------------------------------------- | | `` | ISO 639-1 locale code (e.g., `en`, `de`, `fr`) | ### Options [#options] | Flag | Default | Description | | ----------------------- | ------------- | ---------------------------------------------------------------------- | | `-p, --path ` | *(required)* | Path to the JSON translations file | | `-f, --format ` | `flat` | File format: `flat` or `nested` | | `-t, --type ` | `add-missing` | Update strategy: `add-missing`, `update-existing`, or `delete-missing` | | `--dry-run` | `false` | Preview changes without applying them | ### Update Strategies [#update-strategies] | Type | Description | | ----------------- | ----------------------------------------------------------------------------- | | `add-missing` | Adds translations for keys that do not yet exist in Localess | | `update-existing` | Updates translations for keys that already exist in Localess | | `delete-missing` | Deletes translations in Localess for keys that are absent from the local file | ### File Formats [#file-formats] * **`flat`** — A flat JSON object where keys may use dot notation: ```json { "common.submit": "Submit", "nav.home": "Home" } ``` * **`nested`** — A nested JSON object that is automatically flattened before uploading: ```json { "common": { "submit": "Submit" }, "nav": { "home": "Home" } } ``` ### Examples [#examples] ```bash # Push English translations (add missing keys only) localess translations push en --path ./locales/en.json # Push with update-existing strategy localess translations push en --path ./locales/en.json --type update-existing # Delete keys in Localess absent from the local file localess translations push en --path ./locales/en.json --type delete-missing # Preview changes without applying (dry run) localess translations push en --path ./locales/en.json --dry-run # Push nested-format translations localess translations push de --path ./locales/de.json --format nested ``` ## Pull [#pull] Pull translations from your Localess space and save them to a local file. ```bash localess translations pull --path [options] ``` ### Arguments [#arguments-1] | Argument | Description | | ---------- | ---------------------------------------------- | | `` | ISO 639-1 locale code (e.g., `en`, `de`, `fr`) | ### Options [#options-1] | Flag | Default | Description | | ----------------------- | ------------ | ---------------------------------------------------- | | `-p, --path ` | *(required)* | Output file path | | `-f, --format ` | `flat` | File format: `flat` or `nested` | | `--draft` | `false` | Pull the draft (unpublished) version of translations | ### Examples [#examples-1] ```bash # Pull English translations as flat JSON localess translations pull en --path ./locales/en.json # Pull German translations as nested JSON localess translations pull de --path ./locales/de.json --format nested # Pull draft (unpublished) translations localess translations pull en --path ./locales/en.json --draft ``` # Types Fetch your space's content schemas directly from the Localess API and generate TypeScript type definitions from them. The output file provides full type safety when working with Localess content in your TypeScript projects. Schemas are read live from the database — there is no intermediate OpenAPI document — so the generated types always reflect the current state of your space. ```bash localess types generate [--path ] ``` ## Options [#options] | Flag | Default | Description | | ------------------- | ----------------------- | ------------------------------------------------------- | | `-p, --path ` | `.localess/localess.ts` | Path to write the generated TypeScript definitions file | | `--prefix ` | *(empty)* | Prefix to prepend to all generated type names | > Your API token must have **Development Tools** permission enabled in Localess Space settings. ## Examples [#examples] ```bash # Generate types to the default location localess types generate # Generate types to a custom path localess types generate --path src/types/localess.ts # Generate types with a prefix on all type names localess types generate --prefix Cms ``` ## Using Generated Types [#using-generated-types] ```typescript import type { Page, HeroBlock } from './.localess/localess'; import { getLocalessClient } from "@localess/react"; const client = getLocalessClient(); const content = await client.getContentBySlug('home', { locale: 'en' }); // content.data is now fully typed as Page ``` # Overview The Localess REST API provides predictable, resource-oriented URLs, returns JSON-encoded responses, and uses standard HTTP response codes and verbs. ## Base URL [#base-url] All API endpoints share the following base URL pattern, depending on your deployment: | Deployment | Base URL | | ---------------- | ---------------------------------------------- | | Firebase Hosting | `https://.web.app/api/v1/` | | Firebase App | `https://.firebaseapp.com/api/v1/` | | Custom domain | `https:///api/v1/` | ## Authentication [#authentication] All requests must include a valid API token as a query parameter: ``` GET /api/v1/spaces/{spaceId}/contents/slugs/{slug}?token= ``` You can create and manage tokens in **Settings → Access Tokens** inside the Localess admin UI. All requests must be made over HTTPS. ## Draft content [#draft-content] Append `version=draft` to any request to retrieve unpublished content: ``` GET /api/v1/spaces/{spaceId}/contents/slugs/{slug}?token=&version=draft ``` ## Errors [#errors] Localess uses standard HTTP status codes: | Range | Meaning | | ----- | -------------------------------------------------------------------------------------------------------- | | `2xx` | Request succeeded | | `4xx` | Client error — check your parameters, token, or request body | | `5xx` | Server error — rare; check the [GitHub repository](https://github.com/Lessify/localess) for known issues | # Asset (One) References a single asset (image, file, etc.) uploaded to the space. ## Value shape [#value-shape] ```json { "kind": "ASSET", "uri": "019df317-9ad5-7228-a070-afc8f94a07d2" } ``` Resolve the URI to a full URL with `client.assetLink(value)` (see [`@localess/client`](../sdk/typescript#assets)) or the `resolveAsset` helper in [`@localess/react`](../sdk/react#assets). ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **File Type** | Restricts the asset picker to a file type (e.g., image, video, document). | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Assets (Multiple) References a list of assets uploaded to the space. ## Value shape [#value-shape] ```json [ { "kind": "ASSET", "uri": "019df317-9ad5-7228-a070-afc8f94a07d2" }, { "kind": "ASSET", "uri": "019df317-9ad5-7228-a070-afc8f94a07d2" } ] ``` Resolve each URI to a full URL with `client.assetLink(value)` (see [`@localess/client`](../sdk/typescript#assets)) or the `resolveAsset` helper in [`@localess/react`](../sdk/react#assets). ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **File Type** | Restricts the asset picker to a file type (e.g., image, video, document). | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Boolean Stores `true` or `false`. ## Value shape [#value-shape] ```json true ``` ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Color Stores a color as a hex string in `#rrggbb` format. ## Value shape [#value-shape] ```json "#1e88e5" ``` ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Date Time Stores a date and time as an ISO-8601 string in `YYYY-MM-DDThh:mm` format. ## Value shape [#value-shape] ```json "2013-10-21T21:45" ``` ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Date Stores a calendar date as an ISO-8601 string in `YYYY-MM-DD` format. ## Value shape [#value-shape] ```json "2013-10-21" ``` ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Overview Schemas are the content models of your Localess space. They define what fields a content document contains, what types those fields are, and how editors interact with them. Every content document is bound to a ROOT schema; nested components use NODE schemas; fixed value lists use ENUM schemas. ## Main Screen [#main-screen] Navigate to **Schemas** from the side menu. The list shows all schemas in your space. Schemas list | Column | Description | | --------------- | ---------------------------------------------------------------------------------- | | **Type** | Schema type — Root, Node, or Enum | | **Name** | Display name with the internal identifier shown beneath (e.g. `#docHeaderSection`) | | **Description** | Optional description set on the schema | | **Labels** | Labels assigned to the schema for filtering | | **Updated At** | Timestamp of the last change | | **Actions** | Three-dot menu for edit and delete | ## Filtering [#filtering] | Control | Description | | ---------- | ------------------------------------ | | **Search** | Filter schemas by name or identifier | | **Labels** | Filter by one or more labels | ## Schema Types [#schema-types] | Type | Description | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Root** | Top-level schema. Assigned directly to content documents. Only ROOT schemas appear in the Schema picker when creating a document. | | **Node** | Nested schema. Embedded within other schemas via [Schema](./schemas/schema) or [Schemas](./schemas/schemas) fields. Use it for reusable components like buttons, cards, or sections. | | **Enum** | A fixed list of named values. Used as the source for [Option](./schemas/option) and [Options](./schemas/options) fields. | ## Add Schema [#add-schema] Click **+ Add Schema** and fill in: | Field | Required | Description | | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------- | | **Name** | ✅ | Internal identifier used in the API response and generated types (e.g. `heroSection`). Cannot contain spaces. | | **Display Name** | — | Label shown to editors in the UI. Falls back to Name if not set. | | **Type** | ✅ | Root, Node, or Enum — see [Schema Types](#schema-types) above | | **Description** | — | Optional note describing the schema's purpose | | **Labels** | — | One or more labels for grouping and filtering | ## ROOT and NODE Editor [#root-and-node-editor] Click the edit action on a Root or Node schema to open the editor. Schema editor The editor has two tabs: ### Fields tab [#fields-tab] The Fields tab is where you define the structure of the schema. Each field has a name, a type, and type-specific options. To add a field, type its internal name in the input at the top and click **Add**. The field appears in the list where you can configure it. Fields can be reordered by dragging the handle on the right. Each field entry shows: * The field **type icon** * The **Display Name** and internal `#name` * **Type badge** and any relevant constraint badges (e.g. `Required`, `Schema: button`) * A **delete** button Each field type page documents all available options, including the base options shared by every field. ### Settings tab [#settings-tab] The Settings tab contains schema-level configuration: | Setting | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Display Name** | Label shown in the UI | | **Description** | Optional description for the schema | | **Preview Field** | The field name used as the label when this schema appears as a nested item in the content editor (e.g. set to `title` so nested blocks show their title in the list) | | **Labels** | Labels for filtering | Press **Save** or `⌘S` / `Ctrl+S` to save changes. ## ENUM Editor [#enum-editor] Click the edit action on an Enum schema to open the editor. Enum editor The editor has two tabs: ### Values tab [#values-tab] The Values tab is where you manage the list of available options. To add a value, type its name in the input at the top and click **Add**. Values can be reordered by dragging the handle on the right and deleted with the trash icon. Each value entry shows the **Name** (display label shown to editors, e.g. `Default`) and the internal identifier (e.g. `#default`). The identifier without the `#` prefix is the string stored in the API response. ### Settings tab [#settings-tab-1] | Setting | Description | | ---------------- | --------------------------------- | | **Display Name** | Label shown in the UI | | **Description** | Optional description for the enum | | **Labels** | Labels for filtering | Press **Save** or `⌘S` / `Ctrl+S` to save changes. Enum values are referenced by [Option](option) and [Options](options) fields via the **Source** setting. ## Field Types [#field-types] | Field | Kind | Description | | ---------------------------------- | ------------ | -------------------------------------------------------- | | [Text](./schemas/text) | `TEXT` | Single-line string — titles, headlines | | [Text Area](./schemas/text-area) | `TEXTAREA` | Multi-line plain text — descriptions, summaries | | [Rich Text](./schemas/rich-text) | `RICH_TEXT` | Formatted text with bold, italic, links, lists, headings | | [Markdown](./schemas/markdown) | `MARKDOWN` | Multi-line Markdown string | | [Number](./schemas/number) | `NUMBER` | Integer or decimal — amounts, quantities | | [Boolean](./schemas/boolean) | `BOOLEAN` | True / false toggle | | [Color](./schemas/color) | `COLOR` | Hex color picker — `#rrggbb` | | [Date](./schemas/date) | `DATE` | Calendar date picker — `YYYY-MM-DD` | | [Date Time](./schemas/date-time) | `DATETIME` | Date and time picker — `YYYY-MM-DDThh:mm` | | [Link](./schemas/link) | `LINK` | External URL or internal content reference | | [Option](./schemas/option) | `OPTION` | Single value selected from an Enum or inline list | | [Options](./schemas/options) | `OPTIONS` | Multiple values selected from an Enum or inline list | | [Reference](./schemas/reference) | `REFERENCE` | Reference to a single content document | | [References](./schemas/references) | `REFERENCES` | References to multiple content documents | | [Asset](./schemas/asset) | `ASSET` | A single uploaded file (image, video, document) | | [Assets](./schemas/assets) | `ASSETS` | Multiple uploaded files | | [Schema](./schemas/schema) | `SCHEMA` | A single embedded Node schema block | | [Schemas](./schemas/schemas) | `SCHEMAS` | Multiple embedded Node schema blocks | # Link Stores either an external URL or an internal content reference, with optional `target`. ## Value shape [#value-shape] ### External Link [#external-link] ```json { "kind": "LINK", "type": "url", "target": "_blank", "uri": "https://example.com" } ``` ### Internal Link [#internal-link] ```json { "kind": "LINK", "type": "content", "target": "_blank", "uri": "019df317-9ad5-7228-a070-afc8f94a07d2" } ``` `type` is `"url"` for external links and `"content"` for an internal slug reference. Resolve a link to a final `href` with the `findLink` helper in [`@localess/react`](../sdk/react#link-utilities). ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Markdown Stores a multi-line Markdown string. Use it for articles, long descriptions, and any content where editors need basic formatting via Markdown syntax. For one-line strings use [Text](text); for a structured editor use [Rich Text](rich-text). ## Value shape [#value-shape] ```json "# Heading\n\nParagraph with **bold** text." ``` ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **Min Length** | Minimum number of characters required | | **Max Length** | Maximum number of characters allowed | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Number Stores an integer or decimal number. Use it for quantities, prices, ratings, or any numeric value. ## Value shape [#value-shape] ```json 42 ``` ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **Min Value** | Minimum allowed value | | **Max Value** | Maximum allowed value | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Option (One) Stores a single selected value from a predefined list. Rendered as a dropdown in the content editor. For multiple selections use [Options](options). ## Value shape [#value-shape] ```json "primary" ``` The value stored is the **Value** of the selected option, not its display name. ## Options [#options] | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **Source** | Where the available options come from. `Self` means the options are defined inline on this field; selecting an Enum schema uses its values instead. | | **Options** | When Source is `Self` — the list of available options. Each option has a **Name** (shown in the editor) and a **Value** (stored in the API response). | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Options (Multiple) Stores multiple selected values from a predefined list. Rendered as a multi-select dropdown in the content editor. For a single selection use [Option](option). ## Value shape [#value-shape] ```json ["primary", "featured"] ``` Each element is the **Value** of a selected option, not its display name. ## Options [#options] | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **Source** | Where the available options come from. `Self` means the options are defined inline on this field; selecting an Enum schema uses its values instead. | | **Options** | When Source is `Self` — the list of available options. Each option has a **Name** (shown in the editor) and a **Value** (stored in the API response). | | **Min Values** | Minimum number of values that must be selected | | **Max Values** | Maximum number of values that can be selected | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Reference (One) Stores a reference to a single content document in the space. Use it to link a piece of content to another document — for example, a related article or an author profile. For multiple references use [References](references). ## Value shape [#value-shape] ```json { "kind": "REFERENCE", "uri": "019df317-9ad5-7228-a070-afc8f94a07d2" } ``` When fetching content with `resolveReference: true`, the URI is replaced with the full document object. See [`@localess/client`](../sdk/typescript) for fetch options. ## Options [#options] | Option | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | **Path** | Restricts the document picker to documents under a specific content path (e.g. `blog` shows only documents inside the `blog` folder) | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | Not available for this field type. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # References (Multiple) Stores references to multiple content documents in the space. Use it when a piece of content is related to several other documents — for example, a list of featured articles or team members. For a single reference use [Reference](reference). ## Value shape [#value-shape] ```json [ { "kind": "REFERENCE", "uri": "019df317-9ad5-7228-a070-afc8f94a07d2" }, { "kind": "REFERENCE", "uri": "019df317-9ad5-7228-a070-afc8f94a07d2" } ] ``` When fetching content with `resolveReference: true`, each URI is replaced with the full document object. See [`@localess/client`](../sdk/typescript) for fetch options. ## Options [#options] | Option | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | **Path** | Restricts the document picker to documents under a specific content path (e.g. `blog` shows only documents inside the `blog` folder) | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | Not available for this field type. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Rich Text Stores structured formatted text as a Tiptap JSON document. Use it for article bodies, long descriptions, and any content where editors need formatting controls. For plain unformatted text use [Text Area](text-area); for Markdown syntax use [Markdown](markdown). ## Value shape [#value-shape] ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Hello ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "world." } ] } ] } ``` Render the value in your frontend using the SDK helpers: * `renderRichTextToReact(value)` — [`@localess/react`](../sdk/react#rich-text-rendering) * `llRtToHtml` pipe — [`@localess/angular`](../sdk/angular#llrttohtml--rich-text-to-html) ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **Min Length** | Minimum number of characters required | | **Max Length** | Maximum number of characters allowed | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Schema (One) Embeds a single [Node](.#schema-types) schema block inline within the parent document. Use it for optional or singular nested components — a call-to-action, a feature highlight, or an author card. For a list of blocks use [Schemas](schemas). ## Value shape [#value-shape] ```json { "_id": "abc123", "_schema": "button", "label": "Get started", "url": "https://example.com" } ``` The `_schema` field contains the Node schema identifier. Use it to look up the registered component in your frontend. ## Options [#options] | Option | Description | | ----------------- | ------------------------------------------------------------------------------------------------------ | | **Schema** | The allowed Node schema(s) that editors can place in this field. Leave empty to allow any Node schema. | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Schemas (Multiple) Embeds a list of [Node](.#schema-types) schema blocks inline within the parent document. Use it for repeatable components — page sections, button groups, testimonials, or any ordered list of structured blocks. For a single embedded block use [Schema](schema). ## Value shape [#value-shape] ```json [ { "_id": "abc123", "_schema": "button", "title": "Get started", "description": "Your journey begins here" }, { "_id": "def456", "_schema": "button", "title": "Learn more", "description": "Discover our features" } ] ``` Each item's `_schema` field contains the Node schema identifier. Use it to look up the registered component and render the correct component in your frontend. ## Options [#options] | Option | Description | | ----------------- | --------------------------------------------------------------------------------------------------- | | **Schema** | The allowed Node schema(s) that editors can add to this list. Leave empty to allow any Node schema. | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Text Area Stores a multi-line plain text string. Use it for summaries, descriptions, or any content that spans multiple lines but does not need formatting. For single-line text use [Text](text); for a structured rich editor use [Rich Text](rich-text). ## Value shape [#value-shape] ```json "Localess is an AI-powered content and localisation solution.\nBuilt for fast-moving teams." ``` ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **Min Length** | Minimum number of characters required | | **Max Length** | Maximum number of characters allowed | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Text Stores a single-line string. Use it for titles, headlines, or any short label. For multi-line plain text use [Text Area](text-area); for formatted text use [Rich Text](rich-text). ## Value shape [#value-shape] ```json "All-in-One Content Management Platform" ``` ## Options [#options] | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------- | | **Min Length** | Minimum number of characters required | | **Max Length** | Maximum number of characters allowed | | **Name** | Unique field name in the Schema. Appears in the generated model and in the API response. | | **Display Name** | Label shown to editors in the Content Editor. | | **Required** | When set, editors cannot save the document without a value. | | **Translatable** | When set, the field stores a separate value per locale. | | **Description** | Help text shown to editors under the field. | | **Default Value** | Value applied when a document is first created. | # Angular `@localess/angular` provides two independent entry points — **browser** and **server** — for integrating Localess into Angular applications. > **Security:** The `browser` entry point requires no API token and is safe to use in client-side code. The `server` entry point requires your Localess API token and must only be used in server-side code. ## Installation [#installation] ```bash # npm npm install @localess/angular@latest # yarn yarn add @localess/angular@latest # pnpm pnpm add @localess/angular@latest ``` **Peer dependencies:** `@angular/core`, `@angular/common`, `@angular/compiler`, `@angular/platform-browser` — versions `>=21.0.0 <23.0.0`. ## Quick Start [#quick-start] **1. Register the browser provider** in `app.config.ts`: ```typescript import { provideLocalessBrowser } from '@localess/angular/browser'; export const appConfig: ApplicationConfig = { providers: [ provideLocalessBrowser({ origin: 'https://my-localess.web.app', spaceId: 'YOUR_SPACE_ID', }), ], }; ``` **2. Register the server provider** in `app.config.server.ts`: ```typescript import { mergeApplicationConfig } from '@angular/core'; import { provideLocalessServer } from '@localess/angular/server'; import { appConfig } from './app.config'; const serverConfig: ApplicationConfig = { providers: [ provideLocalessServer({ origin: 'https://my-localess.web.app', spaceId: 'YOUR_SPACE_ID', token: 'YOUR_SECRET_TOKEN', }), ], }; export const config = mergeApplicationConfig(appConfig, serverConfig); ``` **3. Fetch content on the server:** ```typescript import { ServerContentService } from '@localess/angular/server'; const contentService = inject(ServerContentService); const content = await firstValueFrom(contentService.getContentBySlug('home')); ``` > **Playground:** [`playgrounds/angular-ssr`](https://github.com/Lessify/localess-js/tree/main/playgrounds/angular-ssr) is a full working Angular SSR project wired up with `provideLocalessBrowser`/`provideLocalessServer` and the `LocalessService` TransferState pattern shown below. *** ## Browser Module [#browser-module] Import from `@localess/angular/browser`. ### Setup [#setup] Register `provideLocalessBrowser()` once in your root `ApplicationConfig`: ```typescript import { provideLocalessBrowser } from '@localess/angular/browser'; provideLocalessBrowser({ origin: 'https://my-localess.web.app', spaceId: 'YOUR_SPACE_ID', enableSync: true, debug: false, }) ``` | Option | Type | Required | Description | | ------------ | --------- | -------- | ---------------------------------------------------------------- | | `origin` | `string` | ✅ | Fully qualified Localess URL, e.g. `https://my-localess.web.app` | | `spaceId` | `string` | ✅ | Space ID from the Localess Space settings | | `enableSync` | `boolean` | — | When `true`, injects the Visual Editor sync script into the page | | `debug` | `boolean` | — | When `true`, logs internal activity to the browser console | `provideLocalessBrowser()` also registers Angular's built-in `IMAGE_LOADER` provider so that `NgOptimizedImage` automatically appends `?w=` to Localess asset URLs for responsive image optimization. See [Angular Image Optimization](#angular-image-optimization). *** ### Schema Components [#schema-components] `SchemaComponent` is the abstract base class you extend to render a Localess content schema. It automatically sets the `data-ll-id` and `data-ll-schema` attributes on the host element so the Localess Visual Editor can highlight and select components on the page. The base class declares four signal inputs: | Input | Type | Description | | ------------ | --------------------- | ---------------------------------------------- | | `data` | `input.required()` | The schema data object (required) | | `links` | `input()` | Map of content ID → slug, used by `findLink()` | | `references` | `input()` | Map of resolved `ContentReference` objects | | `assets` | `input()` | Map of asset metadata | ```typescript import { Component } from '@angular/core'; import { SchemaComponent } from '@localess/angular/browser'; @Component({ selector: 'app-hero-section', standalone: true, templateUrl: './hero-section.component.html', }) export class HeroSectionComponent extends SchemaComponent {} ``` In the template, read inputs with function-call syntax and use the `assetUrl()` and `findLink()` helpers provided by the base class: ```html

{{ data().title }}

Learn more
``` Use the component in a parent template by passing the schema object and maps from the CMS: ```html ``` *** #### Resolving an asset with `assetUrl()` [#resolving-an-asset-with-asseturl] `assetUrl(asset, params?)` builds the fully qualified CDN URL for a `ContentAsset`. Pass an `AssetTransformParams` object as the second argument to request a resized image or a different output format — see `AssetTransformParams` in [docs/client.md](client.md#asset-transform-parameters): ```html ``` The same `params` argument works identically on the `llAsset` pipe (see below) and the standalone `BrowserAssetService.link()`. *** #### Base class helpers [#base-class-helpers] `SchemaComponent` exposes: | Member | Signature | Description | | -------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `assetUrl(asset, params?)` | `(asset: ContentAsset, params?: AssetTransformParams) => string` | Builds the full CDN URL for a Localess asset, with optional transform params | | `findLink(link)` | `(link: ContentLink) => string` | Resolves a CMS link to a path or URL, using the `links` input | | `config` | `LocalessBrowserConfig` | Injected browser configuration | *** ### Directives [#directives] Use these directives when you have a component or element that is **not** a schema component but should still be selectable in the Visual Editor. #### `[data-ll-id]` and `[data-ll-schema]` [#data-ll-id-and-data-ll-schema] Apply both together to any element to make it recognizable in the Visual Editor: ```html
``` #### `[data-ll-field]` [#data-ll-field] Marks an individual field within a schema for field-level selection in the Visual Editor: ```html

{{ data.subtitle }}

``` #### `[llContent]` [#llcontent] A convenience directive that sets both `data-ll-id` and `data-ll-schema` on the host element from a single `ContentDataSchema` input. Useful for sub-schemas rendered without a dedicated component: ```typescript import { ContentDirective } from '@localess/angular/browser'; @Component({ imports: [ContentDirective], }) export class PageComponent {} ``` ```html
``` *** ### Pipes [#pipes] Import individual pipes into the `imports` array of any standalone component that uses them. #### `llAsset` — Asset URL [#llasset--asset-url] Transforms a `ContentAsset` object into a fully qualified CDN URL. Equivalent to `assetUrl()` on schema components. ```typescript import { AssetPipe } from '@localess/angular/browser'; @Component({ imports: [AssetPipe] }) ``` ```html ... ``` Pass `AssetTransformParams` as a pipe argument to resize or convert the format: ```html ... ... ``` *** #### `llLink` — Link Resolution [#lllink--link-resolution] Resolves a `ContentLink` from the links map to a navigable path or URL. Pass the `links` map as the piped value and the `ContentLink` as the argument: ```typescript import { LinkPipe } from '@localess/angular/browser'; ``` ```html Visit ``` | `ContentLink.type` | Result | | ------------------ | ---------------------------------------------------------------- | | `"content"` | Looks up `link.uri` in the `links` map and returns `/` | | `"url"` | Returns `link.uri` as-is | *** #### `llRtToHtml` — Rich Text to HTML [#llrttohtml--rich-text-to-html] Converts a Localess RichText field (Tiptap JSON) to an HTML string. Supports headings, bold, italic, strike, underline, bullet lists, ordered lists, code blocks, and links. ```typescript import { RichTextToHtmlPipe } from '@localess/angular/browser'; ``` ```html
``` *** #### `llSafeHtml` — Safe HTML [#llsafehtml--safe-html] Bypasses Angular's `DomSanitizer` for a trusted HTML string. Always apply after `llRtToHtml` when binding to `[innerHTML]`: ```typescript import { RichTextToHtmlPipe, SafeHtmlPipe } from '@localess/angular/browser'; @Component({ imports: [RichTextToHtmlPipe, SafeHtmlPipe] }) ``` ```html
``` > **Security:** `llSafeHtml` calls `DomSanitizer.bypassSecurityTrustHtml()`. Only use it with HTML sourced directly from your trusted Localess space. *** ### Browser Asset Service [#browser-asset-service] `BrowserAssetService` generates asset URLs programmatically. It is equivalent to `assetUrl()` on schema components. ```typescript import { BrowserAssetService } from '@localess/angular/browser'; @Component({ ... }) export class MyComponent { private assetService = inject(BrowserAssetService); getImageUrl(asset: ContentAsset): string { return this.assetService.link(asset); } getThumbnailUrl(asset: ContentAsset): string { return this.assetService.link(asset, { w: 400, f: 'webp' }); } } ``` > This service is browser-only. Use `ServerAssetService` on the server. *** ### Visual Editor Integration [#visual-editor-integration] Set `enableSync: true` in `provideLocalessBrowser()` to automatically inject the Visual Editor sync script. To receive real-time content updates, inject `LocalessSyncService` and use `onChange()` — it already covers the `enabled()` check (browser + Visual Editor iframe) and the `ready()` wait (avoiding a race where the listener is attached before the sync script has loaded): ```typescript import { Component, inject, OnInit, signal } from '@angular/core'; import { LocalessSyncService } from '@localess/angular/browser'; @Component({ selector: 'app-slug', standalone: true, templateUrl: './slug.component.html', }) export default class SlugComponent implements OnInit { private readonly sync = inject(LocalessSyncService); liveContent = signal(undefined); ngOnInit(): void { this.sync.onChange(event => this.liveContent.set(event.data)); } } ``` `onChange(callback)` is shorthand for `on(['input', 'change'], callback)`: the `input` event fires on every keystroke, `change` fires when the editor saves, and `callback` is narrowed to that variant. Render `liveContent()` instead of the server-fetched data when it is set to give authors a live preview. For other event types (`save`, `publish`, `unpublish`, `pong`, `enterSchema`, `hoverSchema`, `leaveSchema`), use `on(event, callback)`: ```typescript this.sync.on(['save', 'publish'], event => console.info(`Content ${event.type}d`)); ``` Both methods are no-ops if sync isn't enabled or usable in the current context — no need to check `enabled()` yourself. *** ## Server Module [#server-module] Import from `@localess/angular/server`. All server services call the Localess REST API using a secret API token. They must be registered via `provideLocalessServer()` in the server application config and must never be used in browser code. ### Setup [#setup-1] ```typescript // app.config.server.ts import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; import { provideServerRendering, withRoutes } from '@angular/ssr'; import { provideLocalessServer } from '@localess/angular/server'; import { appConfig } from './app.config'; import { serverRoutes } from './app.routes.server'; const serverConfig: ApplicationConfig = { providers: [ provideServerRendering(withRoutes(serverRoutes)), provideLocalessServer({ origin: 'https://my-localess.web.app', spaceId: 'YOUR_SPACE_ID', token: 'YOUR_SECRET_TOKEN', version: 'draft', // omit for published content }), ], }; export const config = mergeApplicationConfig(appConfig, serverConfig); ``` | Option | Type | Required | Description | | --------- | ------------------- | -------- | ------------------------------------------------------------ | | `origin` | `string` | ✅ | Fully qualified Localess URL | | `spaceId` | `string` | ✅ | Space ID from Localess Space settings | | `token` | `string` | ✅ | API token — keep this secret, never expose it to the browser | | `version` | `'draft' \| string` | — | Set to `'draft'` to fetch unpublished content | | `debug` | `boolean` | — | When `true`, logs API calls to the server console | *** ### Content Service [#content-service] `ServerContentService` fetches CMS content from the Localess API. Results are cached in-memory per server request to prevent redundant network calls. ```typescript import { ServerContentService } from '@localess/angular/server'; @Injectable() export class MyService { private contentService = inject(ServerContentService); } ``` #### `getContentBySlug(slug, params?)` [#getcontentbyslugtslug-params] ```typescript const content = await firstValueFrom( contentService.getContentBySlug('home', { locale: 'en', resolveReference: true, resolveLink: true, }) ); ``` #### `getContentById(id, params?)` [#getcontentbyidtid-params] ```typescript const content = await firstValueFrom( contentService.getContentById('abc123', { locale: 'fr' }) ); ``` #### `getLinks(params?)` [#getlinksparams] Fetches the full links map — a dictionary of content IDs to their slug paths. Pass this to browser-side schema components to enable link resolution. ```typescript const links = await firstValueFrom(contentService.getLinks()); // Filter by content kind or parent const blogLinks = await firstValueFrom( contentService.getLinks({ kind: 'DOCUMENT', parentSlug: 'blog' }) ); ``` #### `ContentFetchParams` [#contentfetchparams] | Parameter | Type | Description | | ------------------ | ------------------- | -------------------------------------------- | | `version` | `'draft' \| string` | Override the global version for this request | | `locale` | `string` | Locale code, e.g. `'en'`, `'fr'` | | `resolveReference` | `boolean` | Inline referenced content objects | | `resolveLink` | `boolean` | Inline link objects | | `resolveAsset` | `boolean` | Inline referenced asset metadata | #### `LinksFetchParams` [#linksfetchparams] | Parameter | Type | Description | | ----------------- | --------- | ---------------------------------------- | | `kind` | `string` | Filter links by content kind | | `parentSlug` | `string` | Return only links under this parent slug | | `excludeChildren` | `boolean` | Exclude descendant slugs | *** ### Server Asset Service [#server-asset-service] `ServerAssetService` generates asset URLs on the server. Its API is identical to `BrowserAssetService`. ```typescript import { ServerAssetService } from '@localess/angular/server'; @Injectable() export class MyService { private assetService = inject(ServerAssetService); getUrl(asset: ContentAsset): string { return this.assetService.link(asset); } } ``` *** ### Translation Service [#translation-service] `ServerTranslationService` fetches all translation strings for a given locale. Results are cached by locale. ```typescript import { ServerTranslationService } from '@localess/angular/server'; @Injectable() export class MyService { private translationService = inject(ServerTranslationService); getTranslations(locale: string): Observable { return this.translationService.fetch(locale); } } ``` The returned `Translations` object is a flat key–value map (`Record`). *** ## SSR with TransferState [#ssr-with-transferstate] In an SSR application, content fetched on the server must be transferred to the browser to avoid a duplicate network request on hydration. The recommended pattern uses an abstract service with two implementations swapped via Angular's DI system. **Abstract service** (`localess.service.ts`): ```typescript import { Injectable, makeStateKey } from '@angular/core'; import { Content, Links, ContentData } from '@localess/angular'; import { Observable } from 'rxjs'; @Injectable() export abstract class LocalessService { LINKS_KEY = makeStateKey('ll:links'); abstract getLinks(): Observable; abstract getContentBySlug(slug: string | string[], locale?: string): Observable>; abstract getContentById(id: string, locale?: string): Observable>; } ``` **Server implementation** (`localess-server.service.ts`): ```typescript import { inject, Injectable, makeStateKey, TransferState } from '@angular/core'; import { tap } from 'rxjs/operators'; import { ServerContentService } from '@localess/angular/server'; import { LocalessService } from './localess.service'; @Injectable() export class LocalessServerService extends LocalessService { private state = inject(TransferState); private contentService = inject(ServerContentService); getLinks() { return this.contentService.getLinks().pipe( tap(links => this.state.set(this.LINKS_KEY, links)) ); } getContentBySlug(slug: string | string[], locale?: string) { const normalizedSlug = Array.isArray(slug) ? slug.join('/') : slug; const key = makeStateKey>(`ll:content:slug:${normalizedSlug}`); return this.contentService.getContentBySlug(normalizedSlug, { locale }).pipe( tap(content => this.state.set(key, content)) ); } getContentById(id: string, locale?: string) { const key = makeStateKey>(`ll:content:id:${id}`); return this.contentService.getContentById(id, { locale }).pipe( tap(content => this.state.set(key, content)) ); } } ``` **Browser implementation** (`localess-browser.service.ts`): ```typescript import { inject, Injectable, makeStateKey, TransferState } from '@angular/core'; import { of } from 'rxjs'; import { LocalessService } from './localess.service'; @Injectable() export class LocalessBrowserService extends LocalessService { private state = inject(TransferState); getLinks() { return of(this.state.get(this.LINKS_KEY, {})); } getContentBySlug(slug: string | string[], locale?: string) { const normalizedSlug = Array.isArray(slug) ? slug.join('/') : slug; const key = makeStateKey>(`ll:content:slug:${normalizedSlug}`); return of(this.state.get(key, {} as Content)); } getContentById(id: string, locale?: string) { const key = makeStateKey>(`ll:content:id:${id}`); return of(this.state.get(key, {} as Content)); } } ``` **Wire them up:** ```typescript // app.config.ts providers: [{ provide: LocalessService, useClass: LocalessBrowserService }] // app.config.server.ts providers: [{ provide: LocalessService, useClass: LocalessServerService }] ``` **Use the abstract service anywhere without worrying about the platform:** ```typescript @Component({ ... }) export class SlugComponent { private localess = inject(LocalessService); content = toSignal(this.localess.getContentBySlug('home')); } ``` *** ## Angular Image Optimization [#angular-image-optimization] `provideLocalessBrowser()` automatically registers Angular's `IMAGE_LOADER` provider. When you use `NgOptimizedImage` (`ngSrc`) with a Localess asset URL, Angular appends `?w=` to the URL for server-side image resizing: ```html Hero image ``` No additional configuration is required. *** ## API Reference [#api-reference] ### `@localess/angular/browser` [#localessangularbrowser] | Export | Kind | Description | | --------------------------------- | -------------- | ------------------------------------------------------------------------- | | `provideLocalessBrowser(options)` | Function | Registers all browser-side providers | | `SchemaComponent` | Abstract Class | Base component with `data`, `links`, `references`, `assets` signal inputs | | `ContentIdDirective` | Directive | `[data-ll-id]` marker | | `ContentSchemaDirective` | Directive | `[data-ll-schema]` marker | | `ContentFieldDirective` | Directive | `[data-ll-field]` marker | | `ContentDirective` | Directive | `[llContent]` — sets both id and schema attributes | | `AssetPipe` | Pipe | `llAsset` — asset to URL, with optional transform params | | `LinkPipe` | Pipe | `llLink` — resolves a `ContentLink` | | `RichTextToHtmlPipe` | Pipe | `llRtToHtml` — Tiptap JSON to HTML | | `SafeHtmlPipe` | Pipe | `llSafeHtml` — bypasses `DomSanitizer` | | `BrowserAssetService` | Service | Programmatic asset URL generation | | `LocalessSyncService` | Service | Visual Editor sync — `on()` / `onChange()` | | `buildAssetQueryString(params?)` | Function | Standalone asset transform query-string builder | | `findLink(links, link)` | Function | Standalone link resolution utility | ### `@localess/angular/server` [#localessangularserver] | Export | Kind | Description | | -------------------------------- | -------- | ------------------------------------- | | `provideLocalessServer(options)` | Function | Registers all server-side providers | | `ServerContentService` | Service | Fetches content by slug, ID, or links | | `ServerAssetService` | Service | Programmatic asset URL generation | | `ServerTranslationService` | Service | Fetches translations by locale | ### `@localess/angular` [#localessangular] Re-exports all types from `@localess/client`: | Type | Description | | ------------------------------------------------------------------ | ------------------------------------------------------------------------ | | `Content` | CMS document with metadata and typed `data` payload | | `ContentData` | Base type for schema data objects | | `ContentDataSchema` | Schema data with `_id` and `_schema` fields | | `ContentAsset` | Asset reference `{ uri: string }` | | `ContentLink` | Link reference `{ type: 'content' \| 'url', uri: string }` | | `ContentRichText` | Tiptap JSON rich text | | `Links` | Map of content ID → `{ fullSlug: string }` | | `References` | Map of referenced content objects | | `Assets` | Map of asset ID → asset metadata | | `Translations` | Flat key–value map of translation strings | | `ContentFetchParams` | Parameters for content fetch requests | | `LinksFetchParams` | Parameters for links fetch requests | | `AssetTransformParams` | Asset transform parameters (`w`, `h`, `f`, `q`, `thumbnail`, `download`) | | `LocalessSync` | Visual Editor sync event types | | `EventToApp` / `EventToAppOf` / `EventCallback` / `EventToAppType` | Visual Editor sync event payload types | # Astro A dedicated `@localess/astro` package is on the roadmap. Until it ships, integrate Astro with Localess by fetching content with the framework-agnostic JavaScript client and (optionally) injecting the Visual Editor sync script. ## 1. Fetch content [#1-fetch-content] Use [`@localess/client`](typescript) inside an Astro page or component frontmatter — code there runs at build/server time, so your API token stays out of the browser bundle. ```astro --- // src/pages/[locale]/index.astro import { localessClient } from "@localess/client"; const client = localessClient({ origin: import.meta.env.LOCALESS_ORIGIN, spaceId: import.meta.env.LOCALESS_SPACE_ID, token: import.meta.env.LOCALESS_TOKEN, }); const { locale } = Astro.params; const content = await client.getContentBySlug("home", { locale }); const ui = await client.getTranslations(locale ?? "en"); ---

{content.data?.title}

``` Generate TypeScript types with the [CLI](../cli/types) for full type safety. ## 2. Enable the Visual Editor (optional) [#2-enable-the-visual-editor-optional] Astro's static-first model means live editing typically runs only in development or on dynamically rendered pages. ### Option A — Programmatic injection [#option-a--programmatic-injection] Use `loadLocalessSync` from `@localess/client` in a client-side script block: ```astro --- const origin = import.meta.env.LOCALESS_ORIGIN; --- ``` ### Option B — Manual script tag [#option-b--manual-script-tag] Add the sync script directly to a layout's ``: ```astro --- const origin = import.meta.env.LOCALESS_ORIGIN; --- ``` ### Subscribing to edit events [#subscribing-to-edit-events] For pages that need live updates, subscribe to `window.localess` events in a client script: ```astro ``` ### Marking editable elements [#marking-editable-elements] Use `localessEditable` and `localessEditableField` from `@localess/client` to mark elements for Visual Editor selection: ```astro --- import { localessEditable, localessEditableField } from "@localess/client"; ---

{data.title}

``` See [Visual Editor](../visual-editor) for the full event list and attribute reference. ## Roadmap [#roadmap] `@localess/astro` will ship integrations for Astro's content collection model and an island component analogous to [`@localess/react`](react)'s `LocalessComponent`. Track progress on [GitHub](https://github.com/Lessify/localess-js). # React Package: [`@localess/react`](https://github.com/Lessify/localess-js/blob/main/packages/react) — v3.0.0 The `@localess/react` package is the official React integration for the Localess headless CMS platform. It provides component mapping, rich text rendering, and Visual Editor synchronization support for React applications. > **⚠️ Security Notice:** > This package uses `@localess/client` internally, which requires an API token for server-side data fetching. Always fetch Localess content server-side (e.g., Next.js Server Components, API routes, or `getServerSideProps`) and never expose your token in client-side code. ## Requirements [#requirements] * Node.js >= 24.0.0 * React 17, 18, or 19 ## Installation [#installation] ```bash # npm npm install @localess/react # yarn yarn add @localess/react # pnpm pnpm add @localess/react ``` ## Choosing the Right Export [#choosing-the-right-export] `@localess/react` provides three different exports to suit different rendering strategies: | Export | Use Case | Live Editing | Static Export | | --------------------- | ----------------------------------------------------- | ------------ | ------------- | | `@localess/react` | Single Page Applications (SPA), client-side rendering | Yes | Yes | | `@localess/react/ssr` | SSR without live editing, Next.js static exports | No | Yes | | `@localess/react/rsc` | React Server Components with live editing | Yes | No | ### When to Use Each Export [#when-to-use-each-export] **Use `@localess/react`** (default) for: * Single Page Applications (SPA) or fully client-rendered React apps * Apps where `localessInit` and components run entirely in the browser **Use `@localess/react/ssr`** for: * Next.js projects with `output: 'export'` (static site generation) * Server-side rendering where live editing is not required * Scenarios where bundle size matters and you want to exclude all browser-only sync code **Use `@localess/react/rsc`** for: * Next.js App Router with React Server Components * Apps that need live Visual Editor editing alongside server rendering * Modern Next.js apps with a server/client component split ### Quick Comparison [#quick-comparison] ```typescript // SPA — everything runs client-side import { localessInit, LocalessComponent, useLocaless } from "@localess/react"; // SSR — server-safe, no live editing, no hooks import { localessInit, LocalessServerComponent } from "@localess/react/ssr"; // RSC — server components + client components for live editing import { localessInit, LocalessServerComponent } from "@localess/react/rsc"; // server import { LocalessDocument, useLocaless, localessEditable } from "@localess/react/rsc"; // client ``` > **Note:** When using Next.js with `output: 'export'`, always use `@localess/react/ssr`. The RSC export is not compatible with static exports. > **Playgrounds:** Full working Next.js projects are available for both modes — [`playgrounds/next`](https://github.com/Lessify/localess-js/tree/main/playgrounds/next) (App Router + RSC, live editing) and [`playgrounds/next-static`](https://github.com/Lessify/localess-js/tree/main/playgrounds/next-static) (`output: 'export'`, static). ## Getting Started [#getting-started] ### 1. Initialize the SDK [#1-initialize-the-sdk] Call `localessInit` once at application startup (e.g., in your root layout or `_app.tsx`) to configure the client, register your components, and optionally enable the Visual Editor. ```typescript import { localessInit } from "@localess/react"; import { Page, Header, Teaser, Footer } from "@/components"; localessInit({ origin: "https://my-localess.web.app", spaceId: "YOUR_SPACE_ID", token: "YOUR_API_TOKEN", enableSync: true, // Enable Visual Editor sync script components: { 'page': Page, 'header': Header, 'teaser': Teaser, 'footer': Footer, }, }); ``` ### Initialization Options [#initialization-options] | Option | Type | Required | Default | Description | | ------------------- | ----------------------------------- | -------- | ------------- | ----------------------------------------------------------------- | | `origin` | `string` | Yes | — | Fully qualified domain with protocol | | `spaceId` | `string` | Yes | — | Localess Space ID, found in Space settings | | `token` | `string` | Yes | — | Localess API token (keep secret — server-side only) | | `version` | `'draft' \| string` | No | `'published'` | Default content version | | `debug` | `boolean` | No | `false` | Enable debug logging | | `cacheTTL` | `number \| false` | No | `300` | Cache TTL in **seconds**. Set `false` to disable caching entirely | | `components` | `Record` | No | `{}` | Map of schema keys to React components | | `fallbackComponent` | `React.ElementType` | No | — | Component rendered when a schema key has no registered component | | `enableSync` | `boolean` | No | `false` | Load the Visual Editor sync script for live-editing support | ## `LocalessComponent` [#localesscomponent] `LocalessComponent` is a dynamic renderer that maps Localess content data to your registered React components by schema key. It automatically applies Visual Editor attributes when sync is enabled. ```typescript import { LocalessComponent } from "@localess/react"; // Render a single content block // Render a list of nested blocks {data.body.map(item => ( ))} ``` ### Props [#props] | Prop | Type | Required | Description | | ------------ | ------------------------ | -------- | -------------------------------------------------------------------------------------------------- | | `data` | `ContentData` | Yes | Content data object from Localess. The component looks up `data._schema` in the component registry | | `links` | `Links` | No | Resolved content links map, forwarded to the rendered component | | `references` | `References` | No | Resolved references map, forwarded to the rendered component | | `assets` | `Assets` | No | Resolved content assets map (keyed by asset ID), forwarded to the rendered component | | `ref` | `React.Ref` | No | Ref forwarded to the rendered component's root element | | `...rest` | `any` | No | Any additional props are forwarded to the rendered component | > If a schema key is not registered and no `fallbackComponent` is configured, `LocalessComponent` renders an error message in the DOM. ## Marking Editable Content [#marking-editable-content] Use these helpers to add Visual Editor attributes to your JSX elements. They enable element highlighting and selection in the Localess Visual Editor. ### `localessEditable(content)` [#localesseditablecontent] Marks a content block root element as editable. ```typescript import { localessEditable } from "@localess/react"; const Header = ({ data }) => ( ); ``` ### `localessEditableField(fieldName)` [#localesseditablefieldtfieldname] Marks a specific field within a content block as editable, with type-safe field name inference when combined with generated types. ```typescript import { localessEditableField } from "@localess/react"; const Hero = ({ data }: { data: HeroBlock }) => (

('title')}>{data.title}

('subtitle')}>{data.subtitle}

); ``` ## Rich Text Rendering [#rich-text-rendering] ### `renderRichTextToReact(content)` [#renderrichtexttoreactcontent] Converts a Localess `ContentRichText` object to a React node tree. Supports the full range of rich text formatting produced by the Localess editor. ```typescript import { renderRichTextToReact } from "@localess/react"; const Article = ({ data }) => (

{data.title}

{renderRichTextToReact(data.body)}
); ``` **Supported rich text elements:** * Document structure * Headings (h1–h6) * Paragraphs * Text formatting: bold, italic, strikethrough, underline * Ordered and unordered lists * Code blocks (with syntax highlighting support) * Links (inline) ## Accessing the Client [#accessing-the-client] ### `getLocalessClient()` [#getlocalessclient] Returns the `LocalessClient` instance created during `localessInit`. Use this in server-side data-fetching functions. ```typescript import { getLocalessClient } from "@localess/react"; async function fetchPageData(locale?: string) { const client = getLocalessClient(); return client.getContentBySlug('home', { locale }); } ``` > Throws an error if called before `localessInit` has been executed. ## Component Registry API [#component-registry-api] These functions allow dynamic management of the component registry after initialization. ```typescript import { registerComponent, unregisterComponent, setComponents, getComponent, setFallbackComponent, getFallbackComponent, isSyncEnabled, } from "@localess/react"; // Register a new component registerComponent('hero-block', HeroBlock); // Unregister a component unregisterComponent('hero-block'); // Replace the entire registry setComponents({ 'page': Page, 'hero': Hero }); // Retrieve a component by schema key const Component = getComponent('hero'); // Configure the fallback component setFallbackComponent(UnknownComponent); // Get the current fallback component const fallback = getFallbackComponent(); // Check if Visual Editor sync is enabled const syncEnabled = isSyncEnabled(); ``` ## Assets [#assets] ### `resolveAsset(asset, params?)` [#resolveassetasset-params] Resolves a `ContentAsset` object to a fully qualified URL using the initialized client's origin. Pass an `AssetTransformParams` object as the second argument to request a resized image or a different output format — the params are appended as query string parameters that the Localess asset endpoint uses to transform the image on the fly. See [Image Transforms](/docs/image-transforms) for the full parameter reference. ```typescript import { resolveAsset } from "@localess/react"; const Image = ({ data }) => ( {data.imageAlt} ); ``` ```tsx import { resolveAsset } from "@localess/react"; function HeroImage({ image }: { image: ContentAsset }) { return ( {image.alt ); } function ProductImage({ image }: { image: ContentAsset }) { return ( {image.alt ); } ``` | Param | Type | Description | | ----------- | ------------------------------------- | ------------------------------------------------------------------- | | `w` | `number` | Target width in pixels | | `h` | `number` | Target height in pixels (combined with `w`, crops to cover the box) | | `q` | `number` | Output quality 1–100 (default `85`; ignored for PNG) | | `f` | `'webp' \| 'jpeg' \| 'png' \| 'avif'` | Converts the output format | | `download` | `boolean` | Forces a browser download via `Content-Disposition` | | `thumbnail` | `boolean` | Extracts the first frame of an animated/video asset before resizing | ## `useLocaless` Hook [#uselocaless-hook] `useLocaless` fetches content by slug in a Client Component and automatically subscribes to Visual Editor live updates when `enableSync` is active. ```typescript 'use client'; import { useLocaless, LocalessComponent } from "@localess/react"; import type { Page } from "./.localess/localess"; export function PageView({ slug }: { slug: string }) { const content = useLocaless(slug, { locale: 'en' }); if (!content) return
Loading…
; return (
{content.data.body.map(item => ( ))}
); } ``` ### Parameters [#parameters] | Parameter | Type | Required | Description | | --------- | -------------------- | -------- | ----------------------------------------------------------------------------------------- | | `slug` | `string \| string[]` | Yes | Content slug. Arrays are joined with `/` — e.g. `['blog', 'post']` → `'blog/post'` | | `options` | `ContentFetchParams` | No | Same fetch options as `getContentBySlug` (locale, version, resolveReference, resolveLink) | Returns `Content | undefined` — `undefined` while the initial fetch is in progress. `Content` includes `data`, `links`, `references`, and `assets` (a map of resolved content assets keyed by asset ID). 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. ## Link Utilities [#link-utilities] ### `findLink(links, link)` [#findlinklinks-link] Resolves a `ContentLink` field to a URL string. Use it to build `href` values from Localess content links. ```typescript import { findLink } from "@localess/react"; // type: 'content' → '/' + fullSlug, or '/not-found' if not in map // type: 'url' → raw URI unchanged const href = findLink(content.links, data.ctaLink); const NavLink = ({ data, links }) => ( {data.label} ); ``` ## Visual Editor Events [#visual-editor-events] ### With `useLocaless` Hook [#with-uselocaless-hook] 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. ```typescript 'use client'; import { useLocaless, LocalessComponent, localessEditable } from "@localess/react"; import type { Page } from "./.localess/localess"; export function PageView({ slug, locale }: { slug: string; locale?: string }) { const content = useLocaless(slug, { locale }); if (!content) return null; return (
{content.data?.body.map(item => ( ))}
); } ``` ### With `LocalessDocument` Component [#with-localessdocument-component] `LocalessDocument` is a component alternative to the hook. Pass it the full server-fetched content response and it handles live sync updates internally, delegating rendering to `LocalessComponent`. ```typescript // app/[locale]/page.tsx (Server Component — fetches data) import { getLocalessClient, LocalessDocument } from "@localess/react/rsc"; import type { Page } from "./.localess/localess"; export default async function HomePage({ params }: { params: Promise<{ locale?: string }> }) { const { locale } = await params; const client = getLocalessClient(); const content = await client.getContentBySlug('home', { locale }); return ; } ``` **Props:** | Prop | Type | Required | Description | | ---------- | ------------------------ | -------- | ----------------------------------------------------------------------- | | `document` | `Content` | Yes | Full content response object (from `getContentBySlug`/`getContentById`) | | `ref` | `React.Ref` | No | Forwarded to the rendered root element | > `LocalessDocument` subscribes to `input` / `change` editor events automatically when `enableSync` is active. > > Import it from `@localess/react/rsc` (as above) when calling it directly from a Server Component, as this example does — that variant renders the component registry lookup itself in the Server Component module graph, where `localessInit()`'s registration is visible. The plain `@localess/react` export's `LocalessDocument` is a Client Component internally; calling it directly from a Server Component moves that lookup into a separate client module graph where the registration set by a Server Component's `localessInit()` call was never applied, and it fails to find any registered component. Use the plain `@localess/react` export's `LocalessDocument` only from inside an actual `'use client'` file (see the SPA example below). > > See the [`playgrounds/next`](https://github.com/Lessify/localess-js/tree/main/playgrounds/next) playground for a full working Next.js App Router + RSC project built on this pattern. ### Manual Integration [#manual-integration] If you manage content state yourself without `useLocaless` or `LocalessDocument`, use `localessSyncOn` / `localessSyncOnChange`. They wrap the `isSyncEnabled()` check and the sync-ready wait internally, so you don't need to guard for browser/iframe context or race the sync script load: ```typescript 'use client'; import { useEffect, useState } from "react"; import { LocalessComponent, localessEditable, localessSyncOnChange } from "@localess/react"; import type { Content, Page } from "./.localess/localess"; export function PageClient({ initialContent }: { initialContent: Content }) { const [pageData, setPageData] = useState(initialContent.data); useEffect(() => { // No-op automatically if sync isn't enabled/usable — no manual guards needed. localessSyncOnChange((event) => setPageData(event.data)); // No cleanup needed: window.localess has no .off() method }, []); return (
{pageData?.body.map(item => ( ))}
); } ``` `localessSyncOnChange(callback)` is shorthand for `localessSyncOn(['input', 'change'], callback)`. Use `localessSyncOn` directly to subscribe to other event types: ```typescript import { localessSyncOn } from "@localess/react"; localessSyncOn(['save', 'publish'], (event) => console.info(`Content ${event.type}d`)); ``` **Available events:** | Event | When | | ------------- | --------------------------------------------- | | `input` | User is typing in a field (real-time preview) | | `change` | Field value confirmed | | `save` | Content saved | | `publish` | Content published | | `unpublish` | Content unpublished | | `pong` | Editor heartbeat response | | `enterSchema` | Editor cursor enters a schema block | | `hoverSchema` | Editor cursor hovers over a schema block | | `leaveSchema` | Editor cursor leaves a schema block | > `window.localess` only exposes `.on()` and `.onChange()` — there is no `.off()` method. Prefer `localessSyncOn`/`localessSyncOnChange` over calling `window.localess` directly — they handle the enabled/ready checks for you. ## Full Example — SPA / Default (`@localess/react`) [#full-example--spa--default-localessreact] For SPAs or fully client-rendered React apps. All imports use the default `@localess/react` export. ### Setup — `app/layout.tsx` [#setup--applayouttsx] ```typescript // Server Component — safe to use API token here import { localessInit } from "@localess/react"; import { Page, Header, Teaser, Footer } from "@/components"; localessInit({ origin: process.env.LOCALESS_ORIGIN!, spaceId: process.env.LOCALESS_SPACE_ID!, token: process.env.LOCALESS_TOKEN!, enableSync: process.env.NODE_ENV !== 'production', components: { Page, Header, Teaser, Footer }, }); export default function RootLayout({ children }: { children: React.ReactNode }) { return {children}; } ``` ### Server Component — `app/[locale]/page.tsx` [#server-component--applocalepagetsx] ```typescript import { getLocalessClient } from "@localess/react"; import type { Content, Page } from "./.localess/localess"; import { PageClientHook } from "./page-client-hook"; export default async function HomePage({ params, }: { params: Promise<{ locale?: string }>; }) { const { locale } = await params; const content = await getLocalessClient().getContentBySlug('home', { locale }); return ; } ``` ### Client Component — Option A: `useLocaless` Hook [#client-component--option-a-uselocaless-hook] ```typescript // app/[locale]/page-client-hook.tsx 'use client'; import { useLocaless, LocalessComponent, localessEditable } from "@localess/react"; import type { Content, Page } from "./.localess/localess"; export function PageClientHook({ initialContent, locale, }: { initialContent: Content; locale?: string; }) { const content = useLocaless('home', { locale }) ?? initialContent; return (
{content.data?.body.map(item => ( ))}
); } ``` ### Client Component — Option B: `LocalessDocument` [#client-component--option-b-localessdocument] > This specific pattern — calling `LocalessDocument` directly from a Server Component with no separate `'use client'` file — needs the `@localess/react/rsc` import, not the plain `@localess/react` export used elsewhere in this SPA example. See [With `LocalessDocument` Component](#with-localessdocument-component) above for why. ```typescript // app/[locale]/page.tsx (Server Component — no separate client file needed) import { getLocalessClient, LocalessDocument } from "@localess/react/rsc"; import type { Page } from "./.localess/localess"; export default async function HomePage({ params, }: { params: Promise<{ locale?: string }>; }) { const { locale } = await params; const content = await getLocalessClient().getContentBySlug('home', { locale }); // LocalessDocument handles sync internally — no 'use client' wrapper needed here return ; } ``` ### Client Component — Option C: Manual [#client-component--option-c-manual] ```typescript // app/[locale]/page-client-manual.tsx 'use client'; import { useEffect, useState } from "react"; import { LocalessComponent, localessEditable, isSyncEnabled, isBrowser } from "@localess/react"; import type { Content, Page } from "./.localess/localess"; export function PageClientManual({ initialContent, }: { initialContent: Content; }) { const [pageData, setPageData] = useState(initialContent.data); useEffect(() => { if (isSyncEnabled() && isBrowser() && window.localess) { window.localess.on(['input', 'change'], (event) => { if (event.type === 'input' || event.type === 'change') { setPageData(event.data); } }); } }, []); return (
{pageData?.body.map(item => ( ))}
); } ``` ## Full Example — Next.js Static Export (`@localess/react/ssr`) [#full-example--nextjs-static-export-localessreactssr] Use `@localess/react/ssr` when your Next.js project uses `output: 'export'` for static site generation. Live editing is not available in this mode. ### `next.config.js` [#nextconfigjs] ```javascript /** @type {import('next').NextConfig} */ module.exports = { output: 'export' }; ``` ### Setup — `lib/localess.ts` [#setup--liblocalessts] ```typescript import { localessInit } from "@localess/react/ssr"; import { Page, Header, Teaser } from "@/components"; export const getClient = localessInit({ origin: process.env.LOCALESS_ORIGIN!, spaceId: process.env.LOCALESS_SPACE_ID!, token: process.env.LOCALESS_TOKEN!, // enableSync is not applicable in static export — omit or set to false components: { Page, Header, Teaser }, }); ``` ### Page — `app/page.tsx` [#page--apppagetsx] ```typescript import { LocalessServerComponent } from "@localess/react/ssr"; import { getLocalessClient } from "@localess/react/ssr"; import "@/lib/localess"; // ensure init runs export default async function Home() { const client = getLocalessClient(); const content = await client.getContentBySlug("home", { locale: "en" }); return (
); } ``` `@localess/react/ssr` also exports `LocalessServerDocument`, which takes the full `Content` response as a single `document` prop (like `LocalessDocument`, but with no sync attributes since live editing has no meaning once the HTML is pre-baked): ```typescript import { LocalessServerDocument } from "@localess/react/ssr"; ``` > **Playground:** [`playgrounds/next-static`](https://github.com/Lessify/localess-js/tree/main/playgrounds/next-static) mirrors the RSC playground but targets `output: 'export'` and uses `@localess/react/ssr` end to end. ## Full Example — Next.js App Router with RSC (`@localess/react/rsc`) [#full-example--nextjs-app-router-with-rsc-localessreactrsc] Use `@localess/react/rsc` when you want React Server Components and Visual Editor live editing together. ### Setup — `app/layout.tsx` [#setup--applayouttsx-1] ```typescript // Server Component — safe to use API token here import { localessInit } from "@localess/react/rsc"; import { Page, Header, Teaser, Footer } from "@/components"; localessInit({ origin: process.env.LOCALESS_ORIGIN!, spaceId: process.env.LOCALESS_SPACE_ID!, token: process.env.LOCALESS_TOKEN!, enableSync: process.env.NODE_ENV !== 'production', components: { Page, Header, Teaser, Footer }, }); export default function RootLayout({ children }: { children: React.ReactNode }) { return {children}; } ``` ### Rendering — `app/[locale]/page.tsx` [#rendering--applocalepagetsx] Use `LocalessDocument` for a zero-boilerplate live-editing integration, or `useLocaless` for client-side re-fetching with more control. **Option A — `LocalessDocument` (recommended):** it's a Server Component internally (only its sync subscription runs client-side), so it renders directly in the Server Component — no separate Client Component file needed. ```typescript import { getLocalessClient, LocalessDocument } from "@localess/react/rsc"; export default async function Home({ params }: { params: { locale: string } }) { const { locale } = await params; const content = await getLocalessClient().getContentBySlug("home", { locale }); return ; } ``` **Option B — `useLocaless` hook:** re-fetches on the client, so it needs an actual `'use client'` file. ```typescript // app/[locale]/page.tsx (Server Component) import { getLocalessClient } from "@localess/react/rsc"; import PageClient from "./page-client"; export default async function Home({ params }: { params: { locale: string } }) { const { locale } = await params; const content = await getLocalessClient().getContentBySlug("home", { locale }); return ; } ``` ```typescript // app/[locale]/page-client.tsx (Client Component) 'use client'; import { useLocaless, LocalessComponent, localessEditable } from "@localess/react/rsc"; export default function PageClient({ initialContent, locale }) { const content = useLocaless("home", { locale }) ?? initialContent; return (
{content.data?.body?.map(item => ( ))}
); } ``` > **Playground:** [`playgrounds/next`](https://github.com/Lessify/localess-js/tree/main/playgrounds/next) is a full working Next.js App Router project built on Option A — `localessInit()` in `page.tsx` and `` rendered directly from the Server Component. ## Export Reference [#export-reference] The table below shows which symbols are available in each export. | Symbol | `@localess/react` | `@localess/react/ssr` | `@localess/react/rsc` | | --------------------------------------------------------------------------------- | ----------------- | --------------------- | --------------------- | | `localessInit` | Yes | Yes | Yes | | `getLocalessClient` | Yes | Yes | Yes | | `registerComponent` / `setComponents` / `getComponent` | Yes | Yes | Yes | | `setFallbackComponent` / `getFallbackComponent` | Yes | Yes | Yes | | `resolveAsset` | Yes | Yes | Yes | | `LocalessComponent` | Yes | No | Yes | | `LocalessServerComponent` / `LocalessServerDocument` | No | Yes | Yes | | `renderRichTextToReact` | Yes | Yes | Yes | | `findLink` | Yes | Yes | Yes | | `isServer` | Yes | Yes | Yes | | All content types | Yes | Yes | Yes | | `LocalessDocument` | Yes | No | Yes | | `useLocaless` | Yes | No | Yes | | `localessEditable` / `localessEditableField` | Yes | Yes | Yes | | `isBrowser` / `isIframe` | Yes | Yes | Yes | | `isSyncEnabled` / `localessSyncOn` / `localessSyncOnChange` / `localessSyncReady` | Yes | No | Yes | | Sync event types (`LocalessSync`, `EventToApp`, `EventToAppOf`, …) | Yes | Yes | Yes | ## AI Coding Agents [#ai-coding-agents] This package ships a [`SKILL.md`](https://github.com/Lessify/localess-js/blob/main/packages/react/SKILL) file that provides AI coding agents (GitHub Copilot, Claude Code, Cursor, and others) with accurate, up-to-date APIs, patterns, and best practices. Reference it from your project's `AGENTS.md`: ``` ## Localess @node_modules/@localess/react/SKILL.md ``` ## License [#license] [MIT](https://github.com/Lessify/localess-js/blob/main/LICENSE) # Svelte A dedicated `@localess/svelte` package is on the roadmap. Until it ships, integrate Svelte with Localess by combining the framework-agnostic JavaScript client (server-side fetch) with the browser sync script (Visual Editor). ## 1. Fetch content [#1-fetch-content] Use [`@localess/client`](typescript) inside a SvelteKit `+page.server.ts` so your API token stays out of the browser. ```typescript // src/routes/[locale]/+page.server.ts import { localessClient } from "@localess/client"; import { LOCALESS_ORIGIN, LOCALESS_SPACE_ID, LOCALESS_TOKEN } from "$env/static/private"; const client = localessClient({ origin: LOCALESS_ORIGIN, spaceId: LOCALESS_SPACE_ID, token: LOCALESS_TOKEN, }); export async function load({ params }) { const content = await client.getContentBySlug("home", { locale: params.locale }); return { content }; } ``` Generate TypeScript types with the [CLI](../cli/types) so the loaded data is fully typed. ## 2. Enable the Visual Editor (optional) [#2-enable-the-visual-editor-optional] Inject the sync script in `app.html`: ```html ``` Or set it dynamically from your layout using the `LOCALESS_ORIGIN` env var: ```svelte ``` ### Subscribing to edit events — Svelte 5 [#subscribing-to-edit-events--svelte-5] ```svelte

{pageData.title}

``` ### Subscribing to edit events — Svelte 4 [#subscribing-to-edit-events--svelte-4] ```svelte

{pageData.title}

``` See [Visual Editor](../visual-editor) for the full event list and attribute reference. ## Roadmap [#roadmap] `@localess/svelte` will ship with helpers analogous to [`@localess/react`](react) — a dynamic component renderer, a `useLocaless`-style store, editable-attribute helpers, and a rich-text renderer. Track progress on [GitHub](https://github.com/Lessify/localess-js). # TypeScript Package: [`@localess/client`](https://github.com/Lessify/localess-js/blob/main/packages/client) — v3.0.0 The `@localess/client` package is the core JavaScript/TypeScript SDK for the Localess headless CMS platform. It provides a type-safe API client for fetching content, translations, and assets, along with Visual Editor integration utilities. > **⚠️ Security Notice:** > This SDK is designed for **server-side use only**. It requires a Localess API Token that must be kept secret. Never use this package in browser/client-side code, as it would expose your API token to the public. In React applications, always fetch data server-side (e.g., Next.js Server Components, API routes, or server-side rendering). ## Requirements [#requirements] * Node.js >= 24.0.0 ## Installation [#installation] ```bash # npm npm install @localess/client # yarn yarn add @localess/client # pnpm pnpm add @localess/client ``` ## Getting Started [#getting-started] ### Initializing the Client [#initializing-the-client] ```typescript import { localessClient } from "@localess/client"; const client = localessClient({ origin: 'https://my-localess.web.app', // Fully qualified domain with protocol spaceId: 'YOUR_SPACE_ID', // Found in Localess Space settings token: 'YOUR_API_TOKEN', // Found in Localess Space settings (keep secret!) }); ``` ### Client Options [#client-options] | Option | Type | Required | Default | Description | | ---------- | ------------------- | -------- | ------------- | ----------------------------------------------------------------------------- | | `origin` | `string` | Yes | — | Fully qualified domain with protocol (e.g., `https://my-localess.web.app`) | | `spaceId` | `string` | Yes | — | Localess Space ID, found in Space settings | | `token` | `string` | Yes | — | Localess API token, found in Space settings | | `version` | `'draft' \| string` | No | `'published'` | Default content version to fetch | | `debug` | `boolean` | No | `false` | Enable debug logging | | `cacheTTL` | `number \| false` | No | `300` | Cache TTL in **seconds** (5 minutes). Set `false` to disable caching entirely | ## Fetching Content [#fetching-content] ### `getContentBySlug(slug, params?)` [#getcontentbyslugtslug-params] Fetch a content document by its slug path. Supports generic typing for full type safety. ```typescript // Basic usage const content = await client.getContentBySlug('docs/overview'); // With type safety (requires generated types from @localess/cli) import type { Page } from './.localess/localess'; const content = await client.getContentBySlug('home', { locale: 'en', resolveReference: true, resolveLink: true, resolveAsset: true, }); ``` ### `getContentById(id, params?)` [#getcontentbyidtid-params] Fetch a content document by its unique ID. Accepts the same parameters as `getContentBySlug`. ```typescript const content = await client.getContentById('FRnIT7CUABoRCdSVVGGs', { locale: 'de', version: 'draft', }); ``` ### Content Fetch Parameters [#content-fetch-parameters] | Parameter | Type | Default | Description | | ------------------ | ------------------- | -------------- | --------------------------------------------- | | `version` | `'draft' \| string` | Client default | Override the client's default content version | | `locale` | `string` | — | ISO 639-1 locale code (e.g., `'en'`, `'de'`) | | `resolveReference` | `boolean` | `false` | Resolve content references inline | | `resolveLink` | `boolean` | `false` | Resolve content links inline | | `resolveAsset` | `boolean` | `false` | Resolve referenced asset metadata inline | ## Fetching Content Links [#fetching-content-links] ### `getLinks(params?)` [#getlinksparams] Fetch all content links from the space, optionally filtered by type or parent. ```typescript // Fetch all links const links = await client.getLinks(); // Fetch only documents under a specific parent const legalLinks = await client.getLinks({ kind: 'DOCUMENT', parentSlug: 'legal', excludeChildren: false, }); ``` | Parameter | Type | Description | | ----------------- | ------------------------ | --------------------------------------------------- | | `kind` | `'DOCUMENT' \| 'FOLDER'` | Filter results by content kind | | `parentSlug` | `string` | Filter by parent slug (e.g., `'legal/policy'`) | | `excludeChildren` | `boolean` | When `true`, excludes nested sub-slugs from results | ## Fetching Translations [#fetching-translations] ### `getTranslations(locale, params?)` [#gettranslationslocale-params] Fetch all translations for a given locale. Returns a flat key-value map. ```typescript const translations = await client.getTranslations('en'); // { "common.submit": "Submit", "nav.home": "Home", ... } // Override the client's default version for this request const draftTranslations = await client.getTranslations('en', { version: 'draft' }); ``` | Parameter (`TranslationFetchParams`) | Type | Default | Description | | ------------------------------------ | ---------------------- | -------------- | ----------------------------------------- | | `version` | `'draft' \| undefined` | Client default | `'draft'` for preview, omit for published | ## Assets [#assets] ### `assetLink(asset)` [#assetlinkasset] Generate a fully qualified URL for a content asset. ```typescript import { localessClient } from "@localess/client"; const client = localessClient({ origin, spaceId, token }); // From a ContentAsset object const url = client.assetLink(content.data.image); // From a URI string const url = client.assetLink('/spaces/abc/assets/photo.jpg'); ``` ## Visual Editor Integration [#visual-editor-integration] ### `loadLocalessSync(origin)` [#loadlocalesssyncorigin] Injects the Localess Visual Editor sync script into the document ``. This enables live-editing capabilities when your site is opened inside the Localess Visual Editor. No-op when not running inside an iframe. ```typescript import { loadLocalessSync } from "@localess/client"; loadLocalessSync('https://my-localess.web.app'); ``` ### `syncScriptUrl()` [#syncscripturl] Returns the URL of the Localess sync script, useful for manual script injection. ```typescript const scriptUrl = client.syncScriptUrl(); ``` ### Marking Editable Content [#marking-editable-content] Use these helpers to add Localess editable attributes to your HTML elements, enabling element selection and highlighting in the Visual Editor. #### `localessEditable(content)` [#localesseditablecontent] Marks a content block as editable. ```typescript import { localessEditable } from "@localess/client"; // Returns: { 'data-ll-id': '...', 'data-ll-schema': '...' }
...
``` #### `localessEditableField(fieldName)` [#localesseditablefieldtfieldname] Marks a specific field within a content block as editable, with type-safe field name inference. ```typescript import { localessEditableField } from "@localess/client"; // Returns: { 'data-ll-field': 'title' }

('title')}>...

``` ## Listening to Visual Editor Events [#listening-to-visual-editor-events] When your application is loaded inside the Localess Visual Editor, you can subscribe to editing events via `window.localess`. ```typescript if (window.localess) { // Subscribe to a single event window.localess.on('change', (event) => { if (event.type === 'change') { setPageData(event.data); } }); // Subscribe to multiple events window.localess.on(['input', 'change'], (event) => { if (event.type === 'input' || event.type === 'change') { setPageData(event.data); } }); } ``` ### Available Event Types [#available-event-types] | Event | Payload | Description | | ------------- | --------------------------------------------- | ----------------------------------------------- | | `input` | `{ type: 'input', data: any }` | Fired while a field is being edited (real-time) | | `change` | `{ type: 'change', data: any }` | Fired after a field value is confirmed | | `save` | `{ type: 'save' }` | Fired when content is saved | | `publish` | `{ type: 'publish' }` | Fired when content is published | | `unpublish` | `{ type: 'unpublish' }` | Fired when content is unpublished | | `pong` | `{ type: 'pong' }` | Heartbeat response from the editor | | `enterSchema` | `{ type: 'enterSchema', id, schema, field? }` | Fired when entering a schema element | | `hoverSchema` | `{ type: 'hoverSchema', id, schema, field? }` | Fired when hovering over a schema element | | `leaveSchema` | `{ type: 'leaveSchema' }` | Fired when leaving a schema element | ## Error Handling [#error-handling] `getLinks`, `getContentBySlug`, `getContentById`, and `getTranslations` throw instead of returning empty data on failure. Network failures reject with the underlying error; non-2xx HTTP responses reject with a `LocalessApiError` (`status`, `statusText`, `url`). Always wrap calls in `try`/`catch`: ```typescript import { LocalessApiError } from "@localess/client"; try { const content = await client.getContentBySlug('home'); } catch (error) { if (error instanceof LocalessApiError) { console.error(error.status, error.statusText); } } ``` ## Caching [#caching] All API responses are cached by default using a TTL (time-to-live) cache. Cache key = full request URL. You can configure caching when initializing the client. ```typescript // Default: 5-minute TTL cache const client = localessClient({ origin, spaceId, token }); // Custom TTL in seconds (e.g., 10 minutes) const client = localessClient({ origin, spaceId, token, cacheTTL: 600 }); // Disable caching entirely const client = localessClient({ origin, spaceId, token, cacheTTL: false }); ``` ## Type Reference [#type-reference] ### `Content` [#contentt] ```typescript interface Content extends ContentMetadata { data?: T; links?: Links; // Populated when resolveLink: true references?: References; // Populated when resolveReference: true } ``` ### `ContentMetadata` [#contentmetadata] ```typescript interface ContentMetadata { id: string; name: string; kind: 'FOLDER' | 'DOCUMENT'; slug: string; fullSlug: string; parentSlug: string; publishedAt?: string; createdAt: string; updatedAt: string; } ``` ### `ContentData` [#contentdata] Base type for all content schema data objects. ```typescript interface ContentDataSchema { _id: string; _schema: string; } interface ContentData extends ContentDataSchema { [field: string]: ContentDataField | undefined; } ``` ### `ContentAsset` [#contentasset] ```typescript interface ContentAsset { kind: 'ASSET'; uri: string; } ``` ### `ContentLink` [#contentlink] ```typescript interface ContentLink { kind: 'LINK'; type: 'url' | 'content'; target: '_blank' | '_self'; uri: string; } ``` ### `ContentReference` [#contentreference] ```typescript interface ContentReference { kind: 'REFERENCE'; uri: string; } ``` ### `ContentRichText` [#contentrichtext] ```typescript interface ContentRichText { type?: string; content?: ContentRichText[]; } ``` ### Other Types [#other-types] * `Links` — A key-value map of content IDs to `ContentMetadata` objects. * `References` — A key-value map of reference IDs to `Content` objects. * `Translations` — A key-value map of translation keys to translated string values. ## Utility Functions [#utility-functions] | Function | Returns | Description | | ------------- | --------- | ----------------------------------------------------------------- | | `isBrowser()` | `boolean` | Returns `true` if code is running in a browser environment | | `isServer()` | `boolean` | Returns `true` if code is running in a server/Node.js environment | | `isIframe()` | `boolean` | Returns `true` if the page is rendered inside an iframe | ## AI Coding Agents [#ai-coding-agents] This package ships a [`SKILL.md`](https://github.com/Lessify/localess-js/blob/main/packages/client/SKILL) file that provides AI coding agents (GitHub Copilot, Claude Code, Cursor, and others) with accurate, up-to-date APIs, patterns, and best practices. Reference it from your project's `AGENTS.md`: ``` ## Localess @node_modules/@localess/client/SKILL.md ``` ## License [#license] [MIT](https://github.com/Lessify/localess-js/blob/main/LICENSE) # Vue A dedicated `@localess/vue` package is on the roadmap. Until it ships, integrate Vue with Localess in two pieces: fetch content with the framework-agnostic JavaScript client, and wire up the Visual Editor with the browser sync script. ## 1. Fetch content [#1-fetch-content] Use [`@localess/client`](typescript) on the server (Nuxt server routes, `asyncData`, an API route, etc.) so your API token never reaches the browser. ```typescript // server/api/page.ts (Nuxt example) import { localessClient } from "@localess/client"; const client = localessClient({ origin: process.env.LOCALESS_ORIGIN!, spaceId: process.env.LOCALESS_SPACE_ID!, token: process.env.LOCALESS_TOKEN!, }); export default defineEventHandler(async (event) => { const { slug, locale } = getQuery(event); return client.getContentBySlug(String(slug), { locale: locale as string }); }); ``` In your component, fetch through that endpoint and render the data. Generate TypeScript types with the [CLI](../cli/types) for full type safety. ## 2. Enable the Visual Editor (optional) [#2-enable-the-visual-editor-optional] Inject the sync script from your Nuxt config or top-level layout. Replace `LOCALESS_ORIGIN` with your instance URL: ```typescript // nuxt.config.ts export default defineNuxtConfig({ app: { head: { script: [ { id: "localess-js-sync", type: "text/javascript", async: true, src: `${process.env.LOCALESS_ORIGIN}/scripts/sync-v1.js`, }, ], }, }, }); ``` ### Subscribing to edit events [#subscribing-to-edit-events] Subscribe to Visual Editor events and update reactive state. Mark editable elements with `localessEditable` and `localessEditableField` from `@localess/client`: ```vue ``` See [Visual Editor](../visual-editor) for the full event list and attribute reference. ## Roadmap [#roadmap] `@localess/vue` will ship with the same surface as [`@localess/react`](react) — `LocalessComponent`, `useLocaless` (composable), `localessEditable`, and a rich-text renderer. Track progress on [GitHub](https://github.com/Lessify/localess-js). # Docker Docker is the quickest way to run Localess on your machine for personal use or to preview features without setting up a Firebase project. > For a production deployment, use [Firebase](firebase) instead. ## Prerequisites [#prerequisites] * [Docker](https://docs.docker.com/get-docker/) v24 or later * [Docker Compose](https://docs.docker.com/compose/install/) v2.20 or later (bundled with Docker Desktop) ## Docker tags [#docker-tags] Localess publishes the following image tags on `ghcr.io/lessify/localess`: | Tag | Description | | ------------------------------- | --------------------------------------------- | | `edge` | Latest commit on the `main` branch | | `latest` | Latest stable release | | `{{major}}.{{minor}}.{{patch}}` | Specific patch release (e.g. `1.2.3`) | | `{{major}}.{{minor}}` | Latest patch for a minor version (e.g. `1.2`) | | `{{major}}` | Latest release for a major version (e.g. `1`) | ## Docker Compose [#docker-compose] Create a `docker-compose.yml` file with the following content: ```yaml services: localess: image: ghcr.io/lessify/localess:latest # replace with desired tag ports: - "4000:4000" # ui - "5001:5001" # functions - "8080:8080" # firestore - "9099:9099" # auth - "9199:9199" # storage - "5000:5000" # hosting volumes: - ./firebase-export:/app/firebase-export ``` The `firebase-export` volume is optional. Mount a local directory here to persist emulator data between container restarts, or to pre-load seed data exported from another Localess instance. ## Start Localess [#start-localess] ```bash docker compose up ``` Once the container is running, the following services are available: | Service | Port | | --------- | ---- | | UI | 4000 | | Functions | 5001 | | Firestore | 8080 | | Auth | 9099 | | Storage | 9199 | | Hosting | 5000 | ## First start [#first-start] On the first run you will need to create an admin account. Navigate to the setup page: ``` http://localhost:4000/auth/setup ``` Follow the on-screen instructions to create your first user and space. ## Stop Localess [#stop-localess] ```bash docker compose down ``` # Firebase Firebase is the recommended way to run Localess in production. It uses Google Cloud infrastructure — Firebase Hosting, Firestore, Cloud Functions, and Cloud Storage — deployed automatically via Cloud Build. > For local exploration or personal use without a Google Cloud account, see the [Docker](docker) or [Local](local) setup instead. ## Prerequisites [#prerequisites] Before you start: * A Google account with billing enabled. Cloud Functions requires the **Blaze (pay-as-you-go)** plan. * The [Google Cloud SDK (`gcloud`)](https://cloud.google.com/sdk/docs/install) installed and authenticated. * A fork of the [Localess repository](https://github.com/Lessify/localess) connected to your Cloud Build trigger (see [Cloud Build](#cloud-build)). ## Create Project [#create-project] Open the [Firebase console](https://console.firebase.google.com/) and create a new project. ## Create Web App connection [#create-web-app-connection] * In the [Firebase console](https://console.firebase.google.com/), open **Firebase Settings** * Go to **your Apps** section * Select the **Web App** icon * Fill in the **App nickname** field * Check **Also set up Firebase Hosting for this app** checkbox * Press **Register app** * The registered app values will be used automatically during build in Cloud Build * Press **Next** * Press **Next** again * Press **Continue to console** ## Authentication [#authentication] Localess uses Firebase Authentication to identify users and keep data secure. Enable **Identity Platform** in the **Settings** tab before configuring sign-in methods. ### Email/Password [#emailpassword] Login by Email and Password is enabled by default. ### Google Identity Provider [#google-identity-provider] * In the [Firebase console](https://console.firebase.google.com/), open the **Auth** section. * On the **Sign-in method** tab, enable the **Google** sign-in method. * Click **Save**. > To restrict access to a specific organisation domain, set the `_LOCALESS_AUTH_CUSTOM_DOMAIN` substitution variable in your Cloud Build trigger. ### Microsoft Identity Provider [#microsoft-identity-provider] * In the [Firebase console](https://console.firebase.google.com/), open the **Auth** section. * On the **Sign-in method** tab, enable the **Microsoft** provider. * Add the **Client ID** and **Client Secret** from the [Azure portal](https://portal.azure.com/): * Register a new app following the [Azure AD v2.0 quickstart](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-v2-register-an-app). * Add `*.firebaseapp.com` as a redirect URI for your Firebase project. * Click **Save**. ## Hosting [#hosting] In the [Firebase console](https://console.firebase.google.com/), open the **Hosting** tab and create a default hosting site. ## Firestore [#firestore] In the [Firebase console](https://console.firebase.google.com/), open the **Firestore** tab and create a default database. ## Service Account [#service-account] Create a service account named `build-deploy` to assign to Cloud Builder. Follow the [official guide](https://cloud.google.com/build/docs/deploying-builds/deploy-firebase#before_you_begin). The service account requires the following IAM roles: | Role | Purpose | | --------------------------- | --------------------------------- | | Cloud Build Service Account | Run builds | | Firebase Admin | Full access to Firebase products | | Service Account User | Act as the service account | | Storage Usage Admin | Manage storage service state | | Storage Object Admin | Full control over storage objects | ## Enable Cloud APIs [#enable-cloud-apis] Open Cloud Shell and run the following command. Some APIs take 5–10 minutes to become active after enabling, so run this ahead of your first deployment: ```bash gcloud services enable \ firebasestorage.googleapis.com \ firebaseextensions.googleapis.com \ cloudfunctions.googleapis.com \ cloudbuild.googleapis.com \ artifactregistry.googleapis.com \ run.googleapis.com \ eventarc.googleapis.com \ pubsub.googleapis.com \ storage.googleapis.com \ translate.googleapis.com \ cloudbilling.googleapis.com ``` The command enables the following APIs: | API | Service | | ----------------------------------- | -------------------------- | | `firebasestorage.googleapis.com` | Cloud Storage for Firebase | | `firebaseextensions.googleapis.com` | Firebase Extensions | | `cloudfunctions.googleapis.com` | Cloud Functions | | `cloudbuild.googleapis.com` | Cloud Build | | `artifactregistry.googleapis.com` | Artifact Registry | | `run.googleapis.com` | Cloud Run | | `eventarc.googleapis.com` | Eventarc | | `pubsub.googleapis.com` | Pub/Sub | | `storage.googleapis.com` | Cloud Storage | | `translate.googleapis.com` | Cloud Translation | | `cloudbilling.googleapis.com` | Cloud Billing | ## Configuration [#configuration] The `cloudbuild.yaml` at the root of the repository is pre-configured for Localess deployments. No manual edits are required. ## Cloud Build [#cloud-build] To automatically deploy on every push, create a Cloud Build trigger: * Open [Cloud Build Triggers](https://console.cloud.google.com/cloud-build/triggers) * Click **Create trigger** and configure: * **Name** — a name for your trigger * **Event** — Push to a branch * **Repository** — your fork of `https://github.com/Lessify/localess` * **Branch** — `main` * **Configuration type** — Cloud Build configuration file (YAML or JSON) * **Location** — Repository * Under **Substitution variables**, add: | Variable | Description | | ------------------------------ | -------------------------------------------------------------------------------------- | | `_LOCALESS_AUTH_CUSTOM_DOMAIN` | Restrict sign-in to this domain (e.g. `lessify.io`). Leave empty to allow all domains. | | `_LOCALESS_AUTH_PROVIDERS` | Comma-separated auth providers. Available values: `GOOGLE`, `MICROSOFT`. | | `_LOCALESS_LOGIN_MESSAGE` | Optional message displayed on the login screen. | * Click **Create**. ## First start [#first-start] After the first successful deployment, open the setup page to create your first user: * `https://.web.app/auth/setup` * `https://.firebaseapp.com/auth/setup` ## Troubleshooting [#troubleshooting] ### Cloud Function API not enabled [#cloud-function-api-not-enabled] ``` functions: missing required API cloudfunctions.googleapis.com. Enabling now... ``` Enable **Cloud Functions API** in Marketplace, then re-run the [Enable Cloud APIs](#enable-cloud-apis) command. ### Artifact Registry API not enabled [#artifact-registry-api-not-enabled] ``` artifactregistry: missing required API artifactregistry.googleapis.com. Enabling now... ``` Enable **Artifact Registry API** in Marketplace, then re-run the [Enable Cloud APIs](#enable-cloud-apis) command. ### Cloud Run API not enabled [#cloud-run-api-not-enabled] ``` functions: missing required API run.googleapis.com. Enabling now... ``` Enable **Cloud Run API** in Marketplace, then re-run the [Enable Cloud APIs](#enable-cloud-apis) command. ### Eventarc API not enabled [#eventarc-api-not-enabled] ``` functions: missing required API eventarc.googleapis.com. Enabling now... ``` Enable **Eventarc API** in Marketplace, then re-run the [Enable Cloud APIs](#enable-cloud-apis) command. ### Missing permissions [#missing-permissions] ``` Missing permissions required for functions deploy. You must have permission iam.serviceAccounts.ActAs on service account project-id@appspot.gserviceaccount.com. ``` Assign the **Service Account User** role to `project-id@appspot.gserviceaccount.com`. Changes may take a few minutes to propagate. ### IAM Roles verification failed [#iam-roles-verification-failed] ``` functions: Failed to verify the project has the correct IAM bindings for a successful deployment. ``` Run the `gcloud projects add-iam-policy-binding` commands shown in the error output. You can execute them directly in Cloud Shell. ### Function requires manual deletion [#function-requires-manual-deletion] ``` Error: The following functions are found in your project but do not exist in your local source code: importLocaleJson(us-central1) ``` Delete the orphaned function from one of: * **Google Cloud Console** → Cloud Functions * **Google Cloud Console** → Cloud Run * **Firebase Console** → Functions ### Cloud Translation API not enabled [#cloud-translation-api-not-enabled] ``` Cloud Translation API has not been used in project {projectId} before or it is disabled ``` Enable **Cloud Translation API** in Marketplace, then re-run the [Enable Cloud APIs](#enable-cloud-apis) command. # Local Run Running Localess locally is the fastest way to explore features, develop integrations, or contribute to the project — no cloud account required. > For a quick preview without cloning the repository, see the [Docker](/docs/setup/docker) setup instead. ## Prerequisites [#prerequisites] * [Node.js](https://nodejs.org) v20 or later * [npm](https://www.npmjs.com) v10 or later (bundled with Node.js) * [Java](https://www.java.com) 11 or later (required by Firebase Emulators) * [Firebase CLI](https://firebase.google.com/docs/cli) v13 or later Install the Firebase CLI if you haven't already: ```bash npm install -g firebase-tools ``` ## Clone the repository [#clone-the-repository] ```bash git clone https://github.com/Lessify/localess.git cd localess ``` ## Install dependencies [#install-dependencies] ```bash npm install ``` ## Start the emulators [#start-the-emulators] Localess uses Firebase Emulators to run all backend services locally: ```bash npm run emulators ``` This starts the following emulators: | Emulator | Port | | --------- | ---- | | Hosting | 5000 | | Auth | 9099 | | Functions | 5001 | | Firestore | 8080 | | Storage | 9199 | ## Start the development server [#start-the-development-server] In a separate terminal, start the Angular dev server: ```bash npm run start ``` Open [http://localhost:4200](http://localhost:4200) in your browser. ## First start [#first-start] On the first run you will need to create an admin account. Navigate to the setup page: ``` http://localhost:4200/auth/setup ``` Follow the on-screen instructions to create your first user and space. # Get Translations # Get Links # Get Content by Slug # Get Content By ID # Get Asset By ID