Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
519 lines (441 sloc) 12.6 KB
'use strict';
const _ = require('lodash');
const Metalsmith = require('metalsmith');
const browserify = require('metalsmith-browserify');
const collections = require('metalsmith-collections');
const copy = require('metalsmith-copy');
const define = require('metalsmith-define');
const express = require('metalsmith-express');
const fileMetadata = require('metalsmith-filemetadata');
const htmlMinifier = require('metalsmith-html-minifier');
const hyphenate = require('metalsmith-hyphenate');
const ignore = require('metalsmith-ignore');
const metadata = require('metalsmith-metadata');
const moveUp = require('metalsmith-move-up');
const paths = require('metalsmith-paths');
const publish = require('metalsmith-publish');
const staticAssets = require('metalsmith-static');
const uglify = require('metalsmith-uglify');
const watch = require('metalsmith-watch');
const markdownit = require('metalsmith-markdownit');
const markdownAbbr = require('markdown-it-abbr');
const markdownFigures = require('markdown-it-implicit-figures');
const markdownFootnote = require('markdown-it-footnote');
const postcss = require('metalsmith-postcss');
const postcssCssnano = require('cssnano');
const postcssCssnext = require('postcss-cssnext');
const postcssImport = require('postcss-import');
const postcssMqpacker = require('css-mqpacker');
const postcssPerfectionist = require('perfectionist');
const cacheBust = require('./lib/metalsmith-cachebust');
const canonicalUrls = require('./lib/metalsmith-canonical-urls');
const csserenity = require('./lib/csserenity');
const dataPreload = require('./lib/data-preload');
const extractPublished = require('./lib/metalsmith-extract-published');
const feed = require('./lib/metalsmith-feed');
const formatFbInstantArticle = require('./lib/format-fb-instant-article.js');
const handlebars = require('./lib/handlebars');
const handlebarsHelpers = require('./lib/handlebars-helpers');
const handlebarsPartials = require('./lib/handlebars-partials');
const inliner = require('./lib/metalsmith-inliner');
const moveUpImageMap = require('./lib/metalsmith-move-up-image-map');
const postBanners = require('./lib/metalsmith-post-banners');
const preloadToHtaccess = require('./lib/preload-to-htaccess');
const rename = require('./lib/metalsmith-rename');
const srcset = require('./lib/metalsmith-srcset');
const BaseUrl = 'https://juliekoubova.net'
const SiteTitle = 'Julie Koubová'
const SiteDescription = 'Ráda se miluje, ráda jí, ráda si jenom tak zpívá. 💖'
const SiteGenerator = 'https://github.com/juliekoubova/juliekoubova.net'
function markdown() {
let md = markdownit({
typographer: true,
html: true
});
md.use(markdownAbbr);
md.use(markdownFigures, {
figcaption: true
});
md.use(markdownFootnote);
return md;
}
function createPostRepresentation(m, name, options) {
options = Object.assign({
collection: `${name}s`,
extension: `.${name}`,
layout: `${name}.html`,
}, options)
m.use(copy({
pattern: 'posts/**/*.html',
extension: options.extension
}))
m.use(fileMetadata([{
pattern: `posts/**/*${options.extension}`,
metadata: {
layout: options.layout,
collection: options.collection
}
}]))
}
function build(options) {
let m = new Metalsmith(__dirname);
m.source('src');
m.destination('build');
m.use(define({
bannerCount: 300,
baseUrl: BaseUrl,
css: '/main.css',
date: new Date(),
description: SiteDescription,
fbAppId: '1714468662102619',
fbPageId: '518151261704109',
fbAuthor: 'https://facebook.com/julie.e.harshaw',
fbType: 'website',
generator: SiteGenerator,
googleAnalyticsProperty: 'UA-58690305-1',
lang: 'cs',
live: options.live,
portrait: {
index: '/2015-04-192px.jpeg',
default: '/2015-04-32px.jpeg'
},
defaultImage: '/2015-04.jpeg',
siteTitle: SiteTitle,
siteTitlePrintSuffix: '.net',
twitterSite: '@julieeharshaw',
title: SiteTitle,
test: options.test,
typekitId: 'qai6bjn',
typekitTimeout: 1250
}));
m.use(metadata({
imageMap: 'img-map.json',
favicon: 'favicon-desc.json'
}));
m.use(ignore([
// ignore hidden files
'**/.*',
// ignore image etc. metadata
'**/*.meta.json',
// publish only the processed images
'**/*.+(jpg|jpeg|png|gif)'
]));
m.use(markdown());
// apply post metadata
m.use(fileMetadata([{
pattern: 'posts/**/*.html',
metadata: {
fbType: 'article',
layout: 'post.html',
collection: 'posts'
}
}]));
// extract dates, turn posts into directories with an index.html,
m.use(extractPublished());
createPostRepresentation(m, 'fb-instant-article')
// move posts (and instant articles) to root
m.use(moveUp('posts/**'));
m.use(moveUpImageMap('posts/**'));
// exclude drafts and scheduled posts unless live
// needs to run before collections()
m.use(publish({
future: true,
futureMeta: 'published'
}));
m.use(collections({
posts: {
sortBy: 'published',
reverse: true
},
'fb-instant-articles': {
sortBy: 'published',
reverse: true
}
}));
// no moving files beyond this point.
// must be after collections() which clobbers .path
m.use(paths({
directoryIndex: 'index.html'
}));
// generate per-post banners
m.use(postBanners({
collection: 'posts'
}));
// set fb instant articles url for use by feed
m.use((files, ms, done) => {
ms.metadata().collections['fb-instant-articles'].forEach(a => {
a.path.href = a.path.href.replace(/\/index.fb-instant-article$/, '/');
});
done();
});
// ===========================================================================
// NO METADATA CHANGES, ONLY TEMPLATING BEYOND THIS POINT
// ===========================================================================
// initialize Handlebars
m.use(handlebarsHelpers({ directory: 'helpers' }));
m.use(handlebarsPartials({ directory: 'partials' }));
m.use(handlebars.inplace({
pattern: '**/*.hbs'
}));
// strip the .hbs extension.
m.use(rename({
pattern: '**/*.hbs',
rename: n => n.replace(/\.hbs$/, ''),
directoryIndex: 'index.html'
}));
// apply per-page layouts
m.use(handlebars.layouts({
pattern: '**/*.+(html|fb-instant-article)',
}));
// apply default layout set in previous step
m.use(handlebars.layouts({
pattern: '**/*.html',
layout: 'default.html'
}));
m.use(hyphenate({
attributes: ['title', 'data-pullquote'],
attributesMapped: {
'href': {
dest: 'data-href-hyphenated',
element: 'a',
condition: (node, value) => !(/^mailto\:/i.test(value))
}
},
elements: ['a', 'aside', 'b', 'em', 'figcaption', 'li', 'p', 'strong'],
pattern: '**/*.+(htm|html|fb-instant-article)'
}));
m.use(srcset({
html: '**/*.html'
}));
m.use(srcset({
html: '**/*.fb-instant-article',
forceMax: true,
}));
m.use(canonicalUrls({
html: '**/*.html',
baseUrl: BaseUrl
}));
m.use(canonicalUrls({
html: '**/*.fb-instant-article',
baseUrl: BaseUrl,
selectors: {
'link[rel=canonical]': 'href',
'a[href^="/"]': 'href',
'a[href^="."]': 'href',
'img[src^="/"]': 'src'
}
}));
m.use(formatFbInstantArticle({
html: '**/*.fb-instant-article'
}));
// RSS
m.use(feed({
collection: 'posts',
limit: false,
siteUrl: BaseUrl,
siteTitle: SiteTitle,
siteDescription: SiteDescription,
generator: SiteGenerator
}));
// Instant Articles RSS
m.use(feed({
collection: 'fb-instant-articles',
destination: 'fb/instant-articles.xml',
limit: false,
siteUrl: BaseUrl,
siteTitle: SiteTitle,
siteDescription: SiteDescription,
generator: SiteGenerator,
postCustomElements: file => [
{ 'content:encoded': { _cdata: file.contents } }
]
}));
m.use(ignore('**/*.fb-instant-article'));
if (!options.live) {
m.use(ignore('fb-logo.html'));
}
// ===========================================================================
// CSS PROCESSING
// ===========================================================================
// we can safely ignore the partials, postcssImport reads them from disk anyway
m.use(ignore('css/**/*'));
// inline @import's
m.use(postcss([
postcssImport({
root: m.source()
})
]));
m.use(postcss([
postcssCssnext(),
postcssMqpacker({ sort: true })
]))
// uncss main.css based on index.html into index.css
// which will be later inlined into index.html
m.use(csserenity({
css: 'main.css',
html: ['index.html'],
output: 'index.css',
ignore: [
'.falling-eurocrat'
]
}));
// uncss main.css based on remaining html files
m.use(csserenity({
css: 'main.css',
html: ['**/*.html', '!index.html'],
output: 'main.css',
ignore: [
'.headroom',
'.headroom h1',
'.headroom header',
'.headroom--pinned',
'.headroom--pinned.headroom--not-top',
'.headroom--unpinned',
'.headroom--top',
'.footnote-ref.footsie-ref--active',
'.footsie',
'.footsie-background',
'.footsie__content',
'.footsie__content .footnote-backref',
'.footsie--bottom',
'.footsie--bottom.footsie--visible',
'.footsie--bottom .footsie__content',
'.footsie--bottom .footsie__wrapper',
'.footsie--popover',
'.footsie--popover.footsie--visible',
'.footsie--popover p',
'.footsie--popover .footsie__content',
'.footsie--popover .footsie__wrapper',
'.footsie--popover__tip',
'.footsie--popover--bottom .footsie--popover__tip',
'.footsie--popover--top .footsie--popover__tip',
'.footsie-button',
'.footsie-button--is-open',
'.footsie__wrapper',
'[data-footsie-text]'
]
}));
m.use(postcss([
options.live
? postcssPerfectionist({
indent: 2
})
: postcssCssnano({
autoprefixer: false,
discardComments: {
removeAll: true
}
})
]))
// ===========================================================================
// INLINE AND COMBINE CSS AND JS
// ===========================================================================
m.use(browserify({
include: 'js/post.js',
debug: options.live
}));
m.use(ignore([
'js/**/*',
'!js/html5shiv-printshiv.js',
'!js/cookie-law*.js',
'!js/inline*.js',
'!js/post*.js'
]));
// uglify javascripts -> .min.js
m.use(uglify({
compress: options.live ? false : undefined,
mangle: !options.live,
output: {
beautify: options.live
},
removeOriginal: !options.live,
sourceMap: options.live
}));
m.use(inliner({
delete: true,
css: 'index.css',
html: 'index.html'
}));
m.use(inliner({
js: 'js/cookie-law.min.js',
html: 'index.html',
delete: true
}));
m.use(inliner({
js: 'js/inline.min.js',
html: '**/*.html',
delete: true
}));
if (!options.live) {
m.use(cacheBust({
pattern: [
'**/*.+(css|js)',
'!js/html5shiv-printshiv.min.js'
]
}));
}
m.use(staticAssets({
src: 'i',
dest: 'i'
}));
m.use(staticAssets({
src: 'static',
dest: '/'
}));
m.use(staticAssets({
src: 'favicon',
dest: '/'
}));
// ===========================================================================
// HTACCESS
// ===========================================================================
m.use(dataPreload());
m.use(preloadToHtaccess());
m.use(ignore('**/preload.+(html|json)'));
// because minimatch ignores dot files by default, we only add the dot as the
// last step, so we can process the file as usual
m.use(rename({
pattern: '**/htaccess',
rename: n => n.replace(
/(^|\/)htaccess/,
'$1.htaccess'
)
}));
// must be after preloads because they parse the html and remove some
// attributes
if (!options.live) {
m.use(htmlMinifier({
decodeEntities: true,
sortAttributes: true,
sortClassName: true,
removeAttributeQuotes: true,
removeComments: true,
removeEmptyAttributes: true,
removeRedundantAttributes: true
}));
}
if (options.live || options.server) {
m.use(express());
}
if (options.live) {
m.use(watch({
paths: {
'${source}/**/*': '**/*',
'layouts/**/*': '**/*'
},
livereload: true
}));
}
return new Promise((resolve, reject) => {
m.build(err => err ? reject(err) : resolve());
});
}
function hasSwitch(arg) {
return _.includes(process.argv, `--${arg}`);
}
const options = {
live: hasSwitch('live'),
test: hasSwitch('test'),
server: hasSwitch('server')
};
build(options).catch(console.error);