Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Metalsmith - Static Site Generator

Metalsmith - Static Site Generator

From zero to metalsmith with one presentation. Contains links :)

Benedikt Rötsch

January 22, 2018
Tweet

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. $ 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" } ] }
  7. 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
  8. --- title: A Catchy Title draft: false --- Lets get

    this started. ## Effusus pedes Lorem markdownum caelo gaudia terra ... Blogpost in MarkDown: example-article.md
  9. 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
  10. • 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
  11. 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
  12. <!DOCTYPE html> <html lang="en"> <head> <title>A Catchy Title - My

    Awesome Blog</title> <link rel="stylesheet" href="//.../bootstrap.min.css"> <link rel="stylesheet" href="//.../highlight.min.css"> <script src="//.../highlight.min.js"></script> <script>(function() { hljs.initHighlightingOnLoad(); })();</script> </head> <body> <div class="container"> <h1>My Awesome Blog</h1> <h2>A Catchy Title</h2> <p>An unfinished article...</p> </div> </body> </html> Generated: example-article.html
  13. Metalsmith(__dirname) ... .use(excerpts()) .use(collections({ articles: { pattern: 'articles/*.html', sortBy: 'date',

    reverse: true } })) .use(layouts({...})) .build((err) => ...) List of articles and pagination
  14. 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
  15. 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
  16. 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
  17. --- 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. $ 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 ...
  23. 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
  24. 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
  25. Some last hints • Plugin order matters! • Most plugins

    use multimatch for globbing (articles/**/*.md)