Creating a post system driven by markdown files

I want my portfolio to be a showcase of not only my work but also whatever interesting development patterns and discoveries I may come to digest and write about. To this end, I want to be able to quickly fire off a post in an easy to write format (markdown) and have my site be smart enough to figure out where and how to display it without requiring anything other than the source markdown file.

This can be finicky to initially get up and running because it requires understanding of both the nuxt.config.js format as well as some Webpack custom loaders. Again, feel free to cheat ahead and read through the source code of my linked site if it’s helpful.

Let’s now build the /software route for our portfolio. This page will be an index-like display of all the individual software posts our visitors can read more about.

To get started, we need a directory in the root of our project that will hold our markdown posts. Let’s call it posts.

mkdir posts

Next we’ll create new Nuxt.js page file. I want this page to live at https://zackproser.com/software, so following Nuxt’s pages convention, I place this file at <working-dir>/pages/software/index.vue

mkdir -p pages/software/ && touch pages/software/index.vue

Here’s the source:

We’ll get into the sub components in a moment, but the first thing to understand is that we’re using a Nuxt.js only method called asyncData to populate the information from our markdown files. Note that asyncData is a page only method — you can’t use it in normal Vue.js components.

In this case, we’re using this special hook to read in all our post markdown files. We do this via a Webpack concept of a require context, which you can read more about here.

Once Webpack has loaded the markdown files, we do some processing to convert the file name of the markdown file to the slug that will be displayed in the URL when that post is loaded. So, for example, if our markdown file is named catfacts-golang.md, our URL would be https://zackproser.com/software/catfacts-golang

Note that we have a final call to filter in Javascript to specify that we only want posts returns whose post.attributes.category is ‘software’. This concept comes from a special extended markdown format called Frontmatter.

Frontmatter markdown integration

Frontmatter is a markdown extension that allows you to define arbitrary “attributes” to your markdown files. Here’s what that looks like in action. Note the topmost section containing arbitrary key value properties:

---

title: A powerful and open source content optimizer

category: software

description: Learn how I leveraged natural language processing technology to build a full-stack app that suggests improvements to your writing

image: optimizer-blog.png

tags: PHP, NLP, API, Web App

---

![Article Optimizer App](/optimizer-screens/symfony-optimizer-splash.png)



## Intro



The Article Optimizer was my first full-scale web app. I built it several years ago to familiarize myself with form-processing in PHP, and within a few months it had grown into a full-scale public web app which now enjoys usage by hundreds of unique visitors per month.

When our Frontmatter markdown integration parses this markdown file, in addition to the usual post body, we’ll also have access in Javascript to these attributes, allowing us to implement whatever logic we want around filtering or displaying posts and their descriptions, tags, etc.

How do we signal to Webpack that all our *.md files should be parsed in this manner? By adding a custom build directive to our nuxt.config.json, like so:

This says, “if a filename matches the regular expression (has .md as it’s last 3 characters), and the file is within either the posts or the blog directories in the root working directory of this project, then use the frontmatter-markdown-loader, which is the npm package name for the desired loader.”

Don’t forget to

npm i frontmatter-markdown-loader --save

to ensure you have the required loader. At this point, we’ve configured our project’s Webpack instance to be able to recognize our markdown files in our posts or blog directories and to run them through the frontmatter loader, making their attributes available in Javascript, such that statements like our call to filter on category === software above will work.

Now that we can load our posts from the blessed directories, we need to display them cleanly in such a way that doesn’t break our responsive design.

Creating an Exhibit component

I call this an Exhibit because it makes sense in my mind — and because it’s reusable: you can use one Exhibit for a Software index page, for example, and one for your Blog page. An Exhibit just renders cards that allow you to scan posts and which link to the full post itself.

We saw above how the software index page includes the custom directive to insert the <exhibit> element into the page. Here’s what the Exhibit itself looks like:

First we iterate through the list of posts we already loaded. There’s a trick happening here that will be familiar to anyone who has wrestled with responsive grid systems and rendering elements in a loop: we only want a maximum of 3 cards in a given row, so we use some inline logic to slice our list of posts into rows of 3 cards each. That’s the most complex part of our component.

The one added fun piece here is the call to

${this.getClassType()}

in an interpolated ES6 template string when specifying the class name for the section. This is something I whipped up so that each page load rotates the background colors between the main types defined in my Argon theme. I like the loud colors and cycling between them and the small touch of randomness, so I wrote this convenience function and made it global by including it in common.js and loading that everywhere within my nuxt.config.js.

Here’s the function itself:

And here’s how to extend your nuxt.config.js to ensure it’s available globally within your components (so you can randomize the colors of everything, naturally):

/* ** Plugins to load before mounting the App */ plugins: [ '~/plugins/argon/argon-kit', '~/plugins/common.js' ],

The rest of our rendering is handled by rendering a Post Card component for each given post. That component looks like this:

The PostCard renders the preview of the post including the image, description and some tags. Note that all of these post.attributes are coming from the Frontmatter markdown loader parsing our special markdown files as described above.

Now, we have a /software index page that is rendering all of our markdown posts that are found in the posts directory. If we should want to add a new post, all we need do now is write the markdown file and put it in that directory and our site handles the rest.

