Research
Security News
Malicious npm Package Targets Solana Developers and Hijacks Funds
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
cuttlebelle
Advanced tools
The react static site generator that separates editing and code concerns
The react static site generator with editing in mind
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.
yarn global add cuttlebelle
npm install cuttlebelle -g
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 yarn build
or npm run build
to run cuttlebelle.
After installing cuttlebelle, create a folder called content
and start populating it.
Your content folder | Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β | Output |
---|---|---|
| β |
|
Consider this example to see how pages are constructed with partials and layouts:
An index.yaml page
index.yml | page layout | |
---|---|---|
| + |
|
A header.md partial
partial header.md | header layout | |
---|---|---|
| + |
|
A body.md partial
partial body.md | body layout | |
---|---|---|
| + |
|
Will give us this HTML
resulting static HTML file |
---|
|
cd /path/to/my/project
cuttlebelle
This will generate all pages into the site
folder (unless specified otherwise).
You can also run our 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.
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
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
Of course there is also a help option. Just run it with the help flag:
cuttlebelle help
The default folder structure divides content into the content/
folder and the layout and component react files into the src/
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
βββ src/ # The `src` 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
Β Β βΒ Β βββ partial1.md # The partials are all in markdown format and can have any name.
Β Β βΒ Β βββ partial2.md # They are only converted if they are referenced inside your `index.yml` file
β
Β Β βββ 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.
index.yml
A typical index.yml
file could look like this:
layout: page # The layout defaults to `page` if itβs not set
title: Homepage # Itβs always a good idea to give your page a title
main: # Defining an array in yaml
- feature-image.md # This is a partial (because it ends with ".md") and points to a markdown file that exists
- cta.md
- contact-cards.md
- /shared/footer.md # This is also a partial but because it starts with a slash "/" the location where this
# partial sits is relative to your content folder and not the page folder youβre in.
header: header.md # You can define a partial to a variable or to an array as seen above
( π‘ All variables that are defined inside a page are available as props under { _pages }
to all 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 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.)
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
src/layout/homepage.js
import React from "react";
export default ( page ) => (
<html>
<head>
<title>{ page.title }</title>
{ page.stylesheet != undefined
? ( <link rel="stylesheet" href={ `/assets/css/${ page.stylesheet }.css` } /> )
: null
}
</head>
<body>
<main>
<h1>{ page.title }</h1>
<div>{ page.main }</div>
</main>
<aside>
{ page.aside }
</aside>
</body>
</html>
);
/assets/homepage.css
main {
background: rebeccapurple;
}
aside {
background: hotpink;
}
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 typical component for a page might look like this:
import React from "react";
export default ( page ) => (
<html>
<head>
<title>{ page.title }</title>
</head>
<body>
<main>
<h1>{ page.title }</h1>
<div>{ page.partials }</div>
</main>
</body>
</html>
);
A typical component for a partial might look like this:
import React from "react";
export default ( page ) => (
<article>
<h2>{ page.title }</h2>
<div>{ page._body }</div>
</article>
);
( π‘ You can access the page your partial was called in via: page._pages[ page._ID ]
.)
A file will receive the following props:
prop name | description | Example |
---|---|---|
_ID | The ID of the current page | props._ID |
_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; with ID as key | props._pages.map() |
_nav | A nested object of your site structure | Object.keys( props._nav ).map() |
_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( props.yourMarkdown ) |
Plus all other variables declared inside the file either as frontmatter
or in the yaml
files.
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/",
+ "src": "/src/",
+ "site": "/site/",
+ "index": "index",
+ "homepage": "index"
+ },
+ "layouts": {
+ "page": "page",
+ "partial": "partial"
+ },
+ "site": {
+ "root": "/",
+ "doctype": "<!DOCTYPE html>",
+ "redirectReact": true,
+ "markdownRenderer": ""
+ },
+ "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": { # The cuttlebelle object
"folder": { # The is where we can adjust folder/file names
"content": "content/", # Where does your content live?
"src": "src/", # Where do your react layouts live?
"site": "site/", # Where do you want to generate your static site to?
"index": "index", # What is the name of the file we look for to generate pages?
"homepage": "index" # What should the index folder be named?
},
"layouts": { # Your layout settings
"page": "page", # What is the default layout for pages?
"partial": "partial" # What is the default layout for partials?
},
"site": { # General settings
"root": "/", # What should cuttlebelle append to links?
"doctype": "<!DOCTYPE html>", # What doctype string do you want to add?
"redirectReact": true # You can disable redirecting `import` calls to the locally installed
# react instance of cuttlebelle rather than your local folder.
"markdownRenderer": "", # A path to a file that `module.exports` an Marked.Renderer() object.
# Learn more about it here: https://github.com/chjj/marked#renderer
# The only addition is the `preparse` key that will be run before we go
# into the marked parsing
}
"docs": { # Docs settings
"root": "files/", # What is the root folder called where all docs
# are generated in
"index": ".template/docs/layout/index.js", # The path to the index layout file
"category": ".template/docs/layout/category.js", # The path to the category layout file
# All following settings are the default props
# each component is given for the example
"IDProp": "page2", # The _ID prop
"navProp": { # The _nav prop
"index": {
"page1": "page1",
"page2": {
"page2/nested": "page2/nested",
},
"page3": "page3",
},
},
"pagesProp": { # The _pages prop
"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",
},
},
},
},
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: "2"
*/
level: PropTypes.oneOf([ '1', '2', '3', '4', '5', '6' ]).isRequired,
/**
* hero: true
*/
hero: PropTypes.bool,
/**
* cards:
* - title: Card 1
* content: Content for card 1
* href: http://link/to
* - title: Card 2
* content: Content for card 2
* href: http://link/to
* - title: Card 3
* content: Content for card 3
* href: http://link/to
* - title: Card 4
* content: Content for card 4
* href: http://link/to
*/
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";
/**
* Hiding this component from the docs
*
* @disable-docs
*/
const Hidden = ( page ) => (
<article className={`globalheader`}>
<h1>{ page.title }</h1>
{ page._body }
</article>
);
Hidden.propTypes = {
/**
* title: Welcome
*/
title: PropTypes.string.isRequired,
/**
* _body: (text)(7)
*/
_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.
To contribute to this still young project you need to install itβs dependencies and run a watch to transpile the files.
yarn
yarn watch
( π‘ Please look at the coding style and work with it, not against it :smile:.)
We use Jest for unit tests.
npm run test
to run the testsnpm run test:detail
will give you coverage infosnpm run test:watch
will spin up the jest watchCopyright (c) Dominik Wilkowski. Licensed under GNU-GPLv3.
FAQs
The react static site generator that separates editing and code concerns
The npm package cuttlebelle receives a total of 14 weekly downloads. As such, cuttlebelle popularity was classified as not popular.
We found that cuttlebelle demonstrated a not healthy version release cadence and project activity because the last version was released a year ago.Β It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.