Skip to content

Entry pipeline

The order in which qino reads, validates, and shapes an entry, and where to transform data.

View Markdown

Every getter builds its entries through the same steps, in the same order. Each step only sees what the previous ones produced.

Steps

  1. Parse. qino reads the file. For Markdown, MDX, and .markdown, it parses the frontmatter with js-yaml. For .json, it parses the whole file. See Frontmatter.
  2. Validate. The schema validates the parsed data. Schema transforms run here. Reserved fields are rejected in both the input and the output. See Schemas.
  3. Add qino fields. qino adds markdown and raw to Markdown entries, _stats when the primitive sets markdownStats, and _meta to every entry. See MarkdownStats.
  4. Read images. Image fields of media are replaced by a QinoImage, with their dimensions and a blur placeholder. Reading fails when an image is missing, of another type, or over its maxSize. See Media.
  5. Resolve relations. Relation references are replaced by their target entries, up to the view's resolveRelations depth. See Relations.
  6. Augment. The view's augment adds derived fields.
  7. Filter. For collections, the view's filter drops entries. getEntry throws when its entry is dropped.
  8. Sort. For getEntries, the view's sort orders the result.

Steps 5 to 8 come from the selected view. See Views.

StepReceives
SchemaThe parsed data, with keys and values as written in the file
relations pathsThe schema output
media pathsThe schema output
titleFieldThe schema output
augmentThe schema output, qino fields, images, and resolved relations
filter and sortThe augmented entry

Some reads never run views:

  • getTree, getFlatTree, getAncestorNodes, getNextNode, and getPreviousNode stop after validation. They read titleField from the schema output.
  • qino check runs steps 1 to 4 on every file, then checks media fields and body images against their media rules.
  • Resolved relation targets go through steps 1 to 4, then resolve their own relations to the remaining depth. Their own views never run.

Transforming data

qino has no transform option. Reshape data in the schema instead: most Standard Schema validators accept a transform callback, and qino runs it during validation, in step 2. The example below turns a comma-separated tags string into an array.

Zod
schema: z
  .object({ title: z.string(), tags: z.string() })
  .transform((post) => ({
    ...post,
    tags: post.tags.split(",").map((tag) => tag.trim()),
  })),
Valibot
schema: v.pipe(
  v.object({ title: v.string(), tags: v.string() }),
  v.transform((post) => ({
    ...post,
    tags: post.tags.split(",").map((tag) => tag.trim()),
  })),
),
ArkType
schema: type({ title: "string", tags: "string" }).pipe((post) => ({
  ...post,
  tags: post.tags.split(",").map((tag) => tag.trim()),
})),

The schema validates the data as written, so validation errors name the fields in the file. Everything after it sees the transformed output, including relations and media paths, titleField, views, and the types from Infer.

A transform must:

  • be synchronous. A validator that returns a Promise is rejected.
  • return an object.
  • not add _meta, nor markdown, raw, or _stats in Markdown entries. qino adds those in step 3.

Transform or augment?

Both derive data. They run at different steps:

Schema transformView augment
RunsStep 2, once per fileStep 6, per view
SeesThe file's dataSchema output, qino fields, images, resolved relations
CanRename, remove, and reshape fieldsAdd fields only
AsyncNoYes
Applies toEvery view, relations, qino checkThe view that declares it

Use a transform to change the shape of your content. Use augment for values that need the body, a related entry, or async work, such as reading time or an author's name.

Example: camel case keys

qino returns keys as written in the source. To read published_at as publishedAt, rename the keys in a transform, for instance with camelcase-keys:

qino/collections/posts.ts
import camelcaseKeys from "camelcase-keys";

export const postCollection = qino.defineCollection({
  directory: "posts",
  extension: ".md",
  schema: z
    .object({ published_at: z.string(), main_author: z.string() })
    .transform((post) => camelcaseKeys(post)),
  relations: { mainAuthor: authorCollection },
});

Relation and media paths use the camelCased keys, since they point into the schema output. _meta, markdown, and raw are added after the transform and keep their names.