您可能以前听说过静态站点生成器,例如Jekyll和Wintersmith ,甚至可能使用过它们。 但是您可能会惊讶于编写自己的静态站点生成器并不太困难。
在本教程中,我将向您展示如何构建自己的Grunt插件,该插件将从模板和Markdown文件为您生成一个静态网站。 然后,您可以将其与希望创建静态站点的任何其他Grunt插件结合使用。
为什么要使用Grunt?
您可能会问, 为什么要为此使用Grunt?
- 如果没什么,这将是学习如何创建自己的Grunt任务的好方法。
- 它提供对Grunt的API的访问,从而简化了许多任务。
- 将其构建为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
并修订配置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 &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
开头的部分-我们所有的代码都需要放入函数体内。 在顶部添加:
// 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解析器配置为通过Highlight.js支持GitHub风格的Markdown和语法高亮显示 (请注意,您需要包括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" && 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');
请注意,这里我们再次使用了Grunt文件API-在这里,我们使用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/
。 如果愿意,可以通过修改确定permalink
变量值的方式来更改URL。
接下来,我们将数据存储在一个对象中,并将其添加到post_items
数组中。 然后我们按日期对它们进行排序,并获取最新的五个。 然后,我们再次循环浏览这些帖子,并获取每个帖子的下一个和上一个帖子。 最后,我们为每个帖子创建一个目录,渲染模板,并将内容写入其中的index.html
文件。 请注意,这意味着我们只能按文件目录引用每个文件,从而提供了简洁的URL。
让我们测试一下。 将以下content/posts/2015-04-12-my-post.md
保存到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
,则应该在build/blog/2015/04/12/my-post/index.html
找到一个全新的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
保存到content/pages/about.md
:
---
title: "About me"
---
All about me
然后,您应该发现再次运行Grunt将在build/about/index.html
生成一个新文件。
实施RSS提要和404页面
我们的下一个任务是生成RSS feed和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);
我们首先从Gruntfile传递的数据中定义Feed的标题,URL和描述。 然后,在将结果保存到atom.xml
之前,我们获得了20条最新的帖子,进行循环浏览,并将它们添加为项目。
为了生成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来完成一些工作,从而使您了解构建静态站点生成器的过程,并希望看到您的想法。
From: https://www.sitepoint.com/building-static-site-generator-grunt-plugin/