Our final step is to render the view of an individual post when a user clicks one from an Exhibit that they want to read. This is where the Nuxt.js pages conventions and their implicit routing rules come into play.

In addition to our /software index page, we now want /software/<slug-goes-here> to work and allow you to access a specific post. This means we need a _slug.vue file to exist underneath our pages/software directory like so: https://github.com/zackproser/argon-portfolio/tree/master/pages/software

The _slug.vue file represents our URL variable, and as such it will be tasked with looking up and rendering the content for the given post. Here’s what that looks like:

We use the same asyncData trick as in our index view, but this time we’re only fetching the single post represented by our slug. And once the post itself is loaded and returned, we can render its attributes and body as before through the usual Vue templating syntax.

This may seem like a lot of effort, but the reason I like this solution is that it’s extremely scalable going forward. If I have 5 extra minutes somewhere and I’m not even around my development machine, I could still get to the GitHub repo interface and commit a new markdown file to my respective post directory and have my Netlify integration pick it up and deploy it live to my site.

Getting your dynamic slugs working in production with Netlify

There’s one final hurdle to vault when it comes to having our dynamic pages get routed to and rendered properly when requested in production from Netlify. We need some way to signal to Netlify that we’re going to have several static pages built and that all of them are valid routes.

We need one route for each of our posts in /software and in /blog (and in whatever other categories you might extend and build yourself). Luckily the way to pull this off is already demonstrated in my nuxt.config.js file here. Here’s the important part:

This might look a little bit funky, but reading the output of this function during build time sheds some initial light on what it’s doing:

1:26:38 AM: + 31 hidden assets 11:26:38 AM: Entrypoint app = server.js server.js.map 11:26:39 AM: ℹ Generating pages 11:26:49 AM: ✔ Generated /blog 11:26:49 AM: ✔ Generated /software 11:26:49 AM: ✔ Generated /software/article-optimizer 11:26:49 AM: ✔ Generated /software/catfacts-golang 11:26:49 AM: ✔ Generated /software/catfacts 11:26:49 AM: ✔ Generated /software/cf-terraforming 11:26:49 AM: ✔ Generated /software/hashtag-blaster 11:26:49 AM: ✔ Generated /software/html5-canyonrunner 11:26:49 AM: ✔ Generated /software/pagegobbler-mac-app 11:26:49 AM: ✔ Generated /software/goodneighbor 11:26:49 AM: ✔ Generated /software/padscoper 11:26:49 AM: ✔ Generated /software/pageripper 11:26:49 AM: ✔ Generated /software/realtime-hunger-map 11:26:49 AM: ✔ Generated /software/speakeasy 11:26:49 AM: ✔ Generated /software/pagegobbler-site 11:26:49 AM: ✔ Generated /software/username-extractor 11:26:49 AM: ✔ Generated /software/vue-portfolio 11:26:49 AM: ✔ Generated /testimonials 11:26:49 AM: ✔ Generated / 11:26:49 AM: ✔ Generated /software/we-node-sdk 11:26:49 AM: ✔ Generated /software/we-php-sdk 11:26:49 AM: ✔ Generated /software/we-python-sdk 11:26:49 AM: ✔ Generated /blog/docker-tips-and-tricks 11:26:49 AM: ✔ Generated /software/wisdomseeker 11:26:49 AM: ✔ Generated /blog/dockerized-express-js 11:26:49 AM: ✔ Generated /blog/modern-deployment 11:26:49 AM: ✔ Generated /blog/open-source-canyonrunner 11:26:49 AM: ✔ Generated /blog/open-sourced-article-optimizer

Inside our call to array concat, we have two functions doing the same thing but on two different directories. First, using the glob package, and using posts as the target directory, we resolve the complete paths for every file ending in .md. The second function does the same thing on the blog directory, and array concat returns a single array containing all these URL paths. Finally, we use this returned master array as the value of the nuxt.config.js parameter dynamicRoutes.

In this way, we are programmatically telling Netlify the dynamic routes that it needs to handle for our site, since our final site is truly just a pile of static files sitting behind nginx and a CDN.

If you do not do this, your site will work perfectly when running locally on your system. However, once you publish to Netlify you’ll get 404s for each of your dynamically generated routes.

Creating a Testimonials section driven by JSON

Keeping with the theme of making small edits to feed new data into a well-established pipeline, we can have our testimonials section driven off a single JSON file that contains everything needed to render each individual quote.

For my custom pipeline, I requested recommendations from some colleagues via LinkedIn. This worked well because LinkedIn facilitates the reminders and gives the person an interface to write their testimonial (that’s not an email thread). It also allows you to leverage the standardized LinkedIn avatar format — so you get headshots for each of the people who recommended you in a uniform size.

I feature testimonials in two places on my site. The first is in a section on the homepage featuring a carousel that automatically zips them across the screen.

Since they are a bit long to read in one go, each card also has a link to the testimonials page, where I just render every testimonial in one responsive column.

Let’s take a look at how the carousel version works. The parent component is a Bootstrap carousel and the testimonial cards are the individual items that rotate through it:

We can see that within the carousel wrapper, the v-for statement creates a new testimonial card for every testimonial found in our JSON file. Getting our JSON into this component could not be easier — as you can see, all we need do is import it.

Our testimonials.json file contains a single array of structurally identical objects, each representing one testimonial. For example: