构建静态站点生成器Grunt插件

您可能以前听说过静态站点生成器,例如JekyllWintersmith ,甚至可能使用过它们。 但是您可能会惊讶于编写自己的静态站点生成器并不太困难。

在本教程中,我将向您展示如何构建自己的Grunt插件,该插件将从模板和Markdown文件为您生成一个静态网站。 然后,您可以将其与希望创建静态站点的任何其他Grunt插件结合使用。

为什么要使用Grunt?

您可能会问, 为什么要为此使用Grunt?

  • 如果没什么,这将是学习如何创建自己的Grunt任务的好方法。
  • 它提供对Grunt的API的访问,从而简化了许多任务。
  • 将其构建为Grunt插件可提供很大的灵活性-您可以将其与其他Grunt插件一起使用,以准确获取所需的工作流程。 例如,您可以选择所需的CSS预处理器,也可以通过更改您使用的其他插件并修改配置,通过Rsync或Github Pages进行部署。 我们的插件只需要获取Markdown文件和模板并生成HTML。
  • 您可以轻松地将其他功能添加为插件-例如,我使用现有的Grunt插件生成我的站点地图。
  • 您可以对其进行编辑以与不同的模板系统一起使用。 例如,我将使用Handlebars作为模板系统,但是使用Jade却是微不足道的。

设置事情

我们的第一步是安装创建插件框架所需的一切。 我假设您已经安装了GitNode.jsgrunt-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 &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开头的部分-我们所有的代码都需要放入函数体内。 在顶部添加:

// 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" &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');

请注意,这里我们再次使用了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/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值