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):
* A plugin for Incremental builds, cutting runtime for generating
* **New Plugin**: Incremental builds, cutting runtime for generating
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!
* 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
@ -104,3 +141,6 @@
[hacking_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.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
[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/).
## Features
* Git for storage
* RSS and Atom feeds
* 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
* Analytics via Google
* 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
* Using LaTeX via [Mathjax](http://mathjax.org/)
* Writing posts in ReStructured Text
* 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
* [redlinernotes](http://redlinernotes.com/blog/)
* [chip the glasses](http://chiptheglasses.com)
* [kenan-bolukbasi.log](http://kenanb.com/)
* [Nothing Really Matters](http://ironhead.xs4all.nl/)
* [A year and a smile](http://blog.sjas.de)
See the [wiki](https://github.com/redline6561/coleslaw/wiki/Blogroll) for a list of coleslaw-powered blogs.
## 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
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).
2. Install Lisp (we recommend SBCL) and [Quicklisp](http://quicklisp.org/).
3. ```wget -c https://raw.github.com/redline6561/coleslaw/master/examples/example.coleslawrc -O ~/.coleslawrc``` # and edit as necessary
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/
Coleslaw should run on any conforming Common Lisp implementation but
testing is primarily done on [SBCL](http://www.sbcl.org/) and
[CCL](http://ccl.clozure.com/).
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
@ -66,5 +99,16 @@ format: html (for raw html) or md (for markdown)
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
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
:name "coleslaw"
:description "Flexible Lisp Blogware"
:version "0.9.5"
:version "0.9.6"
:license "BSD"
:author "Brit Butler <redline6561@gmail.com>"
:pathname "src/"
@ -31,11 +31,10 @@
(intern "COLESLAW-TESTS" :coleslaw-tests))))
(defsystem #:coleslaw-tests
:depends-on (coleslaw fiveam)
:depends-on (coleslaw stefil)
:pathname "tests/"
:serial t
:components ((:file "packages")
(:file "tests")))
:components ())
(defmethod operation-done-p ((op test-op)
(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
### 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
**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.
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
- `(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)`
A page starts, obviously, with a file. When *coleslaw* loads your
@ -102,10 +101,10 @@ reverse-chronological index.
- `(deploy dir)`
Finally, we move the staging directory to a timestamped path under the
the config's `:deploy-dir`, delete the directory pointed to by the old
'.prev' symlink, point '.curr' at '.prev', and point '.curr' at our
freshly built site.
Finally, we move the staging directory to a path under the config's
`:deploy-dir`. If the versioned plugin is enabled, it is a timestamped
path and we delete the directory pointed to by the old '.prev' symlink,
point '.curr' at '.prev', and point '.curr' at our freshly built site.
### 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
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
generator. Thankfully, Content Types were added in 0.8 as a step
towards making *coleslaw* suitable for more use cases. Any subclass of
CONTENT that implements the *document protocol* counts as a content
type. However, only POSTs are currently included in the basic INDEXes
since there isn't yet a formal relationship to determine which content
types should be included on which indexes. Users may easily implement
Content Types were added in 0.8 as a step towards making *coleslaw*
suitable for more use cases. Any subclass of CONTENT that implements
the *document protocol* counts as a content type. However, only POSTs
are currently included in the bundled INDEXes since there isn't yet a
formal relationship to determine which content types should be
included on which indexes. It is straightforward for users to implement
their own dedicated INDEX for new Content Types.
### The Document Protocol
@ -169,16 +167,14 @@ eql-specializing on the class, e.g.
**Instance Methods**:
- `page-url`: Generate a relative path for the object on the site,
usually sans file extension. If there is no extension, an :around
method adds "html" later. The `slug` slot on the instance is
conventionally used to hold a portion of the path that corresponds
to a unique Primary Key or Object ID.
- `page-url`: Retrieve the relative path for the object on the site.
The implementation of `page-url` is not fully specified. For most
content types, we compute and store the path on the instance at
initialization time making `page-url` just a reader method.
- `render`: A method that calls the appropriate template with `theme-fn`,
passing it any needed arguments and returning rendered HTML.
**Invariants**:
- 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.
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!
@ -249,28 +315,13 @@ Unfortunately, this does not solve:
Content Types it includes or the CONTENT which indexes it appears
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
[api_docs]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-api.md
[clmd]: https://github.com/gwkkwg/cl-markdown
[clrz]: https://github.com/redline6561/colorize
[pyg]: http://pygments.org/
[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
* **New functionality via JS**, for example the Disqus and Mathjax
plugins. In this case, the plugin's `enable` function should call
* **New functionality via JS**, for example the Disqus and Mathjax plugins.
In this case, the plugin's `enable` function should call
[`add-injection`](http://redlinernotes.com/docs/coleslaw.html#add-injection_func)
with an injection and a keyword. The injection itself is a list of
the string to insert and a lambda or function that can be called on
a content instance to determine whether the injection should be
included on the page. The keyword specifies whether the injected
text goes in the HEAD or BODY element. The
with an injection and a keyword. The injection is a function that takes a
*Document* and returns a string to insert in the page or nil.
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)
is a good example of this.

View file

@ -1,8 +1,8 @@
# General Use
* Add a list with the plugin name and settings to the ```:plugins```
section of your [.coleslawrc][config_file]. Plugin settings are
described below.
* To enable a plugin, add its name and settings to your
[.coleslawrc][config_file]. Plugin settings are described
below. Note that some plugins require additional setup.
* Available plugins are listed below with usage descriptions and
config examples.
@ -41,9 +41,9 @@
**Example**: `(incremental)`
**Setup**:
- You must run the `examples/dump_db.sh` script to generate a database dump
for your site before enabling the incremental plugin.
**Setup**: You must run the `examples/dump_db.sh` script to
generate a database dump for your site before enabling the
incremental plugin.
## LaTeX via Mathjax
@ -144,6 +144,23 @@ CL-USER> (chirp:complete-authentication "4173325")
#<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
**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
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.
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
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.
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.
## Creating a Theme from Scratch (with code)
@ -114,10 +114,15 @@ The templating language is documented [elsewhere][clt].
However as a short primer:
* 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
inside a template command. So to use a variable you have to say
`{$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}`.
Typical examples are: `{if $injections.body} ... {/if}` or
`{if not isLast($link)} ... {/if}`.
@ -139,19 +144,16 @@ The variable that should be available to all templates is:
#### Index Template Variables
- **tags** A list containing all the tags, each with keys
`.name` and `.slug`.
- **months** A list of all months with posts as `yyyy-mm` strings.
`name` and `url`.
- **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
the following keys:
- `id`, the name of the page that will be rendered
- `content`, a list of content (see below)
- `title`, a string title to display to the user
- **prev** If this index file is part of a chain, the `id`
of the previous index html in the chain.
If this is the first file, the value will be empty.
- **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.
- `name`, a name to use in links or href tags
- `title`, a title to use in H1 or header tags
- **prev** Nil or the previous index with keys: `url` and `title`.
- **next** Nil or the next index with keys: `url` and `title`.
#### 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
**next** are the adjacent posts when put in
chronological order. Each post has the following keys:
- `tags`, a list of tags (each with keys `name` and `slug`)
- `slug`, the slug of the post
- `url`, the relative url of the post
- `tags`, a list of tags (each with keys `name` and `url`)
- `date`, the date of posting
- `text`, the HTML of the post's body
- `title`, the title of the post
@ -190,8 +192,8 @@ A simple `index.tmpl` looks like this:
{namespace coleslaw.theme.trivial}
{template index}
{foreach $obj in $index.content}
<h1>{$obj.title}</h1>
{$obj.text |noAutoescape}
<h1>{$object.title}</h1>
{$object.text |noAutoescape}
{/foreach}
{/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
between the pages so navigation is cumbersome but adding links is simple.
Good luck!
## 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>`.
Just do: `<a href="{$config.domain}/{$object.url}">{$object.name}</a>`.
[clt]: https://developers.google.com/closure/templates/
[ovr]: https://github.com/redline6561/coleslaw/blob/master/docs/overview.md

View file

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

View file

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

View file

@ -21,4 +21,5 @@
</script>")
(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>")
(defun enable (&key shortname)
(add-injection (list (format nil *disqus-header* shortname)
(lambda (x) (typep x 'post))) :body))
(flet ((inject-p (x)
(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")
(location "http://cdn.mathjax.org/mathjax/latest/MathJax.js"))
(flet ((plugin-p (x) (or force (mathjax-p x))))
(let ((mathjax-header (format nil *mathjax-header* config location preset)))
(add-injection (list mathjax-header #'plugin-p) :head))))
(flet ((inject-p (x)
(when (or force (mathjax-p x))
(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)
(defclass sitemap (index)
(defclass sitemap ()
((urls :initarg :urls :reader urls)))
(defmethod page-url ((object sitemap)) "sitemap.xml")

View file

@ -20,8 +20,9 @@
(defmethod initialize-instance :after ((object page) &key)
;; Expect all static-pages to be written in Markdown for now.
(with-accessors ((text content-text)) object
(setf text (render-text text :md))))
(with-slots (url text) object
(setf url (make-pathname :defaults url)
text (render-text text :md))))
(defmethod render ((object page) &key next prev)
;; 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
"The git revision prior to the last push. For use with GET-UPDATED-FILES.")
(defun main (&optional (repo-dir "") oldrev)
"Load the user's config file, then compile and deploy the site. Optionally,
REPO-DIR is the location of the blog repo and OLDREV is the revision prior to
the last push."
(setf *last-revision* oldrev)
(defun main (repo-dir &optional oldrev)
"Load the user's config file, then compile and deploy the blog stored
in REPO-DIR. Optionally, OLDREV is the revision prior to the last push."
(load-config repo-dir)
(setf *last-revision* oldrev)
(load-content)
(compile-theme (theme *config*))
(let ((dir (staging-dir *config*)))
@ -40,19 +39,9 @@ the last push."
(update-symlink "index.html" "1.html")))
(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)
(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))))
(run-program "rsync --delete -avz ~a ~a" staging (deploy-dir *config*))))
(defun update-symlink (path target)
"Update the symlink at PATH to point to TARGET."

View file

@ -10,7 +10,7 @@
(license :initarg :license :reader license)
(page-ext :initarg :page-ext :reader page-ext)
(plugins :initarg :plugins :reader plugins)
(repo :initarg :repo :reader repo)
(repo :initarg :repo :accessor repo)
(routing :initarg :routing :reader routing)
(separator :initarg :separator :reader separator)
(sitenav :initarg :sitenav :reader sitenav)
@ -18,6 +18,10 @@
(theme :initarg :theme :reader theme)
(title :initarg :title :reader title))
(:default-initargs
:feeds nil
:license nil
:plugins nil
:sitenav nil
:charset "UTF-8"
:lang "en"
:page-ext "html"
@ -61,10 +65,11 @@ doesn't exist, use the .coleslawrc in the home directory."
repo-config
(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
preferred over the home directory if provided."
(with-open-file (in (discover-config-path repo-dir) :external-format '(:utf-8))
(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*)))

View file

@ -4,7 +4,12 @@
(defclass tag ()
((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)
"Takes a string and returns a TAG instance with a name and slug."
@ -31,39 +36,47 @@
;; Content Types
(defclass content ()
((file :initarg :file :reader content-file)
((url :initarg :url :reader page-url)
(date :initarg :date :reader content-date)
(tags :initarg :tags :accessor content-tags)
(slug :initarg :slug :accessor content-slug)
(text :initarg :text :accessor content-text))
(:default-initargs :tags nil :date nil :slug nil))
(file :initarg :file :reader content-file)
(tags :initarg :tags :reader content-tags)
(text :initarg :text :reader content-text))
(:default-initargs :tags nil :date nil))
(defmethod initialize-instance :after ((object content) &key)
(with-accessors ((tags content-tags)) object
(with-slots (tags) object
(when (stringp 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)
"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)
(let ((seq (make-string (- (file-length stream)
(file-position stream)))))
(read-sequence seq stream)
(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))))))
(remove #\Nul seq))))
(with-open-file (in file :external-format '(:utf-8))
(unless (string= (read-line in) (separator *config*))
(error "The provided file lacks the expected header."))
(let ((meta (loop for line = (read-line in nil)
until (string= line (separator *config*))
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))))))
(let ((metadata (parse-metadata in))
(content (slurp-remainder in))
(filepath (enough-namestring file (repo *config*))))
(append metadata (list :text content :file filepath))))))
;; Helper Functions

View file

@ -26,24 +26,28 @@
;; Instance Methods
(defgeneric page-url (document)
(:documentation "The url to the DOCUMENT without the domain.")
(: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)))
(:documentation "The relative URL to the DOCUMENT."))
(defgeneric render (document &key &allow-other-keys)
(:documentation "Render the given DOCUMENT to HTML."))
;; 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)
"Add DOCUMENT to the in-memory database. Error if a matching entry is present."
(let ((url (page-url document)))

View file

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

View file

@ -6,13 +6,18 @@
"The list of tags which content has been tagged with.")
(defclass index ()
((slug :initarg :slug :reader index-slug)
(title :initarg :title :reader title-of)
((url :initarg :url :reader page-url)
(name :initarg :name :reader index-name)
(title :initarg :title :reader title-of)
(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)
(funcall (theme-fn 'index) (list :tags *all-tags*
:months *all-months*
(funcall (theme-fn 'index) (list :tags (find-all 'tag-index)
:months (find-all 'month-index)
:config *config*
:index object
:prev prev
@ -24,12 +29,12 @@
(defmethod discover ((doc-type (eql (find-class 'tag-index))))
(let ((content (by-date (find-all 'post))))
(dolist (tag (all-tags))
(dolist (tag *all-tags*)
(add-document (index-by-tag tag content)))))
(defun index-by-tag (tag content)
"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)
:title (format nil "Content tagged ~a" (tag-name tag))))
@ -48,7 +53,7 @@
(defun index-by-month (month content)
"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)
:title (format nil "Content from ~a" month)))
@ -68,18 +73,14 @@
(defun index-by-n (i content)
"Return the index for the Ith page of CONTENT in reverse chronological order."
(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)
:title "Recent Content")))
(defmethod publish ((doc-type (eql (find-class 'numeric-index))))
(let ((indexes (sort (find-all 'numeric-index) #'< :key #'index-slug)))
(dolist (index indexes)
(let ((prev (1- (index-slug index)))
(next (1+ (index-slug index))))
(write-document index nil
:prev (when (plusp prev) prev)
:next (when (<= next (length indexes)) next))))))
(let ((indexes (sort (find-all 'numeric-index) #'< :key #'index-name)))
(loop for (next index prev) on (append '(nil) indexes)
while index do (write-document index nil :prev prev :next next))))
;;; Helper Functions

View file

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

View file

@ -1,20 +1,17 @@
(in-package :coleslaw)
(defclass post (content)
((title :initarg :title :reader title-of)
(author :initarg :author :accessor author-of)
(format :initarg :format :accessor post-format))
((title :initarg :title :reader title-of)
(author :initarg :author :reader author-of)
(format :initarg :format :reader post-format))
(:default-initargs :author nil))
(defmethod initialize-instance :after ((object post) &key)
(with-accessors ((title title-of)
(author author-of)
(format post-format)
(text content-text)) object
(setf (content-slug object) (slugify title)
format (make-keyword (string-upcase format))
text (render-text text format)
author (or author (author *config*)))))
(with-slots (url title author format text) object
(setf url (compute-url object (slugify title))
format (make-keyword (string-upcase format))
text (render-text text format)
author (or author (author *config*)))))
(defmethod render ((object post) &key prev next)
(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.")
(defun add-injection (injection location)
"Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be
a string which will always be added or a (string . lambda). In the latter case,
the lambda takes a single argument, a content object, i.e. a POST or INDEX, and
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))))
"Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be a
function that takes a DOCUMENT and returns NIL or a STRING for template insertion."
(push injection (getf *injections* location)))
(defun find-injections (content)
"Iterate over *INJECTIONS* collecting any that should be added to CONTENT."
(flet ((injections-for (location)
(loop for (injection predicate) in (getf *injections* location)
when (funcall predicate content)
collect injection)))
(loop for injection in (getf *injections* location)
collecting (funcall injection content))))
(list :head (injections-for :head)
:body (injections-for :body))))

View file

@ -54,13 +54,9 @@ an UNWIND-PROTECT, then change back to the current directory."
(setf (getcwd) ,old)))))
(defun exit ()
;; KLUDGE: Just call UIOP for now. Don't want users updating scripts.
"Exit the lisp system returning a 0 status code."
#+sbcl (sb-ext:exit)
#+ccl (ccl:quit)
#+ecl (si:quit)
#+cmucl (ext:quit)
#+clisp (ext:quit)
#-(or sbcl ccl ecl cmucl clisp) (error "Not implemented yet."))
(uiop:quit))
(defun fmt (fmt-str args)
"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}
<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>
<published>{$post.date}</published>
<updated>{$post.date}</updated>

View file

@ -4,20 +4,20 @@
<h1 class="title">{$index.title}</h1>
{foreach $obj in $index.content}
<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="article">{$obj.text |noAutoescape}</div>
</div>
{/foreach}
<div id="relative-nav">
{if $prev} <a href="{$prev}.{$config.pageExt}">Previous</a> {/if}
{if $next} <a href="{$next}.{$config.pageExt}">Next</a> {/if}
{if $prev} <a href="{$config.domain}/{$prev.url}">Previous</a> {/if}
{if $next} <a href="{$config.domain}/{$next.url}">Next</a> {/if}
</div>
{if $tags}
<div id="tagsoup">
<p>This blog covers
{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}
{/foreach}
</div>
@ -26,7 +26,7 @@
<div id="monthsoup">
<p>View content from
{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}
{/foreach}
</div>

View file

@ -6,7 +6,7 @@
<div class="tags">{\n}
{if $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}
{/foreach}
{/if}
@ -21,7 +21,7 @@
{$post.text |noAutoescape}
</div>{\n}
<div class="relative-nav">{\n}
{if $prev} <a href="{$config.domain}/posts/{$prev.slug}.{$config.pageExt}">Previous</a><br> {/if}{\n}
{if $next} <a href="{$config.domain}/posts/{$next.slug}.{$config.pageExt}">Next</a><br> {/if}{\n}
{if $prev} <a href="{$config.domain}/{$prev.url}">Previous</a><br> {/if}{\n}
{if $next} <a href="{$config.domain}/{$next.url}">Next</a><br> {/if}{\n}
</div>{\n}
{/template}

View file

@ -4,7 +4,7 @@
<h1 class="page-header">{$index.title}</h1>
{foreach $obj in $index.content}
<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>
{$obj.text |noAutoescape}
</div>
@ -13,7 +13,7 @@
<div class="row-fluid">
<p>This blog covers
{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}
{/foreach}
</p>
@ -23,7 +23,7 @@
<div class="row-fluid">
<p>View content from
{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}
{/foreach}
</p>

View file

@ -6,7 +6,7 @@
<p>
{if $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}
{/foreach}
{/if}
@ -20,8 +20,8 @@
{$post.text |noAutoescape}
<ul class="pager">
{if $prev}<li class="previous"><a href="{$config.domain}/posts/{$prev.slug}.{$config.pageExt}">&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 $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}/{$next.url}">Next &rarr;</a></li>{/if}{\n}
</ul>
</div>{\n}
{/template}

View file

@ -13,10 +13,10 @@
{foreach $post in $content.content}
<item>
<title>{$post.title}</title>
<link>{$config.domain}/posts/{$post.slug}.{$config.pageExt}</link>
<link>{$config.domain}/{$post.url}</link>
<pubDate>{$post.date}</pubDate>
<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}
<category><![CDATA[ {$tag.name |noAutoescape} ]]></category>
{/foreach}