1. 前言
博客前台也就是现在看到的页面,包括首页、列表页、详情页、单页(关于、友链、各种小工具...)等,需要考虑一定的SEO优化。
一开始是使用Thinkjs
+ nunjucks
模板引擎进行服务端渲染,比较难集成vue、webpack等技术,前后端不分离。后面改用Nuxt
来实现Vue SSR渲染。也方便开发各种单页小工具,如音乐盒、必应每日壁纸等。
技术栈:
- Vue.js 2.0
- Nuxt.js:SSR渲染
- Element-UI
- Axios
- Live2D:看板娘
- Prism.js:代码高亮
2. 目录结构
client
├── common 内容管理平台和博客前台功能模块
├── front 博客前台
| ├── layouts 外部容器
│ | ├── app.vue 工具页面使用的外部容器,没有菜单
│ | └── default 默认容器,有菜单
| ├── pages nuxt页面
| ├── plugins
│ | ├── axios.js axios全局拦截器
│ | ├── element-ui element-ui按需引入
│ | └── filters 注册全局过滤器
| ├── store vuex
│ | └── index.js nuxtServerInit获取菜单及系统配置
| ├── vendor 第三方库
│ | ├── aplayer 音乐播放器
│ | ├── live2d Live2D Cubism SDK v2看板娘
│ └── └── spine-ts spine骨骼动画
├── config 配置
│ ├── nuxt.admin.js 内容管理平台nuxt配置
│ └── nuxt.front.js 博客前台nuxt配置
3. 功能特性
3.1. 状态数据
在nuxtServerInit请求所有页面都需要用到的数据,比如菜单、系统配置
const state = () => ({
categories: [],
configs: {}
})
const mutations = {
SET_CATEGORY: (state, categories) => {
state.categories = categories
},
SET_CONFIGS: (state, configs) => {
state.configs = configs
}
}
const actions = {
async nuxtServerInit({ commit }, { req, $axios }) {
// 获取菜单以及系统配置
const { categories, configs } = await $axios.$get(`/general`)
commit('SET_CATEGORY', categories)
commit('SET_CONFIGS', configs)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
3.2. 列表页
列表页使用watchQuery
属性监听分页、排序、标签参数改变,重新请求列表数据
// mixins.js
export const listPage = {
data () {
return {
total: 0,
listPage: {
page: 1,
pageSize: 20
},
filters: {},
dynamicTags: []
}
},
methods: {
/**
* 列表分页切换
* @param {Object} { page: 当前页, limit: 每页个数 }
*/
changeListPage ({ page }) {
const { name, params, query } = this.$route
const [id] = params.id ? params.id.split('-') : ['list']
this.$router.push({ name, params: { id: `${id}-${page}` }, query })
},
handleSearch () {
const { name, params } = this.$route
const [id] = params.id ? params.id.split('-') : ['list']
const serachParams = this.filterParams(this.filters)
this.$router.push({ name, params: { id: `${id}-1` }, query: serachParams })
},
/**
* 删除筛选标签
* @param {String} tag 列表标签
*/
handleDeleteTag (tag) {
this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1)
this.filters.tags = this.dynamicTags.join()
this.handleSearch()
},
/**
* 添加筛选标签
* @param {String} tag 列表标签
*/
handleAddTag (tag) {
if (this.dynamicTags.includes(tag)) { return }
this.dynamicTags.push(tag)
this.filters.tags = this.dynamicTags.join()
this.handleSearch()
},
/**
* 剔除对象中值为空的属性
* @param {Object} data 源对象
* @returns {Object}
*/
filterParams (data) {
const target = {}
for (const key in data) {
if (data[key] !== '') {
target[key] = data[key]
}
}
return target
}
}
}
文章列表页
export default {
name: 'ArticleList',
mixins: [listPage],
asyncData ({ params, query, $axios }) {
// 请求数据
},
watchQuery: ['sortBy', 'orderBy', 'tags']
})
3.3. Dark Light主题
使用 @nuxtjs/color-mode 模块,实现白天、黑夜主题切换功能
styles/variables.scss
//theme
:root {
--color-primary: #1890ff;
--color-heading: #1f1f1f;
--color-text: #434343;
--color-secondary: #8c8c8c;
--bg: #f5f5f5;
--bg-normal: #fff;
--border-color: #EBEEF5;
--box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.dark-mode {
--color-primary: #1890ff;
--color-heading: #f5f5f5;
--color-text: #d9d9d9;
--color-secondary: #8c8c8c;
--bg: #000;
--bg-normal: #1f1f1f;
--border-color: rgba(255, 255, 255, .2);
}
nuxt.config.js
module.exports = {
buildModules: [
'@nuxtjs/color-mode'
],
css: [
'@/styles/variables.scss'
]
}
样式规则,使用var(变量)就可以获取当前主题下的变量值
body{
background-color: var(--bg);
color: var(--color-text);
transition: background-color .3s;
}
3.4. 代码高亮
使用prismjs来实现文章中的代码高亮功能
另一篇文章也有详细讲到:https://www.timelessq.com/article/detail/8
import Prism from 'prismjs'
import ClipboardJS from 'clipboard'
export default {
name: 'ArticleDetail',
mounted () {
this.initPrism()
},
methods: {
initPrism() {
Prism.plugins.toolbar.registerButton('copy-to-clipboard', (env) => {
const buttonElement = document.createElement('button')
const iconElement = document.createElement('i')
const tooltipElement = document.createElement('em')
buttonElement.className = 'toolbar-item-button'
iconElement.className = 'icon-fuzhi'
tooltipElement.className = 'toolbar-item-button__tips'
tooltipElement.innerText = '复制'
buttonElement.appendChild(iconElement)
buttonElement.appendChild(tooltipElement)
const clipboard = new ClipboardJS(buttonElement, {
text: () => {
return env.code
}
})
clipboard.on('success', () => {
iconElement.className = 'icon-wancheng'
clearTimeout(this.timer)
this.timer = setTimeout(() => {
iconElement.className = 'icon-fuzhi'
}, 3000)
// 复制成功,转载最好带上出处哟
})
clipboard.on('error', () => {
// 复制失败了呢
})
return buttonElement
})
Prism.highlightAll()
}
}
})
3.5. xml渲染
博客网站需要一个sitemap网站地图,用于搜索引擎收录,再弄一个RSS订阅,这些都需要生成XML。
方案一:使用nuxt的sitemap插件,生成静态sitemap.xml文件
https://www.npmjs.com/package/@nuxtjs/sitemap
方案二:
使用thinkjs的View视图功能,通过nunjucks动态渲染xml
https://thinkjs.org/zh-cn/doc/3.0/view.html
server 控制器 controller/front/xml.js
module.exports = class extends think.Controller {
async __before() {
this.baseurl = 'https:' + this.siteurl;
this.archives = await this.model('front/xml').selectArchives();
}
// sitemap地图
async sitemapAction() {
this.assign('siteurl', this.baseurl);
const categories = await this.model('category').getCacheCategory();
const sitemapList = await this.getSitemapList(categories);
this.assign('list', sitemapList);
const lastmod = sitemapList.length ? sitemapList[0].updatetime : think.datetime(new Date(), 'YYYY-MM-DD');
this.assign('lastmod', lastmod);
const filterCategory = categories.filter(item => !item.link);
const targetCategory = filterCategory.map(item => {
const { id, url, level } = item;
const rows = sitemapList.filter(element => element.category_id === id);
let lastmod;
if (rows.length) {
rows.sort((a, b) => new Date(b.updatetime) - new Date(a.updatetime));
lastmod = rows[0].updatetime;
}
return {
url: `${this.baseurl}${url}`,
lastmod: lastmod || think.datetime(new Date(), 'YYYY-MM-DD'),
priority: level === 1 ? 0.9 : (level === 2 ? 0.8 : 0.7)
};
});
this.assign('categoryList', targetCategory);
this.ctx.type = 'text/xml';
return super.display('home/sitemap.xml');
}
/**
* 获取sitemap列表
* @returns {Array} 文章列表 {...url,...priority}
*/
async getSitemapList(categories) {
const archives = JSON.parse(JSON.stringify(this.archives));
const sitemapList = archives.sort((a, b) => {
return new Date(b.updatetime) - new Date(a.updatetime);
});
sitemapList.forEach(item => {
const { id, category_id: categoryId } = item;
const findCategory = categories.find(element => element.id === categoryId);
item.url = findCategory ? `${this.baseurl}/${findCategory.type}/detail/${id}` : '';
item.priority = 0.6;
});
return sitemapList;
}
};
view/home/sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>{$ siteurl $}</loc>
<lastmod>{$ lastmod $}</lastmod>
<priority>1.0</priority>
<changefreq>weekly</changefreq>
</url>
{% for item in categoryList %}
<url>
<loc>{$ item.url $}</loc>
<lastmod>{$ item.lastmod $}</lastmod>
<priority>{$ item.priority $}</priority>
<changefreq>weekly</changefreq>
</url>
{% endfor %}
{% for item in list %}
<url>
<loc>{$ item.url $}</loc>
<lastmod>{$ item.updatetime $}</lastmod>
<priority>{$ item.priority $}</priority>
<changefreq>weekly</changefreq>
</url>
{% endfor %}
</urlset>
RSS也是类似,获取最新的几篇文章
module.exports = class extends think.Controller {
async __before() {
this.baseurl = 'https:' + this.siteurl;
this.archives = await this.model('front/xml').selectArchives();
}
// rss订阅
async rssAction() {
this.assign('siteurl', this.baseurl);
const configs = await this.model('config').getCacheConfig();
this.assign('options', configs);
this.assign('currentTime', (new Date()).toUTCString());
const list = await this.getRssList();
this.assign('list', list);
this.ctx.type = 'text/xml';
return super.display('home/rss.xml');
}
/**
* 获取最新6条动态
* @returns {Array} 文章列表 {...url}
*/
async getRssList() {
const categories = await this.model('category').getCacheCategory();
const archives = JSON.parse(JSON.stringify(this.archives));
const rssList = archives.sort((a, b) => {
return new Date(b.updatetime) - new Date(a.updatetime);
}).slice(0, 6);
rssList.forEach(item => {
const { id, category_id: categoryId } = item;
const findCategory = categories.find(element => element.id === categoryId);
item.url = findCategory ? `${this.baseurl}/${findCategory.type}/detail/${id}` : '';
});
return rssList;
}
};
view/home/rss.xml
<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>{$ options.title $}</title>
<link>{$ siteurl $}</link>
<description>{$ options.description $}</description>
<atom:link href="{$ siteurl $}/rss.html" rel="self" />
<language>zh-cn</language>
<lastBuildDate>{$ currentTime $}</lastBuildDate>
{% for item in list %}
<item>
<title>{$ item.title $}</title>
<link>{$ item.url $}</link>
<description><![CDATA[
{$ item.description | xml | safe $}
]]></description>
<pubDate>{$ item.updatetime $}</pubDate>
<guid>{$ item.url $}</guid>
</item>
{%- endfor %}
</channel>
</rss>
还没有评论,快来抢第一吧