Статьи

Построение статического генератора сайтов Grunt Plugin

Вы, наверное, слышали о генераторах статических сайтов, таких как Jekyll и Wintersmith , и, возможно, даже использовали их. Но вы можете быть удивлены тем, что написать собственный генератор статических сайтов не так уж сложно.

В этом уроке я покажу вам, как создать свой собственный плагин Grunt, который будет генерировать для вас статический сайт из шаблонов и файлов Markdown. Затем вы можете использовать его в сочетании с любыми другими плагинами Grunt, которые вы хотите создать для своего статического сайта.

Зачем использовать Grunt?

Вы можете спросить, зачем использовать Grunt для этого?

  • Если ничего другого, это будет хорошим способом научиться создавать свои собственные задачи Grunt.
  • Он предоставляет доступ к API Grunt, что упрощает множество задач.
  • Создание такого плагина как Grunt обеспечивает большую гибкость — вы можете использовать его с другими плагинами Grunt, чтобы получить именно тот рабочий процесс, который вам нужен. Например, вы можете выбрать любой препроцессор CSS, какой захотите, или вы можете развернуть его через Rsync или на Github Pages, изменив другие используемые плагины и изменив конфигурацию. Нашему плагину нужно будет только взять файлы и шаблоны Markdown и сгенерировать HTML.
  • Вы можете легко добавить дополнительные функции в качестве плагинов — например, я использую существующий плагин Grunt для создания моей карты сайта.
  • Вы можете редактировать это для работы с различными системами шаблонов. Например, я буду использовать Handlebars в качестве моей системы шаблонов, но вместо этого было бы тривиально использовать Jade .

Настройка вещей

Наш первый шаг — установить все, что нам нужно для создания нашего плагина. Я предполагаю, что у вас уже установлены Git , Node.js и grunt-cli . Для начала нам нужно установить grunt-init :

 npm install -g grunt-init 

Далее установите шаблон gruntplugin :

 git clone git://github.com/gruntjs/grunt-init-gruntplugin.git ~/.grunt-init/gruntplugin 

Теперь создайте папку для вашего плагина, которую я называю grunt-mini-static-blog . Перейдите в эту папку и выполните следующую команду:

 grunt-init gruntplugin 

Вам будет задано несколько вопросов о вашем плагине, который будет использоваться для создания файла package.json . Не беспокойтесь, если вы еще не знаете, что ответить, просто используйте значения по умолчанию; Вы можете обновить файл позже. Эта команда создаст шаблон для вашего плагина.

Далее установите ваши зависимости:

 npm install 

Вам также понадобится несколько дополнительных модулей Node, чтобы выполнить некоторые тяжелые работы за вас:

 npm install handlebars highlight.js meta-marked moment rss lodash --save-dev 

Генерация постов

Наша первая задача — создать отдельные записи в блоге. Во-первых, давайте настроим конфигурацию по умолчанию. Откройте Gruntfile.js и Gruntfile.js конфигурацию для mini_static_blog :

 // Configuration to be run (and then tested). mini_static_blog: { default: { options: { data: { author: "My Name", url: "http://www.example.com", disqus: "", title: 'My blog', description: 'A blog' }, template: { post: 'templates/post.hbs', page: 'templates/page.hbs', index: 'templates/index.hbs', header: 'templates/partials/header.hbs', footer: 'templates/partials/footer.hbs', notfound: 'templates/404.hbs' }, src: { posts: 'content/posts/', pages: 'content/pages/' }, www: { dest: 'build' } } } } 

Здесь мы определяем значения по умолчанию для переменных, которые мы будем передавать нашему плагину. Объект data определяет разные данные, через которые мы будем проходить, а объект template определяет различные шаблоны, которые мы будем использовать для сборки нашего статического сайта. Объект src определяет, где плагин должен искать фактическое содержимое, а объект www определяет, где вывод должен быть сохранен.

Это просто значения по умолчанию для нашего плагина — при использовании его в работе вы переопределяете их в Gruntfile проекта и используете свои собственные пользовательские шаблоны. Вы также, вероятно, захотите удалить задачу nodeunit и его конфигурацию, а также всю test папку.

Обратите внимание, что значение disqus по умолчанию пустое, то есть комментарии отключены. Если пользователь хочет использовать Disqus, он может указать имя пользователя в соответствующем поле. Если вы предпочитаете использовать другую систему комментариев, такую ​​как комментарии Facebook, это должно быть просто реализовать вместо этого.

