blacksmith
A generic static site generator built using flatiron
, plates
, and marked
.
- Creating a site with Blacksmith
- Components of a Blacksmith site
- How does blacksmith render my Site?
- Tests
blacksmith
Creating a site with Blacksmith sites have a specific directory structure for storing the various parts of your site: settings, layout, partials, pages, and content. Content exists in two forms:
- Markdown files that
blacksmith
will render. - Supporting content such as css and images.
All content will be rendered into /public
. To render a blacksmith site:
Install Blacksmith
$ npm install blacksmith -g
Render a Site
#
# Defaults to `cwd`
#
$ blacksmith /path/to/your/site
Here's an example of a simple blog that blacksmith
would render.
/site-name
#
# Settings for this blacksmith site.
#
.blacksmith
/content
#
# Actual Markdown content to render.
#
/posts
a-post.md
another-post.md
/layouts
#
# Layouts to use for pages. You can specify
# multiple layouts, but default.html is ... the default.
#
default.html
/metadata
#
# Metadata entities which can be reference in content metdata
#
/authors
author-name.json
/partials
#
# HTML for partials inside of pages
#
post.html
sidebar.html
/pages
#
# Metadata for rendering specific pages
#
index.json
post.json
/public
#
# Any additional files for viewing the site. All
# rendered HTML will be placed here. e.g.:
#
/css
styles.css
/img
favicon.png
blacksmith
site
Components of a Each blacksmith
site defines a hierarchical set of components which can be composed to create any type of site you want! A couple of examples:
- A Blog
- A Documentation Site
- A Custom Splash Page or Content Site
Lets examine each of these components and where they are stored in your filesystem:
Site Settings
The settings for a given blacksmith
site are stored in the .blacksmith
file located in the root directory of your site.
{
//
// Default Layout. Specifying a name here will cause Blacksmith to attempt to read
// /layouts/layout-name.json for rendering info. If no layout-name.json file is found
// blacksmith will use `/layouts/layout-name.html`
//
// Default: default
//
"layout": "layout-name",
"pages": {
//
// These settings will be used for all content in
// /content/posts
//
"post": {
"layout": "custom-layout-for-posts",
"partials": {
"html-id": "additional-partials",
"another-id": "for-this-page-only"
}
}
}
}
You can define settings for all components of your site in the .blacksmith
file or break them out into individual files within component directories. For example, the above example is equivalent to:
/.blacksmith
{
"layout": "layout-name"
}
/pages/post.json
{
"layout": "custom-layout-for-posts",
"partials": {
"html-id": "additional-partials",
"another-id": "for-this-page-only"
}
}
Layouts
Layouts are fully-formed HTML files; they are the top of the rendering hierarchy. All content will be inserted into a layout inside of the HTML element with the id="content"
. Lets look at the worlds simplest layout:
<html>
<head>
<title>Simple Layout</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div id="content">
<!-- All rendered content place here-->
</div>
</body>
</html>
Settings for a layout may be specified in your .blacksmith
file or in /layouts/layout-name.json
. Lets look at an example:
/layouts/layout-name.json NOTE: These properties are not yet supported. See Roadmap for more details.
{
//
// The "template" property specifies which HTML file within the /layouts directory
// to use when rendering this layout. In this way a single HTML file can be reused with
// different partials.
//
// If no template is specified then blacksmith will look for /layouts/layout-name.html
// when rendering.
//
"template": "shared-layout.html",
//
// The "partials" property specifies which HTML partial (or partials) to insert at a given
// id within the layout. These partials will be rendered with the metadata for a given
// individual page with content.
//
"partials": {
"sidebar-container": "sidebar",
//
// Multiple partials can be concatentated into a single HTML id.
//
"multi-container": ["partial1", "partial2"]
}
}
Pages
Pages allows you to specify what type of content you wish to render and where to render it within a given layout. As with layouts, pages may be specified in your .blacksmith
file or in /pages/page-name.json
. Lets look at two examples from our blog:
/pages/index.json
The above example demonstrates particular interest: index.json will always be used to render index.html. The following example specifies that blacksmith
should render a list of post
content, which should be truncated and limited to a maximum of 20 posts. Note: All lists are sorted descending by the date the content was created.
{
//
// The "content" property specifies what blacksmith should render
// inside the HTML element with id="content"
//
"content": {
"list": "post",
"truncate": true,
"limit": 20
}
}
/pages/post.json
The rendering information for an individual post is much simpler than our index.json
. By creating this file, blacksmith
will:
- Render all Markdown files in
/content/posts
using the partial found at/partials/post.html
. - Render the partial found at
/partials/sidebar.html
with the metadata for each Markdown file in/content/posts
and append it to the HTML element with id="content"
{
"partials": {
"content": "sidebar"
}
}
It is important to take note of the convention:
Convention: By default, page content will be rendered in a partial of the same name.
Alternatively we could have specified an explicit partial to use. If no partial is specified and the default partial is not found then no metadata would be rendered (in this case author name, date of post, etc).
All Page Options
A list of all available options that can be specified in a page.json
file are listed below:
{
//
// Specifies the layout for the page.
// Default: Layout for the site.
//
"layout": "custom-layout-for-page",
//
// Specifies a mapping of HTML ids to partial(s). If "content" is specified
// then it is appended after the rendered markdown content.
//
"partials": {
"html-id": "partial-name",
"another-id": ["multiple-partials"]
},
//
// Case 1: Rendering content with a partial
//
"content": "custom-partial"
//
// Case 2: Consolidating multiple content sources in a list.
//
"content": {
"list": "post",
"truncate": true,
"limit": 20
}
}
Content
In blacksmith
, "content" is raw Markdown and supporting files (e.g. images) located within the /content
directory. Content for individual pages should be placed under /content/page-name
.
In our example all content for the post
page should be placed under /content/posts
. It is important to take note of the convention:
Convention: Page names are always singular, but their content folder will always be plural.
The content for an individual page may also be a directory where supporting files (such as images can be placed). For example if we wanted to create a post with images, the directory structure would be:
/site-name
/content
/posts
/post-with-supporting-files
content.md
an-image.png
another-image.png
some-code.js
The directory structure will be respected, but the /content
prefix will be dropped. So the full-url for an-image.png
would be http://your-site.com/post-with-supporting-files/an-image.png
.
Specifying Metadata
All metadata associated with content is stored within the individual Markdown files as link definitions prefixed with 'meta:'. Because of a small limitation in the Markdown format you must use the following syntax to specify metadata:
[meta:author] <> (Author Name)
[meta:title] <> (A Really Long Title Different from the Filename)
[meta:nested:values] <> (Are also supported)
Content Snippets
In large content pages it is often useful to have examples or references to other files to be inserted later in rendering. A classic example is code samples in a blog post. This is easy in blacksmith
using Content Snippets. Consider the content:
/content
/dir-post
index.md
whatever.js
This is a piece of markdown content to be rendered later. It has a reference to the file "whatever.js"
<whatever.js>
In this example <whatever.js>
in index.md
would be replaced with the contents of /content/dir-post/whatever.js
. Note: Only files with text extensions (.js, .rb, .txt, etc) less than 1MB will be inserted.
Code Highlighting
Because blacksmith
uses marked
we get the benefit of Github Flavored Markdown through highlight.js
. By default code highlighting is enabled. All you need to do is add some CSS for your site. For example:
pre {
border: 1px solid #e0ded3;
border-radius: 4px;
margin: 10px 10px 40px 10px;
padding: 10px;
background-color: #f0efe8;
overflow: auto;
font-size: 14px;
font-family: Consolas, "Liberation Mono", Courier, monospace;
}
code {
white-space: pre;
color: rgba(0,0,0, 1);
}
code .keyword { font-weight: bold; color: #6089d4; }
code .string, code .regexp { color: green }
code .class, code .special { color: #6089d4 }
code .number { color: red }
code .comment { color: grey }
Truncated Content
It is necessary to truncate large content pages when rendering them within compilations (i.e. lists). This is supported by blacksmith
with a special identifier in your Markdown files. For example:
/content/posts/dir-post/index.markdown
A simple post that is truncated with content snippets and metadata that is not used in the post.
##!!truncate
<file1.js>
A second file reference with spaces
< file2.js >
[meta:author]: <> (Charlie index)
When rendered in a page like index.json
with { truncate: true }
only the content above ##!!truncate
will be rendered.
Partials
Partials are HTML fragments which are inserted into a layout, a page, or another partial. All partials are rendered with plates
.
Default Metadata
{
"page-details": {
"author": {
//
// Contents of /metdata/authors/author-name.json
//
},
"href": "/full/path/to/page",
"date": "Fully qualified date page was published",
"files": {
"js": [{ "filename": "file1.js", "url": "/full/path/to/file1.js" }],
"img": [{ "filename": "img1.png", "url": "/full/path/to/img1.png" }]
}
}
}
Customizing Partials
All metadata is placed into partials using a set of simple plates
conventions. In the future it may be possible to customize these conventions but that is not currently planned.
- Map URL-like string keys to href="keyname":
map.where('href').is(key).use(key).as('href');
- Insert "content" into class="content":
map.class('content').use('content').as('value');
- Map string keys to class="keyname":
map.class(key).use(key)
- Map everything else to class="keyname":
map.class(key).use(key);
- Recursively map Array keys:
exports.map(metadata[key][0], map);
- Recursively map Object keys:
exports.map(metadata[key], map);
Conditional Metadata
Some content should only exist in partials when a given key is present in the metadata. This is supported by blacksmith
in the following way: The keys in remove
must be specified at the fully qualified path into an Object.
/partials/partial-name.json
{
"remove": {
"page-details": {
"author": ["github", "twitter"]
}
}
}
In the corresponding HTML file for the partial any elements with "class=if-[keyname]" will be removed if there is no value for keyname
. In the above example using the following template:
/partials/partial-name.html
<div class="page-details">
<div class="author">
<h3>About the author</h3>
<div class="name">Author Name</div>
<div class="if-github">
<a href="github-url" class="github">Author Github</a>
</div>
<div class="if-twitter">
<a href="twitter-url" class="twitter">Author Twitter</a>
</div>
</div>
</div>
and this set of metadata:
{
"page-details": {
"author": {
"name": "Charlie Robbins",
"github": "indexzero",
"github-url": "https://github.com/indexzero"
}
}
}
The output would be the following. Notice how everything inside if-twitter
has been removed.
<div class="page-details">
<div class="author">
<h3>About the author</h3>
<div class="name">Charlie Robbins</div>
<div class="if-github">
<a href="https://github.com/indexzero" class="github">indexzero</a>
</div>
</div>
</div>
Metadata References
It is useful when writing a content page to use a key to reference a larger section of metadata stored elsewhere. These other sections of metadata are called metadata references and are loaded by blacksmith
from /metadata
. For example:
/metdata/dogs/sparky-mcpherson.json
{
"name": "sparky-mcpherson",
"breed": "labridoodle",
"color": "brown",
"age": "3",
"temperment": "friendly"
}
/content/posts/a-post.md
(... Rest of Content ...)
[meta:dog] <> (Sparky McPherson)
When the content for this page finally gets rendered to a partial the metadata will not be { 'dog': 'sparky' }
, it will be the entire object stored in /metadata/dogs/sparky.json
. By convention:
Convention: For a given `key:value` blacksmith will lookup metadata references for the `value`
in the directory named with the plural of that `key`.
In the above example the key:value
pair is dog:Sparky Mcpherson
so we attempt to lookup sparky-mcpherson
within /metadata/dogs/sparky-mcpherson.json
.
Note: "Authors" is a special metadata reference which will always be within the metadata.page-details
.
blacksmith
render my Site?
How does It's safe to skip this if you're not tinkering with blacksmith
internals.
Rendering Process
In order to render list pages with all options supported by blacksmith
it is necessary to perform multiple rendering passes on a given site to fully render it. Lets examine that process start to finish:
- 1. Rendering starts
Rendering starts when blacksmith('/full/path/to/your/site', function () { ... })
is invoked. This tells blacksmith
where the root directory for a given site is.
- 2. Load all HTML and rendering settings
Inside Site.prototype.load
will load all HTML and rendering settings stored in:
- .blacksmith
- /layouts/*.json || *.html
- /pages/*.json
- /partials/*.html
We need to load all of these files before any rendering can begin because they store the necessary settings for rendering.
- 3. Read everything under
/content
Before layout, page, and partial rendering can take place all content (i.e. Markdown and supporting files) must be known to blacksmith
. In our example how can we render the page represented by /pages/index.json
without all rendered content? Site.prototype.readContent
recursively reads /content
building this data structure:
/content
/posts
a-post.md
another-post.md
/dir-post
file1.js
file2.js
index.markdown
{
posts: {
"a-post": { _content: { html: "..", markdown: "..", metadata: { ... } } },
"another-post": { _content: { html: "..", markdown: "..", metadata: { ... } } }
"dir-post": {
_content: {
html: "HTML rendered from Markdown",
markdown: "Markdown in index.markdown",
metadata: {
"page-details": {
source: "/dir-post/index.markdown",
title: "Title of page in metadata" || "href of page",
date: "Saturday, November 10, 2012",
href: "/dir-post",
"files": {
"js": [{ "filename": "file1.js", "url": "/full/path/to/file1.js" }],
"img": [{ "filename": "img1.png", "url": "/full/path/to/img1.png" }]
}
}
}
},
_files: ['file1.js', 'file2.js']
}
}
}
-
- Render all pages
Rendering Data Structure
As we discussed blacksmith
uses hierarchical rendering components. Each component inherits the relevant properties from its parent entity. The full hierarchy is:
- Site
- Layout
- Partial
- Page
- Content
- Partial
- Layout
Notice that partials can be specified by both layouts and pages. In the event of a conflict the partials specified by the page will always be preferred.
Lets examine a fully-formed data structure for rendering each of our rendering data structures:
Page
{
//
// There are all inherited from Site and Layout that this
// page is associated with
//
"layout": "layout-name"
"html": {
"layouts": { ... },
"partials": { ... }
},
"partials": {
"html-id": "sidebar",
"another-id": "breadcrumbs",
"multiple-partials": ["that-can", "be-overridden"]
},
//
// Metadata is extracted from link definitions on the page prefixed,
// with "meta:{key}". The {key} is what is stored in the metadata property.
//
// e.g. [meta:author] <> (indexzero)
//
"metadata": {
"author": "indexzero"
}
}
Tests
All tests are written with vows and can be run with npm:
$ npm test
Roadmap
- RSS Feed
- Only render "dirty" files (i.e. those not modified since last render).
- Customize list sorting by key and direction.
- Support nested partials.
- Support rendering page depths greater than 1.
- Support "template" and "partials" in layouts.
- Investigate this bug: flatiron/plates#93