This blog will be built with markdown and have syntax highlighting
Nuxt v2.13.0
just dropped and in this version they've made it even easier to generate a fully static website. In this blog post: Going full static, they detail the updates and importantly the new command to generate the site as static html: nuxt export
.
You can combine this new functionality with @nuxt/content
, a new module that acts as a Git-based Headless CMS. These are some of the features you get out of the box:
Let's get started. Run the create nuxt project command and select universal mode:
npx create-nuxt-app nuxt-blog-starter
Once complete cd
in the newly created folder:
npm install @nuxt/content
In the nuxt.config
file we to add the @nuxt/content
module and set the site to target: "static"
.
export default {
...
target: "static",
mode: "universal",
...
modules: ["@nuxt/content"],
content: {
markdown: {
prism: {
theme: false,
},
},
}
...
};
Create a content folder in the root directory with the following structure and add your markdown file. The folder name will determine the url of the post.
nuxt-blog-starter/
components/
pages/
content/
posts/
my-first-blog-post/
index.md
img/
image.jpg
Add the Markdown file with the YAML header format:
---
title: Praesent sed neque efficitur
description: Aliquam ultrices ex eget leo tincidunt
date: 2020-06-26
image: index.jpg
tags:
- test
- another
---
Ut ut justo arcu. Praesent sed neque efficitur,
venenatis diam mollis, lobortis erat. Praesent eget
imperdiet odio, tincidunt eleifend mauris. Sed luctus lacinia auctor.
We're going to build 3 views:
/pages/posts/_slug.vue
/pages/index.vue
/pages/tags/_slug.vue
To view a single post we need to create a dynamic route page:
nuxt-blog-starter/
pages/
posts/
_.slug.vue
...
<template>
<div class="post">
<h1>{{ post.title }}</h1>
<p class="lead">{{ post.description }}</p>
<nuxt-content :document="post" />
</div>
</template>
<script>
export default {
async asyncData({ params, error, $content }) {
try {
const postPath = `/posts/${params.slug}`;
const [post] = await $content("posts", { deep: true })
.where({ dir: postPath })
.fetch();
return { post };
} catch (err) {
error({
statusCode: 404,
message: "Page could not be found",
});
}
},
head() {
return {
title: this.post.title,
meta: [
{
hid: "description",
name: "description",
content: this.post.description,
},
],
link: [
{
rel: "canonical",
href: "https://nuxt-blog.com/" + this.post.dir,
},
],
};
},
};
</script>
Now if you run npm run dev
and go to http://localhost:3000/posts/my-first-blog-post
you will see the post you created.
Let's create the index of the blog and list all the posts.
nuxt-blog-starter/
pages/
index.vue
...
<template>
<div class="posts">
<h1>Posts</h1>
<div v-for="post in posts" :key="post.dir">
<h3 class="heading">{{ post.title }}</h3>
<p>{{ post.description }}</p>
<p class="tags">
<span v-for="tag in post.tags" :key="tag" class="tag">
<nuxt-link :to="`/tags/${tag}`">{{ tag }}</nuxt-link>
&nbsp;
</span>
</p>
<nuxt-link :to="post.dir">Read more</nuxt-link>
</div>
</div>
</template>
<script>
export default {
async asyncData({ params, error, $content }) {
try {
const posts = await $content("posts", { deep: true }).fetch();
return { posts };
} catch (err) {
error({
statusCode: 404,
message: "Page could not be found",
});
}
},
head() {
return {
title: "Nuxt blog",
meta: [
{
hid: "description",
name: "description",
content: "Cool nuxt blog",
},
],
link: [
{
rel: "canonical",
href: "https://nuxt-blog.com/",
},
],
};
},
};
</script>
Finally let's create the tags lists page.
nuxt-blog-starter/
pages/
tags/
_.slug.vue
...
<template>
<div class="posts">
<h1>Tags: {{ $route.params.slug }}</h1>
<div v-for="post in posts" :key="post.dir">
<h3 class="heading">{{ post.title }}</h3>
<p>{{ post.description }}</p>
<p class="tags">
<span v-for="tag in post.tags" :key="tag" class="tag">
<nuxt-link :to="`/tags/${tag}`">{{ tag }}</nuxt-link>
&nbsp;
</span>
</p>
<nuxt-link :to="post.dir">Read more</nuxt-link>
</div>
</div>
</template>
<script>
export default {
async asyncData({ params, error, $content }) {
try {
const posts = await $content("posts", { deep: true })
.where({ tags: { $contains: params.slug } })
.fetch();
return { posts };
} catch (err) {
error({
statusCode: 404,
message: "Page could not be found",
});
}
},
head() {
return {
title: "Tags",
meta: [
{
hid: "description",
name: "description",
content: "Cool nuxt blog tags",
},
],
link: [
{
rel: "canonical",
href: "https://nuxt-blog.com/tags",
},
],
};
},
};
</script>
Prism comes along with @nuxt/content
but it renders server-side. If we want to use plugins such as line numbers we need to have it render in the client. To do this create a plugin called prism:
import Prism from "prismjs";
// Include a theme:
import "prismjs/themes/prism-tomorrow.css";
// Include the toolbar plugin: (optional)
import "prismjs/plugins/toolbar/prism-toolbar";
import "prismjs/plugins/toolbar/prism-toolbar.css";
// Include the line numbers plugin: (optional)
import "prismjs/plugins/line-numbers/prism-line-numbers";
import "prismjs/plugins/line-numbers/prism-line-numbers.css";
// Include the line highlight plugin: (optional)
import "prismjs/plugins/line-highlight/prism-line-highlight";
import "prismjs/plugins/line-highlight/prism-line-highlight.css";
// Include some other plugins: (optional)
import "prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard";
import "prismjs/plugins/highlight-keywords/prism-highlight-keywords";
import "prismjs/plugins/show-language/prism-show-language";
// Include additional languages
import "prismjs/components/prism-bash.js";
// Set vue SFC to markdown
Prism.languages.vue = Prism.languages.markup;
export default Prism;
Import the prism plugin and call Prism.highlightAll();
in mounted.
<template>
<div class="post">
<h1>{{ post.title }}</h1>
<p class="lead">{{ post.description }}</p>
<nuxt-content :document="post" />
</div>
</template>
<script>
import Prism from "~/plugins/prism";
export default {
async asyncData({ params, error, $content }) {
try {
const postPath = `/posts/${params.slug}`;
const [post] = await $content("posts", { deep: true })
.where({ dir: postPath })
.fetch();
return { post };
} catch (err) {
error({
statusCode: 404,
message: "Page could not be found",
});
}
},
mounted() {
Prism.highlightAll();
},
head() {
return {
title: this.post.title,
meta: [
{
hid: "description",
name: "description",
content: this.post.description,
},
],
link: [
{
rel: "canonical",
href: "https://nuxt-blog.com/" + this.post.dir,
},
],
};
},
};
</script>
@nuxt/content
doesn't support images in the markdown yet. The suggested way around this is to use a Vue component and require the images with webpack:
<template>
<div class="img">
<img :src="imgSrc()" :alt="alt" />
</div>
</template>
<script>
export default {
props: {
src: {
type: String,
required: true,
},
alt: {
type: String,
required: true,
},
},
methods: {
imgSrc() {
try {
const { post } = this.$parent;
return require(`~/content${post.dir}/img/${this.src}`);
} catch (error) {
return null;
}
},
},
};
</script>
N.B. @nuxt/content
requires Vue components used in markdown to use <v-img src="index.jpg" alt="Index"></v-img>
format.
Using images this way creates a warning error in the console but it doesn't affect the build ¯\_(ツ)_/¯
.
---
title: Praesent sed neque efficitur
description: Aliquam ultrices ex eget leo tincidunt
date: 2020-10-10
image: index.jpg
tags:
- test
- another
---
<v-img src="index.jpg" alt="Index"></v-img>
Ut ut justo arcu. Praesent sed neque efficitur,
venenatis diam mollis, lobortis erat. Praesent eget
imperdiet odio, tincidunt eleifend mauris. Sed luctus lacinia auctor.
Now that we've built this component we can add a featured image to the single view
<template>
<div>
<div class="post-header">
<h1 class="h1 post-h1">{{ post.title }}</h1>
<p v-if="post.description" class="excerpt">
{{ post.description }}
</p>
<div class="post-details">
<div class="tags">
<span v-for="(tag, i) in post.tags" :key="i" class="tag">
<nuxt-link :to="'/tags/' + tag">#{{ tag }}</nuxt-link>
</span>
</div>
<div class="date">{{ post.date | date }}</div>
</div>
<v-img
v-if="post.image"
class="post-img"
:src="post.image"
:alt="post.title"
></v-img>
</div>
<nuxt-content :document="post" />
</div>
</template>
<script>
import VImg from "~/components/VImg";
export default {
components: {
VImg
},
async asyncData({ params, error, $content }) {
try {
const postPath = `/posts/${params.slug}`;
const [post] = await $content("posts", { deep: true })
.where({ dir: postPath })
.fetch();
return { post };
} catch (err) {
error({
statusCode: 404,
message: "Page could not be found"
});
}
},
mounted() {
Prism.highlightAll();
},
head() {
return {
title: this.post.title,
meta: [
{
hid: "description",
name: "description",
content: this.post.description
}
],
link: [
{
rel: "canonical",
href: "https://matthewblewitt.com/posts/" + this.post.slug
}
]
};
}
};
</script>
We've created our view and now we can generate our static files in dist/
folder.
nuxt build && nuxt export
We can also run those static files directly by running:
nuxt serve
The blog is now ready to be uploaded to your static hosting of choice on Netflify or an S3 bucket. I've uploaded a basic version of this onto Github https://github.com/matthewblewitt/nuxt-static-generated-blog.
Originally posted on
Happy coding!