# Alinea CMS Docs ### Introduction (/docs) Alinea is an open source headless CMS written in Typescript. It stores content in flat files in your repository so they can be checked into Git. This means you can roll back to a previous version, compare changes, and track who made changes. Content is bundled with deploys so it can be retrieved without network roundtrips. Image: dashboard (/dashboard) ### Configuration (/docs/configuration) All configuration can be managed from the `cms.ts` file. This file is created in the project root (or your `src` folder if it exists) during `alinea init`. File: cms.ts ``` import {createCMS} from 'alinea/next' export const cms = createCMS({ baseUrl: { development: 'http://localhost:3000', production: 'https://alineacms.com' }, enableDrafts: true, preview: true, schema: {...}, workspaces: {...} }) ``` ### `baseUrl` The URL of the frontend where Alinea is used. ### `enableDrafts` Allows content editors to save and preview unpublished changes before publishing. ### `preview` Display an iframe with live previews on the side of the editor. ### `schema` Describe the structure of your content using a [collection of Types](/docs/configuration/schema). ### `workspaces` Content can be bundled in separate [Workspaces](/docs/configuration/workspaces). Defining at least one is required. ### `syncInterval` Optionally set the interval in seconds at which the frontend will poll for updates. ## Good to know ### Dealing with errors Your config file is read and executed during the `alinea dev` and `alinea build` [CLI](/docs/reference/cli) commands. If anything goes wrong, you might see an error such as: ```shellscript Error: Fail at file:///home/alineacms/alinea/node_modules/@alinea/generated/config.js?1706175675574:419:7 at ModuleJob.run (node:internal/modules/esm/module_job:194:25) ``` To debug these situations Alinea compiles your config file with an included source map. To enable node to read the source map and report correct positions you can enable the Node.js `--enable-source-maps` flag. You can add it to the scripts in package.json: ```tsx { "scripts": { "dev": "NODE_OPTIONS=--enable-source-maps alinea dev -- next dev", "build": "NODE_OPTIONS=--enable-source-maps alinea build -- next build" } } ``` Note (info): If you're developing on Windows you can use [cross-env](https://www.npmjs.com/package/cross-env) to achieve the same. The error will now point to the right file: ```shellscript Error: Fail at (/home/alinea/apps/dev/cms.ts:278:7) at ModuleJob.run (node:internal/modules/esm/module_job:194:25) ``` ### Fields (/docs/configuration/fields) Fields make data editable. Alinea ships with a lot of field types but can easily be expanded with [custom fields](/docs/configuration/fields/custom-fields). ``` import {Config, Field} from 'alinea' import {FieldOptions, ScalarField, WithoutLabel} from 'alinea/core' import {InputLabel, useField} from 'alinea/dashboard' interface RangeFieldOptions extends FieldOptions { min?: number max?: number } class RangeField extends ScalarField { } // The constructor function is used to create fields in our schema // later on. It is usually passed a label and options. export function range(label: string, options: WithoutLabel = {}): RangeField { return new RangeField({ options: {label, ...options}, view: RangeInput }) } interface RangeInputProps { field: RangeField } // To view our field we can create a React component. // This component can call the useInput hook to receive the // current value and a method to update it. function RangeInput({field}: RangeInputProps) { const {value, mutator, options} = useField(field) const {min = 0, max = 10} = options return ( mutator(Number(e.target.value))} /> ) } export default Config.type('Kitchen sink', { fields: { ...Field.tabs( Field.tab('Basic fields', { fields: { title: Field.text('Text field'), path: Field.path('Path field', { help: 'Creates a slug of the value of another field' }), richText: Field.richText('Rich text field'), select: Field.select('Select field', { options: { a: 'Option a', b: 'Option b' } }), number: Field.number('Number field', { minValue: 0, maxValue: 10 }), check: Field.check('Check field', {label: 'Check me please'}), date: Field.date('Date field'), code: Field.code('Code field') } }), Field.tab('Link fields', { fields: { externalLink: Field.url('External link'), entry: Field.entry('Internal link'), linkMultiple: Field.link.multiple('Mixed links, multiple'), image: Field.entry('Image link'), file: Field.entry('File link') } }), Field.tab('List fields', { fields: { list: Field.list('My list field', { schema: { Text: Config.type('Text', { fields: { title: Field.text('Item title'), text: Field.richText('Item body text') } }), Image: Config.type('Image', { fields: { image: Field.image('Image') } }) } }) } }), Field.tab('Inline fields', { fields: { street: Field.text('Street', {width: 0.6, inline: true, multiline: true}), streetNr: Field.text('Number', {width: 0.2, inline: true}), box: Field.text('Box', {width: 0.2, inline: true}), zip: Field.text('Zipcode', {width: 0.2, inline: true}), city: Field.text('City', {width: 0.4, inline: true}), country: Field.text('Country', { width: 0.4, inline: true }) } }), Field.tab('Custom fields', { fields: { range: range('Range field') } }) ) } }) ``` ## Configuration While every field will have unique properties, there are a few properties that are generally available. ### `initialValue` Prefill the fields value. ### `hidden` Hide this field in the dashboard but keep its value intact. ### `readOnly` Mark field data as read-only. ### `help` Display a help text next to the fields label. ### `inline` Show a minimal version of the field. In most cases this will mean the input label will be hidden, and the label will show up as a placeholder instead. ### `width` Setting a width value will scale the fields width down, use a number between 0 and 1. This allows you to compose the dashboard UI better based on the content of the fields. ### `shared` Fields can be persisted over all languages if your content is [localised](/docs/reference/internationalization) by setting the `shared` option to `true`. When the entry is published the field data is copied to other locales. This is currently only supported on the root level, not on nested fields. ``` import {Config, Field} from 'alinea' const Type = Config.type('Persist', { fields: { // Persist field data over all locales sharedField: Field.text('Shared text', {shared: true}) } }) ``` ### `required` The `required` option will make sure the field value is not empty when saving when set to `true`. ### `validate` The `validate` option can be used to validate the field value using a custom function. The function should return `true` if the value is valid, `false` if it is not valid and a string if it is not valid and a message should be shown to the user. ``` import {Config, Field} from 'alinea' Field.text('Hello field', { help: 'This field only accepts "hello" as a value', validate(value) { if (value !== 'hello') return 'Only "hello" is allowed!' } }) ``` ## Conditional configuration All field configuration can be adjusted based on the value of other fields. After defining fields in a [Type](/docs/configuration/schema/type) a tracker function can be set up. The tracker function takes a reference to a field and a subscription function. In the subscription function field values can be retrieved and new options returned. ### Example ``` import {Config, Field} from 'alinea' const Example = Config.type('Conditional example', { fields: { textField: Field.text('Text field'), readOnly: Field.check('Make read-only'), hidden: Field.check('Hide field') } }) Config.track.options(Example.textField, get => { const textField = get(Example.textField) const readOnly = get(Example.readOnly) const hidden = get(Example.hidden) return { readOnly, hidden, help: `Text has ${textField.length} characters` } }) ``` ``` import {Config, Field} from 'alinea' const Example = Config.type('Conditional example', { fields: { textField: Field.text('Text field'), readOnly: Field.check('Make read-only'), hidden: Field.check('Hide field') } }) Config.track.options(Example.textField, get => { const textField = get(Example.textField) const readOnly = get(Example.readOnly) const hidden = get(Example.hidden) return { readOnly, hidden, help: `Text has ${textField.length} characters` } }) export default Example ``` ### Check (/docs/configuration/fields/check) A check field is used to input boolean data. ``` import {Field} from 'alinea' Field.check('Checkbox without label') Field.check('Label ipsum dolor sit amet', { description: 'Checkbox with label & description' }) ``` ``` import {Config, Field} from 'alinea' export default Config.type('Check field', { fields: { check: Field.check('Checkbox without label'), checkDescription: Field.check('Label ipsum dolor sit amet', { description: 'Checkbox with label & description' }) } }) ``` ### Code (/docs/configuration/fields/code) A code field is used to input code. ``` import {Field} from 'alinea' Field.code('My code field', { help: 'Paste your code here' }) ``` ``` import {Config, Field} from 'alinea' export default Config.type('Code field', { fields: { code: Field.code('My code field', { help: 'Paste your code here' }) } }) ``` ### Custom fields (/docs/configuration/fields/custom-fields) It's possible to create custom fields. A field needs a constructor function that users call to create instances of it in their configuration. ## Range field example Let's create a custom field to demonstrate. File: fields/Range.ts ``` import {Field} from 'alinea' export type RangeField = Field.Create // The constructor function is used to create fields in our schema // later on. It is usually passed a label and options. export function range(label: string, options: Field.Options = {}): RangeField { return Field.create({ label, options, // Point this view: '@/fields/RangeField.view' }) } ``` File: fields/Range.view.tsx ``` import {InputLabel, useField} from 'alinea/dashboard' import {RangeField} from './Range' interface RangeViewProps { field: RangeField } // To view our field we can create a React component. // This component can call the useField hook to receive the // current value and a method to update it. export default function RangeView({field}: RangeViewProps) { const {value, mutator, options} = useField(field) const {min = 0, max = 10} = options return ( mutator(Number(e.target.value))} /> ) } ``` To use the field in your types later call the constructor function: ``` import {Config} from 'alinea' import {range} from './RangeField' Config.type('My type', { fields: { // ... myRangeField: range('A range field', { min: 0, max: 20 }) } }) ``` ``` import {Field, Config} from 'alinea' import {InputLabel, useField} from 'alinea/dashboard' export type RangeField = Field.Create // The constructor function is used to create fields in our schema // later on. It is usually passed a label and options. export function range(label: string, options: Field.Options = {}): RangeField { return Field.create({ label, options, // Point this view({field}) { const {value, mutator, options} = useField(field) const {min = 0, max = 10} = options return ( mutator(Number(e.target.value))} /> ) } }) } export default Config.type('Custom fields', { fields: { range: range('A range field', {min: 0, max: 20}) } }) ``` ### Date & time (/docs/configuration/fields/date) A date field is used to input a date. A time field is used to input a time. ``` import {Field} from 'alinea' Field.date('Date field') Field.time('Time field') ``` ``` import {Config, Field} from 'alinea' export default Config.type('Date/time field', { fields: { date: Field.date('Date field'), time: Field.time('Time field') } }) ``` ## Configuration ### `initialValue` Prefills the field’s value. For example, you can prefill it with today’s date. ``` const today = new Date().toISOString().split('T')[0]; Field.date('Date (initialValue: today)') ``` ``` import {Config, Field} from 'alinea' const today = new Date().toISOString().split('T')[0]; export default Config.type('Date (initialValue)', { fields: { date: Field.date('Date (initialValue: today)', {initialValue: today}), } }) ``` ### Entry (/docs/configuration/fields/entry) The entry field can be used to link to an internal page. ``` import {Field} from 'alinea' Field.entry('Single entry link') Field.entry.multiple('Multiple entry links') ``` ## Configuration ### `condition` Limit the pages shown in the explorer to this condition. Conditions can be built using fields in the same way as described in the [querying content](/docs/content/query#querying-specific-pages) chapter. ``` import {Field} from 'alinea' Field.entry('Link to author', { condition: { _type: 'Author' } }) Field.entry('Link to author or writer', { condition: { _type: { in: ['Author', 'Writer'] } } }) ``` ### `defaultView` Preset the UI to show rows or thumbnails (possible values: "row", "thumb") ### `inline` Show a minimal version of the field. The field label is hidden. ### `location` Defines the location (workspace and root) where the explorer will be located. ``` import {Field} from 'alinea' Field.entry('Link to author', { location: { root: 'pages', workspace: 'main' } }) ``` ### `pickChildren` Choose from a flat list of direct children of the currently edited entry. ``` import {Field} from 'alinea' Field.entry('Author of this book', { condition: { _type: 'Author' }, pickChildren: true }) ``` ### `max` Limit the amount of rows in case of multiple links. ``` import {Field} from 'alinea' Field.entry.multiple('Select up to 3 authors', { max: 3 }) ``` ### File (/docs/configuration/fields/file) The file field can be used to link to a file. ``` import {Field} from 'alinea' Field.file('Single file link') Field.file.multiple('Multiple file links') ``` ``` import {Config, Field} from 'alinea' export default Config.type('File field', { fields: { file: Field.file('Single file link'), fileMultiple: Field.file.multiple('Multiple file links') } }) ``` ## Configuration ### `max` Limit the amount of rows in case of multiple links. ### Image (/docs/configuration/fields/image) The image field can be used to select an image. ``` import {Field} from 'alinea' Field.image('Single image link') Field.image.multiple('Multiple image links') ``` ``` import {Config, Field} from 'alinea' export default Config.type('Image field', { fields: { image: Field.image('Single image link'), imageMultiple: Field.image.multiple('Multiple image links') } }) ``` ## Configuration ### `max` Limit the amount of rows in case of multiple links. ### `fields` Defines nested sub-fields for the original object, allowing you to attach additional structured data. ``` import {Config, Field} from 'alinea' Field.image('Image', { fields: {alt: Field.text('Alt text')} }) ``` ### Link (/docs/configuration/fields/link) The link field can be used to create one or multiple references to other entries or external resources (like a webpage or an email address). By default, the user can choose between internal pages, external urls or uploaded files. If you want to limit to selection to just one of those options it's possible to declare the field as either an [Entry field](/docs/configuration/fields/entry), [Url field](/docs/configuration/fields/url), [File field](/docs/configuration/fields/file), or [Image field](/docs/configuration/fields/image). ``` import {Field} from 'alinea' Field.link('Single link') Field.link.multiple('Multiple links') ``` ``` import {Config, Field} from 'alinea' export default Config.type('Link field', { fields: { link: Field.link('Single link'), linkMultiple: Field.link.multiple('Multiple links') } }) ``` ## Configuration ### `max` Limit the amount of rows in case of multiple links. ### `fields` Defines nested sub-fields for the original object, allowing you to attach additional structured data. ``` import {Config, Field} from 'alinea' Field.link('Link', { fields: {label: Field.text('Link label')} }) ``` ### List (/docs/configuration/fields/list) A list field contains blocks of fields. Every block is configured using a specific type. These can be created using the schema and type functions as seen before. ``` import {Config, Field} from 'alinea' Field.list('List', { schema: { Item: Config.type('Item', { fields: { title: Field.text('Title'), text: Field.richText('Text') } }) } }) Field.list('List mixed', { schema: { Text: Config.type('Text', { fields: { title: Field.text('Item title'), text: Field.richText('Item body text') } }), Image: Config.type('Image', { fields: { image: Field.image('Image') } }) } }) ``` ``` import {Config, Field} from 'alinea' export default Config.type("List field", { fields: { list: Field.list("List", { schema: { Item: Config.type("Item", { fields: { title: Field.text("Title"), text: Field.richText("Text"), } }) } }) listMixed: Field.list("List mixed", { schema: { Text: Config.type("Text", { fields: { title: Field.text("Title"), text: Field.richText("Text"), } }) Image: Config.type("Image", { fields: { image: Field.image("Image") } }) } }) } }) ``` ### Number (/docs/configuration/fields/number) A number field is used to input numeric data. ``` import {Field} from 'alinea' Field.number('My number field', { minValue: 0, maxValue: 10, step: 1 }) ``` ``` import {Field} from 'alinea' export default Field.number('My number field', { minValue: 0, maxValue: 10, step: 1 }) ``` ## Configuration ### `step` Specifies the interval between legal numbers in the input field. Default is 1. ``` import {Field} from 'alinea' Field.number('My decimal number field', { step: 0.01 }) ``` ``` import {Field} from 'alinea' export default Field.number('My decimal number field', { help: 'You can increase or decrease the value by 0.01', initialValue: 0.01, step: 0.01 }) ``` ### Object (/docs/configuration/fields/object) An object field groups multiple fields together. The fields are defined using the type function. ``` import {Config, Field} from 'alinea' Field.object('Address', { fields: { street: Field.text('Street'), zip: Field.text('Zip code', {width: 0.5}), city: Field.text('City', {width: 0.5}) }) }) ``` ``` import {Config, Field} from 'alinea' export default Config.type('Object field', { fields: { object: Field.object('Address', { fields: { street: Field.text('Street'), zip: Field.text('Zip code', {width: 0.5}), city: Field.text('City', {width: 0.5}) } }) } }) ``` ### Path (/docs/configuration/fields/path) A path field is used to generate a slug based on another field - by default, the title field. ``` import {Field} from 'alinea' Field.text('Title', {required: true, width: 0.5}), Field.path('Path', {required: true, width: 0.5}) ``` ``` import {Config, Field} from 'alinea' export default Config.type('Path field', { fields: { title: Field.text('Title', { initialValue: 'Contentpage' required: true, width: 0.5 }), path: Field.path('Path', { required: true, width: 0.5 }) } }) ``` ## Configuration ### `from` Automatically generate (slugify) the path from another field. ``` import {Config, Field} from 'alinea' export default Config.type('Path field (from)', { fields: { anchorId: Field.text('anchorId', { initialValue: 'Anchord ID', required: true, width: 0.5 }), pathFrom: Field.path('Path from anchorId)', { from: 'anchorId', required: true, width: 0.5 }) } }) ``` ``` import {Config, Field} from 'alinea' export default Config.type('Path field (from)', { fields: { anchorId: Field.text('anchorId', { initialValue: 'Anchord ID' required: true, width: 0.5, }), pathFrom: Field.path('Path from anchorId)', { from: 'anchorId', required: true, width: 0.5 }) } }) ``` ### `initialValue` Prefills the path value. This is useful for cases like setting the homepage path. ``` import {Config, Field} from 'alinea' export default Config.type('Path field (homepage)', { fields: { title: Field.text('Title', { initialValue: 'Homepage', required: true, width: 0.5 }), path: Field.path('Path', { initialValue: 'index', hidden: false, readOnly: true, required: true, width: 0.5 }) } }) ``` ``` import {Config, Field} from 'alinea' export default Config.type('Path field (homepage)', { fields: { title: Field.text('Title', { initialValue: 'Homepage', required: true, width: 0.5 }), path: Field.path('Path', { initialValue: 'index', hidden: false, readOnly: true, required: true, width: 0.5 }) } }) ``` ### Rich Text (/docs/configuration/fields/rich-text) Rich text can contain text marks like bold, italics or underline. Content can be structured using headings. It can even contain other types as blocks that can be moved around freely. ``` import {Field} from 'alinea' Field.richText('Rich Text') Field.richText('Extended with inline schema(s)', { schema: { ImageBlock } }) const ImageBlock = Config.type('Image', { fields: {image: Field.image('Image', {inline: true})} }) ``` ``` import {Config, Field} from 'alinea' const ImageBlock = Config.type('Image', { fields: {image: Field.image('Image', {inline: true})} }) export default Config.type('Rich Text field', { fields: { basic: Field.richText('Rich Text', { initialValue: [ {_type: 'heading', level: 1, content: [ {_type: 'text', text: "Hello world"} ]}, {_type: 'paragraph', content: [ {_type: 'text', text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit."} ]} ] }), richText: Field.richText('Extended with inline schema(s)', { schema: { ImageBlock }, initialValue: [ {_type: 'paragraph', content: [ {_type: 'text', text: "The “Insert block” option appears when you press Enter to create a new line."} ]} ] }) } }) ``` ## Configuration ### `schema` Allow Types of this Schema to be created between text fragments. ### `searchable` Index the content of this field so it can be found in a search query. ### `enableTables` Allow tables to be inserted in this field. ## Rendering rich text Rich text values are encoded in an array. Variant: JSON ```tsx [ { "_type": "heading", "level": 1, "content": [ { "type": "text", "text": "Hello world" } ] }, { "_type": "paragraph", "content": [ { "type": "text", "text": "A paragraph follows" } ] } ] ``` Variant: Types ```tsx type TextDoc = Array type TextNode = | { _type: 'text' text?: string marks?: Array<{ type: string attrs?: Record }> } | { _type: string content?: TextDoc [key: string]: any } ``` Alinea provides a React component to render this array in your app. By default it will use plain tags such as h1, h2, p, ul, li, etc. to represent the text. Any of these can be customized by either passing a React component or a vnode, of which we'll copy the type and props. ``` import {RichText} from 'alinea/ui' } // Use a custom component for h1 headings h1={MyH1Heading} // Use a custom component for links a={LinkComponent} // Attach classes to list items ul={