~kingcons/collards

Another static site generator
Add a note about chroma as a syntax highlighting option.
Add a `help` subcommand.
Add default render methods adapted from personal site.

clone

read-only
https://git.sr.ht/~kingcons/collards
read/write
git@git.sr.ht:~kingcons/collards

You can also use your local clone with git send-email.

#Collards Manual

#Table of Contents

#1 Introduction

A static site generator for those who love Markdown and parens.

A long time ago, I wrote a blog engine called Coleslaw. It was written on a lark as I and two friends all resolved to get off Wordpress. We scheduled a lunch and learn for our unwritten blog engines a week later. The initial implementation was thrown together in a weekend. For many years, it was enough.

Collards is a simple static site generator. It offers:

  • Markdown blog posts with 3bmd

  • Custom pages built with Spinneret

  • Local preview with hot reloading via Hunchentoot

  • Deployment via Rsync with support for subdomains

  • CLI commands via Clingon to build, serve, and deploy

  • Optional RSS feeds, tag indexes, and year indexes

  • A straightforward API for custom content types

The official repository and CI builds are available.

#1.2 The collards ASDF System

#2 Making your first site

#2.1 Installation

While Collards should be portable to any OS, it is only tested on Linux. Notably, the serve command may not work on non-linux OSes.

The only way to install Collards today is building it yourself. Cloning the repository and running make app should be enough. After that, place bin/collards anywhere on your $PATH.

While the Makefile builds with SBCL by default, passing LISP=ccl as an argument to make app should be sufficient to build with Clozure CL. Other lisps have not been tested.

For users of Guix, a package is provided in the repo so running guix shell will get you a working environment.

At some point, collards may become available via binaries or Quicklisp.

#2.2 Creating a collards site

Once collards is installed, building a site involves creating a folder (or git repo if preferred), adding a .collards file, and then adding markdown files to generate posts, lisp files to generate pages, and some special files to generate RSS feeds or index pages.

Once the config file is written, the collards command can be run anywhere within that site folder. Your config might be as simple as:

(:author "Melissa Rogers"
 :domain "melissa.website"
 :ignored ("drafts")
 :title "Adventures in Code")

The config file itself strives to be small, requiring only values for an Author, Domain, and Title. You can also provide a list of Ignored subfolders. If you wish to use the deploy command to rsync your build output to a server, then you must also supply Server settings. Subdomains can optionally be specified to send specific directories output to different paths on the server.

An example config file can be viewed here. The other files in that example directory in the collards repo are used as data for the test suite. For a look at a "production" site that may be more realistic, the repo for my homepage may be of interest.

#2.3 Adding Content

When collards builds a site, it will process all files that it recognizes and copy any other files to an output directory. Then, that output directory can be freely copied to a public webserver either with the deploy command built into collards or manually. The files in the output directory will be organized the same way they were in the site folder. If all your markdown post files are in a blog folder, then the HTML files will go in a blog output subfolder.

Collards recognizes files by extension (see: Content Types) and currently knows about .lisp files which are treated as pages, .md files which are treated as posts, and some custom files like rss.feed which generates RSS feeds and tag.collection or recent.collection which generate indexes of POSTs with specific tags or in reverse chronological order.

As mentioned above, the examples folder in the project repo or my personal site may be useful to see examples of a live site.

#2.4 Templates

Collards uses Spinneret for HTML generation. No templates are provided by default but this may change in a future version and you are welcome to use my templates as a starting point.

The intent of this is merely to encourage people to design sites according to their own style and avoid baking in many assumptions. Defining a template is a matter of defining a COLLARDS.CONTENT:RENDER method that accepts a content object and a site object as arguments and generates HTML with spinneret.

Collards will load all lisp files from an init directory in the site folder before building so template definition and any desired custom behavior can be done there.

A simple post template with next/prev navigation might be:

