Markdown and assets
A picture in a Markdown document is written as the file's delivery path, /d/v1/<project>/assets/<id>, and the CMS records that path on save so the file cannot be deleted while the document uses it.
Why a picture is a path, not a URL
A Markdown body is a string, and it outlives the address it was written on. It is pulled into repositories, restored from backups into other instances, and served to sites on hosts the CMS has never heard of. If a picture in it were an absolute URL, moving the CMS would break the picture on the page and hide the use from the media library. So the dashboard writes a path with no host in front of it, and the site resolves that path against wherever it reads the CMS.
The API also reads those paths out of the body on every save. That gives a Markdown document the same protection a structured image field has: the file's Used by names the document, deleting the file is refused, and delivery lists the file in _meta.assets.
How a file is used, recorded and delivered
A file from the media library is used in one of two ways: through a file field in a structured document, or as a path inside a Markdown or plain-text string.
In a structured document, a file field (Image, Video or File) holds a pointer, { "$asset": "<id>" }, with optional alt, caption and decorative for this use alone. Delivery replaces the pointer with the file's details: its id, url, name, mime, size, width and height, the alt this use resolved to, caption, decorative, visibility and the available variants. It does this at every depth, whatever refs= says, and _meta.assets lists each pointer by its path, such as hero.image. A private file's url is null unless the delivery key holds assets:private.
In a Markdown or plain-text document, a file is a path inside the string. The API stores the body exactly as sent, and on every save it finds each reference to a file of this project: the path /d/v1/<project>/assets/<id>, at the start of the body or after a space, (, a quote, < or =, or the same path behind this deployment's own address, with or without a ?… query after it. For each file it finds:
- the file must exist in the project, or the save is refused with
No asset "<id>".; - the file must be public, or the save is refused with "Private files can't be used in Markdown; make it public or use a component field.";
- the use is recorded with the path
"", because the body is the field, so the file's references list the document and deleting the file answers 409in_use.
A path to another project, or a URL on another host, is left alone. It is a link, not a use of a file.
Delivery serves the Markdown unchanged. It does not rewrite the paths; it reports them. _meta.assets holds one entry per distinct file, { "path": "", "id": "<id>" }, with "missing": true for a file deleted since. It lists up to 500 files per answer, and past that _meta.assetsTruncated is true.
The SDK turns the paths into URLs when it renders. q.renderMarkdown(body) hands every such path, whether it sits in an image, a link or a link definition, to the client's asset resolver. With assets.proxyPublic set, a path becomes <assets.path>/<id> on your own site. Without it, the path is resolved against the CMS address the client reads from. A ?w= or ?f= query is kept, and an absolute URL is left exactly as written. Variables and Markdown covers the renderer's other options.
The dashboard writes the paths for you. The Markdown editor's Visual mode stores plain GitHub-Flavored Markdown, the same bytes the CLI pulls and the SDK renders. Its Image button opens the media library filtered to images, with an upload of its own, and writes  on a line of its own. Pasting or dropping a picture uploads it as public into the project's default folder, and the editor inserts it when the upload lands. No width is written into the body. The site decides how to scale.
How to
Dashboard
- Open a Markdown document in the Content tab. It opens in Visual unless it holds Markdown that the visual editor cannot represent.
- Put the cursor where the picture belongs and press Image in the toolbar. Choose a picture from the media library, or upload one. A private file is listed but cannot be chosen, and the reason appears under it.
- Select the picture and press Image description to set the text a screen reader says, and a title if you want one.
- Switch to Source to see what is stored:
. - Press Save. Open the file in Media: its sheet's Used by now names the document, and Delete is unavailable while it does.
API
Set the variables, with a CLI key that holds content:write and content:publish, and a delivery key for the read:
export QUILLET_API_URL=<cms-url> QUILLET_PROJECT=<project> QUILLET_ENV=<environment>
export QUILLET_MANAGEMENT_KEY=<keyId>:<secret>
export QUILLET_KEY_ID=<delivery-keyId> QUILLET_KEY_SECRET=<delivery-secret>
A="$QUILLET_API_URL/api/v1/projects/$QUILLET_PROJECT"
E="$A/envs/$QUILLET_ENV"Upload a picture. Files belong to the project, not to an environment, so the route has no envs/:
curl -s -F file=@<picture.png> -F 'alt=A workbench at first light' "$A/assets" -u "$QUILLET_MANAGEMENT_KEY"The answer starts with the file's id. Keep it, then write a Markdown document that shows the picture, and publish the document:
export ASSET=<asset-id>
curl -s -X PUT "$E/documents/docs/workshop" -u "$QUILLET_MANAGEMENT_KEY" -H 'Content-Type: application/json' \
-d "{\"type\":\"md\",\"title\":\"The workshop\",\"content\":\"# The workshop\\n\\n\\n\"}"
curl -s -X POST "$E/publish" -u "$QUILLET_MANAGEMENT_KEY" -H 'Content-Type: application/json' \
-d '{"documents":[{"key":"docs/workshop"}]}'Ask who uses the file, read the document as a site would, and try to delete the file:
curl -s "$A/assets/$ASSET/references" -u "$QUILLET_MANAGEMENT_KEY"
curl -s -u "$QUILLET_KEY_ID:$QUILLET_KEY_SECRET" "$QUILLET_API_URL/d/v1/$QUILLET_PROJECT/$QUILLET_ENV/content/docs/workshop"
curl -s -X DELETE "$A/assets/$ASSET" -u "$QUILLET_MANAGEMENT_KEY"{"usedBy":[{"env":"prod","key":"docs/workshop","locale":"en-US","path":""}]}
{"key":"docs/workshop","locale":"en-US","type":"md",
"content":"# The workshop\n\n\n",
"_meta":{…,"refs":[],"assets":[{"path":"","id":"…"}]}}
{"error":"in_use","message":"This file is used by documents; remove it from them first.",
"usedBy":[{"env":"prod","key":"docs/workshop","locale":"en-US"}]}CLI
With QUILLET_API_URL, QUILLET_PROJECT, QUILLET_ENV and QUILLET_MANAGEMENT_KEY set as in the API tab, upload the picture. The CLI prints the id it was given:
npx quillet assets upload <picture.png> --alt "A workbench at first light"Uploaded workbench.png as jxemid6axekvl4z23qugb3pwce (203 B).Write the Markdown with that id, save it and publish it:
export ASSET=<asset-id>
printf '# Opening hours\n\n\n' "$QUILLET_PROJECT" "$ASSET" > hours.md
npx quillet content put docs/hours --file hours.md --title "Opening hours"
npx quillet publish docs/hours --yes
npx quillet assets show "$ASSET"assets upload --folder files the picture in a media folder, named by id or by a path of folder names. In a synced repository, docs/hours.en-US.md holds this body byte for byte.
SDK
With the delivery key and the other settings in the environment, save this as render.mjs and run node render.mjs:
// render.mjs: render a published Markdown document, its picture paths turned into URLs.
import { quillet } from '@quilletcms/sdk/node'
const q = quillet()
const doc = await q.get('docs/workshop')
console.log(doc._meta.assets)
console.log(await q.renderMarkdown(doc.content))[ { path: '', id: '…' } ]
<h1>The workshop</h1>
<p><img src="https://cms.example.com/d/v1/handbook/assets/…" alt="A workbench at first light"></p>With assets.proxyPublic set in the SDK's configuration, the src is your own /media/<id> instead, and no CMS address appears in your HTML.
Limits
- Private files cannot be used in Markdown, because their URLs are signed and expire. For a private file, use an Image or File field in a structured document.
- A path inside a string field of a structured document, a Markdown field included, is not recorded as a use of the file. Give the picture a file field of its own.
- A Rich text field keeps no pictures:
imgis not among the tags it allows. - Markdown beyond GitHub-Flavored Markdown is out of scope. A document that uses raw HTML, footnotes, math or anything else the visual editor cannot represent opens in Source only, with a sentence naming what it found. It loses nothing.
- Collaborative editing and comments are not part of the Markdown editor.
- A
data:picture pasted into Visual is stored in the body, and the sanitiser drops it on the page. - Where
ASSET_PUBLIC_URLis set, a picture written by hand as a URL on the CMS's own API address is not recognised as a use of the file. The path the editor writes always is.