🚧 WIP 🚧
A tiny custom headless CMS that reads files scattered across any number of folders, resolves/"stitches" their links to each other, compiles them to html, then serves them using graphql for consuming however you like.
npm install https://github.com/alanscodelog/stitch-cms
import path from "path"
import { StitchServer, ImageHandler, MarkdownHandler } from "stitch-cms"
/**
* With a structure that looks like this:
* root:
* - client
* - public
* - data
* - ...markdown files
* - uploads
*/
const root = "..."
const blog = new StitchServer({
plugins: [
new MarkdownHandler({
globs: [`${path.resolve(root, "data")}/**/*.md`],
}),
new ImageHandler({
globs: [`${path.resolve(root, "data/uploads")}/**/*`],
outputPath: path.resolve(root, "client/public/resources"),
}),
],
})
// you can await the database initialization but it's not needed, the server will wait for it to finish before fulfilling requests
blog.init()
await blog.listen()
You can then query the server using graphql.
The schema is here, but you should use something like graphql-codegen to generate the types for using locally in your app automatically.
WIP Notes:
Well I wanted a static site generator that had the following:
Also it was a nice chance to abuse learn graphql.
First a dummy database is created and hooked up to the apollo server.
This dummy databases takes in plugins which define what globs to scan for and what files they have control over. Two plugins (one for images and one for markdown) are provided.
Two plugins should not handle globs that have interesecting files. There are guards against this.
Aditionally no files can have the same id. A file's id is used to uniquely identify it and also be able to globally link to it using wiki/obsidian like links.
Plugins will usually need to define four simple methods:
When the database loads it starts chokidar watching the file globs. There's some magic happening to keep track of which plugin has control over what file (two plugins cannot control the same file).
When a file is added, it's handler plugin is called to parse it.
After it's parsed we know it's id, permalink (the plugin is in charge of defining how it's defined) and other necessary properties to be able resolve links to it by other files. We also get a list of links it's searching for. The parser should return these as absolute links or "global" links (the internal part of a wiki/obsidian link). It should not return urls. The plugin should keep the parsed tree (or trees, it's up to the plugin completely) somewhere in the ContentEntry.file property where it can store whatever it wants.
The dummy database then takes care of searching other entries that can resolve the links and calls the plugin with the necessary info to resolve them. It also does the reverse for other entries which link to that entry, and takes care of updating the linkedBy property for all entries.
Afterwards it asks the plugin to compile the entry so that the final output html and metadata is available.
This makes it possible for plugins to resume compiling from the already parsed/transformed tree without reparsing each time. Aditionally there is a cache functionality they can use to at least skip the initial parse step. The compile step cannot be easily cached because of the possibly constantly changing links.
Optionally a plugin, like those that handle images, can choose not to compile or return anything, and instead move the images to the right location in the client. Plugins can also choose to parse not just one property but multiple properties if needed. For example, I make the markdown plugin also parse thumbnail captions.
For parsing, remark has been used with some custom plugins to:
srcset and sizes on any links to images it handles.The only downside about all this is apollo only works with fixed schemas. Allowing the configuration of these seems difficult. Extra properties must all be thrown in one object and left up to the client to parse. Also the client must parse the date property upon receiving.
Generated using TypeDoc