Slide 1

Slide 1 text

Metalsmith.js An extremely simple, pluggable static site generator. BY BENEDIKT RÖTSCH

Slide 2

Slide 2 text

Benedikt Rötsch Passionate web developer since 2004 JS Ecosystem developer at Contentful

Slide 3

Slide 3 text

var absolute = require("absolute"), assert = require("assert"), clone = require("clone"), fs = require("co-fs-extra"), is = require("is"), matter = require("gray-matter"), Mode = require("stat-mode"), path = require("path"), readdir = require("recursive-readdir"), rm = require("rimraf"), thunkify = require("thunkify"), unyield = require("unyield"), utf8 = require("is-utf8"), Ware = require("ware"); function Metalsmith(t) { if (!(this instanceof Metalsmith)) return new Metalsmith(t); assert(t, "You must pass a working directory path."), this.plugins = [], this.ignores = [], this.directory(t), this.metadata({}), this.source("src"), this.destination("build"), this.concurrency(1 / 0), this.clean(!0), this.frontmatter(!0); } readdir = thunkify(readdir), rm = thunkify(rm), module.exports = Metalsmith, Metalsmith.prototype.use = function(t) { return this.plugins.push(t), this; }, Metalsmith.prototype.directory = function(t) { return arguments.length ? (assert(is.string(t), "You must pass a directory path string."), this._directory = t, this) : path.resolve(this._directory); }, Metalsmith.prototype.metadata = function(t) { return arguments.length ? (assert(is.object(t), "You must pass a metadata object."), this._metadata = clone(t), this) : this._metadata; }, Metalsmith.prototype.source = function(t) { return arguments.length ? (assert(is.string(t), "You must pass a source path string."), this._source = t, this) : this.path(this._source); }, Metalsmith.prototype.destination = function(t) { return arguments.length ? (assert(is.string(t), "You must pass a destination path string."), this._destination = t, this) : this.path(this._destination); }, Metalsmith.prototype.concurrency = function(t) { return arguments.length ? (assert(is.number(t), "You must pass a number for concurrency."), this._concurrency = t, this) : this._concurrency; }, Metalsmith.prototype.clean = function(t) { return arguments.length ? (assert(is.boolean(t), "You must pass a boolean."), this._clean = t, this) : this._clean; }, Metalsmith.prototype.frontmatter = function(t) { return arguments.length ? (assert(is.boolean(t), "You must pass a boolean."), this._frontmatter = t, this) : this._frontmatter; }, Metalsmith.prototype.ignore = function(t) { return arguments.length ? (this.ignores = this.ignores.concat(t), this) : this.ignores.slice(); }, Metalsmith.prototype.path = function() { var t = [].slice.call(arguments); return t.unshift(this.directory()), path.resolve.apply(path, t); }, Metalsmith.prototype.build = unyield(function*() { var t = this.clean(), e = this.destination(); t && (yield rm(path.join(e, "*"))); var i = yield this.process(); return yield this.write(i), i; }), Metalsmith.prototype.process = unyield(function*() { var t = yield this.read(); return t = yield this.run(t); }), Metalsmith.prototype.run = unyield(function*(t, e) { var i = new Ware(e || this.plugins); return (yield thunkify(i.run.bind(i))(t, this))[0]; }), Metalsmith.prototype.read = unyield(function*(t) { t = t || this.source(); for (var e, i = this.readFile.bind(this), r = this.concurrency(), s = this.ignores || null, n = yield readdir(t, s), a = [], o = 0; o < n.length; ) e = yield (e = n.slice(o, o + r)).map(i), a = a.concat(e), o += r; return n.reduce(function(e, i, r) { return i = path.relative(t, i), e[i] = a[r], e; }, {}); }), Metalsmith.prototype.readFile = unyield(function*(t) { var e = this.source(), i = {}; absolute(t) || (t = path.resolve(e, t)); try { var r, s = this.frontmatter(), n = yield fs.stat(t), a = yield fs.readFile(t); if (s && utf8(a)) { try { r = matter(a.toString()); } catch (e) { var o = new Error("Invalid frontmatter in the file at: " + t); throw o.code = "invalid_frontmatter", o; } (i = r.data).contents = new Buffer(r.content); } else i.contents = a; i.mode = Mode(n).toOctal(), i.stats = n; } catch (e) { if ("invalid_frontmatter" == e.code) throw e; throw e.message = "Failed to read the file at: " + t + "\n\n" + e.message, e.code = "failed_read", e; } return i; }), Metalsmith.prototype.write = unyield(function*(t, e) { e = e || this.destination(); for (var i = this.writeFile.bind(this), r = this.concurrency(), s = Object.keys(t), n = 0; n < s.length; ) yield s.slice(n, n + r).map(a), n += r; function a(r) { var s = path.resolve(e, r); return i(s, t[r]); } }), Metalsmith.prototype.writeFile = unyield(function*(t, e) { var i = this.destination(); absolute(t) || (t = path.resolve(i, t)); try { yield fs.outputFile(t, e.contents), e.mode && (yield fs.chmod(t, e.mode)); } catch (e) { throw e.message = "Failed to write the file at: " + t + "\n\n" + e.message, e; } }); Metalsmith actually fits on one slide File size does not matter on server side applications… but simplicity does

