Cuttlebelle
The react static site generator with editing in mind
Visit our website
π₯ Why yet another static site generator?
All static site generators I have used restrict you to use one layout per page. Todays webdesign needs have outgrown this and we often find ourself either
adding code into our content pages (markdown files, liquid templates) or content into our code.
That makes updating and maintaining a page hard, especially for a non-technical content author.
I needed a generator that can separate content from code as cleanly as possible while still staying a static site generator and as dynamic as possible.
React comes with the component paradigm and was exactly what Iβm looking for.
JSX enables a very easy templating like way to write components while still keeping the power of
javascript. No more templating languages that only do half of what you need. Use javascript to write your layouts.
Contents
Requirements
To use Cuttlebelle you need at least node >= 10
.
β¬ back to top
Install
npm install cuttlebelle -g
Then run cuttlebelle
in your working folder.
π‘Tip: I recommend installing Cuttlebelle globally as it exposes the cuttlebelle
command to your system.
If you for some reason want to install it locally, consider adding a npm script to your package.json
to make
running cuttlebelle easier:
{
"name": "your name",
"version": "1.0.0",
"description": "Your description",
"main": "index.js",
"scripts": {
+ "build": "cuttlebelle",
+ "watch": "cuttlebelle -w",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"cuttlebelle": "^1.0.0"
}
"keywords": [],
"author": "",
"license": "ISC"
}
Then run npm run build
to run cuttlebelle.
β¬ back to top
Getting started
After installing cuttlebelle, create a folders called content
and code
and start populating them.
Each folder with an index.yml
file will become an index.html
in cuttlebelles generated pages.
Your content folder |
Β Β Β Β Β Β Β Β Β Β
Β Β Β Β Β Β Β Β Β Β
| Output |
---|
.
βββ index
βΒ Β βββ index.yml
βΒ Β βββ partial1.md
βΒ Β βββ partial2.md
βββ page1
βΒ Β βββ index.yml
βΒ Β βββ subpage1
βΒ Β βββ index.yml
βΒ Β βββ partial1.md
βΒ Β βββ partial2.md
βββ page2
βΒ Β βββ index.yml
βΒ Β βββ partial1.md
βΒ Β βββ partial2.md
βββ shared
βββ component1.md
βββ component2.md
| β |
.
βββ index.html
βββ page1
βΒ Β βββ index.html
βΒ Β βββ subpage1
βΒ Β βββ index.html
βββ page2
βββ index.html
|
Consider this example to see how pages are constructed with partials and layouts:
An index.yaml page
index.yml | | page layout |
---|
layout: page
title: Homepage
main:
- header.md
- body.md
| + |
import React from "react";
export default ({ title, main }) => (
<html>
<head>
<title>{ title }</title>
</head>
<body>
<main>
{ main }
</main>
</body>
</html>
);
|
A header.md partial
partial header.md | | header layout |
---|
---
layout: header
headline: First post
sub: Clear content separation
---
| + |
import React from "react";
export default ({ headline, sub }) => (
<header>
<h1 className="header__headline">{ headline }</h1>
{
sub
&& <p className="header__sub">{ sub }</p>
}
</header>
);
|
A body.md partial
partial body.md | | body layout |
---|
---
layout: body
headline: First post
---
**Hello world**
| + |
import React from "react";
export default ({ _body, headline }) => (
<article>
<h2>{ headline }</h2>
<div className="body-text">{ _body }</div>
</article>
);
|
Will give us this HTML
resulting static HTML file |
---|
<!DOCTYPE html>
<html>
<head>
<title>Homepage</title>
</head>
<body>
<main>
<header>
<h1 class="header__headline">First post</h1>
<p class="header__sub">Clear content separation</p>
</header>
<article>
<h2>First post</h2>
<div class="body-text"><strong>Hello world</strong></div>
</article>
</main>
</body>
</html>
|
β¬ back to top
Usage
CLI
cuttlebelle
This will generate all pages into the site
folder (unless specified otherwise).
Init
For when youβre just starting out on a new project use the init
option. It will generate the folder structure for you and add some files for you to get
started quickly.
cuttlebelle init
Watch
You can also run the highly optimized watch while adding content or developing your layouts.
cuttlebelle watch
This command will first build your pages and then watch for changes in any of them.
It will dutifully only build the absolute minimum of pages once it detects a change somewhere. It is so eager to only build those pages that it thinks are
relevant that it misses sometimes. In cases where you add content from the _pages
prop in one of your layouts for instance. I have added a new and somewhat
genius trick to catch cases like that.
Introducing the double save TM
If you feel like the watch may have missed a page and you donβt want to leave your editor to complain about it to the watch, just save your file twice quickly
like a double click. The watch will detect the double saveTM and generate all pages for you again.
π‘Tip: You can change the timeout of the watch to detect a double save via the watchTimeout
setting.
Watch flags
No generator flag
Sometimes you may only want to start a watch and not rebuild all pages. For that use the no-generate
option:
cuttlebelle watch --no-generate
Silent flag
The watch notifies you each time it encounters an error so you donβt have to watch the watch. You can disable that behavior via the silent option.
cuttlebelle watch --silent
Docs
Cuttlebelle comes with a nifty feature that will auto-document your layouts if you use the right propTypes and comments.
Read more about how in the self documenting section.
cuttlebelle docs
Version
To display the version of your cuttlebelle install all you have to do is run the version flag:
cuttlebelle --version
Help
Of course there is also a help option. Just run it with the help flag:
cuttlebelle help
β back to Usage
Your content
The default folder structure divides content into the content/
folder and the layout and component react files into the code/
folder.
.
βββ content/ # The content folder
βΒ Β βββ page1/ # Each folder represents a page and will be converted to `page1/index.html`
β β # π‘ As long as it contains an `index.yml` file.
β β
βΒ Β βββ index/ # The index folder is treated as the homepage and converted to `/index.html`
β β
βΒ Β βββ page2/ # You can nest pages by nesting them in the folder structure
β β
βΒ Β Β Β βββ subpage1/ # As long as this folder has an `index.yml` file
β # it will be converted to `page2/subpage1/index.html`
β
βββ assets/ # The assets folder, every file you add here will be moved to your output.
βββ code/ # The `code` folder is where your layout lives
( π‘ All folders can be configured in your package.json
file via the cuttlebelle customizations.)
Now letβs look into one folder:
.
βββ content
Β Β βββ page1
Β Β βΒ Β βββ index.yml # This folder includes an `index.yml` file so it will be converted into
β β # a page in the output of cuttlebelle.
Β Β βΒ Β βββ partial1.md # The partials are all in markdown format and can have any name.
Β Β βΒ Β βββ partial2.md # You can reference partials from your index.yml or another partial.
β
Β Β βββ shared # A folder wonβt be generated if it doesnβt have an `index.yml` file
Β Β βββ component1.md # You can use such folders to share partials between pages
Β Β βββ component2.md # This is just a suggestion. Partials can live anywhere.
Your index.yml
A typical index.yml
file could look like this:
layout: page
title: Homepage
main:
- feature-image.md
- cta.md
- contact-cards.md
- /shared/footer.md
header: header.md
content: Hello world
( π‘ All variables that are defined inside a page are available as props under { _pages }
to all partials.)
Your partials
And a typical partial.md
file could look like this:
--- # Each markdown file can have frontmatter
layout: cards # The power of cuttlebelle is each partial has itβs own layout
# The layout of partials defaults to `partial` if itβs not set
headline: Partial headline # You can add any number of variables
cards: # Even arrays
- id: ID1 # Or objects
title: Card1
content: content for first card
- id: ID2
title: Card2
content: content for second card
---
Content
<!-- The content of the markdown file is exposed as { _body } to the props -->
( π‘ Of course all variables are again available as props to the layout by their own name.)
β back to Usage
Your assets
All files included inside the assets/
folder are moved to site/assets/
. This is where you should keep your CSS, SVGs and images.
Just create a prop inside your index.yml
pages to include them into your pages:
content/index/index.yml
:
layout: layout/homepage
title: Homepage
stylesheet: homepage
main:
- /shared/header.md
- homepage.md
- /shared/footer.md
aside:
- nav.md
- callout.md
code/layout/homepage.js
import React from "react";
export default ({ title, stylesheet, title, main, aside }) => (
<html>
<head>
<title>{ title }</title>
{ stylesheet != undefined
? ( <link rel="stylesheet" href={ `/assets/css/${ stylesheet }.css` } /> )
: null
}
</head>
<body>
<main>
<h1>{ title }</h1>
<div>{ main }</div>
</main>
<aside>
{ aside }
</aside>
</body>
</html>
);
/assets/homepage.css
main {
background: rebeccapurple;
}
aside {
background: hotpink;
}
β back to Usage
Your layout
The layout are all react components. You have to assign a layout to each page and partial. Each component will have a
bunch of props exposed to it.
A page layout
A typical component for a page might look like this:
import React from "react";
export default ({ title, partials }) => (
<html>
<head>
<title>{ title }</title>
</head>
<body>
<main>
<h1>{ title }</h1>
<div>{ partials }</div>
</main>
</body>
</html>
);
A partial layout
A typical component for a partial might look like this:
import React from "react";
export default ({ _body, title }) => (
<article>
<h2>{ title }</h2>
<div>{ _body }</div>
</article>
);
( π‘ You can access the page your partial was called in via: props._pages[ props._ID ]
.)
Props
A file will receive the following props:
prop name | description | Example |
---|
_ID | The ID of the current page | props._ID |
_self | The relative path to the content file; can be md or yaml file | props._self |
_isDocs | A boolean value, true in docs context only | props._isDocs |
_parents | An array of all parent pages IDs | props._parents |
_body | The body of your markdown file (empty for index.yml files) | props._body |
_pages | An object of all pages and their props; with ID as key | props._pages.map() |
_nav | A nested object of your site structure | Object.keys( props._nav ).map() |
_globalProp | A prop that can be set globally from the package.json | props._globalProp |
_storeSet | You can set data to persist between react components by setting them with this helper | props._storeSet({ variable: "value" }) |
_store | To get that data just call this prop function | props._store() |
_relativeURL | A helper function to make an absolute URL relative | props._relativeURL( URL, yourLocation ) |
_parseMD | A helper function to parse markdown into HTML | props._parseMD('Your **markdown**!') |
_parseYaml | A helper function to parse yaml into an object | props._parseYaml('test:\n - one\n - two') |
_parseReact | A helper function to parse a react component into a string | props._parseReact( <Component prop="hi"/> ) |
Plus all other variables declared inside the file either as frontmatter
or in the yaml
files.
Async data fetching
If you require data from an API or some other async operations you can use the static getInitialProps
method in your component:
import React, { Component } from 'react';
class GetData extends Component {
static async getInitialProps( props ) {
const data = await FetchMyDataFromSomewhere( props._ID );
return { data };
}
render() {
return (
<div>
My Data: { this.props.data }
</div>
);
}
}
export default GetData;
getInitialProps
will be executed before we render the HTML and whatever you return from it will be passed into your react component as a prop.
The function will have access to all props of that partial as well.
( π‘ Make sure you return an object so that you can find your prop again.)
β back to Usage
Customizations
Cuttlebelle can be customized via your own package.json
file.
( π‘ You can generate it via npm init
if you donβt have package.json
.)
See below all configuration with default values:
{
"name": "your name",
"version": "1.0.0",
"description": "Your description",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
+ "cuttlebelle": {
+ "folder": {
+ "content": "/content/",
+ "code": "/code/",
+ "site": "/site/",
+ "docs": "/docs/",
+ "assets": "/assets/",
+ "index": "index",
+ "homepage": "index"
+ },
+ "layouts": {
+ "page": "page",
+ "partial": "partial"
+ },
+ "site": {
+ "root": "/",
+ "doctype": "<!DOCTYPE html>",
+ "redirectReact": true,
+ "markdownRenderer": "",
+ "watchTimeout": 400,
+ "browserSync": {},
+ "globalProp": {},
+ },
+ "docs": {
+ "root": "files/",
+ "index": ".template/docs/layout/index.js",
+ "category": ".template/docs/layout/category.js",
+ "IDProp": "page2",
+ "navProp": {},
+ "pagesProp": {}
+ }
+ },
"keywords": [],
"author": "",
"license": "ISC"
}
A breakdown:
"cuttlebelle": {
"folder": {
"content": "content/",
"code": "code/",
"assets": "assets/",
"site": "site/",
"docs": "docs",
"index": "index",
"homepage": "index"
},
"layouts": {
"page": "page",
"partial": "partial"
},
"site": {
"root": "/",
"doctype": "<!DOCTYPE html>",
"redirectReact": true,
"markdownRenderer": "",
"watchTimeout": 400,
"browserSync": {},
"globalProp": {}
},
"docs": {
"root": "files/",
"index": ".template/docs/layout/index.js",
"category": ".template/docs/layout/category.js",
"IDProp": "page2",
"selfProp": "body.md",
"navProp": {
"index": {
"page1": "page1",
"page2": {
"page2/nested": "page2/nested"
},
"page3": "page3"
}
},
"pagesProp": {
"page1": {
"_url": "/page1",
"title": "Page 1"
},
"page2": {
"_url": "/page2",
"title": "Page 2"
},
"page2/nested": {
"_url": "/page2/nested",
"title": "Nested in page 2"
},
"page3": {
"_url": "/page3",
"title": "Page 3"
},
"index": {
"_url": "/",
"title": "Homepage"
}
}
}
}
β¬ back to top
Self-documenting
Because you now can separate the content flow from the development flow you will still need to communicate what partials and layouts the content authors have
to their disposal and how they might use it.
Cuttlebelle has a built in feature that will generate documentation for your components automatically as long as you use
PropTypes and a comment above them that reflects the yaml
.
Cards.propTypes = {
level: PropTypes.oneOf([ '1', '2', '3', '4', '5', '6' ]).isRequired,
hero: PropTypes.bool,
cards: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
href: PropTypes.string,
})
).isRequired,
};
You can also hide a component from the docs by adding the @disable-docs
to the main comment before declaring your component:
import PropTypes from 'prop-types';
import React from "react";
const Hidden = ({ _body, title }) => (
<article className={`globalheader`}>
<h1>{ title }</h1>
{ _body }
</article>
);
Hidden.propTypes = {
title: PropTypes.string.isRequired,
_body: PropTypes.node.isRequired,
};
export default Hidden;
Once all your components have those comments cuttlebelle can generate the docs for you. All you have to do it run:
cuttlebelle docs
The docs will be generated by default in the docs/
folder of your project.
Supported PropTypes
- PropTypes.array
- PropTypes.bool
- PropTypes.number
- PropTypes.object
- PropTypes.string
- PropTypes.node
- PropTypes.element
- PropTypes.oneOfType
- PropTypes.arrayOf
- PropTypes.objectOf
- PropTypes.shape
- PropTypes.any
PropTypes that canβt be passed with YAML
- PropTypes.func
- PropTypes.symbol
- PropTypes.instanceOf
β¬ back to top
Build
We use Yarn on this project to ensure deterministic builds across node versions
To contribute to this still young project you need to install itβs dependencies and run a watch to transpile the files.
yarn install
yarn run watch
( π‘ Please look at the coding style and work with it, not against it :smile:.)
β¬ back to top
Tests
Cuttlebelle is being automatically tested on the below systems and node versions.
https://travis-ci.org/cuttlebelle/cuttlebelle.svg?branch=master
OS | Node version | Node version | Node version |
---|
Linux | ~8 | ~10 | ~12 |
OSX | ~8 | ~10 | ~12 |
I got an end-to-end test script that compares fixtures to what cuttlebelle
generates. In each of those folders I test for specific things and make sure
the checksum of the generated files match the fixtures. In addition to that I created as many
unit tests as I can via Jest.
Flow types are also checked where they are specified.
yarn run test
to run all testsyarn run test:end-to-end
will run the end-to-end test onlyyarn run test:unit-test
will run the unit test onlyyarn run test:flow
will run the flow type testyarn run test:detail
will give you coverage infos for the unit testsyarn run test:watch
will spin up the jest watch
β¬ back to top
Release History
See CHANGELOG.
β¬ back to top
License
Copyright (c) Dominik Wilkowski. Licensed under GNU-GPLv3.
β¬ back to top
};