Merge pull request #61 from redline6561/basic-deploy

Release: 0.9.6!
This commit is contained in:
Brit Butler 2014-09-26 10:30:00 -04:00
commit 1c587f8616
38 changed files with 555 additions and 291 deletions

44
NEWS.md
View file

@ -1,8 +1,45 @@
Legend:
* Site-Breaking Change:
A change that will break most config files or coleslaw installations.
It is expected to effect all users but should require only minor
user effort to resolve.
* Incompatible Change:
A change to Coleslaw's exported interface. Plugins or Themes that have
not been upstreamed are effected and may require minor effort to fix.
## Changes for 0.9.6 (2014-09-27):
* **SITE-BREAKING CHANGE**: Coleslaw now defaults to a "basic" deploy
instead of the previous symlinked, timestamped deploy strategy.
To retain the previous behavior, add `(versioned)` to your config's
`:plugins` list.
* **Incompatible Change**: Custom themes will be broken by a change
to URL handling. Previously, we were hand-constructing URLs in the
templates. All site objects now store their URL in an instance slot.
In general, hrefs should be of the form `<a href="{$config.domain}/{$obj.url}"> ...</a>`.
* **Incompatible Change**: The interface of the `add-injection` function
has changed. If you have written a plugin which uses `add-injection`
you should update it to conform to the [new interface][plg-api].
* **New Plugin**: Support for [twitter summary cards][ts-cards] on blog
posts has been added thanks to @PuercoPop.
* **Docs**: Improved README and Theming docs. New Config File docs.
* Changes to `:routing` would previously break links in the templates
but now work seamlessly due to the updated URL handling.
* Loading content is more robust when empty lines or metadata are passed.
Thanks to @PuercoPop for the bug report and preliminary fix.
* The config `:repo` option is now deprecated as its value has become
a required argument to `coleslaw:main`. The value passed to `main`
will override the config value going forward.
* Improved handling of directories and error-reporting when they
don't exist is available thanks to @PuercoPop.
* The templates are now HTML5 valid thanks to @Ferada.
* Fixed a bug where RSS/Atom tag feeds were being published multiple times.
## Changes for 0.9.5 (2014-06-13): ## Changes for 0.9.5 (2014-06-13):
* A plugin for Incremental builds, cutting runtime for generating * **New Plugin**: Incremental builds, cutting runtime for generating
medium to large sites roughly in half! medium to large sites roughly in half!
* A Twitter plugin to tweet about your new posts. Thanks to @PuercoPop! * **New Plugin**: A Twitter plugin to tweet about your new posts. Thanks to @PuercoPop!
* Config options for the HTML lang and charset attributes. Thanks to @ryumei! * Config options for the HTML lang and charset attributes. Thanks to @ryumei!
* Coleslaw now exports a `get-updated-files` function which can be * Coleslaw now exports a `get-updated-files` function which can be
used to get a list of file-status/file-name pairs that were changed used to get a list of file-status/file-name pairs that were changed
@ -104,3 +141,6 @@
[hacking_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md [hacking_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md
[theming_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/themes.md [theming_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/themes.md
[example.rc]: https://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc [example.rc]: https://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc
[plg-use]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-use.md
[plg-api]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-api.md#extension-points
[ts-cards]: https://dev.twitter.com/cards/types/summary

View file

@ -10,6 +10,7 @@
Coleslaw is Flexible Lisp Blogware similar to [Frog](https://github.com/greghendershott/frog), [Jekyll](http://jekyllrb.com/), or [Hakyll](http://jaspervdj.be/hakyll/). Coleslaw is Flexible Lisp Blogware similar to [Frog](https://github.com/greghendershott/frog), [Jekyll](http://jekyllrb.com/), or [Hakyll](http://jaspervdj.be/hakyll/).
## Features ## Features
* Git for storage * Git for storage
* RSS and Atom feeds * RSS and Atom feeds
* Markdown Support with Code Highlighting provided by [colorize](http://www.cliki.net/colorize) * Markdown Support with Code Highlighting provided by [colorize](http://www.cliki.net/colorize)
@ -21,41 +22,73 @@ Coleslaw is Flexible Lisp Blogware similar to [Frog](https://github.com/greghend
* Incremental builds * Incremental builds
* Analytics via Google * Analytics via Google
* Comments via [Disqus](http://disqus.com/) * Comments via [Disqus](http://disqus.com/)
* Hosting via [Github Pages](https://pages.github.com/), [Heroku](http://heroku.com/), or [Amazon S3](http://aws.amazon.com/s3/) * Hosting via [Github Pages](https://pages.github.com/) or [Amazon S3](http://aws.amazon.com/s3/)
* [Tweeting](http://twitter.com/) about new posts * [Tweeting](http://twitter.com/) about new posts
* Using LaTeX via [Mathjax](http://mathjax.org/) * Using LaTeX via [Mathjax](http://mathjax.org/)
* Writing posts in ReStructured Text * Writing posts in ReStructured Text
* Importing posts from [Wordpress](http://wordpress.org/) * Importing posts from [Wordpress](http://wordpress.org/)
* There is also a [Heroku buildpack](https://github.com/jsmpereira/coleslaw-heroku) maintained by Jose Pereira.
## Example Sites ## Example Sites
* [redlinernotes](http://redlinernotes.com/blog/)
* [chip the glasses](http://chiptheglasses.com) See the [wiki](https://github.com/redline6561/coleslaw/wiki/Blogroll) for a list of coleslaw-powered blogs.
* [kenan-bolukbasi.log](http://kenanb.com/)
* [Nothing Really Matters](http://ironhead.xs4all.nl/)
* [A year and a smile](http://blog.sjas.de)
## Hacking ## Hacking
A core goal of *coleslaw* is to be both pleasant to read and easy to hack on and extend. If you want to understand the internals and bend *coleslaw* to do new and interesting things, I strongly encourage you to read the [Hacker's Guide to Coleslaw](https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md). A core goal of *coleslaw* is to be both pleasant to read and easy to
hack on and extend. If you want to understand the internals and bend
*coleslaw* to do new and interesting things, I strongly encourage you
to read the [Hacker's Guide to Coleslaw][hackers]. You'll find some
current **TODO** items towards the bottom.
[hackers]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md
## Installation ## Installation
This software should be portable to any conforming Common Lisp implementation but testing is primarily done on [SBCL](http://www.sbcl.org/) and [CCL](http://ccl.clozure.com/).
Server side setup:
1. Setup git and create a bare repo as shown [here](http://git-scm.com/book/en/Git-on-the-Server-Setting-Up-the-Server). Coleslaw should run on any conforming Common Lisp implementation but
2. Install Lisp (we recommend SBCL) and [Quicklisp](http://quicklisp.org/). testing is primarily done on [SBCL](http://www.sbcl.org/) and
3. ```wget -c https://raw.github.com/redline6561/coleslaw/master/examples/example.coleslawrc -O ~/.coleslawrc``` # and edit as necessary [CCL](http://ccl.clozure.com/).
4. ```wget -c https://raw.github.com/redline6561/coleslaw/master/examples/example.post-receive -O your-blog.git/hooks/post-receive``` # and edit as necessary
5. ```chmod +x your-blog/.git/hooks/post-receive```
6. Create or clone your blog repo locally. Add your server as a remote with ```git remote add prod git@my-host.com:path/to/repo.git```
7. Point the web server of your choice at the symlink /path/to/deploy-dir/.curr/
Now whenever you push a new commit to the server, coleslaw will update your blog automatically! You may need to `git push -u prod master` the first time. Coleslaw can either be run **manually** on a local machine or
triggered **automatically** on git push to a server. If you want a
server install, run these commands on your server _after_ setting up a
[git bare repo](http://git-scm.com/book/en/Git-on-the-Server-Setting-Up-the-Server).
Otherwise, run the commands on your local machine.
1. Install a Common Lisp implementation (we recommend SBCL) and
[Quicklisp](http://quicklisp.org/).
2. Place a config file for coleslaw in your `$HOME` directory. If you
want to run multiple blogs with coleslaw, you can keep each blog's
config file in that blog's repo. Feel free to copy and edit the
[example config][ex_config] or consult the [config docs][conf_docs]
to create one from scratch.
3. This step depends on whether you're setting up a local or server install.
* Server Install: Copy and `chmod +x` the
[example post-receive hook][post_hook] to your blog's bare repo.
* Local Install: Just run the following commands in the
REPL whenever you're ready to regenerate your blog:
```
(ql:quickload :coleslaw)
(coleslaw:main "/path/to/my/blog/")
```
4. Optionally, point the web server of your liking at your config-specified
`:deploy-dir`. Or "deploy-dir/.curr" if the `versioned` plugin is enabled.
Now just write posts, git commit and build by hand or by push.
[ex_config]: https://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc
[conf_docs]: https://github.com/redline6561/coleslaw/blob/master/docs/config.md
[post_hook]: https://github.com/redline6561/coleslaw/blob/master/examples/example.post-receive
## The Content Format
Coleslaw expects content to have a file extension matching the class
of the content. (I.e. `.post` for blog posts, `.page` for static
pages, etc.)
There should also be a metadata header on all files
starting and ending with the config-specified `:separator`, ";;;;;" by
default. Example:
## The Post Format
Coleslaw expects post files to be formatted as follows:
``` ```
;;;;; ;;;;;
title: foo title: foo
@ -66,5 +99,16 @@ format: html (for raw html) or md (for markdown)
your post your post
``` ```
Posts require the `title:` and `format:` fields.
Pages require the `title:` and `url:` fields.
To omit a field, simply do not have the line present, empty lines and
fields (e.g. "tags:" followed by whitespace) will be ignored.
## Theming ## Theming
Two themes are provided: hyde and readable (based on [bootswatch readable](http://bootswatch.com/readable/)). Hyde is the default. A guide to creating themes for coleslaw lives [here](https://github.com/redline6561/coleslaw/blob/master/docs/themes.md).
Two themes are provided: hyde, the default, and readable (based on
[bootswatch readable](http://bootswatch.com/readable/)).
A guide to creating themes for coleslaw lives
[here](https://github.com/redline6561/coleslaw/blob/master/docs/themes.md).

5
TODO
View file

@ -1,5 +0,0 @@
TODO:
Coleslaw.next
; See if there are any good ideas we can steal from [Frog](https://github.com/greghendershott/frog)
;; needs: shout template/render function. Twitter\Disqus integration with shouts?
; Incremental compilation: only "touched" posts+tags+months and by-n. -> 1.0

View file

@ -1,7 +1,7 @@
(defsystem #:coleslaw (defsystem #:coleslaw
:name "coleslaw" :name "coleslaw"
:description "Flexible Lisp Blogware" :description "Flexible Lisp Blogware"
:version "0.9.5" :version "0.9.6"
:license "BSD" :license "BSD"
:author "Brit Butler <redline6561@gmail.com>" :author "Brit Butler <redline6561@gmail.com>"
:pathname "src/" :pathname "src/"
@ -31,11 +31,10 @@
(intern "COLESLAW-TESTS" :coleslaw-tests)))) (intern "COLESLAW-TESTS" :coleslaw-tests))))
(defsystem #:coleslaw-tests (defsystem #:coleslaw-tests
:depends-on (coleslaw fiveam) :depends-on (coleslaw stefil)
:pathname "tests/" :pathname "tests/"
:serial t :serial t
:components ((:file "packages") :components ())
(:file "tests")))
(defmethod operation-done-p ((op test-op) (defmethod operation-done-p ((op test-op)
(c (eql (find-system :coleslaw)))) (c (eql (find-system :coleslaw))))

35
docs/config.md Normal file
View file

@ -0,0 +1,35 @@
# Configuration
## Where
Coleslaw needs a `.coleslawrc` file to operate properly. That file is usually located at
$HOME/.coleslawrc but may also be placed in the blog repo itself.
## What
The only *required* information in the config is:
* `:author` => to be placed on post pages and in the copyright/CC-BY-SA notice
* `:deploy-dir` => for Coleslaw's generated HTML to go in
* `:domain` => to generate absolute links to the site content
* `:routing` => to determine the URL scheme of content on the site
* `:title` => to provide a site title
* `:theme` => to select one of the themes in "coleslaw/themes/"
It is usually recommend to start from the [example config][ex_config] and pare down from there.
[ex_config]: https://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc
## Extras
There are also many *optional* config parameters such as:
* `:charset` => to set HTML attributes for international characters, default: "UTF-8"
* `:feeds` => to generate RSS and Atom feeds for certain tagged content
* `:lang` => to set HTML attributes indicating the site language, default: "en"
* `:license` => to override the displayed content license, the default is CC-BY-SA
* `:page-ext` => to set the suffix of generated files, default: "html"
* `:plugins` => to configure and enable coleslaw's [various plugins][plugin-use]
* `:separator` => to set the separator for content metadata, default: ";;;;;"
* `:sitenav` => to provide relevant links and ease navigation
* `:staging-dir` => for Coleslaw to do intermediate work, default: "/tmp/coleslaw"
[plugin-use]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-use.md

View file

@ -38,21 +38,6 @@ reduced runtime to 1.36 seconds, almost cutting it in half.
## Core Concepts ## Core Concepts
### Data and Deployment
**Coleslaw** is pretty fundamentally tied to the idea of git as both a
backing data store and a deployment method (via `git push`). The
consequence is that you need a bare repo somewhere with a post-recieve
hook. That post-recieve hook ([example][post_receive_hook])
will checkout the repo to a **$TMPDIR** and call `(coleslaw:main $TMPDIR)`.
It is then coleslaw's job to load all of your content, your config and
templates, and render the content to disk. Deployment is done by
moving the files to a location specified in the config and updating a
symlink. It is assumed a web server is set up to serve from that
symlink. However, there are plugins for deploying to Heroku, S3, and
Github Pages.
### Plugins ### Plugins
**Coleslaw** strongly encourages extending functionality via plugins. **Coleslaw** strongly encourages extending functionality via plugins.
@ -79,8 +64,22 @@ and return rendered HTML. **Coleslaw** defines a helper called
there are RSS, ATOM, and sitemap templates *coleslaw* uses automatically. there are RSS, ATOM, and sitemap templates *coleslaw* uses automatically.
No need for individual themes to reimplement a standard, after all! No need for individual themes to reimplement a standard, after all!
Unfortunately, it is not very pleasant to debug broken templates.
Efforts to remedy this are being pursued for the next release.
Two particular issues to note are transposed Closure commands,
e.g. "${foo}" instead of "{$foo}", and trying to use nonexistent
keys or slots which fails silently instead of producing an error.
### The Lifecycle of a Page ### The Lifecycle of a Page
- `(progn
(load-config "/my/blog/repo/path")
(compile-theme (theme *config*)))`
Coleslaw first needs the config loaded and theme compiled,
as neither the blog location, the theme to use, and other
crucial information are not yet known.
- `(load-content)` - `(load-content)`
A page starts, obviously, with a file. When *coleslaw* loads your A page starts, obviously, with a file. When *coleslaw* loads your
@ -102,10 +101,10 @@ reverse-chronological index.
- `(deploy dir)` - `(deploy dir)`
Finally, we move the staging directory to a timestamped path under the Finally, we move the staging directory to a path under the config's
the config's `:deploy-dir`, delete the directory pointed to by the old `:deploy-dir`. If the versioned plugin is enabled, it is a timestamped
'.prev' symlink, point '.curr' at '.prev', and point '.curr' at our path and we delete the directory pointed to by the old '.prev' symlink,
freshly built site. point '.curr' at '.prev', and point '.curr' at our freshly built site.
### Blogs vs Sites ### Blogs vs Sites
@ -116,13 +115,12 @@ INDEXes. Roughly speaking, a POST is a blog entry and an INDEX is a
collection of POSTs or other content. An INDEX really only serves to collection of POSTs or other content. An INDEX really only serves to
group a set of content objects on a page, it isn't content itself. group a set of content objects on a page, it isn't content itself.
This isn't ideal if you're looking for a full-on static site Content Types were added in 0.8 as a step towards making *coleslaw*
generator. Thankfully, Content Types were added in 0.8 as a step suitable for more use cases. Any subclass of CONTENT that implements
towards making *coleslaw* suitable for more use cases. Any subclass of the *document protocol* counts as a content type. However, only POSTs
CONTENT that implements the *document protocol* counts as a content are currently included in the bundled INDEXes since there isn't yet a
type. However, only POSTs are currently included in the basic INDEXes formal relationship to determine which content types should be
since there isn't yet a formal relationship to determine which content included on which indexes. It is straightforward for users to implement
types should be included on which indexes. Users may easily implement
their own dedicated INDEX for new Content Types. their own dedicated INDEX for new Content Types.
### The Document Protocol ### The Document Protocol
@ -169,16 +167,14 @@ eql-specializing on the class, e.g.
**Instance Methods**: **Instance Methods**:
- `page-url`: Generate a relative path for the object on the site, - `page-url`: Retrieve the relative path for the object on the site.
usually sans file extension. If there is no extension, an :around The implementation of `page-url` is not fully specified. For most
method adds "html" later. The `slug` slot on the instance is content types, we compute and store the path on the instance at
conventionally used to hold a portion of the path that corresponds initialization time making `page-url` just a reader method.
to a unique Primary Key or Object ID.
- `render`: A method that calls the appropriate template with `theme-fn`, - `render`: A method that calls the appropriate template with `theme-fn`,
passing it any needed arguments and returning rendered HTML. passing it any needed arguments and returning rendered HTML.
**Invariants**: **Invariants**:
- Any Content Types (subclasses of CONTENT) are expected to be stored in - Any Content Types (subclasses of CONTENT) are expected to be stored in
@ -217,7 +213,77 @@ order. Feeds exist to special case RSS and ATOM generation.
Currently, there is only 1 content type: POST, for blog entries. Currently, there is only 1 content type: POST, for blog entries.
PAGE, a content type for static page support, is available as a plugin. PAGE, a content type for static page support, is available as a plugin.
## Areas for Improvement ## Areas for Improvement (i.e. The Roadmap)
### TODO for 0.9.7
* Test suite improvements:
* `load-content`/`read-content`/parsing
* Content Discovery
* Theme Compilation
* Content Publishing
* Common Plugins including Injections
* Add proper errors to read-content/load-content? Not just ignoring bad data. Line info, etc.
* Improved template debugging? "${" instead of "{$", static checks for valid slots, etc.
At least a serious investigation into how such things might be provided.
* Some minor scripting conveniences with cl-launch? (Scaffold a post/page, Enable incremental, Build, etc).
### Assorted Cleanups
* Try to get tag-index urls out of the tags. Post templates use them.
* Profile/memoize find-all calls in **INDEX** `render` method.
### Real Error Handling
One reason Coleslaw's code base is so small is probably the
omission of any serious error handling. Trying to debug
coleslaw if there's a problem during a build is unpleasant
at best, especially for anyone not coming from the lisp world.
We need to start handling errors and reporting errors in ways
that are useful to the user. Example errors users have encountered:
1. Loading of Content. If `read-content` fails to parse a file, we
should tell the user what file failed and why. We also should
probably enforce more constraints about metadata. E.g. Empty
metadata is not allowed/meaningful. Trailing space after separator, etc.
2. Trying to load content from the bare repo instead of the clone.
i.e. Specifying the `:repo` in .coleslawrc as the bare repo.
The README should clarify this point and the need for posts to be
".post" files.
3. Custom themes that try to access non-existent properties of content
do not currently error. They just wind up returning whitespace.
When the theme compiles, we should alert the user to any obvious
issues with it.
4. Dear Lord it was miserable even debugging a transposed character error
in one of the templates. "${foo}" instead of "{$foo}". But fuck supporting
multiple templating backends I have enough problems. What can we do?
### Scripting Conveniences
It would be convenient to add command-line tools/scripts to run coleslaw,
set up the db for incremental builds, scaffold a new post, etc. for new users.
Xach's buildapp or Fare's cl-launch would be useful here. frog and hakyll are
reasonable points of inspiration for commands to offer.
### Plugin Constraints
There is no system for determining what plugins work together or
enforcing the requirements or constraints of any particular
plugin. That is to say, the plugins are not actually modular. They are
closer to controlled monkey-patching.
While adding a [real module system to common lisp][asdf3] is probably
out of scope, we might be able to add some kind of [contract library][qpq]
for implementing this functionality. At the very least, a way to check
some assertions and error out at plugin load time if they fail should be
doable. I might not be able to [make illegal states unrepresentable][misu],
but I can sure as hell make them harder to construct than they are now.
@PuercoPop has suggested looking into how [wookie does plugins][wookie].
It's much more heavyweight but might be worth looking into. If we go that
route, the plugin support code will be almost half the coleslaw core.
Weigh the tradeoffs carefully.
### New Content Type: Shouts! ### New Content Type: Shouts!
@ -249,28 +315,13 @@ Unfortunately, this does not solve:
Content Types it includes or the CONTENT which indexes it appears Content Types it includes or the CONTENT which indexes it appears
on is not yet clear. on is not yet clear.
### Incremental Compilation
Incremental compilation is doable, even straightforward if you ignore
indexes. It is also preferable to building the site in parallel as
avoiding work is better than using more workers. Moreover, being
able to determine (and expose) what files just changed enables new
functionality such as plugins that cross-post to tumblr.
This is a cool project and the effects are far reaching. Among other
things the existing deployment model would not work as it involves
rebuilding the entire site. In all likelihood we would want to update
the site 'in-place'. How to replace the compilation and deployment
model via a plugin has not yet been explored. Atomicity of filesystem
operations would be a reasonable concern. Also, every numbered INDEX
would have to be regenerated along with any tag or month indexes
matching the modified files. If incremental compilation is a goal,
simply disabling the indexes may be appropriate for certain users.
[post_receive_hook]: https://github.com/redline6561/coleslaw/blob/master/examples/example.post-receive
[closure_template]: https://github.com/archimag/cl-closure-template [closure_template]: https://github.com/archimag/cl-closure-template
[api_docs]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-api.md [api_docs]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-api.md
[clmd]: https://github.com/gwkkwg/cl-markdown [clmd]: https://github.com/gwkkwg/cl-markdown
[clrz]: https://github.com/redline6561/colorize [clrz]: https://github.com/redline6561/colorize
[pyg]: http://pygments.org/ [pyg]: http://pygments.org/
[incf]: https://github.com/redline6561/coleslaw/blob/master/plugins/incremental.lisp [incf]: https://github.com/redline6561/coleslaw/blob/master/plugins/incremental.lisp
[asdf3]: https://github.com/fare/asdf3-2013
[qpq]: https://github.com/sellout/quid-pro-quo
[misu]: https://blogs.janestreet.com/effective-ml-revisited/
[wookie]: https://github.com/orthecreedence/wookie/blob/master/plugin.lisp#L181

View file

@ -15,14 +15,12 @@
# Extension Points # Extension Points
* **New functionality via JS**, for example the Disqus and Mathjax * **New functionality via JS**, for example the Disqus and Mathjax plugins.
plugins. In this case, the plugin's `enable` function should call In this case, the plugin's `enable` function should call
[`add-injection`](http://redlinernotes.com/docs/coleslaw.html#add-injection_func) [`add-injection`](http://redlinernotes.com/docs/coleslaw.html#add-injection_func)
with an injection and a keyword. The injection itself is a list of with an injection and a keyword. The injection is a function that takes a
the string to insert and a lambda or function that can be called on *Document* and returns a string to insert in the page or nil.
a content instance to determine whether the injection should be The keyword specifies whether the injected text goes in the HEAD or BODY element. The
included on the page. The keyword specifies whether the injected
text goes in the HEAD or BODY element. The
[Disqus plugin](http://github.com/redline6561/coleslaw/blob/master/plugins/disqus.lisp) [Disqus plugin](http://github.com/redline6561/coleslaw/blob/master/plugins/disqus.lisp)
is a good example of this. is a good example of this.

View file

@ -1,8 +1,8 @@
# General Use # General Use
* Add a list with the plugin name and settings to the ```:plugins``` * To enable a plugin, add its name and settings to your
section of your [.coleslawrc][config_file]. Plugin settings are [.coleslawrc][config_file]. Plugin settings are described
described below. below. Note that some plugins require additional setup.
* Available plugins are listed below with usage descriptions and * Available plugins are listed below with usage descriptions and
config examples. config examples.
@ -41,9 +41,9 @@
**Example**: `(incremental)` **Example**: `(incremental)`
**Setup**: **Setup**: You must run the `examples/dump_db.sh` script to
- You must run the `examples/dump_db.sh` script to generate a database dump generate a database dump for your site before enabling the
for your site before enabling the incremental plugin. incremental plugin.
## LaTeX via Mathjax ## LaTeX via Mathjax
@ -144,6 +144,23 @@ CL-USER> (chirp:complete-authentication "4173325")
#<CHIRP-OBJECTS:USER PuercoPop #18405433> #<CHIRP-OBJECTS:USER PuercoPop #18405433>
``` ```
## Twitter Summary Cards
**Description**: Add Summary Card metadata to blog posts
to enhance twitter links to that content.
**Example**: `(twitter-summary-card :twitter-handle "@redline6561")
## Versioned Deploys
**Description**: Originally, this was Coleslaw's only deploy behavior.
Instead of deploying directly to `:deploy-dir`, creates `.curr` and
`.prev` symlinks in the *deploy-dir*, which point to timestamped
directories of the last two deploys of the site. Deploys prior to the
last two are automatically cleaned up.
**Example**: `(versioned)`
## Wordpress Importer ## Wordpress Importer
**NOTE**: This plugin really should be rewritten to act as a **NOTE**: This plugin really should be rewritten to act as a

View file

@ -32,7 +32,7 @@ Every page other than those in the `posts/` directory is an `index`.
**Every** page uses the `base.tmpl` and fills in the content using **Every** page uses the `base.tmpl` and fills in the content using
either the `post` or `index` templates. No important logic should be either the `post` or `index` templates. No important logic should be
in *any* template, they are only used to give provide consistent layout. in *any* template, they are only used to provide a consistent layout.
* `base.tmpl` This template generates the outer shell of the HTML. * `base.tmpl` This template generates the outer shell of the HTML.
It keeps a consistent look and feel for all pages in the blog. The It keeps a consistent look and feel for all pages in the blog. The
@ -64,7 +64,7 @@ simplest to either modify the existing default theme, `hyde`, or copy
it in entirety and then tweak only the CSS of your new theme. A large it in entirety and then tweak only the CSS of your new theme. A large
amount of visual difference can be had with a minimum of (or no) amount of visual difference can be had with a minimum of (or no)
template hacking. There is plenty of advice on CSS styling on the web. template hacking. There is plenty of advice on CSS styling on the web.
I'm no expert but feel free to send pull requests modifying theme's I'm no expert but feel free to send pull requests modifying a theme's
CSS or improving this section, perhaps by recommending a CSS resource. CSS or improving this section, perhaps by recommending a CSS resource.
## Creating a Theme from Scratch (with code) ## Creating a Theme from Scratch (with code)
@ -114,10 +114,15 @@ The templating language is documented [elsewhere][clt].
However as a short primer: However as a short primer:
* Everything is output literally, except template commands. * Everything is output literally, except template commands.
* Template commands are enclosed in `{` and `}` * Template commands are enclosed in `{` and `}`.
* Variables, which are provided by coleslaw, can be referenced * Variables, which are provided by coleslaw, can be referenced
inside a template command. So to use a variable you have to say inside a template command. So to use a variable you have to say
`{$variable}` or `{$variable.key}`. `{$variable}` or `{$variable.key}`.
**WARNING**: At present, cl-closure-template does not have great debugging.
If you typo this, e.g. `${variable}`, you will receive an *uninformative*
and apparently unrelated error. Also, attempted access of non-existent keys
fails silently. We are exploring options for making debugging easier in a
future release.
* If statements are written as `{if ...} ... {else} ... {/if}`. * If statements are written as `{if ...} ... {else} ... {/if}`.
Typical examples are: `{if $injections.body} ... {/if}` or Typical examples are: `{if $injections.body} ... {/if}` or
`{if not isLast($link)} ... {/if}`. `{if not isLast($link)} ... {/if}`.
@ -139,19 +144,16 @@ The variable that should be available to all templates is:
#### Index Template Variables #### Index Template Variables
- **tags** A list containing all the tags, each with keys - **tags** A list containing all the tags, each with keys
`.name` and `.slug`. `name` and `url`.
- **months** A list of all months with posts as `yyyy-mm` strings. - **months** A list of all the content months, each with keys
`name` and `url`.
- **index** This is the meat of the content. This variable has - **index** This is the meat of the content. This variable has
the following keys: the following keys:
- `id`, the name of the page that will be rendered
- `content`, a list of content (see below) - `content`, a list of content (see below)
- `title`, a string title to display to the user - `name`, a name to use in links or href tags
- **prev** If this index file is part of a chain, the `id` - `title`, a title to use in H1 or header tags
of the previous index html in the chain. - **prev** Nil or the previous index with keys: `url` and `title`.
If this is the first file, the value will be empty. - **next** Nil or the next index with keys: `url` and `title`.
- **next** If this index file is part of a chain, the `id`
of the next index html in the chain.
If this is the last file, the value will be empty.
#### Post Template Variable #### Post Template Variable
@ -160,8 +162,8 @@ The variable that should be available to all templates is:
- **post** All these variables are post objects. **prev** and - **post** All these variables are post objects. **prev** and
**next** are the adjacent posts when put in **next** are the adjacent posts when put in
chronological order. Each post has the following keys: chronological order. Each post has the following keys:
- `tags`, a list of tags (each with keys `name` and `slug`) - `url`, the relative url of the post
- `slug`, the slug of the post - `tags`, a list of tags (each with keys `name` and `url`)
- `date`, the date of posting - `date`, the date of posting
- `text`, the HTML of the post's body - `text`, the HTML of the post's body
- `title`, the title of the post - `title`, the title of the post
@ -190,8 +192,8 @@ A simple `index.tmpl` looks like this:
{namespace coleslaw.theme.trivial} {namespace coleslaw.theme.trivial}
{template index} {template index}
{foreach $obj in $index.content} {foreach $obj in $index.content}
<h1>{$obj.title}</h1> <h1>{$object.title}</h1>
{$obj.text |noAutoescape} {$object.text |noAutoescape}
{/foreach} {/foreach}
{/template} {/template}
``` ```
@ -209,14 +211,7 @@ And a simple `post.tmpl` is similarly:
All of the files are now populated with content. There are still no links All of the files are now populated with content. There are still no links
between the pages so navigation is cumbersome but adding links is simple. between the pages so navigation is cumbersome but adding links is simple.
Good luck! Just do: `<a href="{$config.domain}/{$object.url}">{$object.name}</a>`.
## Note on adding links
As mentioned earlier, most files have a file name which is a slug of
some sort. So if you want to create a link to a tag file you should
do something like this:
`<a href="${config.domain}/tags/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>`.
[clt]: https://developers.google.com/closure/templates/ [clt]: https://developers.google.com/closure/templates/
[ovr]: https://github.com/redline6561/coleslaw/blob/master/docs/overview.md [ovr]: https://github.com/redline6561/coleslaw/blob/master/docs/overview.md

View file

@ -2,12 +2,14 @@
:deploy-dir "/home/git/blog/" :deploy-dir "/home/git/blog/"
:domain "http://blog.redlinernotes.com" :domain "http://blog.redlinernotes.com"
:feeds ("lisp") :feeds ("lisp")
:plugins ((mathjax) :plugins ((analytics :tracking-code "foo")
(disqus :shortname "my-site-name")
; (incremental) ;; *Remove comment to enable incremental builds.
(mathjax)
(sitemap) (sitemap)
(static-pages) (static-pages)
(disqus :shortname "my-site-name") ; (versioned) ;; *Remove comment to enable symlinked, timestamped deploys.
(analytics :tracking-code "foo")) )
:repo "/home/git/tmp/improvedmeans/"
:routing ((:post "posts/~a") :routing ((:post "posts/~a")
(:tag-index "tag/~a") (:tag-index "tag/~a")
(:month-index "date/~a") (:month-index "date/~a")
@ -22,3 +24,5 @@
:staging-dir "/tmp/coleslaw/" :staging-dir "/tmp/coleslaw/"
:title "Improved Means for Achieving Deteriorated Ends" :title "Improved Means for Achieving Deteriorated Ends"
:theme "hyde") :theme "hyde")
;; * Prerequisites described in plugin docs.

View file

@ -1,6 +1,5 @@
########## CONFIGURATION VALUES ########## ########## CONFIGURATION VALUES ##########
# TMP_GIT_CLONE _must_ match the :repo argument in your .coleslawrc!
TMP_GIT_CLONE=$HOME/tmp/improvedmeans/ TMP_GIT_CLONE=$HOME/tmp/improvedmeans/
# Set LISP to your preferred implementation. The following # Set LISP to your preferred implementation. The following
@ -26,10 +25,11 @@ while read oldrev newrev refname; do
if [ $LISP = sbcl ]; then if [ $LISP = sbcl ]; then
sbcl --eval "(ql:quickload 'coleslaw)" \ sbcl --eval "(ql:quickload 'coleslaw)" \
--eval "(coleslaw:main \"$TMP_GIT_CLONE\" \"$oldrev\")" \ --eval "(coleslaw:main \"$TMP_GIT_CLONE\" \"$oldrev\")" \
--eval "(coleslaw::exit)" --eval "(uiop:quit)"
elif [ $LISP = ccl ]; then elif [ $LISP = ccl ]; then
ccl -e "(ql:quickload 'coleslaw) (coleslaw:main \"$TMP_GIT_CLONE\" \"$oldrev\") (coleslaw::exit)" ccl -e "(ql:quickload 'coleslaw) (coleslaw:main \"$TMP_GIT_CLONE\" \"$oldrev\") (uiop:quit)"
else else
echo -e "$LISP is not a supported lisp dialect at this time. Exiting.\n"
exit 1 exit 1
fi fi
fi fi

View file

@ -21,4 +21,5 @@
</script>") </script>")
(defun enable (&key tracking-code) (defun enable (&key tracking-code)
(add-injection (format nil *analytics-js* tracking-code) :head)) (let ((snippet (format nil *analytics-js* tracking-code)))
(add-injection (constantly snippet) :head)))

View file

@ -24,5 +24,7 @@
<a href=\"http://disqus.com\" class=\"dsq-brlink\">comments powered by <span class=\"logo-disqus\">Disqus</span></a>") <a href=\"http://disqus.com\" class=\"dsq-brlink\">comments powered by <span class=\"logo-disqus\">Disqus</span></a>")
(defun enable (&key shortname) (defun enable (&key shortname)
(add-injection (list (format nil *disqus-header* shortname) (flet ((inject-p (x)
(lambda (x) (typep x 'post))) :body)) (when (typep x 'post)
(format nil *disqus-header* shortname))))
(add-injection #'inject-p :body)))

View file

@ -24,6 +24,7 @@
(defun enable (&key force config (preset "TeX-AMS-MML_HTMLorMML") (defun enable (&key force config (preset "TeX-AMS-MML_HTMLorMML")
(location "http://cdn.mathjax.org/mathjax/latest/MathJax.js")) (location "http://cdn.mathjax.org/mathjax/latest/MathJax.js"))
(flet ((plugin-p (x) (or force (mathjax-p x)))) (flet ((inject-p (x)
(let ((mathjax-header (format nil *mathjax-header* config location preset))) (when (or force (mathjax-p x))
(add-injection (list mathjax-header #'plugin-p) :head)))) (format nil *mathjax-header* config location preset))))
(add-injection #'inject-p :head)))

View file

@ -1,16 +0,0 @@
(eval-when (:compile-toplevel :load-toplevel)
(ql:quickload 'lparallel))
(defpackage :coleslaw-parallel
(:use :cl)
(:export #:enable))
(in-package :coleslaw-parallel)
;; TODO: The bulk of the speedup here should come from parallelizing discover.
;; Publish will also benefit. Whether it's better to spin off threads for each
;; content type/index type or the operations *within* discover/publish is not
;; known, the higher granularity of doing it at the iterating over types level
;; is certainly easier to prototype though.
(defun enable ())

View file

@ -13,7 +13,7 @@
(in-package :coleslaw-sitemap) (in-package :coleslaw-sitemap)
(defclass sitemap (index) (defclass sitemap ()
((urls :initarg :urls :reader urls))) ((urls :initarg :urls :reader urls)))
(defmethod page-url ((object sitemap)) "sitemap.xml") (defmethod page-url ((object sitemap)) "sitemap.xml")

View file

@ -20,8 +20,9 @@
(defmethod initialize-instance :after ((object page) &key) (defmethod initialize-instance :after ((object page) &key)
;; Expect all static-pages to be written in Markdown for now. ;; Expect all static-pages to be written in Markdown for now.
(with-accessors ((text content-text)) object (with-slots (url text) object
(setf text (render-text text :md)))) (setf url (make-pathname :defaults url)
text (render-text text :md))))
(defmethod render ((object page) &key next prev) (defmethod render ((object page) &key next prev)
;; For the time being, we'll re-use the normal post theme. ;; For the time being, we'll re-use the normal post theme.

View file

@ -0,0 +1,25 @@
(defpackage :coleslaw-twitter-summary-card
(:use :cl :coleslaw)
(:export #:enable))
(in-package :coleslaw-twitter-summary-card)
(defun summary-card (post twitter-handle)
"TODO: Figure if and how to include twitter:url meta property."
(format nil "<meta property=\"twitter:card\" content=\"summary\" />
~@[<meta property=\"twitter:author\" content=\"~A\" />~]
<meta property=\"twitter:title\" content=\"~A\" />
<meta property=\"twitter:description\" content=\"~A\" />"
twitter-handle
(title-of post)
(let ((text (content-text post)))
(if (< 200 (length text))
(subseq text 0 199)
text))))
(defun enable (&key twitter-handle)
(add-injection
(lambda (x)
(when (typep x 'post)
(summary-card x twitter-handle)))
:head))

24
plugins/versioned.lisp Normal file
View file

@ -0,0 +1,24 @@
(defpackage :coleslaw-versioned
(:use :cl)
(:import-from :coleslaw #:*config*
#:deploy-dir
#:rel-path
#:run-program
#:update-symlink))
(in-package :coleslaw-versioned)
(defmethod coleslaw:deploy (staging)
(let* ((dest (deploy-dir *config*))
(new-build (rel-path dest "generated/~a" (get-universal-time)))
(prev (rel-path dest ".prev"))
(curr (rel-path dest ".curr")))
(ensure-directories-exist new-build)
(run-program "mv ~a ~a" staging new-build)
(when (and (probe-file prev) (truename prev))
(run-program "rm -r ~a" (truename prev)))
(when (probe-file curr)
(update-symlink prev (truename curr)))
(update-symlink curr new-build)))
(defun enable ())

View file

@ -3,12 +3,11 @@
(defvar *last-revision* nil (defvar *last-revision* nil
"The git revision prior to the last push. For use with GET-UPDATED-FILES.") "The git revision prior to the last push. For use with GET-UPDATED-FILES.")
(defun main (&optional (repo-dir "") oldrev) (defun main (repo-dir &optional oldrev)
"Load the user's config file, then compile and deploy the site. Optionally, "Load the user's config file, then compile and deploy the blog stored
REPO-DIR is the location of the blog repo and OLDREV is the revision prior to in REPO-DIR. Optionally, OLDREV is the revision prior to the last push."
the last push."
(setf *last-revision* oldrev)
(load-config repo-dir) (load-config repo-dir)
(setf *last-revision* oldrev)
(load-content) (load-content)
(compile-theme (theme *config*)) (compile-theme (theme *config*))
(let ((dir (staging-dir *config*))) (let ((dir (staging-dir *config*)))
@ -40,19 +39,9 @@ the last push."
(update-symlink "index.html" "1.html"))) (update-symlink "index.html" "1.html")))
(defgeneric deploy (staging) (defgeneric deploy (staging)
(:documentation "Deploy the STAGING dir, updating the .prev and .curr symlinks.") (:documentation "Deploy the STAGING build to the directory specified in the config.")
(:method (staging) (:method (staging)
(let* ((dest (deploy-dir *config*)) (run-program "rsync --delete -avz ~a ~a" staging (deploy-dir *config*))))
(new-build (rel-path dest "generated/~a" (get-universal-time)))
(prev (rel-path dest ".prev"))
(curr (rel-path dest ".curr")))
(ensure-directories-exist new-build)
(run-program "mv ~a ~a" staging new-build)
(when (and (probe-file prev) (truename prev))
(run-program "rm -r ~a" (truename prev)))
(when (probe-file curr)
(update-symlink prev (truename curr)))
(update-symlink curr new-build))))
(defun update-symlink (path target) (defun update-symlink (path target)
"Update the symlink at PATH to point to TARGET." "Update the symlink at PATH to point to TARGET."

View file

@ -10,7 +10,7 @@
(license :initarg :license :reader license) (license :initarg :license :reader license)
(page-ext :initarg :page-ext :reader page-ext) (page-ext :initarg :page-ext :reader page-ext)
(plugins :initarg :plugins :reader plugins) (plugins :initarg :plugins :reader plugins)
(repo :initarg :repo :reader repo) (repo :initarg :repo :accessor repo)
(routing :initarg :routing :reader routing) (routing :initarg :routing :reader routing)
(separator :initarg :separator :reader separator) (separator :initarg :separator :reader separator)
(sitenav :initarg :sitenav :reader sitenav) (sitenav :initarg :sitenav :reader sitenav)
@ -18,6 +18,10 @@
(theme :initarg :theme :reader theme) (theme :initarg :theme :reader theme)
(title :initarg :title :reader title)) (title :initarg :title :reader title))
(:default-initargs (:default-initargs
:feeds nil
:license nil
:plugins nil
:sitenav nil
:charset "UTF-8" :charset "UTF-8"
:lang "en" :lang "en"
:page-ext "html" :page-ext "html"
@ -61,10 +65,11 @@ doesn't exist, use the .coleslawrc in the home directory."
repo-config repo-config
(rel-path (user-homedir-pathname) ".coleslawrc")))) (rel-path (user-homedir-pathname) ".coleslawrc"))))
(defun load-config (&optional repo-dir) (defun load-config (&optional (repo-dir ""))
"Find and load the coleslaw configuration from .coleslawrc. REPO-DIR will be "Find and load the coleslaw configuration from .coleslawrc. REPO-DIR will be
preferred over the home directory if provided." preferred over the home directory if provided."
(with-open-file (in (discover-config-path repo-dir) :external-format '(:utf-8)) (with-open-file (in (discover-config-path repo-dir) :external-format '(:utf-8))
(let ((config-form (read in))) (let ((config-form (read in)))
(setf *config* (construct 'blog config-form)))) (setf *config* (construct 'blog config-form)
(repo *config*) repo-dir)))
(load-plugins (plugins *config*))) (load-plugins (plugins *config*)))

View file

@ -4,7 +4,12 @@
(defclass tag () (defclass tag ()
((name :initarg :name :reader tag-name) ((name :initarg :name :reader tag-name)
(slug :initarg :slug :reader tag-slug))) (slug :initarg :slug :reader tag-slug)
(url :initarg :url)))
(defmethod initialize-instance :after ((tag tag) &key)
(with-slots (url slug) tag
(setf url (compute-url nil slug 'tag-index))))
(defun make-tag (str) (defun make-tag (str)
"Takes a string and returns a TAG instance with a name and slug." "Takes a string and returns a TAG instance with a name and slug."
@ -31,39 +36,47 @@
;; Content Types ;; Content Types
(defclass content () (defclass content ()
((file :initarg :file :reader content-file) ((url :initarg :url :reader page-url)
(date :initarg :date :reader content-date) (date :initarg :date :reader content-date)
(tags :initarg :tags :accessor content-tags) (file :initarg :file :reader content-file)
(slug :initarg :slug :accessor content-slug) (tags :initarg :tags :reader content-tags)
(text :initarg :text :accessor content-text)) (text :initarg :text :reader content-text))
(:default-initargs :tags nil :date nil :slug nil)) (:default-initargs :tags nil :date nil))
(defmethod initialize-instance :after ((object content) &key) (defmethod initialize-instance :after ((object content) &key)
(with-accessors ((tags content-tags)) object (with-slots (tags) object
(when (stringp tags) (when (stringp tags)
(setf tags (mapcar #'make-tag (cl-ppcre:split "," tags)))))) (setf tags (mapcar #'make-tag (cl-ppcre:split "," tags))))))
(defun parse-initarg (line)
"Given a metadata header, LINE, parse an initarg name/value pair from it."
(let ((name (string-upcase (subseq line 0 (position #\: line))))
(match (nth-value 1 (scan-to-strings "[a-zA-Z]+:\\s+(.*)" line))))
(when match
(list (make-keyword name) (aref match 0)))))
(defun parse-metadata (stream)
"Given a STREAM, parse metadata from it or signal an appropriate condition."
(flet ((get-next-line (input)
(string-trim '(#\Space #\Newline #\Tab) (read-line input nil))))
(unless (string= (get-next-line stream) (separator *config*))
(error "The file lacks the expected header: ~a" (separator *config*)))
(loop for line = (get-next-line stream)
until (string= line (separator *config*))
appending (parse-initarg line))))
(defun read-content (file) (defun read-content (file)
"Returns a plist of metadata from FILE with :text holding the content as a string." "Returns a plist of metadata from FILE with :text holding the content."
(flet ((slurp-remainder (stream) (flet ((slurp-remainder (stream)
(let ((seq (make-string (- (file-length stream) (let ((seq (make-string (- (file-length stream)
(file-position stream))))) (file-position stream)))))
(read-sequence seq stream) (read-sequence seq stream)
(remove #\Nul seq))) (remove #\Nul seq))))
(parse-field (str)
(nth-value 1 (cl-ppcre:scan-to-strings "[a-zA-Z]+:\\s+(.*)" str)))
(field-name (line)
(make-keyword (string-upcase (subseq line 0 (position #\: line))))))
(with-open-file (in file :external-format '(:utf-8)) (with-open-file (in file :external-format '(:utf-8))
(unless (string= (read-line in) (separator *config*)) (let ((metadata (parse-metadata in))
(error "The provided file lacks the expected header.")) (content (slurp-remainder in))
(let ((meta (loop for line = (read-line in nil) (filepath (enough-namestring file (repo *config*))))
until (string= line (separator *config*)) (append metadata (list :text content :file filepath))))))
appending (list (field-name line)
(aref (parse-field line) 0))))
(filepath (enough-namestring file (repo *config*)))
(content (slurp-remainder in)))
(append meta (list :text content :file filepath))))))
;; Helper Functions ;; Helper Functions

View file

@ -26,24 +26,28 @@
;; Instance Methods ;; Instance Methods
(defgeneric page-url (document) (defgeneric page-url (document)
(:documentation "The url to the DOCUMENT without the domain.") (:documentation "The relative URL to the DOCUMENT."))
(:method (document)
(let* ((class-name (class-name (class-of document)))
(route (assoc (make-keyword class-name) (routing *config*))))
(if route
(format nil (second route) (slot-value document 'slug))
(error "No routing method found for: ~A" class-name)))))
(defmethod page-url :around ((document t))
(let* ((result (call-next-method))
(type (or (pathname-type result) "html")))
(make-pathname :type type :defaults result)))
(defgeneric render (document &key &allow-other-keys) (defgeneric render (document &key &allow-other-keys)
(:documentation "Render the given DOCUMENT to HTML.")) (:documentation "Render the given DOCUMENT to HTML."))
;; Helper Functions ;; Helper Functions
(defun compute-url (document unique-id &optional class)
"Compute the relative URL for a DOCUMENT based on its UNIQUE-ID. If CLASS
is provided, it overrides the route used."
(let* ((class-name (or class (class-name (class-of document))))
(route (get-route class-name)))
(unless route
(error "No routing method found for: ~A" class-name))
(let* ((result (format nil route unique-id))
(type (or (pathname-type result) (page-ext *config*))))
(make-pathname :type type :defaults result))))
(defun get-route (doc-type)
"Return the route format string for DOC-TYPE."
(second (assoc (make-keyword doc-type) (routing *config*))))
(defun add-document (document) (defun add-document (document)
"Add DOCUMENT to the in-memory database. Error if a matching entry is present." "Add DOCUMENT to the in-memory database. Error if a matching entry is present."
(let ((url (page-url document))) (let ((url (page-url document)))

View file

@ -2,8 +2,9 @@
;;; Atom and RSS Feeds ;;; Atom and RSS Feeds
(defclass feed (index) (defclass base-feed () ((format :initarg :format :reader feed-format)))
((format :initform nil :initarg :format :accessor feed-format)))
(defclass feed (index base-feed) ())
(defmethod discover ((doc-type (eql (find-class 'feed)))) (defmethod discover ((doc-type (eql (find-class 'feed))))
(let ((content (by-date (find-all 'post)))) (let ((content (by-date (find-all 'post))))
@ -19,7 +20,7 @@
;;; Tag Feeds ;;; Tag Feeds
(defclass tag-feed (feed) ()) (defclass tag-feed (index base-feed) ())
(defmethod discover ((doc-type (eql (find-class 'tag-feed)))) (defmethod discover ((doc-type (eql (find-class 'tag-feed))))
(let ((content (by-date (find-all 'post)))) (let ((content (by-date (find-all 'post))))

View file

@ -6,13 +6,18 @@
"The list of tags which content has been tagged with.") "The list of tags which content has been tagged with.")
(defclass index () (defclass index ()
((slug :initarg :slug :reader index-slug) ((url :initarg :url :reader page-url)
(title :initarg :title :reader title-of) (name :initarg :name :reader index-name)
(title :initarg :title :reader title-of)
(content :initarg :content :reader index-content))) (content :initarg :content :reader index-content)))
(defmethod initialize-instance :after ((object index) &key slug)
(with-slots (url) object
(setf url (compute-url object slug))))
(defmethod render ((object index) &key prev next) (defmethod render ((object index) &key prev next)
(funcall (theme-fn 'index) (list :tags *all-tags* (funcall (theme-fn 'index) (list :tags (find-all 'tag-index)
:months *all-months* :months (find-all 'month-index)
:config *config* :config *config*
:index object :index object
:prev prev :prev prev
@ -24,12 +29,12 @@
(defmethod discover ((doc-type (eql (find-class 'tag-index)))) (defmethod discover ((doc-type (eql (find-class 'tag-index))))
(let ((content (by-date (find-all 'post)))) (let ((content (by-date (find-all 'post))))
(dolist (tag (all-tags)) (dolist (tag *all-tags*)
(add-document (index-by-tag tag content))))) (add-document (index-by-tag tag content)))))
(defun index-by-tag (tag content) (defun index-by-tag (tag content)
"Return an index of all CONTENT matching the given TAG." "Return an index of all CONTENT matching the given TAG."
(make-instance 'tag-index :slug (tag-slug tag) (make-instance 'tag-index :slug (tag-slug tag) :name (tag-name tag)
:content (remove-if-not (lambda (x) (tag-p tag x)) content) :content (remove-if-not (lambda (x) (tag-p tag x)) content)
:title (format nil "Content tagged ~a" (tag-name tag)))) :title (format nil "Content tagged ~a" (tag-name tag))))
@ -48,7 +53,7 @@
(defun index-by-month (month content) (defun index-by-month (month content)
"Return an index of all CONTENT matching the given MONTH." "Return an index of all CONTENT matching the given MONTH."
(make-instance 'month-index :slug month (make-instance 'month-index :slug month :name month
:content (remove-if-not (lambda (x) (month-p month x)) content) :content (remove-if-not (lambda (x) (month-p month x)) content)
:title (format nil "Content from ~a" month))) :title (format nil "Content from ~a" month)))
@ -68,18 +73,14 @@
(defun index-by-n (i content) (defun index-by-n (i content)
"Return the index for the Ith page of CONTENT in reverse chronological order." "Return the index for the Ith page of CONTENT in reverse chronological order."
(let ((content (subseq content (* 10 i)))) (let ((content (subseq content (* 10 i))))
(make-instance 'numeric-index :slug (1+ i) (make-instance 'numeric-index :slug (1+ i) :name (1+ i)
:content (take-up-to 10 content) :content (take-up-to 10 content)
:title "Recent Content"))) :title "Recent Content")))
(defmethod publish ((doc-type (eql (find-class 'numeric-index)))) (defmethod publish ((doc-type (eql (find-class 'numeric-index))))
(let ((indexes (sort (find-all 'numeric-index) #'< :key #'index-slug))) (let ((indexes (sort (find-all 'numeric-index) #'< :key #'index-name)))
(dolist (index indexes) (loop for (next index prev) on (append '(nil) indexes)
(let ((prev (1- (index-slug index))) while index do (write-document index nil :prev prev :next next))))
(next (1+ (index-slug index))))
(write-document index nil
:prev (when (plusp prev) prev)
:next (when (<= next (length indexes)) next))))))
;;; Helper Functions ;;; Helper Functions

View file

@ -5,6 +5,7 @@
#:make-keyword #:make-keyword
#:mappend) #:mappend)
(:import-from :cl-fad #:file-exists-p) (:import-from :cl-fad #:file-exists-p)
(:import-from :cl-ppcre #:scan-to-strings)
(:import-from :closure-template #:compile-template) (:import-from :closure-template #:compile-template)
(:import-from :local-time #:format-rfc1123-timestring) (:import-from :local-time #:format-rfc1123-timestring)
(:import-from :uiop #:getcwd (:import-from :uiop #:getcwd
@ -22,12 +23,13 @@
#:title-of #:title-of
#:author-of #:author-of
#:find-content-by-path #:find-content-by-path
;; Plugin API + Theming ;; Theming + Plugin API
#:theme-fn
#:plugin-conf-error #:plugin-conf-error
#:render-text #:render-text
#:add-injection #:add-injection
#:get-updated-files #:get-updated-files
#:theme-fn #:deploy
;; The Document Protocol ;; The Document Protocol
#:discover #:discover
#:publish #:publish
@ -37,4 +39,5 @@
#:purge-all #:purge-all
#:add-document #:add-document
#:delete-document #:delete-document
#:write-document)) #:write-document
#:content-text))

View file

@ -1,20 +1,17 @@
(in-package :coleslaw) (in-package :coleslaw)
(defclass post (content) (defclass post (content)
((title :initarg :title :reader title-of) ((title :initarg :title :reader title-of)
(author :initarg :author :accessor author-of) (author :initarg :author :reader author-of)
(format :initarg :format :accessor post-format)) (format :initarg :format :reader post-format))
(:default-initargs :author nil)) (:default-initargs :author nil))
(defmethod initialize-instance :after ((object post) &key) (defmethod initialize-instance :after ((object post) &key)
(with-accessors ((title title-of) (with-slots (url title author format text) object
(author author-of) (setf url (compute-url object (slugify title))
(format post-format) format (make-keyword (string-upcase format))
(text content-text)) object text (render-text text format)
(setf (content-slug object) (slugify title) author (or author (author *config*)))))
format (make-keyword (string-upcase format))
text (render-text text format)
author (or author (author *config*)))))
(defmethod render ((object post) &key prev next) (defmethod render ((object post) &key prev next)
(funcall (theme-fn 'post) (list :config *config* (funcall (theme-fn 'post) (list :config *config*

View file

@ -4,21 +4,15 @@
"A list that stores pairs of (string . predicate) to inject in the page.") "A list that stores pairs of (string . predicate) to inject in the page.")
(defun add-injection (injection location) (defun add-injection (injection location)
"Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be "Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be a
a string which will always be added or a (string . lambda). In the latter case, function that takes a DOCUMENT and returns NIL or a STRING for template insertion."
the lambda takes a single argument, a content object, i.e. a POST or INDEX, and (push injection (getf *injections* location)))
any return value other than nil indicates the injection should be added."
(let ((result (etypecase injection
(string (list injection #'identity))
(list injection))))
(push result (getf *injections* location))))
(defun find-injections (content) (defun find-injections (content)
"Iterate over *INJECTIONS* collecting any that should be added to CONTENT." "Iterate over *INJECTIONS* collecting any that should be added to CONTENT."
(flet ((injections-for (location) (flet ((injections-for (location)
(loop for (injection predicate) in (getf *injections* location) (loop for injection in (getf *injections* location)
when (funcall predicate content) collecting (funcall injection content))))
collect injection)))
(list :head (injections-for :head) (list :head (injections-for :head)
:body (injections-for :body)))) :body (injections-for :body))))

View file

@ -54,13 +54,9 @@ an UNWIND-PROTECT, then change back to the current directory."
(setf (getcwd) ,old))))) (setf (getcwd) ,old)))))
(defun exit () (defun exit ()
;; KLUDGE: Just call UIOP for now. Don't want users updating scripts.
"Exit the lisp system returning a 0 status code." "Exit the lisp system returning a 0 status code."
#+sbcl (sb-ext:exit) (uiop:quit))
#+ccl (ccl:quit)
#+ecl (si:quit)
#+cmucl (ext:quit)
#+clisp (ext:quit)
#-(or sbcl ccl ecl cmucl clisp) (error "Not implemented yet."))
(defun fmt (fmt-str args) (defun fmt (fmt-str args)
"A convenient FORMAT interface for string building." "A convenient FORMAT interface for string building."

View file

@ -1,3 +0,0 @@
(defpackage :coleslaw-tests
(:use :cl :fiveam)
(:export #:run!))

View file

@ -0,0 +1,61 @@
(defpackage :summary-card-tests
(:use :cl :coleslaw :stefil))
(in-package :summary-card-tests)
(defsuite summary-cards)
(in-suite summary-cards)
;; TODO: Create a fixture to either load a mocked config or load set of plugins.
;; Then wrap these tests to use that fixture. Then add these to defsystem, setup
;; general test run with other packages.
(coleslaw::enable-plugin :twitter-summary-card)
(defvar *short-post*
(make-instance 'post :title "hai" :text "very greetings" :format "html"))
(defvar *long-post*
(make-instance 'post :title "What a Wonderful World"
:text "I see trees of green, red roses too. I see them
bloom, for me and you. And I think to myself, what a wonderful world.
I see skies of blue,
And clouds of white.
The bright blessed day,
The dark sacred night.
And I think to myself,
What a wonderful world.
The colors of the rainbow,
So pretty in the sky.
Are also on the faces,
Of people going by,
I see friends shaking hands.
Saying, \"How do you do?\"
They're really saying,
\"I love you\".
I hear babies cry,
I watch them grow,
They'll learn much more,
Than I'll ever know.
And I think to myself,
What a wonderful world.
Yes, I think to myself,
What a wonderful world. " :format "html"))
(deftest summary-card-sans-twitter-handle ()
(let ((summary-card (summary-card *short-post* nil)))
(is (null (cl-ppcre:scan "twitter:author" summary-card)))))
(deftest summary-card-with-twitter-handle ()
(let ((summary-card (summary-card *short-post* "@PuercoPop")))
(is (cl-ppcre:scan "twitter:author" summary-card))))
(deftest summary-card-trims-long-post ()
(let ((summary-card (summary-card *long-post* nil)))
(multiple-value-bind ())
;; (scan "twitter:description\" content=\"(.*)\"" summary-card)
summary-card))

View file

@ -1,13 +0,0 @@
(in-package :coleslaw-tests)
(defmacro deftest (name docstring &body body)
`(test ,name
,docstring
,@body))
(def-suite coleslaw-tests)
(in-suite coleslaw-tests)
(deftest sanity-test
"A blog should compile and deploy correctly."
(is (zerop (coleslaw:main))))

View file

@ -14,7 +14,7 @@
{foreach $post in $content.content} {foreach $post in $content.content}
<entry> <entry>
<link type="text/html" rel="alternate" href="{$config.domain}/posts/{$post.slug}.{$config.pageExt}"/> <link type="text/html" rel="alternate" href="{$config.domain}/{$post.url}"/>
<title>{$post.title}</title> <title>{$post.title}</title>
<published>{$post.date}</published> <published>{$post.date}</published>
<updated>{$post.date}</updated> <updated>{$post.date}</updated>

View file

@ -4,20 +4,20 @@
<h1 class="title">{$index.title}</h1> <h1 class="title">{$index.title}</h1>
{foreach $obj in $index.content} {foreach $obj in $index.content}
<div class="article-meta"> <div class="article-meta">
<a class="article-title" href="{$config.domain}/posts/{$obj.slug}.{$config.pageExt}">{$obj.title}</a> <a class="article-title" href="{$config.domain}/{$obj.url}">{$obj.title}</a>
<div class="date"> posted on {$obj.date}</div> <div class="date"> posted on {$obj.date}</div>
<div class="article">{$obj.text |noAutoescape}</div> <div class="article">{$obj.text |noAutoescape}</div>
</div> </div>
{/foreach} {/foreach}
<div id="relative-nav"> <div id="relative-nav">
{if $prev} <a href="{$prev}.{$config.pageExt}">Previous</a> {/if} {if $prev} <a href="{$config.domain}/{$prev.url}">Previous</a> {/if}
{if $next} <a href="{$next}.{$config.pageExt}">Next</a> {/if} {if $next} <a href="{$config.domain}/{$next.url}">Next</a> {/if}
</div> </div>
{if $tags} {if $tags}
<div id="tagsoup"> <div id="tagsoup">
<p>This blog covers <p>This blog covers
{foreach $tag in $tags} {foreach $tag in $tags}
<a href="{$config.domain}/tag/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>{nil} <a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
{if not isLast($tag)},{sp}{/if} {if not isLast($tag)},{sp}{/if}
{/foreach} {/foreach}
</div> </div>
@ -26,7 +26,7 @@
<div id="monthsoup"> <div id="monthsoup">
<p>View content from <p>View content from
{foreach $month in $months} {foreach $month in $months}
<a href="{$config.domain}/date/{$month}.{$config.pageExt}">{$month}</a>{nil} <a href="{$config.domain}/{$month.url}">{$month.name}</a>{nil}
{if not isLast($month)},{sp}{/if} {if not isLast($month)},{sp}{/if}
{/foreach} {/foreach}
</div> </div>

View file

@ -6,7 +6,7 @@
<div class="tags">{\n} <div class="tags">{\n}
{if $post.tags} {if $post.tags}
Tagged as {foreach $tag in $post.tags} Tagged as {foreach $tag in $post.tags}
<a href="{$config.domain}/tag/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>{nil} <a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
{if not isLast($tag)},{sp}{/if} {if not isLast($tag)},{sp}{/if}
{/foreach} {/foreach}
{/if} {/if}
@ -21,7 +21,7 @@
{$post.text |noAutoescape} {$post.text |noAutoescape}
</div>{\n} </div>{\n}
<div class="relative-nav">{\n} <div class="relative-nav">{\n}
{if $prev} <a href="{$config.domain}/posts/{$prev.slug}.{$config.pageExt}">Previous</a><br> {/if}{\n} {if $prev} <a href="{$config.domain}/{$prev.url}">Previous</a><br> {/if}{\n}
{if $next} <a href="{$config.domain}/posts/{$next.slug}.{$config.pageExt}">Next</a><br> {/if}{\n} {if $next} <a href="{$config.domain}/{$next.url}">Next</a><br> {/if}{\n}
</div>{\n} </div>{\n}
{/template} {/template}

View file

@ -4,7 +4,7 @@
<h1 class="page-header">{$index.title}</h1> <h1 class="page-header">{$index.title}</h1>
{foreach $obj in $index.content} {foreach $obj in $index.content}
<div class="row-fluid"> <div class="row-fluid">
<h1><a href="{$config.domain}/posts/{$obj.slug}.{$config.pageExt}">{$obj.title}</a></h1> <h1><a href="{$config.domain}/{$obj.url}">{$obj.title}</a></h1>
<p class="date-posted">posted on {$obj.date}</p> <p class="date-posted">posted on {$obj.date}</p>
{$obj.text |noAutoescape} {$obj.text |noAutoescape}
</div> </div>
@ -13,7 +13,7 @@
<div class="row-fluid"> <div class="row-fluid">
<p>This blog covers <p>This blog covers
{foreach $tag in $tags} {foreach $tag in $tags}
<a href="{$config.domain}/tag/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>{nil} <a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
{if not isLast($tag)},{sp}{/if} {if not isLast($tag)},{sp}{/if}
{/foreach} {/foreach}
</p> </p>
@ -23,7 +23,7 @@
<div class="row-fluid"> <div class="row-fluid">
<p>View content from <p>View content from
{foreach $month in $months} {foreach $month in $months}
<a href="{$config.domain}/date/{$month}.{$config.pageExt}">{$month}</a>{nil} <a href="{$config.domain}/{$month.url}">{$month.name}</a>{nil}
{if not isLast($month)},{sp}{/if} {if not isLast($month)},{sp}{/if}
{/foreach} {/foreach}
</p> </p>

View file

@ -6,7 +6,7 @@
<p> <p>
{if $post.tags} {if $post.tags}
Tagged as {foreach $tag in $post.tags} Tagged as {foreach $tag in $post.tags}
<a href="{$config.domain}/tag/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>{nil} <a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
{if not isLast($tag)},{sp}{/if} {if not isLast($tag)},{sp}{/if}
{/foreach} {/foreach}
{/if} {/if}
@ -20,8 +20,8 @@
{$post.text |noAutoescape} {$post.text |noAutoescape}
<ul class="pager"> <ul class="pager">
{if $prev}<li class="previous"><a href="{$config.domain}/posts/{$prev.slug}.{$config.pageExt}">&larr; Previous</a></li>{/if}{\n} {if $prev}<li class="previous"><a href="{$config.domain}/{$prev.url}">&larr; Previous</a></li>{/if}{\n}
{if $next}<li class="next"><a href="{$config.domain}/posts/{$next.slug}.{$config.pageExt}">Next &rarr;</a></li>{/if}{\n} {if $next}<li class="next"><a href="{$config.domain}/{$next.url}">Next &rarr;</a></li>{/if}{\n}
</ul> </ul>
</div>{\n} </div>{\n}
{/template} {/template}

View file

@ -13,10 +13,10 @@
{foreach $post in $content.content} {foreach $post in $content.content}
<item> <item>
<title>{$post.title}</title> <title>{$post.title}</title>
<link>{$config.domain}/posts/{$post.slug}.{$config.pageExt}</link> <link>{$config.domain}/{$post.url}</link>
<pubDate>{$post.date}</pubDate> <pubDate>{$post.date}</pubDate>
<author>{$config.author}</author> <author>{$config.author}</author>
<guid isPermaLink="true">{$config.domain}/posts/{$post.slug}.{$config.pageExt}</guid> <guid isPermaLink="true">{$config.domain}/{$post.url}</guid>
{foreach $tag in $post.tags} {foreach $tag in $post.tags}
<category><![CDATA[ {$tag.name |noAutoescape} ]]></category> <category><![CDATA[ {$tag.name |noAutoescape} ]]></category>
{/foreach} {/foreach}