Slide 4

Slide 4 text

Some Metalsmith facts: ● Started early 2014 by Segment.io ● Metalsmith core doesn’t do much, most magic happens in plugins ● Simple core means more or less small learning curve ● The Node.js website uses Metalsmith ● Actually can be used to transform any file format to another ianstormtaylor

Slide 5

Slide 5 text

Metalsmith is not ☠ … the interest is still rising Data from npm-stats.org

Slide 6

Slide 6 text

Metalsmith Lifecycle 1. Read all files from source directory 2. Iterate over all files 3. Transform files into JS object representation a. Get file stats b. Read file content c. Extract and parse frontmatter header 4. Pass file object into plugins in given order 5. Write resulting files to destination directory

Slide 7

Slide 7 text

sources/some-article.md Metalsmith object map --- title: A Catchy Title draft: false --- An unfinished article... { 'relative_to_sourcepath/some-article.md ': { title: 'A Catchy Title ', draft: false, contents: 'An unfinished article... ', mode: '0664', stats: { /* keys with information on the file */ } } } File to object conversion

Slide 8

Slide 8 text

new Metalsmith(dir: string) // Init metalsmith with directory .use(plugin: Function) // Use a plugin .build(fn: Function) // Run the actual build // Optional config .source(path = './src') .destination(path = './build') .ignore(path: [string, Function, Array<[string, Function]>]) .concurrency(max: number) .clean(bool = true) .frontmatter(bool = true) .metadata(data: Object) Complete Metalsmith API

Slide 9

Slide 9 text

$ metalsmith -c config.json CLI is available metalsmith.json { "source": "src", "destination" : "build", "plugins": [ {"metalsmith-drafts" : true}, {"metalsmith-markdown" : true}, {"metalsmith-permalinks" : "posts/:title" }, {"metalsmith-templates" : "handlebars" } ] }

Slide 10

Slide 10 text

EXAMPLE

Slide 11

Slide 11 text

$ npm install metalsmith metalsmith-markdown Lets render some markdown

Slide 12

Slide 12 text

const Metalsmith = require('metalsmith') const markdown = require('metalsmith-markdown') Metalsmith(__dirname) .source('content') .destination('dist') .use(markdown()) .build((err) => { if (err) throw err }) Build script: index.js

Slide 13

Slide 13 text

--- title: A Catchy Title draft: false --- Lets get this started. ## Effusus pedes Lorem markdownum caelo gaudia terra ... Blogpost in MarkDown: example-article.md

Slide 14

Slide 14 text

$ node index.js Build it

Slide 15

Slide 15 text

content └── example-article.md dist └── example-article.html index.js Folder structure after build

Slide 16

Slide 16 text

Lets get this started.

Effusus pedes

Lorem markdownum caelo gaudia terra... Generated: example-article.html