Мы также создадим несколько базовых шаблонов, чтобы увидеть их в действии:

шаблоны / обертоны / header.hbs

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0"> <meta name="description" content="{{ data.description }}"> <link rel="alternate" type="application/rss+xml" title="{{data.title}} - feed" href="/atom.xml" /> <title>{{#if meta.title}}{{meta.title}} - {{/if}}{{data.title}}</title> </head> <body> <header> <h1><a href="/">{{data.title}}</a></h1> <h2>{{ data.description }}</h2> </header> 

шаблоны / частичные / footer.hbs

 <footer> <p>Copyright &amp;copy; {{ data.author }} {{ year }}.</p> </footer> </body> </html> 

шаблоны / 404.hbs

 {{> header }} <div class="container"> <h1>Whoops, that page doesn't seem to exist</h1> <p>You might want to go back to <a href="/">the home page</a></p> </div> {{> footer }} 

templates.index.hbs

 {{> header }} {{#each posts}} <article> <p>{{ this.meta.formattedDate }}</p> <h1><a href="{{ this.path }}">{{this.meta.title}}</a></h1> {{{ this.post.content }}} </article> {{/each}} {{#if prevChunk}} <a href="/posts/{{ prevChunk }}/">Newer</a> {{/if}} {{#if nextChunk}} <a href="/posts/{{ nextChunk }}/">Older</a> {{/if}} {{> footer }} 

шаблоны / page.hbs

 {{> header }} <article class="post"> <h1>{{meta.title}}</h1> {{{ post.content }}} </article> {{> footer }} 

шаблоны / post.hbs

 {{> header }} <article class="post"> <p class="date">{{ this.meta.formattedDate }}</p> <h1>{{meta.title}}</h1> {{{ post.content }}} <section class="comments"> {{#if data.disqus }} <div id="disqus_thread"></div> <script type="text/javascript"> window.disqus_identifier=""; window.disqus_url="{{ data.url }}{{ path }}/"; window.disqus_title="{{meta.title}}"; </script> <script type="text/javascript" src="http://disqus.com/forums/{{ data.disqus }}/embed.js"></script> <noscript><a href="http://{{ data.disqus }}.disqus.com/?url=ref">View the discussion thread.</a></noscript> {{/if}} </section> </article> {{#if next}} <a href="{{ next.path }}">{{next.title}}</a> {{/if}} {{#if prev}} <a href="{{ prev.path }}">{{prev.title}}</a> {{/if}} {{> footer }} 

После этого мы можем начать работу над самим плагином. Сгенерированный шаблон будет включать в себя папку с именем tasks , и здесь будет файл с именем mini_static_blog.js . Найдите раздел, который начинается с grunt.registerMultiTask — весь наш код должен grunt.registerMultiTask внутри тела функции. Добавьте это вверху:

 // Import external libraries var Handlebars = require('handlebars'), Moment = require('moment'), RSS = require('rss'), hljs = require('highlight.js'), MarkedMetadata = require('meta-marked'), _ = require('lodash'), parseUrl = require('url'); // Declare variables var output, path; // Import options var options = this.options({ year: new Date().getFullYear(), size: 5 }); options.domain = parseUrl.parse(options.data.url).hostname; 

Здесь мы импортируем внешние библиотеки, которые будем использовать, и объявляем еще пару переменных. Мы также выбираем год и размер каждой страницы и получаем имя домена из имени хоста, определенного в Gruntfile.

Далее мы регистрируем шаблоны верхнего и нижнего колонтитула как частичные, чтобы их могли использовать другие шаблоны:

 // Register partials Handlebars.registerPartial({ header: grunt.file.read(options.template.header), footer: grunt.file.read(options.template.footer) }); 

Обратите внимание на использование grunt.file.read для фактического получения содержимого файла шаблона.

Затем мы настраиваем наш анализатор Markdown для поддержки GitHub-ароматизированной Markdown и подсветки синтаксиса с помощью Highlight.js (обратите внимание, что вам нужно будет включить CSS для Highlight.js, чтобы увидеть его выделенным).

 // Get languages var langs = hljs.listLanguages(); // Get Marked Metadata MarkedMetadata.setOptions({ gfm: true, tables: true, smartLists: true, smartypants: true, langPrefix: 'hljs lang-', highlight: function (code, lang) { if (typeof lang !== "undefined" &amp;&amp; langs.indexOf(lang) > 0) { return hljs.highlight(lang, code).value; } else { return hljs.highlightAuto(code).value; } } }); 

Обратите внимание, что сначала мы получаем список доступных языков, а затем в функции подсветки мы проверяем, был ли обнаружен язык, и, если это так, явно выбираем этот язык.

Затем мы извлекаем файлы Markdown, содержащие страницу и источник публикации:

 // Get matching files var posts = grunt.file.expand(options.src.posts + '*.md', options.src.posts + '*.markdown'); var pages = grunt.file.expand(options.src.pages + '*.md', options.src.pages + '*.markdown'); 

Обратите внимание, что мы снова используем файловый API Grunt — здесь мы используем expand чтобы получить все файлы в каталогах posts и pages.

Мы также компилируем наши шаблоны Handlebars:

 // Get Handlebars templates var postTemplate = Handlebars.compile(grunt.file.read(options.template.post)); var pageTemplate = Handlebars.compile(grunt.file.read(options.template.page)); var indexTemplate = Handlebars.compile(grunt.file.read(options.template.index)); var notFoundTemplate = Handlebars.compile(grunt.file.read(options.template.notfound)); 

Как и раньше, мы используем grunt.file.read для извлечения содержимого файлов шаблонов и компиляции их с помощью Handlebars.

Наш следующий шаг — создать сообщения:

 // Generate posts var post_items = []; posts.forEach(function (file) { // Convert it to Markdown var content = grunt.file.read(file); var md = new MarkedMetadata(content); var mdcontent = md.html; var meta = md.meta; // Get path var permalink = '/blog/' + (file.replace(options.src.posts, '').replace(/(\d{4})-(\d{2})-(\d{2})-/, '$1/$2/$3/').replace('.markdown', '').replace('.md', '')); var path = options.www.dest + permalink; // Render the Handlebars template with the content var data = { year: options.year, data: options.data, domain: options.domain, path: permalink + '/', meta: { title: meta.title.replace(/"/g, ''), date: meta.date, formattedDate: new Moment(new Date(meta.date)).format('Do MMMM YYYY h:mm a'), categories: meta.categories }, post: { content: mdcontent, rawcontent: content } }; post_items.push(data); }); // Sort posts post_items = _.sortBy(post_items, function (item) { return item.meta.date; }); // Get recent posts var recent_posts = post_items.slice(Math.max(post_items.length - 5, 1)).reverse(); // Output them post_items.forEach(function (data, index, list) { // Get next and previous if (index < (list.length - 1)) { data.next = { title: list[index + 1].meta.title, path: list[index + 1].path }; } if (index > 0) { data.prev = { title: list[index - 1].meta.title, path: list[index - 1].path }; } // Get recent posts data.recent_posts = recent_posts; // Render template var output = postTemplate(data); // Write post to destination grunt.file.mkdir(options.www.dest + data.path); grunt.file.write(options.www.dest + data.path + '/index.html', output); 

Мы перебираем сообщения, читаем содержимое каждого и извлекаем содержимое и метаданные. Затем мы определяем путь к файлу для каждого, основываясь на его имени файла. Каждое сообщение должно называться примерно как 2015-04-06-my-post.md , а путь к сгенерированному файлу будет примерно таким: /blog/2015/04/05/my-post/ . Вы можете изменить URL-адреса, если хотите, изменив способ определения значения permalink переменной.

Далее мы сохраняем данные в объекте и добавляем их в массив post_items . Затем мы сортируем их по дате и выбираем пять самых последних. Затем мы снова перебираем сообщения и получаем следующую и предыдущую записи для каждого. Наконец, мы создаем каталог для каждого сообщения, визуализируем шаблон и записываем содержимое в файл index.html внутри него. Обратите внимание, что это означает, что мы можем ссылаться на каждый файл только по его каталогу, создавая хорошие чистые URL.

Давайте проверим это. Сохранить следующее в content/posts/2015-04-12-my-post.md :

 --- title: "My blog post" date: 2015-02-15 18:11:22 +0000 --- This is my blog post. 

Если вы запускаете grunt , вы должны найти новый HTML-файл в build/blog/2015/04/12/my-post/index.html .

Генерация страниц

Создание страниц немного проще, так как нам не нужно беспокоиться о датах:

 // Generate pages pages.forEach(function (file) { // Convert it to Markdown var content = grunt.file.read(file); var md = new MarkedMetadata(content); var mdcontent = md.html; var meta = md.meta; var permalink = '/' + (file.replace(options.src.pages, '').replace('.markdown', '').replace('.md', '')); var path = options.www.dest + permalink; // Render the Handlebars template with the content var data = { year: options.year, data: options.data, domain: options.domain, path: path, meta: { title: meta.title.replace(/"/g, ''), date: meta.date }, post: { content: mdcontent, rawcontent: content }, recent_posts: recent_posts }; var output = pageTemplate(data); // Write page to destination grunt.file.mkdir(path); grunt.file.write(path + '/index.html', output); }); 

Основной принцип тот же — мы перебираем файлы Markdown в папке страниц и отображаем каждый с соответствующим шаблоном. Если вы сохраните следующее в content/pages/about.md :

 --- title: "About me" --- All about me 

Затем вы должны обнаружить, что при повторном запуске Grunt будет создан новый файл в build/about/index.html .

Реализация RSS-канала и 404 страницы

Наша следующая задача — создать канал RSS и страницу 404. Мы можем создать канал, используя модуль RSS, который мы установили ранее:

 // Generate RSS feed var feed = new RSS({ title: options.data.title, description: options.data.description, url: options.data.url }); // Get the posts for (var post in post_items.reverse().slice(0, 20)) { // Add to feed feed.item({ title: post_items[post].meta.title, description: post_items[post].post.content, url: options.data.url + post_items[post].path, date: post_items[post].meta.date }); } // Write the content to the file path = options.www.dest + '/atom.xml'; grunt.file.write(path, feed.xml({indent: true})); // Create 404 page var newObj = { data: options.data, year: options.year, domain: options.domain }; output = notFoundTemplate(newObj); path = options.www.dest; grunt.file.mkdir(path); grunt.file.write(path + '/404.html', output); 

Сначала мы определяем заголовок, URL и описание нашего фида из данных, передаваемых из Gruntfile. Затем мы получаем 20 самых последних сообщений, перебираем их и добавляем каждое как элемент, прежде чем сохранить результат в atom.xml .

Чтобы сгенерировать страницу 404, мы передаем несколько наших параметров в шаблон и сохраняем вывод в 404.html .

Создание страниц разбитых на страницы

Мы также хотим создать постраничный список постов:

 // Generate index // First, break it into chunks var postChunks = []; while (post_items.length > 0) { postChunks.push(post_items.splice(0, options.size)); } // Then, loop through each chunk and write the content to the file for (var chunk in postChunks) { var data = { year: options.year, data: options.data, domain: options.domain, posts: [] }; // Get the posts for (post in postChunks[chunk]) { data.posts.push(postChunks[chunk][post]); } // Generate content if (Number(chunk) + 1 < postChunks.length) { data.nextChunk = Number(chunk) + 2; } if (Number(chunk) + 1 > 1) { data.prevChunk = Number(chunk); } data.recent_posts = recent_posts; output = indexTemplate(data); // If this is the first page, also write it as the index if (chunk === "0") { grunt.file.write(options.www.dest + '/index.html', output); } // Write the content to the file path = options.www.dest + '/posts/' + (Number(chunk) + 1); grunt.file.mkdir(path); grunt.file.write(path + '/index.html', output); } 

Сначала мы разбиваем наш список постов на куски по 5. Затем мы генерируем HTML для каждого чанка и записываем его в файл. Формат пути, который я выбрал, означает, что типичный путь будет что-то вроде /posts/1/index.html . Мы также сохраняем первую страницу как домашнюю страницу сайта.

Идеи для дальнейшего развития

На практике этот плагин будет только одной частью вашей цепочки инструментов для создания и развертывания вашего блога. Вам нужно будет объединить его с другими плагинами Grunt и переопределить шаблоны, чтобы создать полезный метод создания и развертывания рабочего статического блога. Но если вы готовы потратить время на настройку и установку других необходимых плагинов Grunt, это может быть очень мощным и гибким способом ведения блога. Вы можете найти источник здесь .

Существует много возможностей для дальнейшего развития. Вот некоторые идеи, которые вы можете изучить:

  • Реализация поиска с Lunr.js
  • Реализация категорий
  • Изменение системы шаблонов или комментариев

Возможно, вы захотите проверить grunt-blogbuilder , который является более полной версией этого плагина, для идей о том, как их реализовать.

Я надеюсь, что это руководство дало вам некоторое представление о том, что входит в создание статического генератора сайтов, используя Grunt для выполнения некоторых работ, и я с нетерпением жду возможности увидеть, что вы придумали.