title |
---|
Vision |
If I would need to describe what this is about in one sentence, I would say - database for your content. But this doesn't really help to grasp the whole concept. Let's take a closer look.
Content layer exists in all static site generators (one way or another). Basically:
- there is folder with content (markdown, json, yaml, images etc)
- there is function to get list of all entries. Also it can sort, filter and paginate
- there is function to get one entry. It can parse content (frontmatter, markdown) and render (to html, for example)
- often there is caching layer and reactive interface
Let's see examples.
- folder with content: hardcoded to
content
- list of all entries:
.Site.Pages
- to sort:
.Site.Pages.ByTitle
- to paginate:
.Paginate collection pageNumber
- to filter:
.Site.RegularPages.ByTitle param value
- to filter:
.Resources.ByType value
,.Resources.GetMatch value
- to sort:
- one entry:
- data:
- html:
.Content
- git metadata:
.GitInfo
- frontmatter:
.Params.value
- html:
- folder with content: hardcoded to
src/content
- list of all entries:
await getCollection(collection);
- to filter:
await getCollection(collection, ({ data }) => {... });
- to sort:
.sort((a,b) => { ... })
(standard JS) - to paginate:
paginate(collection, { pageSize: 2 })
- to filter:
- one entry:
const entry = await getEntry(collection, slug);
- data:
const { Content, headings } = await entry.render();
- html:
<Content />
- frontmatter:
entry.data
- html:
But compared to Hugo we can as well specify schema for the content:
// 1. Import utilities from `astro:content`
import { z, defineCollection } from "astro:content";
// 2. Define a `type` and `schema` for each collection
const blogCollection = defineCollection({
type: "content", // v2.5.0 and later
schema: z.object({
title: z.string(),
tags: z.array(z.string()),
image: z.string().optional(),
}),
});
// 3. Export a single `collections` object to register your collection(s)
export const collections = {
// Equivalent to `src/content/**/*.{md,mdx}`
blog: blogCollection,
};
- folder with content: defined in
defineDocumentType
- list of all entries:
allItems
- to filter:
allItems.filter(x => {...})
(standard JS) - to sort:
allItems.sort((a, b) => {...})
(standard JS)
- to filter:
- one entry:
allItems.find
(standard JS, I guess)
And we can define schema:
import { defineDocumentType, makeSource } from "contentlayer/source-files";
export const Post = defineDocumentType(() => ({
name: "Post",
filePathPattern: `**/*.md`,
fields: {
title: { type: "string", required: true },
date: { type: "date", required: true },
},
computedFields: {
url: {
type: "string",
resolve: (post) => `/posts/${post._raw.flattenedPath}`,
},
},
}));
Articles (markdown files) plus hyperlinks ([some](/thing)
) form graph. Some solutions allows to treat content as graph:
- link resolution: wiki-links, portable markdown links
- backlinks
- visualize content as graph
- detect broken links
- detect broken links: remark-lint-no-dead-urls, mdv, markdown-link-check, remark-validate-links
- visualize content as graph: markdown-links, markmap.js, dundalek/markmap
- markdown links: obsidian-export
Content layer already exposes some basic query interface. But there are solutions which brings this idea further, they expose query interface as query language.
Most notable solutions in this area are: docsql, obsidian-dataview. They allow to use SQL-like language to query the content.
Other options would be to use some kind of faceted search interface. Or use graph-query language, like Cypher or Datalog.
Main disadvantage of all solutions mentioned above (maybe exept Contentlayer
) is that they are built-in into another applications and not reusable. I think it would be beneficial to implement core library which, later could be reused for:
- content layer for Astro (or Next.js, Nuxt etc.)
- Language Server (LSP)
- CLI to transform markdown files, for example, from Obsidian vault to Hugo format
- second-brain-note-taking app, like Obsidian or Foam