Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0bc6f8c
Implement basic display of personalization type for item
JiriLojda Jan 12, 2026
0197607
Show list of variants under the current variant display
JiriLojda Jan 12, 2026
fcc93e9
Use react-query for fetching
JiriLojda Jan 13, 2026
84873fd
Add support for creating new variants
JiriLojda Jan 13, 2026
fca4b93
Add support for deleting variants
JiriLojda Jan 13, 2026
a5edf33
Add sync script and update readme
JiriLojda Jan 14, 2026
7b6dfde
Enable deleting/creating variants when non-base variant is in the editor
JiriLojda Jan 16, 2026
03e3297
Assign the current item to newly created variant as well
JiriLojda Jan 16, 2026
1206775
Filter out current item's audience from available audiences to create
JiriLojda Jan 16, 2026
b59d186
Use zod to check function bodies and share types between FE and BE
JiriLojda Jan 16, 2026
6a48f31
Add example client to showcase FE implementation
JiriLojda Jan 20, 2026
14df295
Update dependencies
JiriLojda Jan 20, 2026
021384e
Autofix biome errors
JiriLojda Jan 20, 2026
623785c
Fix biome errors
JiriLojda Jan 20, 2026
7d1aecf
Update package name and remove unnecessary tsconfig option
JiriLojda Jan 22, 2026
7494b53
Clean up main README.md
JiriLojda Jan 22, 2026
d62cc6f
Clean up example-client README.md
JiriLojda Jan 22, 2026
4a626aa
Update ESLint configuration
JiriLojda Jan 22, 2026
b53a9dc
Refactor scripts to use arrays instead of Sets
JiriLojda Jan 22, 2026
f423662
Consolidate duplicate types and functions in hooks
JiriLojda Jan 22, 2026
9df0fc8
Refactor Netlify functions shared utilities
JiriLojda Jan 22, 2026
c5ebb28
Clean up Netlify function implementations
JiriLojda Jan 22, 2026
9cc732b
Clean up example-client source files and add model generator
JiriLojda Jan 22, 2026
3c00d06
Improve naming in useExistingVariants
JiriLojda Jan 22, 2026
65a3af7
Update Netlify deploy button with repository URL
JiriLojda Jan 23, 2026
ac935d7
Move deploy button to the top in readme
JiriLojda Jan 23, 2026
2adfa79
Fix biome errors
JiriLojda Jan 23, 2026
3793315
Disable noLeakedRender rule until biome bug is fixed
JiriLojda Jan 23, 2026
996db02
Use kontent.ai model generator in example client
JiriLojda Jan 24, 2026
696ece0
Simplify snippet guard in fetch-content-type
JiriLojda Jan 24, 2026
d7f714a
Remove unnecessary let in fetch-taxonomy
JiriLojda Jan 24, 2026
20345db
Fix biome errors
JiriLojda Jan 24, 2026
a6ac147
Install example client dependencies on CI as well
JiriLojda Jan 24, 2026
f045040
Await main in all scripts
JiriLojda Jan 26, 2026
2febe3a
Fix sync-content-model script
JiriLojda Jan 26, 2026
5309898
Refactor common element utils
JiriLojda Jan 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Kontent.ai Management API Key
# Required for Netlify functions to access the Management API
# Get this from Kontent.ai Project Settings > API Keys > Management API
KONTENT_MANAGEMENT_API_KEY=your-management-api-key-here

# Kontent.ai Environment ID (only needed for sync scripts)
# Get this from Kontent.ai Project Settings > General
KONTENT_ENVIRONMENT_ID=your-environment-id-here
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install example-client dependencies
run: pnpm install --frozen-lockfile
working-directory: example-client

- name: Run Biome checks
run: pnpm biome:ci

Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,10 @@ Desktop.ini
*.tmp
*.temp
.cache
deno.lock

# Netlify
.netlify

# Content model (customized from .example)
scripts/content-model/taxonomyGroups.json
202 changes: 128 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,129 +1,183 @@
# Kontent.ai Custom App Starter
# Personalization Custom App for Kontent.ai

A React + TypeScript starter template for building [Kontent.ai custom apps](https://kontent.ai/learn/docs/build-apps/custom-apps/overview). This template provides a quick setup with all the essentials to start developing your custom app.
A custom app for managing content personalization variants in Kontent.ai. This app allows content editors to create, view, and delete personalized content variants for different audiences directly within the item editor.

## Deploy to Netlify

## Getting Started
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/kontent-ai/personalization-custom-app-example)

### Installation
1. Click the deploy button or connect your repository to Netlify
2. Add the `KONTENT_MANAGEMENT_API_KEY` environment variable in Netlify settings
3. Deploy the site
4. Update the custom app URL in Kontent.ai to your Netlify URL


## Features

