diff --git a/lib/default-theme/Layout.vue b/lib/default-theme/Layout.vue
index df988d69d7..6f19d394ec 100644
--- a/lib/default-theme/Layout.vue
+++ b/lib/default-theme/Layout.vue
@@ -21,19 +21,25 @@
diff --git a/lib/default-theme/mixins/index.js b/lib/default-theme/mixins/index.js
new file mode 100644
index 0000000000..0a9c460e86
--- /dev/null
+++ b/lib/default-theme/mixins/index.js
@@ -0,0 +1,9 @@
+import nprogressMixin from './nprogress'
+import scrollListenerMixin from './scrollListener'
+import updateHeadMixin from './updateHead'
+
+export {
+ nprogressMixin,
+ scrollListenerMixin,
+ updateHeadMixin
+}
diff --git a/lib/default-theme/mixins/nprogress.js b/lib/default-theme/mixins/nprogress.js
new file mode 100644
index 0000000000..7736bac922
--- /dev/null
+++ b/lib/default-theme/mixins/nprogress.js
@@ -0,0 +1,20 @@
+import Vue from 'vue'
+import nprogress from 'nprogress'
+import { pathToComponentName } from '@app/util'
+
+export default {
+ mounted () {
+ nprogress.configure({ showSpinner: false })
+
+ this.$router.beforeEach((to, from, next) => {
+ if (to.path !== from.path && !Vue.component(pathToComponentName(to.path))) {
+ nprogress.start()
+ }
+ next()
+ })
+
+ this.$router.afterEach(() => {
+ nprogress.done()
+ })
+ }
+}
diff --git a/lib/default-theme/mixins/scrollListener.js b/lib/default-theme/mixins/scrollListener.js
new file mode 100644
index 0000000000..b1e0b8fe28
--- /dev/null
+++ b/lib/default-theme/mixins/scrollListener.js
@@ -0,0 +1,43 @@
+import throttle from 'lodash.throttle'
+import store from '@app/store'
+
+export default {
+ mounted () {
+ window.addEventListener('scroll', this.onScroll)
+ },
+ beforeDestroy () {
+ window.removeEventListener('scroll', this.onScroll)
+ },
+ methods: {
+ onScroll: throttle(function () {
+ this.setActiveHash()
+ }, 300),
+ setActiveHash () {
+ const sidebarLinks = [].slice.call(document.querySelectorAll('.sidebar-link'))
+ const anchors = [].slice.call(document.querySelectorAll('.header-anchor'))
+ .filter(anchor => sidebarLinks.some(sidebarLink => sidebarLink.hash === anchor.hash))
+
+ const scrollTop = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop)
+
+ for (let i = 0; i < anchors.length; i++) {
+ const anchor = anchors[i]
+ const nextAnchor = anchors[i + 1]
+
+ const isActive = i === 0 && scrollTop === 0 ||
+ (scrollTop >= anchor.parentElement.offsetTop + 10 &&
+ (!nextAnchor || scrollTop < nextAnchor.parentElement.offsetTop - 10))
+
+ if (isActive && this.$route.hash !== anchor.hash) {
+ store.disableScrollBehavior = true
+ this.$router.replace(anchor.hash, () => {
+ // execute after scrollBehavior handler.
+ this.$nextTick(() => {
+ store.disableScrollBehavior = false
+ })
+ })
+ return
+ }
+ }
+ }
+ }
+}
diff --git a/lib/default-theme/mixins/updateHead.js b/lib/default-theme/mixins/updateHead.js
new file mode 100644
index 0000000000..179563ec9a
--- /dev/null
+++ b/lib/default-theme/mixins/updateHead.js
@@ -0,0 +1,48 @@
+export default {
+ created () {
+ if (this.$ssrContext) {
+ this.$ssrContext.title = this.$title
+ this.$ssrContext.lang = this.$lang
+ this.$ssrContext.description = this.$page.description || this.$description
+ }
+ },
+ mounted () {
+ // update title / meta tags
+ this.currentMetaTags = []
+ const updateMeta = () => {
+ document.title = this.$title
+ document.documentElement.lang = this.$lang
+ const meta = [
+ {
+ name: 'description',
+ content: this.$description
+ },
+ ...(this.$page.frontmatter.meta || [])
+ ]
+ this.currentMetaTags = updateMetaTags(meta, this.currentMetaTags)
+ }
+ this.$watch('$page', updateMeta)
+ updateMeta()
+ },
+ beforeDestroy () {
+ updateMetaTags(null, this.currentMetaTags)
+ }
+}
+
+function updateMetaTags (meta, current) {
+ if (current) {
+ current.forEach(c => {
+ document.head.removeChild(c)
+ })
+ }
+ if (meta) {
+ return meta.map(m => {
+ const tag = document.createElement('meta')
+ Object.keys(m).forEach(key => {
+ tag.setAttribute(key, m[key])
+ })
+ document.head.appendChild(tag)
+ return tag
+ })
+ }
+}