├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .snyk
├── .stats
├── Dockerfile
├── LICENSE
├── README.md
├── app
├── controller
│ ├── home.js
│ └── yuque.js
├── public
│ └── service-worker.js
├── router.js
├── schedule
│ └── refresh-bing-images.js
├── service
│ ├── bing.js
│ └── yuque.js
└── util
│ └── md.js
├── config.yml
├── config
├── config.default.js
├── config.local.js
├── config.prod.js
├── config.unittest.js
├── plugin.js
└── webpack.config.js
├── jsconfig.json
├── package.json
├── test
└── controller
│ ├── home.test.js
│ └── yuque.test.js
└── themes
└── txd
├── common
└── router.jsx
├── containers
├── Footer
│ ├── index.jsx
│ └── index.scss
├── Header
│ ├── index.jsx
│ └── index.scss
├── HomeFooter
│ ├── index.jsx
│ └── index.scss
├── HomeHeader
│ ├── index.jsx
│ └── index.scss
├── Layout
│ └── index.jsx
└── ModalNav
│ ├── index.jsx
│ └── index.scss
├── index.jsx
├── pages
├── 404
│ ├── index.jsx
│ └── index.scss
├── About
│ ├── index.jsx
│ └── index.scss
├── Blog
│ ├── index.jsx
│ └── index.scss
├── Careers
│ └── index.jsx
├── Home
│ ├── index.jsx
│ └── index.scss
└── Post
│ ├── index.jsx
│ └── index.scss
├── services
└── yuque.js
├── stores
├── app.js
└── post.js
├── styles
├── base
│ ├── _animate.scss
│ ├── _base.scss
│ ├── _common.scss
│ ├── _layout.scss
│ └── _type.scss
├── markdown.scss
├── mixins
│ ├── _anim.scss
│ ├── _breakpoints.scss
│ ├── _manifest.scss
│ ├── _responsive.scss
│ ├── _type.scss
│ └── _util.scss
├── site.scss
└── variables
│ ├── _colors.scss
│ ├── _dimensions.scss
│ ├── _fontfamily.scss
│ └── _manifest.scss
├── utils
├── axios.js
└── format.js
└── widgets
├── Hscroll
├── hscroll.js
└── index.jsx
├── IconLink
├── index.jsx
└── index.scss
├── Icons
├── index.jsx
├── svgicon.js
└── svgicon_config.js
├── Loader
└── index.jsx
├── MobileSummary
├── index.jsx
└── index.scss
├── PostCard
├── index.jsx
└── index.scss
├── PostContent
├── index.jsx
└── index.scss
├── PostLink
├── index.jsx
├── index.scss
└── post_link.jsx
├── PostMeta
├── index.jsx
└── index.scss
└── PostSummary
├── index.jsx
└── index.scss
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["transform-decorators-legacy"]
3 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | app/proxy*
2 | coverage
3 | dist
4 | fntest
5 | mocks
6 | node_modules
7 | spm_modules
8 | test/fixtures
9 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'eslint-config-beidou',
3 | rules: {
4 | 'react/jsx-uses-react': 'error',
5 | 'react/jsx-uses-vars': 'error',
6 | 'react/forbid-prop-types': [1, { forbid: ['any'] }],
7 | 'react/prefer-stateless-function': 0,
8 | 'no-template-curly-in-string': 0,
9 | 'react/no-danger': 0,
10 | 'react/prop-types': 0,
11 | 'no-mixed-operators': 0
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | .isomorphic
4 | .node-diamond-client-cache
5 | .project
6 | .settings
7 | .npminstall.done
8 | app/proxy
9 | app/proxy_class_map.js
10 | app/proxy-class
11 | app/proxy-enums
12 | assembly
13 | assembly/
14 | assets
15 | build/*
16 | config/serverEnv
17 | config/env
18 | debug
19 | logs
20 | mocks_data
21 | mocks_data/proxy/**/__*
22 | node_modules
23 | npm-debug.log
24 | run
25 | track
26 | .run/
27 | .vscode/
28 | !.run/isomorphic
29 |
30 | package-lock.json
31 | yarn.lock
32 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/*.md
2 | **/*.svg
3 | package.json
4 | app/
5 | config/
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "printWidth": 100,
5 | "overrides": [
6 | {
7 | "files": ".prettierrc",
8 | "options": { "parser": "json" }
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------
/.stats:
--------------------------------------------------------------------------------
1 | Hash: 3c16366ce0f452ee741f
2 | Version: webpack 3.12.0
3 | Time: 9112ms
4 | Asset Size Chunks Chunk Names
5 | index.js?3c16366ce0f452ee741f 338 kB 0 [emitted] [big] index
6 | manifest.js 805 bytes 1 [emitted] manifest
7 | index.css 94.1 kB 0 [emitted] index
8 | precache-manifest.60144109bbe17651b1eb050c29e4f6f4.js 30 bytes [emitted]
9 | service-worker.js 1.31 kB [emitted]
10 | [0] ./node_modules/react/index.js 190 bytes {0} [built]
11 | [24] ./node_modules/react-dom/index.js 1.36 kB {0} [built]
12 | [38] ./themes/txd/pages/Home/index.scss 41 bytes {0} [built]
13 | [39] multi ./themes/txd/index.jsx 28 bytes {0} [built]
14 | [40] ./themes/txd/index.jsx 6 kB {0} [built]
15 | [49] ./themes/txd/common/router.jsx 1.62 kB {0} [built]
16 | [77] ./themes/txd/stores/app.js 3.64 kB {0} [built]
17 | [101] ./themes/txd/containers/Layout/index.jsx 1.89 kB {0} [built]
18 | [114] ./themes/txd/pages/Home/index.jsx 4.72 kB {0} [built]
19 | [117] ./themes/txd/pages/Post/index.jsx 4.7 kB {0} [built]
20 | [127] ./themes/txd/pages/Blog/index.jsx 5.03 kB {0} [built]
21 | [131] ./themes/txd/pages/About/index.jsx 5.87 kB {0} [built]
22 | [134] ./themes/txd/pages/Careers/index.jsx 134 bytes {0} [built]
23 | [135] ./themes/txd/pages/404/index.jsx 2.11 kB {0} [built]
24 | [137] ./themes/txd/styles/site.scss 41 bytes {0} [built]
25 | + 143 hidden modules
26 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/pages/Home/index.scss:
27 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/pages/Home/index.scss 5.12 kB {0} [built]
28 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
29 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/pages/404/index.scss:
30 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/pages/404/index.scss 217 bytes {0} [built]
31 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
32 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/styles/markdown.scss:
33 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/styles/markdown.scss 37.7 kB {0} [built]
34 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
35 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/styles/site.scss:
36 | [0] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
37 | [1] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/styles/site.scss 6.09 kB {0} [built]
38 | [2] ./node_modules/css-loader?{"importLoaders":1,"minimize":true,"sourceMap":false,"modules":false}!./node_modules/postcss-loader/lib??postcss!./node_modules/normalize.css/normalize.css 2.06 kB {0} [built]
39 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/pages/Blog/index.scss:
40 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/pages/Blog/index.scss 4.08 kB {0} [built]
41 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
42 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/pages/About/index.scss:
43 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/pages/About/index.scss 3.71 kB {0} [built]
44 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
45 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/widgets/PostMeta/index.scss:
46 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/widgets/PostMeta/index.scss 3.62 kB {0} [built]
47 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
48 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/widgets/PostSummary/index.scss:
49 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/widgets/PostSummary/index.scss 4.5 kB {0} [built]
50 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
51 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/pages/Post/index.scss:
52 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/pages/Post/index.scss 4.13 kB {0} [built]
53 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
54 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/containers/Footer/index.scss:
55 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/containers/Footer/index.scss 2.83 kB {0} [built]
56 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
57 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/widgets/PostCard/index.scss:
58 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/widgets/PostCard/index.scss 5.41 kB {0} [built]
59 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
60 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/widgets/PostContent/index.scss:
61 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/widgets/PostContent/index.scss 3.48 kB {0} [built]
62 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
63 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/containers/HomeFooter/index.scss:
64 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/containers/HomeFooter/index.scss 2.91 kB {0} [built]
65 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
66 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/containers/HomeHeader/index.scss:
67 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/containers/HomeHeader/index.scss 3.94 kB {0} [built]
68 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
69 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/containers/Header/index.scss:
70 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/containers/Header/index.scss 3.98 kB {0} [built]
71 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
72 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/containers/ModalNav/index.scss:
73 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/containers/ModalNav/index.scss 3.51 kB {0} [built]
74 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8.11.3-alpine
2 |
3 | ENV TIME_ZONE=Asia/Shanghai
4 |
5 | RUN \
6 | mkdir -p /usr/src/app \
7 | && apk add --no-cache tzdata \
8 | && echo "${TIME_ZONE}" > /etc/timezone \
9 | && ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime
10 |
11 | WORKDIR /usr/src/app
12 |
13 | COPY package.json /usr/src/app/
14 |
15 | # RUN npm i
16 |
17 | RUN npm i --registry=https://registry.npmmirror.com
18 |
19 | COPY . /usr/src/app
20 |
21 | EXPOSE 7001
22 |
23 | CMD npm run start
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 xcold
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # yuque-blog
2 |
3 | yuque—blog 是一款基于[语雀](http://yuque.com/)内容管理平台的博客系统,用户可以在语雀上进行文档仓库的管理,然后在自定义的站点中展示这些内容
4 |
5 | ## 主要特性
6 |
7 | - 优秀的文档编辑和管理体验(Powered by 语雀)
8 | - 极速输出博客页面
9 | - 可定制的博客主题
10 | - 支持服务端渲染
11 | - 支持 PWA 及离线访问
12 | - 便捷的运维体验,提供一键部署的 Docker 镜像
13 |
14 | ## 技术栈
15 |
16 | - 后端:Beidou (基于 Egg 和 React 的高性能同构框架)
17 | - 前端:React / Reach-Router / Mobx / Axios / Mock.js / WorkBox
18 |
19 | ## 配置文件
20 |
21 | > config.yml
22 |
23 | ```
24 | # 主题
25 | theme: txd
26 |
27 | # 语雀 API 设置
28 | yuque:
29 | base: https://www.yuque.com/api/v2
30 | login: yinzhi
31 | repo: blog
32 |
33 | # Site
34 | title: 小冷的备忘录
35 | subtitle: 但凡能引起思考的句子,都是些好句子
36 | keywords: 小冷的备忘录,HTML/CSS/JAVASCRIPT,前端工程师,Angular,Ionic,Vue,React,Node.js,Powershell,Qt5
37 | description: 小冷的备忘录,HTML/CSS/JAVASCRIPT,前端工程师,Angular,Ionic,Vue,React,Node.js,Powershell,Qt5
38 | author: 小冷
39 | language: zh-CN
40 |
41 | # 友情链接
42 | links:
43 | -
44 | name: 阿里巴巴
45 | url: https://www.alibaba.com
46 | -
47 | name: 阿里巴巴国际UED
48 | url: http://www.aliued.com/
49 | -
50 | name: 阿里巴巴U一点
51 | url: http://www.aliued.cn/
52 |
53 | # 导航链接
54 | navigators:
55 | -
56 | name: HOME
57 | url: /
58 | -
59 | name: BLOG
60 | url: /blogs
61 |
62 | ```
63 |
64 | ## Usage
65 |
66 | ### Install
67 |
68 | ```
69 | npm install
70 | ```
71 |
72 | ### 启动开发环境
73 |
74 | ```
75 | npm run dev
76 | ```
77 |
78 | ### 生产环境前端构建
79 |
80 | ```
81 | npm run build
82 | ```
83 |
84 | ### 生产环境开启
85 |
86 | ```
87 | npm start
88 | ```
89 |
90 | 访问: http://localhost:6001/
91 |
92 | ## Docker
93 |
94 | ```
95 | $ docker build -t egg-boilerplate .
96 | $ docker run -p 7001:7001 egg-boilerplate
97 | ```
98 |
99 | ## TODO
100 |
101 | - [x] 后端项目初始化
102 | - [x] 前端项目初始化
103 | - [x] 语雀仓库相关开放 API 服务
104 | - [x] 接口单元测试
105 | - [x] 接口代理开发
106 | - [x] mock 数据准备
107 | - [x] PWA 整站离线支持
108 | - [x] 页面开发
109 | - [x] 内容可配置
110 | - [x] 主题可定制
111 | - [ ] 页面优化
112 |
113 |
114 | ## License
115 |
116 | [MIT](LICENSE)
117 |
--------------------------------------------------------------------------------
/app/controller/home.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { Controller } = require('egg');
4 | const fs = require('fs');
5 | const path = require('path');
6 |
7 | const mobileUserAgentRegx =
8 | /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/;
9 |
10 | module.exports = (app) => {
11 | const { config } = app;
12 | const { blog: blogConfig } = config;
13 |
14 | class HomeController extends Controller {
15 | async renderPage(page, data) {
16 | const { ctx } = this;
17 | const userAgent = ctx.get('User-Agent');
18 | const mobileMode = !!userAgent && mobileUserAgentRegx.test(userAgent);
19 | await ctx.render('index', {
20 | config: blogConfig,
21 | env: config.env,
22 | mobileMode,
23 | ...data,
24 | });
25 | }
26 |
27 | async defaultRoute() {
28 | const { ctx } = this;
29 | const posts = await ctx.service.yuque.getArticleList();
30 | await this.renderPage('index', {
31 | posts: posts.data,
32 | });
33 | }
34 |
35 | async postRoute() {
36 | const { ctx } = this;
37 | const { slug } = ctx.params;
38 | const post = await ctx.service.yuque.getArticleDetail(slug);
39 | if (!post.data) {
40 | return ctx.redirect('/404.html');
41 | }
42 | await this.renderPage('index', {
43 | post: post.data,
44 | });
45 | }
46 |
47 | async serviceWorker() {
48 | const { ctx } = this;
49 | const filePath = path.join(app.baseDir, 'app/public/service-worker.js');
50 | const promise = new Promise((resolve, reject) => {
51 | fs.readFile(filePath, (err, data) => {
52 | if (err) {
53 | return reject(err);
54 | }
55 | resolve(data);
56 | });
57 | });
58 | const text = await promise;
59 | ctx.type = 'application/javascript';
60 | ctx.body = text;
61 | }
62 | }
63 | return HomeController;
64 | };
65 |
66 |
--------------------------------------------------------------------------------
/app/controller/yuque.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { Controller } = require('egg');
4 |
5 | class YuqueController extends Controller {
6 | async getArticleList() {
7 | const { ctx } = this;
8 | const result = await ctx.service.yuque.getArticleList();
9 | ctx.body = result;
10 | }
11 |
12 | async getArticleDetail() {
13 | const { ctx } = this;
14 | const { slug } = ctx.params;
15 | const result = await ctx.service.yuque.getArticleDetail(slug);
16 | ctx.body = result;
17 | }
18 |
19 | async getArticleToc() {
20 | const { ctx } = this;
21 | const result = await ctx.service.yuque.getArticleToc();
22 | ctx.body = result;
23 | }
24 |
25 | async getUser() {
26 | const { ctx } = this;
27 | const { id } = ctx.params;
28 | const result = await ctx.service.yuque.getUser(id);
29 | ctx.body = result;
30 | }
31 | }
32 |
33 | module.exports = YuqueController;
34 |
--------------------------------------------------------------------------------
/app/public/service-worker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Welcome to your Workbox-powered service worker!
3 | *
4 | * You'll need to register this file in your web app and you should
5 | * disable HTTP caching for this file too.
6 | * See https://goo.gl/nhQhGp
7 | *
8 | * The rest of the code is auto-generated. Please don't update this file
9 | * directly; instead, make changes to your Workbox build configuration
10 | * and re-run your build process.
11 | * See https://goo.gl/2aRDsh
12 | */
13 | /* eslint no-undef: "off" */
14 | /* eslint no-restricted-globals: "off" */
15 |
16 | importScripts('https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js');
17 |
18 | workbox.setConfig({
19 | modulePathPrefix: 'https://g.alicdn.com/kg/workbox/3.3.0/',
20 | });
21 |
22 | workbox.core.setCacheNameDetails({ prefix: 'webpack-pwa' });
23 |
24 | workbox.skipWaiting();
25 | workbox.clientsClaim();
26 |
27 | /**
28 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to
29 | * requests for URLs in the manifest.
30 | * See https://goo.gl/S9QRab
31 | */
32 | self.__precacheManifest = [].concat(self.__precacheManifest || []);
33 | workbox.precaching.suppressWarnings();
34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {
35 | directoryIndex: '/',
36 | });
37 |
38 | workbox.routing.registerRoute(
39 | /\.(?:png|jpg|jpeg|svg)$/,
40 | workbox.strategies.cacheFirst({
41 | cacheName: 'images1',
42 | plugins: [
43 | new workbox.expiration.Plugin({
44 | maxEntries: 100,
45 | purgeOnQuotaError: false,
46 | }),
47 | ],
48 | }),
49 | 'GET'
50 | );
51 |
52 | workbox.routing.registerRoute(/\.*/, workbox.strategies.networkFirst(), 'GET');
53 |
--------------------------------------------------------------------------------
/app/router.js:
--------------------------------------------------------------------------------
1 | module.exports = (app) => {
2 | // 语雀文章 API
3 | app.get('/api/yuque/ariticles', app.controller.yuque.getArticleList);
4 | app.get('/api/yuque/ariticleToc', app.controller.yuque.getArticleToc);
5 | app.get('/api/yuque/ariticle/:slug', app.controller.yuque.getArticleDetail);
6 | app.get('/api/yuque/user/:id', app.controller.yuque.getUser);
7 |
8 | // sw
9 | app.get('', '/service-worker.js', app.controller.home.serviceWorker);
10 |
11 | // 页面相关
12 | app.get('', '/post/:slug', app.controller.home.postRoute);
13 | app.get('', '/404.html', app.controller.home.defaultRoute);
14 | app.get('', '/*', app.controller.home.defaultRoute);
15 | };
16 |
--------------------------------------------------------------------------------
/app/schedule/refresh-bing-images.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const lodash = require('lodash');
4 |
5 | module.exports = (app) => {
6 | return {
7 | schedule: {
8 | cron: '0 0 */3 * * *',
9 | type: 'all',
10 | immediate: true,
11 | },
12 |
13 | async task(ctx) {
14 | const { service } = ctx;
15 | const images = await service.bing.getImages();
16 | let bingImages = lodash.get(images, 'images', []);
17 | bingImages = bingImages.map(
18 | item => `//cn.bing.com/${item.urlbase}_800x600.jpg`
19 | );
20 | if (bingImages.length > 0) {
21 | app.bingImages = bingImages;
22 | }
23 | },
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/app/service/bing.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { Service } = require('egg');
4 |
5 | module.exports = () => {
6 | const Api = 'https://cn.bing.com/HPImageArchive.aspx';
7 | class BingService extends Service {
8 | async getImages() {
9 | const { ctx } = this;
10 | const now = Date.now();
11 | const api = `${Api}?format=js&idx=0&n=8&nc=${now}&pid=hp`;
12 | const result = await ctx.curl(api, {
13 | dataType: 'json',
14 | });
15 | return result.data;
16 | }
17 | }
18 | return BingService;
19 | };
20 |
--------------------------------------------------------------------------------
/app/service/yuque.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('assert');
4 | const lodash = require('lodash');
5 | const { Service } = require('egg');
6 |
7 | const md = require('../util/md');
8 |
9 | module.exports = (app) => {
10 | const { config } = app;
11 | const { blog } = config;
12 | const { yuque: yuqueConfig } = blog;
13 | const { base, login, repo } = yuqueConfig;
14 | assert(login && repo, 'login 和 repo 必须配置');
15 | const namespace = `${login}/${repo}`;
16 | const API_HOST = base || 'https://www.yuque.com/api/v2';
17 |
18 | class YuqueService extends Service {
19 | getRandomImage() {
20 | const { bingImages = [] } = app;
21 | const rand = Math.floor(Math.random() * bingImages.length);
22 | return bingImages[rand];
23 | }
24 |
25 | async getArticleList() {
26 | const { ctx } = this;
27 | const api = `${API_HOST}/repos/${namespace}/docs`;
28 | const result = await ctx.curl(api, {
29 | dataType: 'json',
30 | });
31 | const { data } = result;
32 | let list;
33 | try {
34 | list = data.data;
35 | list.forEach((article) => {
36 | article.thumb = this.getRandomImage();
37 | });
38 | } catch (error) {
39 | ctx.logger.error(error);
40 | }
41 | data.data = list;
42 | return data;
43 | }
44 |
45 | async getArticleDetail(slug) {
46 | const { ctx } = this;
47 | const data = await this.getArticleDetailRaw(slug);
48 | let article;
49 | try {
50 | article = data.data;
51 | article = lodash.pick(article, [
52 | 'id',
53 | 'slug',
54 | 'title',
55 | 'book_id',
56 | 'user_id',
57 | 'format',
58 | 'public',
59 | 'status',
60 | 'likes_count',
61 | 'comments_count',
62 | 'content_updated_at',
63 | 'deleted_at',
64 | 'created_at',
65 | 'updated_at',
66 | 'published_at',
67 | 'word_count',
68 | '_serializer',
69 | 'book',
70 | 'creator',
71 | 'body',
72 | 'body_html',
73 | 'body_draft',
74 | ]);
75 | article.body_html = md.render(article.body);
76 | article.thumb = this.getRandomImage();
77 | } catch (error) {
78 | ctx.logger.error(error);
79 | }
80 | data.data = article;
81 | return data;
82 | }
83 |
84 | async getArticleDetailRaw(slug) {
85 | const { ctx } = this;
86 | const api = `${API_HOST}/repos/${namespace}/docs/${slug}?raw=true`;
87 | const result = await ctx.curl(api, {
88 | dataType: 'json',
89 | });
90 | return result.data;
91 | }
92 |
93 | async getArticleToc() {
94 | const { ctx } = this;
95 | const api = `${API_HOST}/repos/${namespace}/toc`;
96 | const result = await ctx.curl(api, {
97 | dataType: 'json',
98 | });
99 | return result.data;
100 | }
101 |
102 | async getUser(id) {
103 | const { ctx } = this;
104 | const api = `${API_HOST}/users/${id}`;
105 | const result = await ctx.curl(api, {
106 | dataType: 'json',
107 | });
108 | return result.data;
109 | }
110 | }
111 | return YuqueService;
112 | };
113 |
--------------------------------------------------------------------------------
/app/util/md.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Mkit = require('markdown-it');
4 | const hljs = require('highlight.js');
5 | const markdownItGithubPreamble = require('markdown-it-github-preamble');
6 | const markdownItFootnote = require('markdown-it-footnote');
7 | const markdownItKatex = require('markdown-it-katex');
8 |
9 | const md = new Mkit({
10 | html: true,
11 | linkify: true,
12 | highlight(str, lang) {
13 | if (lang && hljs.getLanguage(lang)) {
14 | try {
15 | return hljs.highlight(lang, str).value;
16 | } catch (err) {
17 | console.log(err);
18 | }
19 | }
20 | return ''; // use external default escaping
21 | },
22 | })
23 | .use(markdownItGithubPreamble)
24 | .use(markdownItFootnote)
25 | .use(markdownItKatex);
26 |
27 | module.exports = md;
28 |
--------------------------------------------------------------------------------
/config.yml:
--------------------------------------------------------------------------------
1 | # 主题
2 | theme: txd
3 |
4 | # 语雀 API 设置
5 | yuque:
6 | base: https://www.yuque.com/api/v2
7 | login: yinzhi
8 | repo: blog
9 |
10 | # Site
11 | title: 小冷的备忘录
12 | subtitle: 但凡能引起思考的句子,都是些好句子
13 | keywords: 小冷的备忘录,HTML/CSS/JAVASCRIPT,前端工程师,Angular,Ionic,Vue,React,Node.js,Powershell,Qt5
14 | description: 小冷的备忘录,HTML/CSS/JAVASCRIPT,前端工程师,Angular,Ionic,Vue,React,Node.js,Powershell,Qt5
15 | author: 小冷
16 | language: zh-CN
17 |
18 | # 友情链接
19 | links:
20 | -
21 | name: 阿里巴巴
22 | url: https://www.alibaba.com
23 | -
24 | name: 阿里巴巴国际UED
25 | url: http://www.aliued.com/
26 | -
27 | name: 阿里巴巴U一点
28 | url: http://www.aliued.cn/
29 |
30 | # 导航链接
31 | navigators:
32 | -
33 | name: HOME
34 | url: /
35 | -
36 | name: BLOG
37 | url: /blogs
38 |
--------------------------------------------------------------------------------
/config/config.default.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const yaml = require('js-yaml');
6 |
7 | module.exports = (appInfo) => {
8 | let blogConfig = {};
9 | // Get document, or throw exception on error
10 | try {
11 | blogConfig = yaml.safeLoad(
12 | fs.readFileSync(
13 | path.join(appInfo.baseDir, 'config.yml'),
14 | 'utf8'
15 | )
16 | );
17 | } catch (e) {
18 | console.error(e.message);
19 | process.exit(-1);
20 | }
21 | const clientViewRoot = path.join(
22 | appInfo.baseDir, `themes/${blogConfig.theme}`
23 | );
24 | const serverViewRoot = path.join(appInfo.baseDir, '/app/view');
25 | const config = {
26 | keys: 'key',
27 | client: clientViewRoot,
28 | view: {
29 | defaultExtension: '.jsx',
30 | root: `$${serverViewRoot},${clientViewRoot}`,
31 | },
32 | isomorphic: {
33 | babel: {
34 | plugins: [
35 | require.resolve('babel-plugin-transform-decorators-legacy'),
36 | ],
37 | },
38 | alias: {
39 | client: clientViewRoot,
40 | },
41 | },
42 | webpack: {
43 | custom: {
44 | configPath: path.join(appInfo.baseDir, 'config/webpack.config.js'),
45 | },
46 | },
47 | blog: blogConfig,
48 | };
49 | return config;
50 | };
51 |
--------------------------------------------------------------------------------
/config/config.local.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {};
4 |
--------------------------------------------------------------------------------
/config/config.prod.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {};
4 |
--------------------------------------------------------------------------------
/config/config.unittest.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = () => {
4 | const config = {
5 | view: {
6 | defaultViewEngine: 'react',
7 | defaultExtension: '.jsx',
8 | },
9 | };
10 | return config;
11 | };
12 |
--------------------------------------------------------------------------------
/config/plugin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {};
4 |
--------------------------------------------------------------------------------
/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
4 |
5 | const isDev = process.env.NODE_ENV === 'development';
6 |
7 | module.exports = (app, defaultConfig) => {
8 | // For mobx decorators
9 | for (const loader of defaultConfig.module.rules) {
10 | if (loader.test.test('.jsx') && loader.test.test('.js')) {
11 | if (!Array.isArray(loader.use.options.plugins)) {
12 | loader.use.options.plugins = [];
13 | }
14 | loader.use.options.plugins.push(
15 | require.resolve('babel-plugin-transform-decorators-legacy')
16 | );
17 | break;
18 | }
19 | }
20 |
21 | // development
22 | if (isDev) {
23 | return defaultConfig;
24 | }
25 |
26 | // production
27 | defaultConfig.plugins.push(
28 | new BundleAnalyzerPlugin()
29 | );
30 |
31 | return defaultConfig;
32 | };
33 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true
4 | }
5 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yuque-blog",
3 | "version": "1.0.0",
4 | "description": "backend by yuque.com",
5 | "scripts": {
6 | "start": "beidou start --daemon",
7 | "debug": "beidou debug",
8 | "stop": "beidou stop",
9 | "dev": "beidou dev",
10 | "build": "beidou build",
11 | "build:node": "beidou build --target=node",
12 | "lint": "eslint --fix --ext .jsx,.js client",
13 | "lint:style": "stylelint \"clent/**/*.scss\" --syntax scss",
14 | "prettier": "prettier --write './client/**/**/**/*.{js,json,scss,css}' '*.js'",
15 | "test": "egg-bin test",
16 | "snyk-protect": "snyk protect",
17 | "prepublish": "npm run snyk-protect"
18 | },
19 | "devDependencies": {
20 | "babel-eslint": "^8.2.1",
21 | "babel-plugin-transform-decorators-legacy": "^1.3.5",
22 | "egg-mock": "^3.19.2",
23 | "eslint": "^4.15.0",
24 | "eslint-config-beidou": "^1.0.3",
25 | "eslint-config-prettier": "^2.9.0",
26 | "eslint-plugin-import": "^2.8.0",
27 | "eslint-plugin-react": "^7.5.1",
28 | "prettier": "^1.14.0",
29 | "stylelint": "^9.4.0",
30 | "stylelint-config-prettier": "^4.0.0",
31 | "webpack-bundle-analyzer": "^3.6.0"
32 | },
33 | "dependencies": {
34 | "axios": "^1.6.4",
35 | "babel-polyfill": "^6.26.0",
36 | "beidou-cli": "^1.0.5",
37 | "beidou-core": "^2.0.0",
38 | "classnames": "^2.2.6",
39 | "domready": "^1.0.8",
40 | "highlight.js": "^10.4.1",
41 | "js-yaml": "^3.12.0",
42 | "markdown-it": "^10.0.0",
43 | "markdown-it-footnote": "^3.0.1",
44 | "markdown-it-github-preamble": "^1.0.0",
45 | "markdown-it-katex": "^2.0.3",
46 | "marked": "^2.0.0",
47 | "mobx": "^5.0.3",
48 | "mobx-devtools": "^0.9.18",
49 | "mobx-react": "^5.2.3",
50 | "mobx-react-devtools": "^6.0.2",
51 | "normalize.css": "^8.0.0",
52 | "parallax-js": "^3.1.0",
53 | "prop-types": "^15.6.2",
54 | "react": "^16.3.0",
55 | "react-dom": "^16.3.0",
56 | "react-router-dom": "^4.3.1",
57 | "snyk": "^1.316.1"
58 | },
59 | "engines": {
60 | "node": ">= 8.0.0"
61 | },
62 | "license": "MIT",
63 | "snyk": true
64 | }
65 |
--------------------------------------------------------------------------------
/test/controller/home.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { app } = require('egg-mock/bootstrap');
4 | const assert = require('assert');
5 |
6 | // unittest don't support ssr!
7 | describe('test/controller/home.test.js', () => {
8 | describe('GET /', () => {
9 | it('should GET /', async () => {
10 | const result = await app
11 | .httpRequest()
12 | .get('/');
13 | // Excuse me! I didn't find a solution to support ssr unit test.
14 | assert.equal(result.status, 500);
15 | });
16 | });
17 |
18 | describe('GET /post/:slug', () => {
19 | it('should GET /post/:slug', async () => {
20 | const result = await app
21 | .httpRequest()
22 | .get('/post/gdquyk');
23 | assert.equal(result.status, 500);
24 | });
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/test/controller/yuque.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { app } = require('egg-mock/bootstrap');
4 | const assert = require('assert');
5 |
6 | describe('test/controller/yuque.test.js', () => {
7 | describe('GET /api/yuque/ariticles', () => {
8 | it('should GET /api/yuque/ariticles', async () => {
9 | const result = await app
10 | .httpRequest()
11 | .get('/api/yuque/ariticles');
12 | assert.equal(result.status, 200);
13 | assert.equal(Array.isArray(result.body.data), true);
14 | });
15 | });
16 |
17 | describe('GET /api/yuque/ariticleToc', () => {
18 | it('should GET /api/yuque/ariticleToc', async () => {
19 | const result = await app
20 | .httpRequest()
21 | .get('/api/yuque/ariticleToc');
22 | assert.equal(result.status, 200);
23 | assert.equal(Array.isArray(result.body.data), true);
24 | });
25 | });
26 |
27 | describe('GET /api/yuque/ariticle/:slug', () => {
28 | it('should GET /api/yuque/ariticle/:slug', async () => {
29 | const result = await app
30 | .httpRequest()
31 | .get('/api/yuque/ariticle/gdquyk');
32 | assert.equal(result.status, 200);
33 | assert.equal(typeof result.body.data, 'object');
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/themes/txd/common/router.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import { BrowserRouter, StaticRouter, Switch } from 'react-router-dom';
5 | import { Provider } from 'mobx-react';
6 | import DevTool from 'mobx-react-devtools';
7 |
8 | import AppStore from '../stores/app';
9 | import PostStore from '../stores/post';
10 |
11 | import { DefaultLayout, HomeLayout, BlogLayout } from '../containers/Layout';
12 | import Home from '../pages/Home';
13 | import Post from '../pages/Post';
14 | import Blog from '../pages/Blog';
15 | import About from '../pages/About';
16 | import Careers from '../pages/Careers';
17 | import NotFound from '../pages/404';
18 |
19 | import '../styles/site.scss';
20 |
21 | const isDev = process.env.NODE_ENV === 'development';
22 |
23 | const Router = __CLIENT__ ? BrowserRouter : StaticRouter;
24 |
25 | let postStore;
26 | let appStore;
27 |
28 | if (__CLIENT__) {
29 | const initialState = window.initialState || {};
30 | postStore = PostStore.fromJS(initialState);
31 | appStore = AppStore.fromJS(initialState);
32 | // 监听窗口变化
33 | appStore.listenWindow();
34 | }
35 |
36 | export default (props) => {
37 | const { context, location } = props;
38 | return (
39 |