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 lets 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": {},
}
_nav

A nested object of your site structure.

.
├── overview
├── products
│   └── product1
│       └── detail
└── index

Given the structure above; this is what the _nav object would look like:

{
  "overview": "overview",
  "products": {
    "product1": {
      "detail": "detail"
    }
  },
  "index": "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).

Lets 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 dont need to be quoted unless youre 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 its best to either have it or dont 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>`;
}
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 dont have to worry about that.

module.exports = exports = function renderer({ Marked }) {

  Marked.preparse = ( markdown ) => {
    return markdown
      .replace(/\™/g, '<span class="markdown-trademark">&trade;</span>')
      .replace(/\’/g, '<span class="markdown-quote">&rsquo;</span>')
      .replace(/\—/g, '<span class="markdown-mdash">&mdash;</span>')
      .replace(/\–/g, '<span class="markdown-ndash">&ndash;</span>')
      .replace(/\.\.\./g, '<span class="markdown-ellipsis">&hellip;</span>');
  };

  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;
};
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
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 its 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.navProp

To allow us to auto-build the docs we need to fill the default props with some examples.

This is the _nav sample data.

{
  "name": "your site",
  "version": "1.0.0",
  "description": "",
  "cuttlebelle": {
    "docs": {
      "navProp": {
        "index": {
          "page1": "page1",
          "page2": {
            "page2/nested": "page2/nested"
          },
          "page3": "page3"
        }
      }
    }
  }
}
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"
        }
      }
    }
  }
}