;; Note: I recommend using package nicknames for convenience.
;; I.e. To allow for post:body instead of collards.post:body, etc.
(defmethod content:render ((content post:post)
                           (site site:site))
  (spinneret:with-html
    (:doctype)
    (:html
      (:head
        (:title "foo")
        (:link :href css-path :rel "stylesheet" :type "text/css"))
      (:body
        (:div.navigation
         (:p "Nav goes here"))
        (:section
          (:div.article-content
           (:raw (post:render-markdown (post:body content))))
          (:div.neighbors
           (util:when-let (prev (registry:prev content))
             (:a :href (content:url-for prev) (content:title prev)))
           (util:when-let (next (registry:next content))
             (:a :href (content:url-for next) (content:title next)))))
        (:div.fineprint
         (:hr)
         "Unless otherwise credited all material CC-BY-SA"
         (site:author site)))))

#3 Content Formats

As mentioned in Adding Content, Collards associates the extension of a file with a particular parser. Markdown is particularly noteworthy both because our parser provides some useful extensions and because it can be used to create Pages as well as Posts based on the frontmatter. A good example of how to write pages using Spinneret will be forthcoming in a future release.

#3.1 Markdown

Since Collards uses 3bmd for markdown parsing, their docs will be the most authoritative source of info on our markdown support. That said, the behavior of frontmatter in .md files is unique to collards and it is valuable to have a brief overview of the useful extensions from 3bmd we have enabled.

#3.1.1 Frontmatter

It is critical to supply metadata for content generated from markdown. All markdown files intended for processing by Collards are expected to have an initial frontmatter section written in YAML, which you may be familiar with from other blogware like Jekyll.

#Example Post

Frontmatter is separated from the main content by ---. For example:

---
title: Writing a Static Site Generator
tags: lisp, software-dev
date: 2025-02-03 08:00:00
draft: true
summary: Some experiences writing a static site generator for personal use.
---
## A Sensible Build System

In many ways, static site generators can be viewed as build tools to
turn arbitrary content formats into HTML...

This example demonstrates all the available metadata for our post format. Title is the only required field but comma-separated tags, date, draft, and summary are all supported. Collards does not handle actual Date parsing yet so a date format beginning with YYYY-MM-DD is recommended for now.

#Example Page

The only important attribute not demonstrated in the previous example is the type attribute. type is an escape hatch to allow using markdown files for generating Pages though nothing prevents you from using it to generate new kinds of content defined in your config, like a remark.js powered slideshow. My personal config actually does just that.

Much more common though is the desire to generate a Page from markdown which is useful since they don't participate in tag, year, or recent collections.

---
title: About Me
type: page
---
## Hobbies and Projects
...

#3.1.2 Markdown Extensions

Two 3bmd extensions are used: Code blocks and Wikilinks. Code blocks are highlighted by Colorize but Pygments or Chroma can be used instead by throwing one of the following snippets in your init files: (setf 3bmd-code-blocks:*renderer* :pygments) or (setf 3bmd-code-blocks:*renderer* :chroma)

For further details, consult the 3bmd documentation.

Wikilinks will come in handy when you want to link between pieces of content within a collards site. Using a Content ID between double brackets will look up the specified content in the site registry and link to it. For example: [[page:about]] or [[post:2024-reflections]].

#4 CLI Commands

Answering the siren song of my own laziness, rather than laboriously writing docs about the CLI commands, I have repurposed the usage docs generated by Clingon. If anything is unclear, feel free to ask me questions via email or mastodon. I may be slow to respond.

#4.1 The Collards command

  • [function] COLLARDS::COLLARDS/COMMAND

    NAME:
      collards - A static site generator
    
    USAGE:
      collards [global-options] [<command>] [command-options] [arguments ...]
    
    OPTIONS:
          --help     display usage information and exit
          --version  display version and exit
    
    COMMANDS:
      build   Build a collards site.
      serve   Live reload changed site files and serve them locally.
      deploy  Sync build output to the server specified in .collards.
      help    Display help for collards.
    
    AUTHORS:
      Brit Butler
    
    LICENSE:
      MIT
    
    

#4.2 The Build subcommand

  • [function] BUILD/COMMAND

    NAME:
      build - Build a collards site.
    
    USAGE:
      build [options] [arguments ...]
    
    OPTIONS:
          --help          display usage information and exit
          --version       display version and exit
      -s, --site <VALUE>  Path to the site to build [default:
                          /home/cons/projects/lisp/collards/]
      -t, --timing        Print the duration of each build step in milliseconds
    
    

#4.3 The Serve subcommand

  • [function] SERVE/COMMAND

    NAME:
      serve - Live reload changed site files and serve them locally.
    
    USAGE:
      serve [options] [arguments ...]
    
    OPTIONS:
          --help          display usage information and exit
          --version       display version and exit
      -p, --port <INT>    The localhost port to use for site hosting. [default: 4242]
      -s, --site <VALUE>  The path to the site to serve. [default:
                          /home/cons/projects/lisp/collards/]
      -w, --swank-server  Start a swank server to connect a REPL/editor to.
    
    

#4.4 The Deploy subcommand

  • [function] DEPLOY/COMMAND

    NAME:
      deploy - Sync build output to the server specified in .collards.
    
    USAGE:
      deploy [options] [arguments ...]
    
    OPTIONS:
          --help          display usage information and exit
          --version       display version and exit
      -n, --no-build      Skip building the site.
      -s, --site <VALUE>  The path of the site to build and deploy. [default:
                          /home/cons/projects/lisp/collards/]
      -y, --assume-yes    Rather than prompting, run all deploy commands immediately.
    
    

#4.5 The Help subcommand

Help takes a single topic as an argument and displays the documentation for that topic. If no topic is supplied, the list of available topics is shown.

  • [function] HELP/COMMAND

    NAME:
      help - Display help for collards.
    
    USAGE:
      help [options] [arguments ...]
    
    OPTIONS:
          --help     display usage information and exit
          --version  display version and exit
    
    

#5 The Core API

It is time that we described the API which powers Collards. The docs from this point forward are more concerned with understanding the inner workings of the abstractions and build process than using the CLI tools and building a site.

#5.1 Building a Site

Before doing anything else, collards needs a site object. The site object holds basic configuration info as well as a registry of Content Objects that will be used to build the site.

A site can be built from any folder with a .collards file which serves as a configuration file that the site is created from. An example collards config can be found here.

  • [class] SITE

    An object to store configuration for the site under construction such as AUTHOR, DOMAIN, TITLE, and any other arguments. It also stores a registry, mapping @CONTENT-IDs to @CONTENT-OBJECTS which is queried during the build process.

  • [reader] AUTHOR SITE (:AUTHOR)

    The name or username of the site author.

  • [reader] SERVER SITE (:SERVER)

    A plist of :host and :webroot indicating the host and root path to deploy the build output to.

  • [reader] DOMAIN SITE (:DOMAIN)

    The domain where the site will be hosted.

  • [reader] SUBDOMAINS SITE (:SUBDOMAINS)

    An list of plists of :path and :domain. This controls the subdomain to which specific paths in the build output will be deployed.

  • [reader] TITLE SITE (:TITLE)

    The title of the website.

  • [reader] IGNORED SITE (:IGNORED)

    A list of ignored top-level directories in the site. Assumed to be site-relative paths if they do not begin with '/'.

  • [reader] ASSET-DIR SITE (:ASSET-DIR)

    A directory to store site assets in. Assumed to be a site-relative path if it does not begin with '/'.

  • [reader] BUILD-DIR SITE (:BUILD-DIR)

    A directory to store the generated site in. Assumed to be a site-relative path if it does not begin with '/'.

  • [reader] INIT-DIR SITE (:INIT-DIR)

    A directory in the source repo for user code. Lisp files in this directory are loaded first, non-lisp files are ignored. Assumed to be a site-relative path if it does not begin with '/'.

  • [reader] REGISTRY SITE (= (SERAPEUM:DICT))

    Used to store and lookup content during the site build.

The folder with the .collards file is considered to be the 'site root'. This is significant because Collards places rendered files in the build folder relative to where they were under the site root.

As a consequence, it is essential that the .collards file can be found and useful to have helpers for determining the relative path of site files. Errors will be raised if the .collards file cannot be found or if there are any missing arguments needed to construct a functioning site.

  • [function] RELATIVE-PATH PATHNAME

    Given a PATHNAME under the site, return the relative path to the file. If a .collards config file cannot be found from traversing the directories in PATHNAME, a MISSING-CONFIG-ERROR will be signaled.

  • [function] ABSOLUTE-PATH &REST ARGS

    Build a path starting at the ROOT-DIR and appending all supplied ARGS.

  • [condition] MISSING-CONFIG-ERROR

    An error raised when no .collards file was found.

  • [condition] MISSING-SETTING-ERROR

    An error raised when a required setting is undefined.

The *SITE* instance is dynamically bound during the build process but should not be interacted with by user code. LOAD-CONFIG is used behind the scenes to create the site instance.

  • [variable] *SITE* NIL

    The SITE instance under construction.

    This should never be used directly by client code. All interactions with *SITE* are done through the documented interface and generally run from CLI tasks which dynamically bind the site.

  • [function] LOAD-CONFIG PATHNAME

    Given a PATHNAME under a collards project, find and load the site config.

For situations where there is a need to iterate over the files in the site a helper macro named DO-FILES is provided. It automatically ignores the build, asset, and init folders, any .git folder within the site, and hidden files.

A lower-level CALL-WITH-FILES function is also provided for special cases.

  • [function] CALL-WITH-FILES DIRECTORY IGNORED FN

    Walk the given DIRECTORY calling FN on the files within each subdir. Hidden files and folders are excluded, along with any subpaths of the IGNORED list.

  • [macro] DO-FILES (VAR) &BODY BODY

    Iterate over the files within *SITE*, binding each to VAR and then executing BODY. Skips hidden files and folders as well as the subpaths of IGNORED.

#5.2 Content Objects

Content Objects are what collards uses to represent files it will render, usually to HTML, to build the site. All Content objects must have at least a source file they are loaded from and a title used to generate a slug.

  • [class] CONTENT

    A simple base class for any object that will be routable within collards. Relies on the title and slug to generate an identifier.

  • [reader] FILE CONTENT (:FILE)

    The source file the content was loaded from.

  • [reader] SLUG CONTENT (:SLUG)

    The slug generated for the content.

  • [reader] TITLE CONTENT (:TITLE)

    The title to use for the content.

As part of HTML generation, collards finds all files to process in the site folder and builds content objects with them. To make a content object, we first determine the file extension and call PARSE on it, then take the result of parsing and give that to GET-TYPE which determines what class BUILD should make an instance of. NOTE: The collection and feed types are treated specially since they generate multiple HTML files from a single input file.

Consequently, user-defined content types can be created by defining:

  1. A new subclass of CONTENT

  2. A GET-TYPE method with an eql-specialized file extension returning the class to instantiate

  3. A PARSE method on that extension returning a plist of initargs

  4. A RENDER method to accept an instance of the content and generate HTML (i.e. a template)

Nothing is required to "inform" collards about the new content type because we introspect the content methods at runtime to determine what files have the necessary machinery for parsing and building.

All this could be done in an init file so no upstream modification to collards is needed. As described in the Templates section, defining RENDER methods is left to the user as part of templating.

  • [generic-function] GET-TYPE METADATA EXTENSION

    Given METADATA and an EXTENSION, return the symbol of a class to instantiate. METADATA can supply a :type key to override the result.

  • [generic-function] PARSE PATHNAME EXTENSION

    Read PATHNAME to parse metadata for the given EXTENSION.

  • [generic-function] RENDER CONTENT SITE

    Generate HTML for the given CONTENT using SITE if needed.

  • [function] FIND-SUBCLASS NAME

    Search for an appropriate subclass of CONTENT matching NAME.

  • [function] KNOWN-TYPE? PATHNAME

    Search for an applicable PARSE method for PATHNAME. If a match is found, return the extension as a keyword.

  • [function] BUILD PATHNAME

    Make a content object from the supplied PATHNAME.

Since pages need to have unique, safe URLs we provide ID and PATH functions that help determine where content should reside on the site and provide a shorthand for uniquely identifying it. SLUGIFY is run on the TITLE of all content during BUILD to help determine its output path.

  • [function] ID CONTENT

    Generate a unique identifier for CONTENT based on the class name and slug of the content. See: @CONTENT-ID and @SLUG.

  • [function] PATH CONTENT

    Return the relative path of CONTENT under the site.

  • [function] SLUGIFY STRING

    Transform STRING by first substituting dashes for whitespace as defined by SERAPEUM:WHITESPACEP, then removing any non-alphanumeric chars besides '-' or '/', and downcasing the result.

Finally, there are some internal helpers for determining urls and whether URLs should be relative or absolute, mostly for the build and serve commands.

  • [variable] *RELATIVE-URLS* NIL

    This controls whether URL-FOR generates relative or absolute URLs. It is used to ensure that the serve command produces a working local site.

  • [generic-function] OUTPUT-PATH-FOR CONTENT &KEY EXTENSION

    Generate an absolute path for CONTENT under the build dir.

  • [generic-function] URL-FOR CONTENT &KEY EXTENSION

    Generate a URL for CONTENT.

#5.3 The Site Registry

The Site registry allows for storing and retrieving loaded content objects as well as providing some helpers to ease HTML generation. There is also an iteration macro provided so that the build command may quickly traverse all known content.

  • [macro] DO-CONTENT (VAR &KEY (TEST 'IDENTITY) (SORT NIL) (KEY NIL) (SITE 'SITE:*SITE*)) &BODY BODY

    Filter the content in the SITE registry by TEST, optionally sorting it if a SORT is provided. KEY controls the SORT key. Once filtered, iterate over the items, binding each to VAR and executing BODY.

Because it is often interesting to view "related" content, there are PREV and NEXT generic functions for use when organizing posts chronologically or generating collections by tag or year.

  • [generic-function] PREV CONTENT &OPTIONAL SITE

    Given a CONTENT object, find the previous object in the registry when sorted in reverse chronological order.

  • [generic-function] NEXT CONTENT &OPTIONAL SITE

    Given a CONTENT object, find the next object in the registry when sorted in reverse chronological order.

Finally, there is the primary functionality to lookup and register content. While no content objects define custom methods at present, these are left as Generic Functions in case method combination or specialization is useful.

  • [generic-function] LOOKUP CONTENT-ID &OPTIONAL SITE

    Given a CONTENT-ID, search for a matching piece of content in the site registry. If no match is found, MISSING-CONTENT-ERROR is signaled.

  • [generic-function] REGISTER CONTENT &OPTIONAL SITE

    Register the supplied CONTENT with site. If an existing piece of content is found that shares the same CONTENT-ID but has a different source file, a DUPLICATE-ERROR condition will be signaled.

  • [condition] DUPLICATE-ERROR ERROR

    When content is supplied for registration and uses a @CONTENT-ID that has already been registered, a DUPLICATE-ERROR is signaled. It is not signaled if both the original and new content have the same path.

  • [condition] MISSING-CONTENT-ERROR

    Signaled when a piece of content could not be located.

#6 Content Types

#6.1 Static Assets

  • [class] ASSET CONTENT:CONTENT

    ASSETs are intended for larger files, usually media or binary files that need convenient linking but are best kept in cloud storage.

  • [function] ASSET? OBJECT

    Returns t if OBJECT is an ASSET.

  • [function] BUILD PATHNAME

#6.2 Pages

  • [class] PAGE CONTENT:CONTENT

    PAGEs are user-defined static web pages with custom layout. They are intended to provide maximum flexibility by allowing user-supplied code to generate arbitrary content as part of the build process.

  • [reader] BODY PAGE (:BODY)

    Optional markdown content for producing a page. The POST template is used instead of RENDER-FN when a BODY value is present.

  • [reader] RENDER-FN PAGE (:RENDER-FN)

    A function accepting content and site that generates HTML.

  • [function] PAGE? OBJECT

    Returns t if OBJECT is a PAGE.

  • [macro] DEFINE-PAGE &KEY TITLE RENDER-FN

    Given a TITLE and a symbol RENDER-FN accepting a PAGE object and a SITE, supply the page metadata for access by LOAD-CONTENT.

#6.3 Blog Posts

  • [class] POST CONTENT:CONTENT

    POSTs are collards way of representing articles with a common template, usually grouped by topic and date, and part of a journal.

  • [reader] DATE POST (:DATE)

    The date the post was created.

  • [reader] TAGS POST (:TAGS)

    A comma separated list of tags for the post.

  • [reader] BODY POST (:BODY)

    The main content of the post.

  • [reader] DRAFT? POST (:DRAFT)

    Whether the post should be published or not.

  • [reader] SUMMARY POST (:SUMMARY)

    An optional short summary of the post topic.

  • [function] POST? OBJECT

    Returns t if OBJECT is a POST.

  • [function] ALL-POSTS &OPTIONAL (SITE SITE:*SITE*)

    Given SITE, return a list of POST objects in reverse chronological order.

  • [function] RENDER-MARKDOWN MARKDOWN

    Given a MARKDOWN formatted string, use 3bmd to render it and treat any wikilinks as objects to be looked up via URL-FOR in the registry.

#6.4 RSS Feeds

  • [class] FEED CONTENT:CONTENT

    A generic feed class for generating Atom or RSS.

  • [function] FEED? OBJECT

    Returns T if OBJECT is a FEED.

#6.5 Collections

Most sites have interest in grouping subsets of content together by different means. Tags are one way to accomplish this but collards also models groupings by date as a kind of collection.

Collections are generated in a second phase after all initial content is loaded. Currently tag collections and annual collections are generated. Only post objects participate in collections.

It is worth noting that collections, once generated, are stored in the registry along with other content but they are not loaded by CONTENT:BUILD like other content objects, instead relying on GENERATE. tag:foo-bar and year:2024 are examples of collection content-ids.

  • [class] COLLECTION CONTENT:CONTENT

    A simple wrapper class for managing groups of content.

  • [function] COLLECTION? OBJECT

    Returns t if OBJECT is a COLLECTION.

  • [reader] KIND COLLECTION (:KIND)

    The kind of collection to build after content is loaded.

  • [accessor] ITEMS COLLECTION (:ITEMS)

    The content items in the collection.

  • [generic-function] GATHER COLLECTION-TYPE SITE

    Collect all the usages of COLLECTION-TYPE in SITE posts.

  • [generic-function] POPULATE COLLECTION SITE

    Add the relevant posts on SITE to the given COLLECTION.

  • [function] GENERATE &KEY (SITE SITE:*SITE*)

    Generate collections from the registry after all other content is loaded.

#7 Glossary

  • [glossary-term] Content Objects

    A Content Object represents a file collards will process that produces a unique URL on the site. A content object must store at least the path to its source file and a title to use during URL generation.

  • [glossary-term] Content Type

    A Content Type is a data type representing a kind of file that collards knows how to recognize (parse) and generate a page for (render). Out of the box, Collards has the following content types: Pages, Posts, Feeds, Collections, and Assets.

    It is possible for users to define their own Content Types in their init files by supplying a subclass of COLLARDS.CONTENT:CONTENT along with a COLLARDS.CONTENT:GET-TYPE method, a COLLARDS.CONTENT:PARSE method, and a COLLARDS.CONTENT:RENDER method returning HTML.

    The GET-TYPE method should be EQL-specialized on the file extension, for example :md for markdown files, and return the class to instantiate (e.g. with FIND-CLASS), not a symbol.

    The PARSE method should read the supplied file and return a plist of metadata including at minimum :file and :title attributes.

  • [glossary-term] Content ID

    A Content ID is a string uniquely identifying a piece of content managed by collards. It is constructed from the Content Type and Slug of a piece of content, joined by a ':'. For example, page:about or post:favorite-albums-of-2023.

    A notable limitation of the current scheme for content IDs is that titles must form a unique slug for all content of the same type. i.e. There cannot be two pages with the same slug, even if they are in different folders.

  • [glossary-term] Slug

    A Slug is the unique identifying part of a web address, such as '/favorite-albums.html'. Collards does not consider file extensions as part of slugs and generates a slug for each piece of content when it is registered by transforming the title with a SLUGIFY function.

    Users can feel free to redefine COLLARDS.CONTENT:SLUGIFY within their init files since init files are loaded before site processing occurs.


#[generated by MGL-PAX]
Do not follow this link