Table of Contents
Intro
A few months ago, I've met vue.js for the first time. To get familiar with it and also PWA(Progressive Web App) concept, which was also stranger to me at that time, I tried a small toy project todo-vuetify. And it was really exciting due to its brilliant concept, scalability, versatility.
Recently I decided to make my small blog with Vuepress and Vuetify to extend my vue.js experience with handy material design component framework. But as you know, due to vuepress is still on alpha phase, there was not enough information regarding how to use Vuetify with Vuepress.
Vuepress
Vuepress Installation
Although it is still on alpha phase, installing Vuepress is not that tricky thanks to it's official documentation. I recommend you to check Inside an Existing Project part to install vuepress properly inside your existing project.
yarn add -D vuepress
And please don't forget to add below alias in package.json to run dev server with yarn docs:dev or to generate static assets with yarn docs:build command.
{
"scripts": {
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
},
"devDependencies": {
"vuepress": "^0.14.8"
},
}
2
3
4
5
6
7
8
9
Folder Structure
You don't have to worry about the verbosity. For now, empty files would be OK and actually some of them will be still empty until the end of this tutorial. I just tried to follow the convention as long as possible and you may change it to your style later.
docs
+-- .vuepress
| +-- components
| | +-- ListByDate.vue
| | +-- ListByTag.vue
| | +-- PostMeta.vue
| +-- theme
| | +-- styles
| | | +-- external
| | +-- Blog.vue
| | +-- Home.vue
| | +-- Layout.vue
| +-- config.js
| +-- enhanceApp.js
+-- blog
| +-- tags
| +-- README.md
| +-- README.md
+-- README.md
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Components of Theme folder
Layout.vue
When user visit your site(e.g. https://username.github.io), Layout.vue component will acting like a gateway. Based on the path value, it will call Home.vue or Blog.vue component.
<template>
...
<component :is="layout" />
...
</template>
<script>
import Home from './Home.vue'
import Blog from './Blog.vue'
export default {
name: "Layout",
components: { Home, Blog },
computed: {
layout() {
const path = this.$page;
if (path === '/') {
return 'home'
} else {
return 'blog'
}
}
}
}
</script>
<style scoped>
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Home.vue
Before we study our Blog component, it would be better to check much simpler one first. There is a Content tag so called Content Outlet between template tag. Using this, you can make any component to render your markdown file anywhere.
In our example, Home.vue component will render README.md markdown file of docs folder. You might remember that we are still at root of our site by Layout.vue component.
<template>
<div id="home">
<Content />
</div>
</template>
<script>
export default {
name: "Home"
}
</script>
<style scoped>
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
Blog.vue
For now, this should be almost same as Home.vue except id and name. But at Vuetify part, we will modify stylus tag to blend Vuepress with Vuetify well.
The thing is, this component will try to render README.md file of blog folder. And at that point, generally we expect posting list(by date if possible) on it. So we will call some sort of listing component in README.md of blog folder as below simple code.
<ListByDate />
Components of Components folder
ListByDate.vue
When this component is called, it retrieves all of the filtered and sorted frontmatter meta data of your postings in the blog folder. For the time being, below template part is a initial implementation to make listing function work. We will update it with Vuetify later.
<template>
<div v-if="posts.length">
<template v-for="(post, index) in posts">
<div :to="post.path">{{ post.frontmatter.title }}</div>
<div>{{ post.frontmatter.description }}</div>
</template>
</div>
</template>
<script>
export default {
props: ["page"],
computed: {
posts() {
return this.$site.pages
.filter(x => { return x.path.match(new RegExp('/blog/.+\.html')); })
.sort((a, b) => { return new Date(b.frontmatter.date) - new Date(a.frontmatter.date); });
}
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PostMeta.vue and ListByTag.vue
PostMeta.vue is a component to show frontmatter metadata like title, date and tags on each posting. So you can call your component somewhere on your markdown file as below sample. And it also means that PostMeta.vue and ListByTag.vue components are optional feature. If you want to add some function like that to your blog, please check my final implementation on Github.
---
title: Blending Vuepress with Vuetify
date: 2019-01-01
description: Let me introduce how to blend Vuepress(Static Site Generator) with Vuetify(Material Design Component Framework) to make this blog.
tags: ['Vue', 'Vuepress', 'Vuetify']
---
<PostMeta />
2
3
4
5
6
7
8
Vuetify
Vuetify Installation
Of course there is a great documentation for Vuetify configuration Vuetify Quick start, but there are many optional ways to install Vuetify. So I will introduce my simply-for-blog version.
yarn add vuetify
config.js
Below link tag between header tag should be assigned little bit special way on config.js to include Material Design Icons.
module.exports = {
title: 'test',
description: 'test',
head: [
['link', { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons'}]
]
};
2
3
4
5
6
7
enhanceApp.js
enhanceApp.js is pretty important. Although we will try to handle our css carefully with Scoped Component Style to avoid css confusion between Vuetify and Vuepress default theme, the problem still standing on the markdown syntax highlighter.
To solve this problem, you must copy original css files from each of its package folders, make them to be imported by vuepress and vuetify with modified version. I mean originally Vuetify documentation recommend to import its css file as line number two, but it should be like line number three.
import Vuetify from 'vuetify'
// import 'vuetify/dist/vuetify.min.css'
import './theme/styles/external/vuetify.css'
import "./theme/styles/external/prism-tomorrow.css";
export default ({
Vue, // the version of Vue being used in the VuePress app
options, // the options for the root Vue instance
router, // the router instance for the app
siteData // site metadata
}) => {
Vue.use(Vuetify)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Below is modified version of vuetify.css to make syntax highlight feature of Vuetify silent and to make that of prismjs works properly. For your information, you don't have to modify prism-tomorrow.css.
...
/*
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
}
*/
...
/*
code,
kbd {
display: inline-block;
border-radius: 3px;
white-space: pre-wrap;
font-size: 85%;
font-weight: 900;
}
code:after,
kbd:after,
code:before,
kbd:before {
content: "\A0";
letter-spacing: -1px;
}
code {
background-color: #f5f5f5;
color: #bd4147;
box-shadow: 0px 2px 1px -1px rgba(0,0,0,0.2), 0px 1px 1px 0px rgba(0,0,0,0.14), 0px 1px 3px 0px rgba(0,0,0,0.12);
}
kbd {
background: #424242;
color: #fff;
}
*/
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Trimming
Well now, finally, you can update your blog with Vuetify's rich material design components like v-container > v-layout > v-flex, v-card, v-list, parallex, carousel, etc. But still you might need a few minor optimization.
Some parts of them are better to be referenced from original stylus file of Vuepress default theme to keep your project more simple, so I will add my version of Blog.vue theme component style for your information but anyway actually it's totally up to you from here.
<style scoped lang="stylus">
$textColor = #2c3e50
$codeBgColor = #282c34
$lineNumbersWrapperWidth = 3.5rem
$codeLang = js ts html md vue css sass scss less stylus go java c sh yaml py
#blog /deep/
/*This part is up to you*/
h1, h2, h3, h4
margin 2vh
&:hover
.header-anchor
text-decoration-line underline
p, ul, ol
margin 2vh
li
ul, ol
margin 0vh
a
text-decoration-line none
img
max-width: 100%;
height: auto;
/*Referenced theme.style from here*/
code, kbd, .line-number
font-family source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace
/*Referenced code.style from here*/
.content
code
color lighten($textColor, 20%)
padding 0.25rem 0.5rem
margin 0
font-size 0.85em
background-color rgba(27,31,35,0.05)
border-radius 3px
.content
pre, pre[class*="language-"]
line-height 1.4
padding 1.25rem 1.5rem
margin 0.85rem 0
background-color $codeBgColor
border-radius 6px
overflow auto
code
color #fff
padding 0
background-color transparent
border-radius 0
div[class*="language-"]
margin 2vh
position relative
background-color $codeBgColor
border-radius 6px
.highlight-lines
user-select none
padding-top 1.3rem
position absolute
top 0
left 0
width 100%
line-height 1.4
.highlighted
background-color rgba(0, 0, 0, 66%)
pre, pre[class*="language-"]
background transparent
position relative
z-index 1
&::before
position absolute
z-index 3
top 0.8em
right 1em
font-size 0.75rem
color rgba(255, 255, 255, 0.4)
&:not(.line-numbers-mode)
.line-numbers-wrapper
display none
&.line-numbers-mode
.highlight-lines .highlighted
position relative
&:before
content ' '
position absolute
z-index 3
left 0
top 0
display block
width $lineNumbersWrapperWidth
height 100%
background-color rgba(0, 0, 0, 66%)
pre
padding-left $lineNumbersWrapperWidth + 1 rem
vertical-align middle
.line-numbers-wrapper
position absolute
top 0
width $lineNumbersWrapperWidth
text-align center
color rgba(255, 255, 255, 0.3)
padding 1.25rem 0
line-height 1.4
br
user-select none
.line-number
position relative
z-index 4
user-select none
font-size 0.85em
&::after
content ''
position absolute
z-index 2
top 0
left 0
width $lineNumbersWrapperWidth
height 100%
border-radius 6px 0 0 6px
border-right 1px solid rgba(0, 0, 0, 66%)
background-color $codeBgColor
for lang in $codeLang
div{'[class~="language-' + lang + '"]'}
&:before
content ('' + lang)
div[class~="language-javascript"]
&:before
content "js"
div[class~="language-typescript"]
&:before
content "ts"
div[class~="language-markup"]
&:before
content "html"
div[class~="language-markdown"]
&:before
content "md"
div[class~="language-json"]:before
content "json"
div[class~="language-ruby"]:before
content "rb"
div[class~="language-python"]:before
content "py"
div[class~="language-bash"]:before
content "sh"
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162