Slide 17

Slide 17 text

Result

Slide 18

Slide 18 text

Metalsmith(__dirname) ... .metadata({ pageTitle: 'My Awesome Blog' }) .use(layouts({ default: 'default.pug,’ // required pattern: '**/*.html', // multimatch directory: 'layouts', engineOptions: {} })) .build((err) => ...) Add some layout and styling

Slide 19

Slide 19 text

● Uses layout engine based on file ending ● Supports any™ layouting engine due to jstransformers ● Supports foo.hbs and foo.handlebars ● There is even foo.typescript ¯\_(ツ)_/¯ ● Full list here of transformers hidden in source https://github.com/jstransformers/inputformat-to-jstransformer/blob/master/dictionary.json ● I prefer to use pug. Learn more: pugjs.org A few words on metalsmith-layouts

Slide 20

Slide 20 text

doctype html html(lang='en') head title #{title} - #{pageTitle} link(rel='stylesheet', href='.../bootstrap.min.css') link(rel='stylesheet', href='.../highlight.min.css') script(src='.../highlight.min.js') script. (function() { hljs.initHighlightingOnLoad(); })(); ... body header.container h1 #{pageTitle} h2 #{title} | !{contents} Layout: default.pug

Slide 21

Slide 21 text

$ node index.js Build it

Slide 22

Slide 22 text

layouts └── default.pug content └── example-article.md dist └── example-article.html index.js Folder structure after build

Slide 23

Slide 23 text

A Catchy Title - My Awesome Blog (function() { hljs.initHighlightingOnLoad(); })();

My Awesome Blog

A Catchy Title

An unfinished article...

Generated: example-article.html

Slide 24

Slide 24 text

Result

Slide 25

Slide 25 text

Metalsmith(__dirname) ... .use(excerpts()) .use(collections({ articles: { pattern: 'articles/*.html', sortBy: 'date', reverse: true } })) .use(layouts({...})) .build((err) => ...) List of articles and pagination

Slide 26

Slide 26 text

doctype html html(lang='en') ... body header.container h1 #{pageTitle} .btn-group.btn-group-lg(role='navigation', aria-label='menu') a.btn(href='/') Home a.btn(href='/about.html') About me block content Base Layout: layout.pug

Slide 27

Slide 27 text

extends layout.pug block content .container .row .card .card-body h1 #{title} | !{contents} .row .btn-group.btn-group-lg.mt-4.mb-4(role='group', aria-label='menu') if next a.btn.btn-info(href=`/${next.path}`) Next: #{next.title} if previous a.btn.btn-info(href=`/${previous.path}`) Previous: #{previous.title} Layout for articles: article.pug

Slide 28

Slide 28 text

extends layout.pug block content .container .content h1 #{title} | !{contents} ul.list-group each article in collections.articles li.list-group-item article h1 #{article.title} | !{article.excerpt} a.btn.btn-primary(href=`/${article.path}`) Read more Layout for article list: index.pug

Slide 29

Slide 29 text

--- title: A Catchy Title date: 2018-01-22 layout: article.pug draft: false --- Lets get this started. ## Effusus pedes Lorem markdownum caelo gaudia terra ... Blogpost in MarkDown: example-article.md

Slide 30

Slide 30 text

$ node index.js Build it

Slide 31

Slide 31 text

layouts ├── article.pug ├── default.pug ├── index.pug └── layout.pug content ├── about.md ├── articles │ ├── another-article.md │ └── example-article.md └── index.md dist ├── about.html ├── articles │ ├── another-article.html │ └── example-article.html └── index.html index.js Folder structure after build

Slide 32

Slide 32 text

Result index.html articles/example-article.html

Slide 33

Slide 33 text

Development Workflow

Slide 34

Slide 34 text

Watching for changes ● Don't use metalsmith-watch , it caused issues with various plugins. ● Use: browser-sync and nodemon ● Avoid gulp or grunt, npm scripts are enough

Slide 35

Slide 35 text

It works with webpack ● Keep webpack & metalsmith separated ● Generate website with metalsmith ● Build assets with webpack ● Use metalsmith-assets to merge metalsmith & webpack file ● See: https://github.com/axe312ger/metalsmith-webpack-suite

Slide 36

Slide 36 text

export default function plugin(opts){ opts.pattern = opts.pattern || [] return (files, metalsmith, done) => { Object.keys(files).forEach((file) => { if(multimatch(file, opts.pattern).length) { // Your code here. file.title = `${file.title} - My awesome blog` } }) setImmediate(done) } } Write your own plugin

Slide 37

Slide 37 text

Debugging

Slide 38

Slide 38 text

$ DEBUG=metalsmith* node index.js metalsmith-markdown checking file: example-article.md +0ms metalsmith-markdown converting file: example-article.md +3ms metalsmith-collections checking file: articles/another-article.html +0ms metalsmith-collections checking file: articles/example-article.html +1ms metalsmith-collections sorting collection: articles +1ms metalsmith-collections referencing collection: articles +0ms metalsmith-collections adding metadata: articles +0ms metalsmith-excerpts checking file: articles/another-article.html +0ms Metalsmith uses node-debug ● Install: $ npm install metalsmith-debug ● Simple: $ DEBUG=metalsmith* node index.js ● Detailed: $ DEBUG=metalsmith:metadata,metalsmith-markdown ...

Slide 39

Slide 39 text

Metalsmith Debug UI https://github.com/leviwheatcroft/metalsmith-debug-ui

Slide 40

Slide 40 text

Plugins Handpicked from 200+

Slide 41

Slide 41 text

Awsome Plugins #1 Page Layout metalsmith-layouts - Wrap layouts around posts metalsmith-in-place - Use template tags within posts Page Content metalsmith-markdown - Markdown with marked metalsmith-markdownit - Markdown with markdownit metalsmith-code-highlight - Highlight.js Code Highlighting metalsmith-youtube - Add youtube videos metalsmith-disqus - Disqus comments metalsmith-autotoc - Generate a table of contents Content Structure metalsmith-multi-language - i18n metalsmith-project-images - Add images to metadata metalsmith-collections - Group posts metalsmith-tags - Tag your posts metalsmith-excerpts - Extract excerpts metalsmith-permalinks - Permanent links Content Management metalsmith-drafts - Mark posts as draft metalsmith-author - Add author information Integrations metalsmith-algolia - Algolia search contentful-metalsmith - Contentful metalsmith-prismic - Prismic https://gist.github.com/axe312ger/a59e26778bede796678b398b95984214

Slide 42

Slide 42 text

Assets metalsmith-assets - Copy assets metalsmith-image-resizer - Simple resize metalsmith-sharp - Complex image manipulation SEO metalsmith-sitemap - Sitemap.xml metalsmith-redirect - Set up redirects Metadata metalsmith-default-values - Default metadata with patterns metalsmith-metadata - Load metadata from files Output optimization metalsmith-imagemin - Minify images metalsmith-purifycss - Remove unused css metalsmith-html-tidy - Tidy up html metalsmith-html-minifier - Minify html Debugging & Dev Tools metalsmith-debug-ui - Graphical debug UI metalsmith-browser-sync - Live Browser Reload metalsmith-changed - Only build changed files (simple) metalsmith-incremental - Only build changed files (advanced) Awsome Plugins #2 https://gist.github.com/axe312ger/a59e26778bede796678b398b95984214

Slide 43

Slide 43 text

Some last hints ● Plugin order matters! ● Most plugins use multimatch for globbing (articles/**/*.md)

Slide 44

Slide 44 text

Hosting now.sh anywhere like any static site generator

Slide 45

Slide 45 text

● Metalsmith Website - http://www.metalsmith.io/ ● Awesome Metalsmith - https://github.com/metalsmith/awesome-metalsmith ● Metalsmith Slack - https://metalsmith-slack.herokuapp.com/ Resources

Slide 46

Slide 46 text

Questions?

Slide 47

Slide 47 text

BENEDIKT RÖTSCH @axe312ger axe312ger