- **View Variants**: See all content variants (including base content) for the current item
- **Create Variants**: Create new personalized variants for different audiences
- **Delete Variants**: Remove variants
- **Audience Management**: Track which audiences already have variants
- **Cross-linking**: Automatically links all variants together for easy navigation

## Prerequisites

- Node.js 20+
- pnpm
- A Kontent.ai environment with Management API access

## Quick Start

### 1. Install Dependencies

```bash
pnpm i
pnpm install
```

### Development
### 2. Configure Environment Variables

Start the development server:
Copy the example environment file and add your credentials (see `.env.example` for details):

```bash
pnpm dev
cp .env.example .env
```

The app will be available at `https://localhost:5173`. The dev server uses a self-signed certificate for HTTPS, which is required for custom apps. Your browser will show a security warning on first access - this is expected for local development.
### 3. Configure Your Audiences

### Build
The app includes an example audiences taxonomy that you must customize for your needs.

Build for production:
1. Copy the example taxonomy file:

```bash
pnpm build
cp scripts/content-model/taxonomyGroups.json.example scripts/content-model/taxonomyGroups.json
```

2. Edit `scripts/content-model/taxonomyGroups.json` and customize the `personalization_audiences` taxonomy with your own audience terms:

```json
{
"name": "Personalization Audiences",
"codename": "personalization_audiences",
"terms": [
{ "name": "Your Audience 1", "codename": "your_audience_1", "terms": [] },
{ "name": "Your Audience 2", "codename": "your_audience_2", "terms": [] }
]
}
```

Preview the production build:
> **Important**: Keep the `variant_type` taxonomy unchanged - it's required by the app. Only customize the `personalization_audiences` section.

### 4. Sync Content Model

Run the sync command to create the required taxonomies and content type snippet in your Kontent.ai environment:

```bash
pnpm preview
pnpm sync
```

