Editing content
Your cms instance can create, update, publish and delete entries from code, for example to import content from another system or to seed a new project. Changes go through your handler, like saves in the dashboard: during development they are written to your content files.
Run a script
Scripts that edit content need a running dashboard to send their changes to. Run them as the command of alinea dev, which starts the dashboard, runs your script with the right environment and stops when the script exits:
npx alinea dev -- npx tsx scripts/import.tsimport {cms} from '../cms'
import {BlogPost} from '../schema/BlogPost'
const post = await cms.create({
type: BlogPost,
set: {title: 'Hello world', intro: 'My first post'}
})
console.log(`Created ${post._url}`)tsx runs the TypeScript file and resolves the path aliases of your tsconfig.json. With Bun, run bun alinea dev -- bun scripts/import.ts. The script writes to your content files like an editor would: review the result with git diff and commit it.
Create entries
cms.create creates an entry, saves it and returns it with all its fields:
const post = await cms.create({
type: BlogPost,
root: 'pages',
locale: 'en',
set: {title: 'Hello world', intro: 'My first post'}
})type: the type of the new entry. Required.set: the field values.titleis required,pathis derived from it when you leave it out and gets a numeric suffix when a sibling already uses it.workspace,root: where to create the entry, defaults to the first workspace and its first root.parentId: create the entry as a child of another entry. Passroot(andworkspace) as well when the parent is not in the default root: they are not taken from the parent.locale: required in translated roots, one of the root's locales. Creating an entry with an existingidand a new locale adds a translation.status:'published'(default),'draft'or'archived'.insertOrder:'last'(default) or'first'among its siblings.id: use your own id instead of a generated one.
Update entries
cms.update changes fields of an existing entry and returns the updated entry. Only the fields in set change, each one is replaced as a whole:
await cms.update({
type: BlogPost,
id: post._id,
locale: 'en',
set: {intro: 'An updated intro'}
})update edits the published version unless you pass status: 'draft' or 'archived'. In a translated root, pass the locale of the translation to edit: it defaults to null, the locale of untranslated entries.
Create translations
In a translated root, all translations of an entry share its id. To translate an entry, call cms.create with the id of the existing entry and the new locale:
const post = await cms.create({
type: BlogPost,
root: 'pages',
locale: 'en',
set: {title: 'Hello world', intro: 'My first post'}
})
await cms.create({
type: BlogPost,
id: post._id,
locale: 'fr',
set: {title: 'Bonjour le monde', intro: 'Mon premier article'}
})typeandsetare required as for any new entry, including atitle. The path is derived from the translated title unless you set one.The translation is created in the workspace and root of the entry's other locales, so you can leave out
workspaceandroot.Pass the
parentIdof a child entry as well. Its parent needs a translation in that locale first.When the translation is published, fields with
shared: truethat you leave out ofsetare copied from a published translation.A locale that already has a translation can't be created again: change it with
cms.updateand itslocaleinstead.
Commit several changes at once
The Edit namespace builds operations without running them. Pass any number of them to cms.commit to save them together, in one commit. An operation knows the id of its entry before it is committed, so you can link or nest entries you create in the same commit:
import {Edit} from 'alinea'
const blog = Edit.create({
type: Blog,
root: 'pages',
set: {title: 'Blog'}
})
const posts = postData.map(data =>
Edit.create({
type: BlogPost,
root: 'pages',
parentId: blog.id,
set: {title: data.title}
})
)
await cms.commit(blog, ...posts)The operations:
Edit.create(query)andEdit.update(query): take the same options ascms.createandcms.update.Edit.publish({id, status, locale}): publish the'draft'or'archived'version of an entry.Edit.archive({id, locale}): archive the published version.Edit.move({id, target, dropPosition}): move an entry'before'or'after'a sibling, or'on'another entry to make it a child. AddtargetType: 'root'to move it to the top level of a root.Edit.remove(...ids): delete entries with all their translations and children. Seeded entries can't be removed.Edit.upload(query): upload a file, see below.
cms has a method for each of them that commits right away: cms.publish, cms.archive, cms.move, cms.remove, cms.upload, plus cms.unpublish and cms.discard (remove the draft or archived version of an entry).
Build field values
Most fields store plain values. Rich text, lists and links store structured JSON with ids and ordering keys. The Edit helpers build those values for you:
import {Config, Edit, Field} from 'alinea'
const body = Field.richText('Body')
const sections = Field.list('Sections', {
schema: {
Text: Config.type('Text', {
fields: {
title: Field.text('Title'),
text: body
}
})
}
})
const text = Edit.richText(body)
.addHtml('<h2>Main heading</h2><p>Parsed from <strong>HTML</strong>.</p>')
.value()
const rows = Edit.list(sections)
.add('Text', {title: 'The row title', text})
.value()
const author = Edit.link(BlogPost.author).addEntry(authorId).value()Edit.richText(field):add(type, block)appends a rich text block,addHtml(html)parses HTML into rich text,value()returns the result.Edit.list(field):add(type, row)appends a row,insertAt(index, type, row)andremoveAt(index)edit an existing list passed as second argument.Edit.link(field):addEntry(id),addImage(id),addFile(id)oraddUrl({url, title, target}).Edit.links(field)does the same for fields with multiple links.
Upload files
cms.upload uploads a file to the media library and returns the new media file entry. Use the id to link the file from an image or file field:
import {Edit} from 'alinea'
import {createPreview} from 'alinea/core/media/CreatePreview'
import {readFile} from 'node:fs/promises'
const file = new File([await readFile('./photo.jpg')], 'photo.jpg', {
type: 'image/jpeg'
})
const image = await cms.upload({file, createPreview})
await cms.update({
type: Page,
id: pageId,
set: {hero: Edit.link(Page.hero).addImage(image._id).value()}
})file: aFile, or a[name, bytes]tuple.workspace,root,parentId: where to store it, defaults to the media root of the first workspace. Pass the id of a media folder asparentIdto upload into it.createPreview: pass it for images to store their dimensions, average color, focus point and a small preview. It uses thesharppackage on the server, which you need to install yourself.replaceId: replace the file of an existing media entry.
Uploads count against the maxUploadSize of your config.
Good to know
Every commit is checked against the roles of the user who makes it, see Roles and permissions. In development that is the local admin.
Validation from your schema (
required,validate) also runs when you publish from code:cms.createandcms.updatethrow when a published version has invalid fields. Drafts (status: 'draft') are not validated.To normalize content files after bulk edits, run
alinea build --fix.