Python for Process Innovation
#Python, #Javascript, #SAP, #Automation, #ML, #AI
Blending Vuepress with Vuetify
Update: 2018-12-31

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
1

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"
  },
}
1
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
1
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>
1
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>
1
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 />
1

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>
1
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 />
1
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
1

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'}]
    ]
};
1
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)
}
1
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;
}
*/

...
1
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>
1
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
Powered by Vue.js(Vuepress + Vuetify)