## Examples
This uses [@kontent-ai/data-ops](https://github.com/kontent-ai/data-ops) to sync only the personalization-related content model entities.

### 5. Add Snippet to Content Type

### Observing Context Changes
1. Go to your Kontent.ai environment
2. Navigate to **Content model** > **Content types**
3. Edit the content type you want to enable personalization for
4. Add a **Snippet** element > Select **Personalization** snippet
5. Save the content type

The `useAppContext` hook automatically subscribes to context changes:
### 6. Configure Custom App in Kontent.ai

```typescript
import { useAppContext } from './contexts/AppContext';
1. Go to **Environment settings** > **Custom apps**
2. Click **Create new**
3. Configure the app:
- **Name**: Personalization
- **Source URL**: Your deployed app URL (or `https://localhost:8888` for development)
- **Display mode**: Dialog
4. Save the configuration

const context = useAppContext();
### 7. Start Development Server

```bash
pnpm dev
```

### Accessing App Configuration
The app will be available at `https://localhost:8888`.

The `useAppConfig` hook returns the parsed app configuration:
## Content Model Structure

```typescript
import { useAppConfig } from './contexts/AppContext';
### Taxonomies

const config = useAppConfig();
```
The app requires two taxonomies:

### Single Context Fetch
#### Variant Type (`variant_type`)

While the SDK provides `getCustomAppContext()` for fetching the context once without subscribing to changes, **we recommend using the reactive `useCustomAppContext` hook instead**. The hook ensures your app stays up-to-date with the latest context automatically.
Identifies whether an item is base content or a personalized variant. **Do not modify this taxonomy.**

If you need a single fetch for specific use cases:
| Term | Codename |
|------|----------|
| Base Content | `base_content` |
| Variant | `variant` |

```typescript
import { getCustomAppContext } from '@kontent-ai/custom-app-sdk';
#### Personalization Audiences (`personalization_audiences`)

const response = await getCustomAppContext();
if (!response.isError) {
console.log(response.context);
}
```
User-defined audiences for targeting. **Customize this taxonomy with your own audiences.**

### Restricting Supported Contexts
### Content Type Snippet

By default, the app supports all page contexts (Item Editor, Content Inventory, and Other). To restrict your app to specific contexts, edit the `createAppContext` call in [`src/contexts/AppContext.tsx`](./src/contexts/AppContext.tsx):
The **Personalization** snippet adds these elements to your content type:

```typescript
// Only allow Item Editor context
export const { AppContextProvider, useAppContext, useAppConfig } = createAppContext([
"itemEditor",
] as const);
| Element | Type | Purpose |
|---------|------|---------|
| Variant Type | Taxonomy | Identifies base content vs variant |
| Personalization Audience | Taxonomy | Target audience for the variant |
| Content Variants | Linked Items | Links to all variants (managed by the app) |

// Allow Item Editor and Content Inventory
export const { AppContextProvider, useAppContext, useAppConfig } = createAppContext([
"itemEditor",
"contentInventory",
] as const);
```
## How It Works

When a restricted context is configured:
- If the app is opened in an unsupported context, a friendly error page is displayed
- TypeScript narrows the return type of `useAppContext()` based on the allowed contexts
### Creating a Variant

For example, with `["itemEditor"]`, the `useAppContext()` hook returns `CustomAppItemEditorContext` with guaranteed access to `contentItemId` and `validationErrors` properties.
1. Open a base content item in the item editor
2. The Personalization app shows in the sidebar
3. Select an audience from the dropdown
4. Click **Create** to create a new variant

### Adjusting Popup Size
The app will:
- Create a new content item named `{Original Name} ({Audience})`
- Copy all content from the base item
- Set the variant type to "Variant"
- Set the personalization audience
- Link the new variant in all related items' Content Variants field

Control the size of your custom app when displayed in a popup:
### Viewing Variants

```typescript
import { setPopupSize } from '@kontent-ai/custom-app-sdk';
When viewing any item with the personalization snippet:
- The app shows whether it's base content or a variant
- Lists all other variants with their audiences
- Provides links to open each variant in the editor

await setPopupSize(
{ unit: 'px', value: 800 }, // width
{ unit: 'px', value: 600 } // height
);
```
### Deleting a Variant

Only available when viewing base content:
1. Click the trash icon on a variant card
2. Confirm the deletion in the modal
3. The variant is unlinked from all items and deleted

## Updating Audiences

To add, remove, or modify your personalization audiences:

## Deploying Your Custom App
1. Edit `scripts/content-model/taxonomyGroups.json`
2. Run `pnpm sync` to update the taxonomy in Kontent.ai

[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/kontent-ai/custom-app-starter-react.git)
Note: Removing audience terms that are in use by existing variants may cause issues. Update your content items first.

1. Build the app: `pnpm build`
2. Deploy the `dist` folder to your hosting provider (Netlify, Vercel, etc.)
3. Configure the custom app in Kontent.ai:
- Go to Environment settings > Custom apps
- Add a new custom app with your deployed URL
- Configure the URL pattern where the app should appear
- Optionally, change the app to the dialog mode
## Example Client

## Learn More
See the [example-client](./example-client/README.md) directory for a React application demonstrating how to consume personalized content on the frontend. The example shows:

- [Kontent.ai Custom Apps Documentation](https://kontent.ai/learn/docs/build-apps/custom-apps/overview)
- [Kontent.ai Custom App SDK](https://github.com/kontent-ai/custom-app-sdk-js)
- How to fetch base content with linked variants
- Resolving the correct variant based on user audience
- Fallback behavior when no matching variant exists

## License

Expand Down
11 changes: 0 additions & 11 deletions biome.json

This file was deleted.

18 changes: 18 additions & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["@kontent-ai/biome-config/base", "@kontent-ai/biome-config/react"],
"files": {
"includes": [
"src/**/*.{ts,tsx}",
"!example-client/src/types/generated/**/*.ts" // Ignore generated types until the model generator is updated to use biome
]
},
"linter": {
"rules": {
"nursery": {
// TODO: Remove once https://github.com/biomejs/biome/issues/8664 is fixed
"noLeakedRender": "off"
}
}
}
}
30 changes: 28 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import kontentAiConfig from "@kontent-ai/eslint-config";
import kontentAiReactConfig from "@kontent-ai/eslint-config/react";
import { defineConfig } from "eslint/config";

export default defineConfig([
{
files: ["**/*.{ts,tsx}"],
ignores: ["dist", "eslint.config.js", "vite.config.ts", "src/vite-env.d.ts"],
files: ["src/**/*.{ts,tsx}"],
Comment thread
IvanKiral marked this conversation as resolved.
ignores: ["dist", "src/vite-env.d.ts"],
extends: [kontentAiReactConfig],
languageOptions: {
parserOptions: {
Expand All @@ -16,4 +17,29 @@ export default defineConfig([
"@typescript-eslint/strict-boolean-expressions": "off",
},
},
{
files: ["netlify/functions/**/*.ts"],
extends: [kontentAiConfig],
languageOptions: {
parserOptions: {
project: "./netlify/functions/tsconfig.json",
},
},
rules: {
"@typescript-eslint/strict-boolean-expressions": "off",
},
},
{
files: ["example-client/src/**/*.{ts,tsx}"],
extends: [kontentAiReactConfig],
languageOptions: {
parserOptions: {
project: "./example-client/tsconfig.json",
},
},
rules: {
"react/jsx-max-props-per-line": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
},
},
]);
10 changes: 10 additions & 0 deletions example-client/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Kontent.ai Configuration

# Environment ID (required for both frontend and sync scripts)
VITE_KONTENT_ENVIRONMENT_ID=your_environment_id_here

# Preview API Key (required for frontend to fetch draft content)
VITE_KONTENT_PREVIEW_API_KEY=your_preview_api_key_here

# Management API Key (required for sync scripts only, not exposed to browser)
KONTENT_MANAGEMENT_API_KEY=your_management_api_key_here
Loading
Loading