Layout
Each layout has some default props it inherits from Cuttlebelle.
Those props begin with an underscore and are described below.
Pure component
A quick default pure component. 99% of the time this is what you want in your Cuttlebelle layouts:
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
/**
* The PureComponent description
*/
const PureComponent = ({ _body, title }) => (
<Fragment>
<span>{ title }</span>
{ _body }
</Fragment>
);
PureComponent.propTypes = {
/**
* title: Our example title
*/
title: PropTypes.string,
/**
* _body: (text)(5)
*/
_body: PropTypes.node.isRequired,
};
PureComponent.defaultProps = {};
export default PureComponent;
Class component
A quick class component. Use this when you have additional methods you want to run or when you want to fetch async data via the getInitialProps
method.
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
/**
* The ClassComponent description
*/
class ClassComponent extends Component {
constructor({ title, partials }) {
super();
this.title = title;
this.partials = partials;
}
render() {
return (
<Fragment>
<span>{ this.title }</span>
<div>{ partials }</div>
</Fragment>
);
}
};
ClassComponent.propTypes = {
/**
* title: Our example title
*/
title: PropTypes.string,
/**
* partials: (partials)(3)
*/
partials: PropTypes.node.isRequired,
};
ClassComponent.defaultProps = {};
export default ClassComponent;
getInitialProps
To run an async function in your layout to e.g. fetch an API, declare a static method called getInitialProps
inside your layout.
After you got the data from your async operation, make sure you return the data in an object with a key so the data can be merged back into the props for your
constructor
or render
method.
import React, { Component } from 'react';
class GetData extends Component {
static async getInitialProps({ _ID }) {
const data = await FetchMyDataFromSomewhere( _ID );
return { data };
}
constructor({ data }) {
super();
this.data = data || 'no data';
}
render() {
return (
<div>
My Data: { this.data }
</div>
);
}
};
export default GetData;
The getInitialProps
method gets all default props passed.
_ID
The _ID
of the current page. IDs in cuttlebelle point to a page and a derived from their relative url. They are therefor unique to each page.
The exception is the index page. This page will be named index
by default and can be changed in the settings.
/foo/bar/ => foo/bar
/foo/ => foo
/ => index
_self
_self
is the relative path of the current partial markdown or index.yml.
console.log( _self ); // page2/index.yml
console.log( _self ); // page1/partial1.md
console.log( _self ); // page4/subpage/partial-x.md
_isDocs
_isDocs
is a boolean that let’s you know if your layout is running in docs content or not. In the docs you may not have all variables available to you so
this comes in handy to write alternative logic so your automated documentation is still spot on.
console.log( _isDocs );
// false when compiling with `cuttlebelle`
// but true for `cuttlebelle docs`
_parents
An array of all parent pages IDs
.
├── overview
├── products
│ └── product1
│ └── detail
└── index
Taking the above file structure into account, the parents for the page product/product1/detail
would be:
[
"products/product1/detail"
"products/product1",
"products",
"index",
]
_storeSet
You can set data to persist between partials by setting them with this helper.
In one partial:
_storeSet({ variable: "value" });
And if you call _store()
inside another partial below you get:
{ variable: "value" }
_store
To get the data set by _storeSet
just call this helper function:
partial1.js
:
_storeSet({ variable: "value" });
partial2.js
:
{ variable: "value" }
_pages
An object of all pages and their props; with _ID
as their key. Each page also get the _url
prop automatically attached.
.
├── overview
├── products
│ └── product1
│ └── detail
└── index
Given the structure above; this is what the _pages
object would look like:
{
"overview": { /* all props and data etc */ },
"products": {},
"products/product1": {},
"products/product1/detail": {}
"index": {},
}
_globalProp
A prop that can be set globally from the package.json
{
"name": "your name",
"version": "1.0.0",
"description": "Your description",
"main": "index.js",
+ "cuttlebelle": {
+ "site": {
+ "globalProp": "dev"
+ }
+ },
"keywords": [],
"author": "",
"license": "ISC"
}
And inside your components:
import React from 'react';
const Thing = ({ _globalProp }) => (
<div>Global: { _globalProp }</div>
);
export default Thing;
_parseMD
A helper function to parse markdown into HTML.
const markdownString = 'Hello **World**';
_parseMD( markdownString );
The output would be 'Hello <strong>World</strong>'
.
_parseYaml
A helper function to parse YAML into an javascript object.
const yamlString = `
test:
- one
- two
`;
_parseYaml( yamlString );
The output would be [ 'one', 'two' ]
.
_relativeURL
A helper function to make an absolute URL relative. First argument is the target, second is the base.
_relativeURL( '/foo/bar', '/foo' );
This would give us 'bar'
.
In Cuttlebelle, use it with the page _ID
_relativeURL( '/foo/bar', _ID )
If _ID
is e.g. /foobar
then the output of above will be: '../foo/bar'
.
_body
The body of your markdown file (empty for index.yml
files).
Let’s assume a markdown file like below:
Hi there **world**.
And a layout file like so:
import PropTypes from 'prop-types';
import React from 'react';
const Thing = ({ _body }) => (
<div className="body">{ _body }</div>
);
Thing.propTypes = {
/**
* _body: (partials)(3)
*/
_body: PropTypes.node.isRequired,
};
export default Thing;
Then our output would be:
<div class="body">Hi there <strong>world</strong>.<div>
YAML
Cuttlebelle uses YAML to describe a page and more complex layouts.
For YAML, whitespace and indentation is very important so make sure you indent with two spaces consistently.
Comments
Commenting your work helps your future self.
thing: hello # this is the thing we need for x
open: false # and thing needs to be closed
String
Strings don’t need to be quoted unless you’re using special characters. For multi-line strings use the |
character.
Best for strings you want to parse through markdown later.
thing: hello
special: '# headline'
multi-line: |
line 1
line 2
Array
Use arrays for lists of data that can be extended by the content author.
reasons:
- because
- duh
- anyway
Boolean
A boolean value is best used for binary choices. From the content authors perspective it’s best to either have it or don’t declare it at all.
thing: true
thing: false
Object
Objects are more explicit for editing. The content author can see what is declared where.
cta:
text: Buy now
url: /buy
Array - object
Sometimes you need n items of something. Like a link list.
linklist:
- text: Install the thing
url: /install
- text: Use the thing
url: /docs
- text: Enhance the thing
url: /contribute
Object - array
And sometimes one of your objects has n number of items attached to it.
select:
label: Please select a state
selected: NSW
choices:
- VIC
- WA
- NT
- NSW
- QL
- ACT
- SA
- TAS
Extend Markdown
You can extend the built-in markdown parser by defining a markdownRenderer
file inside your site
settings.
Read more about how to set this up in the settings section.
How to
To extend the built-in markdown parser you have to create a file and add the path to that file to your cuttlebelle.site.markdownRenderer
setting inside
your package.json
file.
// export a function called `renderer`
module.exports = exports = function renderer({
Marked, // The Marked instance you want to extend
_ID, // The ID of the current page
_parents, // An array of all parent pages IDs
_storeSet, // The store setter
_store, // The store getter
_nav, // A nested object of your site structure
_globalProp, // A prop that can be set globally from the `package.json`
_relativeURL // A helper function to make an absolute URL relative
}) {
// extend one of the listed methods here
Marked.hr = () => {
return `<hr />`;
};
// and remember to return the Marked instance
return Marked;
};
And if you want to extend two or more methods you add them all onto the Marked
method.
module.exports = exports = function renderer({ Marked }) {
Marked.hr = () => {
return `<hr />`;
};
Marked.blockquote = ( quote ) => {
return `<blockquote>${ quote }</blockquote>`;
};
return Marked;
};
code
The code
default function.
Marked.code = ( code, language ) => {
if( !language ) {
return `<pre><code>${ code }\n</code></pre>`;
}
return `<pre class="language-${ language }"><code>${ code }\n</code></pre>\n`;
}
blockquote
The blockquote
default function.
Marked.blockquote = ( quote ) => {
return `<blockquote>\n${ quote }</blockquote>\n`;
}
html
The html
default function.
Marked.html = ( html ) => {
return html;
}
heading
The heading
default function.
Marked.heading = ( text, level ) => {
return `<h${ level } id="${ text.toLowerCase().replace(/[^\w]+/g, '-') }">${ text }</h${ level }>\n`;
}
hr
The hr
default function.
Marked.hr = () => {
return `<hr>\n`;
}
list
The list
default function.
Marked.list = ( body, ordered ) => {
const type = ordered ? 'ol' : 'ul';
return `<${ type }>\n${ body }</${ type }>\n`;
}
listitem
The listitem
default function.
Marked.listitem = ( text ) => {
return `<li>${ text }</li>\n`;
}
paragraph
The paragraph
default function.
Marked.paragraph = ( text ) => {
return `<p>${ text }</p>\n`;
}
table
The table
default function.
Marked.table = ( header, body ) => {
return `<table>\n<thead>\n${ header }</thead>\n<tbody>\n${ body }</tbody>\n</table>\n`;
}
tablerow
The tablerow
default function.
Marked.tablerow = ( content ) => {
return `<tr>\n${ content }</tr>\n`;
}
tablecell
The tablecell
default function.
Marked.tablecell = ( content, flags ) => {
const type = flags.header ? 'th' : 'td';
const tag = flags.align
? `<${ type } style="text-align:${ flags.align }">`
: `<${ type }>`;
return `${ tag }${ content }</${ type }>\n`;
}
The flag
option can be:
{
header: true || false,
align: 'center' || 'left' || 'right',
}
strong
The strong
default function.
Marked.strong = ( text ) => {
return `<strong>${ text }</strong>`;
}
em
The em
default function.
Marked.em = ( text ) => {
return `<em>${ text }</em>`;
}
codespan
The codespan
default function.
Marked.codespan = ( text ) => {
return `<code>${ text }</code>`;
}
br
The br
default function.
Marked.br = () => {
return `<br>`;
}
del
The del
default function.
Marked.del = ( text ) => {
return `<del>${ text }</del>`;
}
link
The link
default function.
Marked.link = ( href, title, text ) => {
let out = `<a href="${ href }"`;
if( title ) {
out += ` title="${ title }"`;
}
out += `>${ text }</a>`;
return out;
}
image
The image
default function.
Marked.image = ( href, title, text ) => {
let out = `<img src="${ href }" alt="${ text }"`;
if( title ) {
out += ` title="${ title }"`;
}
out += '>';
return out;
}
text
The text
default function.
Marked.text = ( text ) => {
return text;
}
preparse
The preparse
default function.
Cuttlebelle lets you run code on the content of markdown before it goes into the markdown renderer via the preparse
function.
Marked.preparse = ( markdown ) => {
return markdown;
};
Html entities
Encode HTML entities so content authors don’t have to worry about that.
module.exports = exports = function renderer({ Marked }) {
Marked.preparse = ( markdown ) => {
return markdown
.replace(/\™/g, '<span class="markdown-trademark">™</span>')
.replace(/\’/g, '<span class="markdown-quote">’</span>')
.replace(/\—/g, '<span class="markdown-mdash">—</span>')
.replace(/\–/g, '<span class="markdown-ndash">–</span>')
.replace(/\.\.\./g, '<span class="markdown-ellipsis">…</span>');
};
return Marked;
};
Relative links
The script below will make all links relative to your current page.
module.exports = exports = function renderer({ Marked, _relativeURL, _ID }) {
Marked.link = ( href, title, text ) => {
if(
!href.startsWith('http://') &&
!href.startsWith('https://') &&
!href.startsWith('#') &&
!href.startsWith('mailto:') &&
typeof _relativeURL === 'function'
) {
href = _relativeURL( href, _ID );
}
return `<a href="${ href }"${ title ? ` title="${ title }"` : '' }>${ text }</a>`;
};
return Marked;
};
Relative image urls
The script below will make all your relative image urls relative to your current page.
module.exports = exports = function renderer({ Marked, _relativeURL, _ID }) {
Marked.image = ( href, title, text ) => {
let sourcePath = href;
if( !sourcePath.startsWith('http://') && !sourcePath.startsWith('https://') ) {
sourcePath = _relativeURL( href, _ID );
}
let out = `<img src="${ sourcePath }" alt="${ text }"`;
if( title ) {
out += ` title="${ title }"`;
}
out += '>';
return out;
}
return Marked;
};
External links
The script below will add rel="external"
to external links.
module.exports = exports = function renderer({ Marked }) {
Marked.link = ( href, title, text ) => {
let attr = '';
if( href.startsWith('http://') || href.startsWith('https://') ) {
attr = ` `rel="external"``;
}
return `<a href="${ href }"${ title ? ` title="${ title }"` : '' }${ attr }>${ text }</a>`;
};
return Marked;
};
Headlines with classes
Sometimes you want to split the semantic value and the look of a headline. The below script will let your content author use the new markdown syntax:
# [2]headline
which will render <h1 class="display-2">headline</h1>
or # [3]headline
which will render <h1 class="display-3">headline</h1>
.
module.exports = exports = function renderer({ Marked }) {
const headingLevels = {
1: 'display-1',
2: 'display-2',
3: 'display-3',
4: 'display-4',
5: 'display-5',
6: 'display-6',
};
Marked.heading = ( text, level ) => {
let display;
if( text.startsWith('[') ) {
const displayText = text.split(']');
display = displayText[ 0 ].substring( 1 );
text = displayText.splice( 1 ).join(']');
}
else {
display = Object.keys( headingLevels ).reverse()[ level ];
}
return `<h${ level }${ headingLevels[ display ] ? ` class="${ headingLevels[ display ] }"` : `` }>${ text }</h${ level }>`;
};
return Marked;
};
Markdown
Cuttlebelles markdown is a text-to-HTML conversion tool for content authors. Markdown allows you to write using an easy-to-read, easy-to-write plain text format, then convert it to structurally valid HTML.
Paragraph/line break
To create a paragraph simply add a line of text. To add a line break add an empty line.
One paragraph
Still the same paragraph
New paragraph
Headline
To add a headline add the #
symbol in-front of your line. The amount of hashes represents the headline level.
# A headline level 1
## A headline level 2
### A headline level 3
Link
To link to another page just us the [text](link)
notation.
A [link](/checkout) inside text.
Another [link](https://dominik-wilkowski.com) could be to another site.
Image
Adding an image should be done with a proper layout but if you want inline images you can use the notation below.
![image alt text](/assets/logo.png)
Italic
Making some text italic only requires you to use *
.
Text with a single *italic* word.
Bold
Making some text bold only requires you to use **
.
Text with a single **bold** word.
Code
Mark a word as code with the ``` character.
Sometimes we talk `code`.
Code block
If you have a block of code you should us three back-ticks ```.
```
code block
```
Auto-documentation
We use PropTypes to let Cuttlebelle know what each layout needs. It then can generate component documentation for you all automatically.
Cuttlebelle reads the comment right above your PropType declaration for sample data for the documentation site.
Magic partial
You can use the magic string (partials)(2)
to fill your documentation with a partial blocks. The number inside the ()
defines how many
partial blocks will be rendered for you in the documentation.
Page.propTypes = {
/**
* _body: (partials)(4)
*/
_body: PropTypes.node.isRequired,
}
Magic text
You can use the magic string (text)(2)
to fill your documentation with some placeholder text. The number inside the ()
defines how many
sentences will be rendered for you in the documentation.
Page.propTypes = {
/**
* description: (text)(2)
*/
description: PropTypes.node.isRequired,
}
Optional / required
Tell your content authors that a certain prop is optional or required.
Page.propTypes = {
/**
* title: Homepage
*/
title: PropTypes.string.isRequired, // This prop is required
/**
* subtitle: The place to be
*/
subtitle: PropTypes.string, // This prop is optional
}
String
Define your prop as a string and warn if the type is mismatched.
Page.propTypes = {
/**
* title: Homepage
*/
title: PropTypes.string,
}
Number
Define your prop as a integer and warn if the type is mismatched.
Page.propTypes = {
/**
* years: 12
*/
years: PropTypes.number,
}
Keyword
If you have a finite of words you allow.
Page.propTypes = {
/**
* type: command
*/
type: PropTypes.oneOf([ 'command', 'ask' ]),
}
Array
A list of things like strings, numbers booleans etc.
Page.propTypes = {
/**
* list:
* - one
* - two
* - three
* - 4
* - true
*/
list: PropTypes.array,
}
Object
An object of things. This is often more content author friendly as it is more explicit.
Page.propTypes = {
/**
* link:
* text: Homepage
* url: /
*/
link: PropTypes.shape({
text: PropTypes.string,
url: PropTypes.string,
}),
}
Array - object
A list of objects. Some values are marked as required here just as an example.
/**
* linklist:
* - text: Install the thing
* url: /install
* - text: Use the thing
* url: /docs
* - text: Enhance the thing
* url: /contribute
*/
linklist: PropTypes.arrayOf(
PropTypes.shape({
text: PropTypes.string,
url: PropTypes.string.isRequired,
})
).isRequired,
Object - array
Sometimes you need to define one part of an object with an unknown amount of options.
Page.propTypes = {
/**
* select:
* label: Please select a state
* selected: NSW
* choices:
* - VIC
* - WA
* - NT
* - NSW
* - QL
* - ACT
* - SA
* - TAS
*/
select: PropTypes.shape({
label: PropTypes.string,
selected: PropTypes.string,
choices: PropTypes.array,
}),
}
Hide from docs
You can hide a layout for the documentation when you think it would just confuse the content author or when it’s just used as a code component.
Place the @disable-docs
string anywhere into your component.
import PropTypes from 'prop-types';
import React from 'react';
/**
* The partial component
*
* @disable-docs
*/
const Partial = ({ _body }) => (
<div className="body">{ _body }</div>
);
Partial.propTypes = {
/**
* _body: (partials)(4)
*/
_body: PropTypes.node.isRequired,
};
Partial.defaultProps = {};
export default Partial;
Settings
The default settings of Cuttlebelle can be changed by adding an cuttlebelle
object to your package.json
file.
All below examples show the default values.
folder.content
The folder in which your content lives. Path relative to your package.json
file.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"folder": {
"content": "content/"
}
}
}
folder.code
The folder in which your layout components live. Path relative to your package.json
file.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"folder": {
"code": "code/"
}
}
}
folder.site
The folder in which your site content is supposed to be generated into. Path relative to your package.json
file.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"folder": {
"site": "site/"
}
}
}
folder.docs
The folder in which your layout documentation is supposed to be generated into. Path relative to your package.json
file.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"folder": {
"docs": "docs/"
}
}
}
folder.index
The name of the YAML file that describes the partials of a page.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"folder": {
"index": "index"
}
}
}
folder.homepage
The name of the homepage folder.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"folder": {
"homepage": "index"
}
}
}
layouts.page
The name and location of the default page layout. Path relative to your code
folder.
The .js
extension should be omitted.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"layouts": {
"page": "page"
}
}
}
layouts.partial
The name and location of the default partial layout. Path relative to your code
folder.
The .js
extension should be omitted.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"layouts": {
"partial": "partial"
}
}
}
site.root
What should cuttlebelle append to links?
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"site": {
"root": "/"
}
}
}
site.doctype
Because react does not allow you to add complex doctypes to an HTML page, this is where you can declare your own custom doctype.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"site": {
"doctype": "<!DOCTYPE html>"
}
}
}
site.redirectReact
You can disable redirecting import
calls to the locally installed react instance of cuttlebelle rather than your local folder.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"site": {
"redirectReact": true
}
}
}
site.markdownRenderer
The path to a file that module.exports
an Marked.Renderer() object. Learn more about it in the
marked documentation.
The only Cuttlebelle addition is the preparse
key that will be run before we go into the markdown parsing.
Path relative to your package.json
file.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"site": {
"markdownRenderer": ""
}
}
}
site.watchTimeout
This is the time in milliseconds the watch waits to detect a double saves action.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"site": {
"watchTimeout": 400
}
}
}
site.browserSync
You can overwrite some browser sync settings. For full options check out the documentation for BrowserSync
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"site": {
"browserSync": {
"port": 1337,
"open": false
}
}
}
}
site.globalProp
You can defined a global prop here that is passed into every component.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"site": {
"globalProp": {
"fo": "bar"
}
}
}
}
docs.root
What is the root folder called where all categories are generated in.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"docs": {
"root": "files/"
}
}
}
docs.index
The path to the index layout file. Path relative to your package.json
file.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"docs": {
"index": ".template/docs/layout/index.js"
}
}
}
docs.category
The path to the category layout file. Path relative to your package.json
file.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"docs": {
"category": ".template/docs/layout/category.js"
}
}
}
docs.IDProp
To allow us to auto-build the docs we need to fill the default props with some examples.
This is the _ID
sample data.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"docs": {
"IDProp": "page2"
}
}
}
docs.pagesProp
To allow us to auto-build the docs we need to fill the default props with some examples.
This is the _pages
sample data.
{
"name": "your site",
"version": "1.0.0",
"description": "",
"cuttlebelle": {
"docs": {
"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"
}
}
}
}
}