{"id":263893,"date":"2017-12-22T08:59:16","date_gmt":"2017-12-22T15:59:16","guid":{"rendered":"http:\/\/css-tricks.com\/?p=263893"},"modified":"2018-09-19T13:27:17","modified_gmt":"2018-09-19T20:27:17","slug":"the-rise-of-the-butt-less-website","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/the-rise-of-the-butt-less-website\/","title":{"rendered":"The Rise of the Butt-less Website"},"content":{"rendered":"

It seems like all the cool kids have divided themselves into two cliques: the Headless CMS<\/strong> crowd on one side and the Static Site Generator<\/strong> crowd on the other. While I admit those are pretty cool team names, I found myself unable to pick a side. To paraphrase Groucho Marx, \u201cI don’t care to belong to any club that will have me as a member.\u201d<\/p>\n

<\/p>\n

For my own simple blog (which is embarrassingly empty at the moment), a static site generator could be a great fit. Systems like Hugo<\/a> and Jekyll<\/a> have both been highly recommended by developers I love and trust and look great at first glance, but I hit stumbling blocks when I wanted to change my theme or set up more complex JavaScript and interactions across pages. There are ways to solve both these issues, but that\u2019s not the kind of weekend I want to have.<\/p>\n

Besides that, I love to experiment, make new things, and I\u2019ve got a major crush on Vue<\/a> at the moment. Having a Headless CMS setup with a front-end that is decoupled from the back-end could be a great combination for me, but after 7+ years of PHP slinging with WordPress, the whole setup feels like overkill for my basic needs.<\/p>\n

What I really<\/em> want is a static site generator that will let me write a blog as a component of a larger single-page app so I have room to try new things and still have full control over styling, without the need for a database or any sort of back-end. This is a long way of telling you that I\u2019ve found my own club to join, with a decidedly un-cool name.<\/p>\n

Get ready for it… <\/p>\n

The Butt-less Website<\/h3>\n

Because there\u2019s no back-end, get it? 😶<\/p>\n

It takes a few steps to go butt-less:<\/p>\n

    \n
  1. Setup a single page app with Vue<\/li>\n
  2. Generate each route at build time<\/li>\n
  3. Create blog and article components<\/li>\n
  4. Integrate Webpack to parse Markdown content<\/li>\n
  5. Extend functionality with plugins<\/li>\n
  6. Profit!<\/li>\n<\/ol>\n

    That last point has to be a part of every proposal, right?<\/p>\n

    I know it looks like a lot of steps but this is not quite as tough as it seems. Let’s break down the steps together.<\/p>\n

    Setup a single page app with Vue<\/h3>\n

    Let’s get Vue up and running. We’re going to need Webpack<\/a> to do that.<\/p>\n

    I get it, Webpack is pretty intimidating even when you know what\u2019s going on. It\u2019s probably best to let someone else do the really hard work, so we\u2019ll use the Vue Progressive Web App Boilerplate<\/a> as our foundation and make a few tweaks.<\/p>\n

    We could use the default setup from the repo, but even while I was writing this article, there were changes being made there. In the interest of not having this all break on us, we will use a repo I created<\/a> for demonstration purposes. The repo has a branch for each step we’ll be covering in this post to help follow along.<\/p>\n

    View on GitHub<\/a><\/p>\n

    Cloning the repo and check out the step-1<\/code> branch:<\/p>\n

    $ git clone https:\/\/github.com\/evanfuture\/vue-yes-blog.git step-1\r\n$ cd vue-yes-blog\r\n$ npm install\r\n$ npm run dev<\/code><\/pre>\n

    One of my favorite parts of modern development is that it takes a mere thirty seconds to get a progressive web app up and running!<\/p>\n

    Next, let\u2019s complicate things.<\/p>\n

    Generate each route at build time<\/h3>\n

    Out of the box, single page apps only have a single entry point. In other words, it lives lives at a single URL. This makes sense in some cases, but we want our app to feel like a normal website. <\/p>\n

    We\u2019ll need to make use of the history mode in the Vue Router file<\/a> in order to do that. First, we\u2019ll turn that on by adding mode: 'history'<\/code> to the Router object\u2019s properties like so:<\/p>\n

    \/\/ src\/router\/index.js\r\nVue.use(Router);\r\n\r\nexport default new Router({\r\n  mode: 'history',\r\n  routes: [\r\n\/\/ ...<\/code><\/pre>\n

    Our starter app has two routes. In addition to Hello<\/code>, we have a second view component called Banana<\/code> that lives at the route \/banana<\/code>. Without history mode, the URL for that page would be http:\/\/localhost:1982\/#\/banana<\/code>. History mode cleans that up to http:\/\/localhost:1982\/banana<\/code>. Much more elegant!<\/p>\n

    All this works pretty well in development mode (npm run dev<\/code>), but let\u2019s take a peek at what it would look like in production. Here’s how we compile everything:<\/p>\n

    $ npm run build<\/code><\/pre>\n

    That command will generate your Vue site into the .\/dist<\/code> folder. To see it live, there\u2019s a handy command for starting up a super simple HTTP server on your Mac:<\/p>\n

    $ cd dist\r\n$ python -m SimpleHTTPServer<\/code><\/pre>\n

    Sorry Windows folks, I don\u2019t know the equivalent!<\/p>\n

    Now visit localhost:8000<\/code> in your browser. You\u2019ll see your site as it will appear in a production environment. Click on the Banana link, and all is well.<\/p>\n

    Refresh the page. Oops! This reveals our first problem with single page apps: there is only one HTML file being generated at build time, so there\u2019s no way for the browser to know that \/banana<\/code> should target the main app page and load the route without fancy Apache-style redirects!<\/p>\n

    Of course, there’s an app for that. Or, at least a plugin. The basic usage is noted in the Vue Progressive Web App Boilerplate documentation<\/a>. Here’s how it says we can spin up the plugin:<\/p>\n

    $ npm install -D prerender-spa-plugin<\/code><\/pre>\n

    Let’s add our routes to the Webpack production configuration file<\/a>:<\/p>\n

    \/\/ .\/build\/webpack.prod.conf.js\r\n\/\/ ...\r\nconst SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin')\r\nconst PrerenderSpaPlugin = require('prerender-spa-plugin')\r\nconst loadMinified = require('.\/load-minified')\r\n\/\/ ...\r\nconst webpackConfig = merge(baseWebpackConfig, {\r\n  \/\/ ...\r\n  plugins: [\r\n  \/\/ ...\r\n    new SWPrecacheWebpackPlugin({\r\n      \/\/ ...\r\n      minify: true,\r\n      stripPrefix: 'dist\/'\r\n    }),\r\n    \/\/ prerender app\r\n    new PrerenderSpaPlugin(\r\n      \/\/ Path to compiled app\r\n      path.join(__dirname, '..\/dist'),\r\n      \/\/ List of endpoints you wish to prerender\r\n      [ '\/', '\/banana' ]\r\n    )\r\n  ]\r\n})<\/code><\/pre>\n

    That\u2019s it. Now, when you run a new build, each route in that array will be rendered as a new entry point to the app. Congratulations, we\u2019ve basically just enabled static site generation!<\/p>\n

    Create blog and article components<\/h3>\n

    If you\u2019re skipping ahead, we\u2019re now up to the step-2<\/code> branch of my demo repo. Go ahead and check it out:<\/p>\n

    $ git checkout step-2<\/code><\/pre>\n

    This step is pretty straightforward. We\u2019ll create two new components, and link them together.<\/p>\n

    Blog Component<\/h4>\n

    Let’s register the the blog component. We’ll create a new file called YesBlog.vue<\/code> in the \/src\/components<\/code> directory and drop in the markup for the view:<\/p>\n

    \/\/ .\/src\/components\/YesBlog.vue\r\n<template>\r\n  <div class=\"blog\">\r\n    <h1>Blog<\/h1>\r\n    <router-link to=\"\/\">Home<\/router-link>\r\n    <hr\/>\r\n    <article v-for=\"article in articles\" :key=\"article.slug\" class=\"article\">\r\n      <router-link class=\"article__link\" :to=\"`\/blog\/${ article.slug }`\">\r\n        <h2 class=\"article__title\">{{ article.title }}<\/h2>\r\n        <p class=\"article__description\">{{article.description}}<\/p>\r\n      <\/router-link>\r\n    <\/article>\r\n  <\/div>\r\n<\/template>\r\n\r\n<script>\r\nexport default {\r\n  name: 'blog',\r\n  computed: {\r\n    articles() {\r\n      return [\r\n        {\r\n          slug: 'first-article',\r\n          title: 'Article One',\r\n          description: 'This is article one\\'s description',\r\n        },\r\n        {\r\n          slug: 'second-article',\r\n          title: 'Article Two',\r\n          description: 'This is article two\\'s description',\r\n        },\r\n      ];\r\n    },\r\n  },\r\n};\r\n<\/script><\/code><\/pre>\n

    All we\u2019re really doing here is creating a placeholder array (articles<\/code>) that will be filled with article objects. This array creates our article list and uses the slug<\/code> parameter as the post ID. The title<\/code> and description<\/code> parameters fill out the post details. For now, it\u2019s all hard-coded while we get the rest of our code in place.<\/p>\n

    Article Component<\/h4>\n

    The article component is a similar process. We’ll create a new file called YesArticle.vue<\/code> and establish the markup for the view:<\/p>\n

    \/\/ .\/src\/components\/YesArticle.vue\r\n<template>\r\n  <div class=\"article\">\r\n    <h1 class=\"blog__title\">{{article.title}}<\/h1>\r\n    <router-link to=\"\/blog\">Back<\/router-link>\r\n    <hr\/>\r\n    <div class=\"article__body\" v-html=\"article.body\"><\/div>\r\n  <\/div>\r\n<\/template>\r\n\r\n<script>\r\n  export default {\r\n    name: 'YesArticle',\r\n    props: {\r\n      id: {\r\n        type: String,\r\n        required: true,\r\n      },\r\n    },\r\n    data() {\r\n      return {\r\n        article: {\r\n          title: this.id,\r\n          body: '<h2>Testing<\/h2><p>Ok, let\\'s do more now!<\/p>',\r\n        },\r\n      };\r\n    },\r\n  };\r\n<\/script><\/code><\/pre>\n

    We\u2019ll use the props passed along by the router to know what article ID we\u2019re working with. For now, we\u2019ll just use that as the post title, and hardcode the body.<\/p>\n

    Routing<\/h4>\n

    We can’t move ahead until we add our new views to the router. This will ensure that our URLs are valid and allows our navigation to function properly. Here is the entirety of the router file<\/a>:<\/p>\n

    \/\/ .\/src\/router\/index.js\r\nimport Router from 'vue-router';\r\nimport Hello from '@\/components\/Hello';\r\nimport Banana from '@\/components\/Banana';\r\nimport YesBlog from '@\/components\/YesBlog';\r\nimport YesArticle from '@\/components\/YesArticle';\r\n\r\nVue.use(Router);\r\n\r\nexport default new Router({\r\n  mode: 'history',\r\n  routes: [\r\n    {\r\n      path: '\/',\r\n      name: 'Hello',\r\n      component: Hello,\r\n    },\r\n    {\r\n      path: '\/banana',\r\n      name: 'Banana',\r\n      component: Banana,\r\n    },\r\n    {\r\n      path: '\/blog',\r\n      name: 'YesBlog',\r\n      component: YesBlog,\r\n    },\r\n    {\r\n      path: '\/blog\/:id',\r\n      name: 'YesArticle',\r\n      props: true,\r\n      component: YesArticle,\r\n    },\r\n  ],\r\n});<\/code><\/pre>\n

    Notice that we’ve appended \/:id<\/code> to the YesArtcle<\/code> component’s path and set its props to true<\/code>. These are crucial because they establish the dynamic routing we set up in the component’s props array in the component file.<\/p>\n

    Finally, we can add a link to our homepage that points to the blog. This is what we drop into the Hello.vue file<\/a> to get that going: <\/p>\n

    <router-link to=\"\/blog\">Blog<\/router-link><\/code><\/pre>\n

    Pre-rendering<\/h4>\n

    We’ve done a lot of work so far but none of it will stick until we pre-render our routes. Pre-rendering is a fancy way of saying that we tell the app what routes exist and to dump the right markup into the right route. We added a Webpack plugin for this earlier, so here’s what we can add to our Webpack production configuration file<\/a>:<\/p>\n

    \/\/ .\/build\/webpack.prod.conf.js\r\n\/\/ ...\r\n  \/\/ List of endpoints you wish to prerender\r\n  [ '\/', '\/banana', '\/blog', '\/blog\/first-article', '\/blog\/second-article' ]\r\n\/\/ ...<\/code><\/pre>\n

    I have to admit, this process can be cumbersome and annoying. I mean, who wants to touch multiple files to create a URL?! Thankfully, we can automate this, which we’ll cover further down.<\/p>\n

    Integrate Webpack to parse Markdown content<\/h3>\n

    We\u2019re now up to the step-3<\/code> branch. Check it out if you’re following along in the code:<\/p>\n

    $ git checkout step-3<\/code><\/pre>\n

    The Posts<\/h4>\n

    We\u2019ll be using Markdown<\/a> to write our posts, with some FrontMatter<\/a> to create meta data functionality.<\/p>\n

    Fire up a new file in the posts<\/code> directory to create our very first post:<\/p>\n

    \/\/ .\/src\/posts\/first-article.md\r\n---\r\ntitle: Article One from MD\r\ndescription: In which the hero starts fresh\r\ncreated: 2017-10-01T08:01:50+02\r\nupdated:\r\nstatus: publish\r\n---\r\nHere is the text of the article.  It's pretty great, isn't it?\r\n\r\n\r\n\r\n\/\/ .\/src\/posts\/second-article.md\r\n---\r\ntitle: Article Two from MD\r\ndescription: This is another article\r\ncreated: 2017-10-01T08:01:50+02\r\nupdated:\r\nstatus: publish\r\n---\r\n## Let's start with an H2\r\nAnd then some text\r\nAnd then some code:\r\n```html\r\n<div class=\"container\">\r\n  <div class=\"main\">\r\n    <div class=\"article insert-wp-tags-here\">\r\n      <h1>Title<\/h1>\r\n      <div class=\"article-content\">\r\n        <p class=\"intro\">Intro Text<\/p>\r\n        <p><\/p>\r\n      <\/div>\r\n      <div class=\"article-meta\"><\/div>\r\n    <\/div>\r\n  <\/div>\r\n<\/div>\r\n```<\/code><\/pre>\n

    Dynamic Routing<\/h4>\n

    One annoying thing at the moment is that we need to hardcode our routes for the pre-rendering plugin. Luckily, it isn\u2019t complicated to make this dynamic with a bit of Node magic. First, we\u2019ll create a helper in our utility file<\/a> to find the files:<\/p>\n

    \/\/ .\/build\/utils.js\r\n\/\/ ...\r\nconst ExtractTextPlugin = require('extract-text-webpack-plugin')\r\nconst fs = require('fs')\r\n\r\nexports.filesToRoutes = function (directory, extension, routePrefix = '') {\r\n  function findFilesInDir(startPath, filter){\r\n    let results = []\r\n    if (!fs.existsSync(startPath)) {\r\n      console.log(\"no dir \", startPath)\r\n      return\r\n    }\r\n    const files = fs.readdirSync(startPath)\r\n    for (let i = 0; i < files.length; i++) {\r\n      const filename = path.join(startPath, files[i])\r\n      const stat = fs.lstatSync(filename)\r\n      if (stat.isDirectory()) {\r\n        results = results.concat(findFilesInDir(filename, filter)) \/\/recurse\r\n      } else if (filename.indexOf(filter) >= 0) {\r\n        results.push(filename)\r\n      }\r\n    }\r\n    return results\r\n  }\r\n\r\n  return findFilesInDir(path.join(__dirname, directory), extension)\r\n    .map((filename) => {\r\n      return filename\r\n        .replace(path.join(__dirname, directory), routePrefix)\r\n        .replace(extension, '')\r\n      })\r\n}\r\n\r\nexports.assetsPath = function (_path) {\r\n\/\/ ...<\/code><\/pre>\n

    This can really just be copied and pasted, but what we\u2019ve done here is create a utility method called filesToRoutes()<\/code> which will take in a directory<\/code>, extension<\/code>, and an optional routePrefix<\/code>, and return an array of routes based on a recursive file search within that directory.<\/p>\n

    All we have to do to make our blog post routes dynamic is merge this new array into our PrerenderSpaPlugin<\/code> routes. The power of ES6 makes this really simple:<\/p>\n

    \/\/ .\/build\/webpack.prod.conf.js\r\n\/\/ ...\r\nnew PrerenderSpaPlugin(\r\n  \/\/ Path to compiled app\r\n  path.join(__dirname, '..\/dist'),\r\n  \/\/ List of endpoints you wish to prerender\r\n  [\r\n    '\/',\r\n    '\/banana',\r\n    '\/blog',\r\n    ...utils.filesToRoutes('..\/src\/posts', '.md', '\/blog')\r\n  ]\r\n)<\/code><\/pre>\n

    Since we’ve already imported utils<\/code> at the top of the file for other purposes, we can just use the spread operator ...<\/code> to merge the new dynamic routes array into this one, and we\u2019re done. Now our pre-rendering is completely dynamic, only dependent on us adding a new file!<\/p>\n

    Webpack Loaders<\/h4>\n

    We\u2019re now up to the step-4<\/code> branch: <\/p>\n

    $ git checkout step-4<\/code><\/pre>\n

    In order to actually turn our Markdown files into parse-able content, we\u2019ll need some Webpack loaders in place. Again, someone else has done all the work for us, so we only have to install and add them to our config.<\/p>\n

    Update September 2018: Seif Sayed wrote in to say that the package ‘markdown-it-front-matter-loader’ is deprecated. The rest of this article still uses that, but that you probably should use markdown-with-front-matter-loader<\/a> instead, and has the bonus that you don’t need a json-loader to go with it.<\/p>\n

    $ npm install -D json-loader markdown-it-front-matter-loader markdown-it highlight.js yaml-front-matter<\/code><\/pre>\n

    We will only be calling the json-loader<\/code> and markdown-it-front-matter-loader<\/code> from our Webpack config, but the latter has peer dependencies of markdown-it<\/code> and highlight.js<\/code>, so we\u2019ll install those at the same time. Also, nothing warns us about this, but yaml-front-matter<\/code> is also required, so the command above adds that as well.<\/p>\n

    To use these fancy new loaders, we\u2019re going to add a block to our Webpack base config<\/a>:<\/p>\n

    \/\/ .\/build\/webpack.base.conf.js\r\n\/\/ ...\r\nmodule.exports = {\r\n  \/\/ ...\r\n  module: {\r\n    rules: [\r\n  \/\/ ...\r\n      {\r\n        test: \/\\.(woff2?|eot|ttf|otf)(\\?.*)?$\/,\r\n        loader: 'url-loader',\r\n        options: {\r\n          limit: 10000,\r\n          name: utils.assetsPath('fonts\/[name].[hash:7].[ext]')\r\n        }\r\n      },\r\n      {\r\n        test: \/\\.md$\/,\r\n        loaders: ['json-loader', 'markdown-it-front-matter-loader'],\r\n      },\r\n    ]\r\n  }\r\n}<\/code><\/pre>\n

    Now, any time Webpack encounters a require statement with a .md<\/code> extension, it will use the front-matter-loader<\/code> (which will correctly parse the metadata block from our articles as well as the code blocks), and take the output JSON and run it through the json-loader<\/code>. This way, we know we\u2019re ending up with an object for each article that looks like this:<\/p>\n

    \/\/ first-article.md [Object]\r\n{\r\n  body: \"<p>Here is the text of the article. It's pretty great, isn't it?<\/p>\\n\"\r\n  created: \"2017-10-01T06:01:50.000Z\"\r\n  description: \"In which the hero starts fresh\"\r\n  raw: \"\\n\\nHere is the text of the article. It's pretty great, isn't it?\\n\"\r\n  slug: \"first-article\"\r\n  status: \"publish\"\r\n  title: \"Article One from MD\"\r\n  updated: null\r\n}<\/code><\/pre>\n

    This is exactly what we need and it\u2019s pretty easy to extend with other metadata if you need to. But so far, this doesn\u2019t do anything! We need to require<\/code> these in one of our components so that Webpack can find and load it.<\/p>\n

    We could just write:<\/p>\n

    require('..\/posts\/first-article.md')<\/code><\/pre>\n

    …but then we\u2019d have to do that for every article we create, and that won\u2019t be any fun as our blog grows. We need a way to dynamically require all our Markdown files.<\/p>\n

    Dynamic Requiring<\/h4>\n

    Luckily, Webpack does this! It wasn\u2019t easy to find documentation for this but here it is<\/a>. There is a method called require.context()<\/code> that we can use to do just what we need. We\u2019ll add it to the script section of our YesBlog<\/code> component:<\/p>\n

    \/\/ .\/src\/components\/YesBlog.vue\r\n\/\/ ...\r\n<script>\r\n  const posts = {};\r\n  const req = require.context('..\/posts\/', false, \/\\.md$\/);\r\n  req.keys().forEach((key) => {\r\n    posts[key] = req(key);\r\n  });\r\n\r\n  export default {\r\n    name: 'blog',\r\n    computed: {\r\n      articles() {\r\n        const articleArray = [];\r\n        Object.keys(posts).forEach((key) => {\r\n          const article = posts[key];\r\n          article.slug = key.replace('.\/', '').replace('.md', '');\r\n          articleArray.push(article);\r\n        });\r\n        return articleArray;\r\n      },\r\n    },\r\n  };\r\n<\/script>\r\n\/\/ ...<\/code><\/pre>\n

    What\u2019s happening here? We\u2019re creating a posts object that we\u2019ll first populate with articles, then use later within the component. Since we\u2019re pre-rendering all our content, this object will be instantly available.<\/p>\n

    The require.context()<\/code> method accepts three arguments. <\/p>\n