├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── lib └── generator.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yusen 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 | # hexo-generator-restful 2 | 3 | Generate restful json data for Hexo plugins. 4 | 5 | 生成 restful 风格的 json 数据,可以当作 api 接口,开始构建一个 SPA 应用吧。 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm install hexo-generator-restful --save 11 | ``` 12 | 13 | ## Config 14 | 15 | 以下为默认配置,属性值为 `false` 表示不生成。 16 | 17 | ```yml 18 | restful: 19 | # site 可配置为数组选择性生成某些属性 20 | # site: ['title', 'subtitle', 'description', 'author', 'since', email', 'favicon', 'avatar'] 21 | site: true # hexo.config mix theme.config 22 | posts_size: 10 # 文章列表分页,0 表示不分页 23 | posts_props: # 文章列表项的需要生成的属性 24 | title: true 25 | slug: true 26 | date: true 27 | updated: true 28 | comments: true 29 | path: true 30 | excerpt: false 31 | cover: true # 封面图,取文章第一张图片 32 | content: false 33 | keywords: false 34 | categories: true 35 | tags: true 36 | categories: true # 分类数据 37 | use_category_slug: false # Use slug for filename of category data 38 | tags: true # 标签数据 39 | use_tag_slug: false # Use slug for filename of tag data 40 | post: true # 文章数据 41 | pages: false # 额外的 Hexo 页面数据, 如 About 42 | ``` 43 | 44 | ## Document 45 | 46 | ### Get Hexo Config 47 | 48 | 获取所有 Hexo 配置(站点配置和主题配置)。 49 | 50 | ###### Request 51 | 52 | ``` 53 | GET /api/site.json 54 | ``` 55 | 56 | ###### Response 57 | 58 | [/api/site.json](http://www.imys.net/api/site.json) 59 | 60 | ### Get Posts 61 | 62 | 如果配置 `posts_size: 0` 则不分页,以下请求会获取全部文章。 63 | 64 | ###### Request 65 | 66 | ``` 67 | GET /api/posts.json 68 | ``` 69 | 70 | ###### Response 71 | 72 | 示例为分页配置下的数据,会包含分页属性 `total`、`pageSize`、`pageCount`,不分页的数据不包含这三项。 73 | 74 | [/api/posts.json](http://www.imys.net/api/posts.json) 75 | 76 | ### Get Posts By Page 77 | 78 | 获取分页数据 79 | 80 | ###### Request 81 | 82 | ``` 83 | GET /api/posts/:PageNum.json 84 | ``` 85 | 86 | ###### Response 87 | 88 | [/api/posts/1.json](http://www.imys.net/api/posts/1.json) 89 | 90 | ### Get All Tags 91 | 92 | 获取所有文章标签,如果文章无标签则不生成。 93 | 94 | ###### Request 95 | 96 | ``` 97 | GET /api/tags.json 98 | ``` 99 | 100 | ###### Response 101 | 102 | [/api/tags.json](http://www.imys.net/api/tags.json) 103 | 104 | ### Get Posts By Tag 105 | 106 | 获取某一标签下的所有文章 107 | 108 | ###### Request 109 | 110 | ``` 111 | GET /api/tags/:TagName.json 112 | ``` 113 | 114 | ###### Response 115 | 116 | [/api/tags/Hexo.json](http://www.imys.net/api/tags/Hexo.json) 117 | 118 | ### Get All Categories 119 | 120 | 获取所有文章分类,如果文章无分类则不生成。 121 | 122 | ###### Request 123 | 124 | ``` 125 | GET /api/categories.json 126 | ``` 127 | 128 | ###### Response 129 | 130 | 数据格式同 Get All Tags 131 | 132 | ### Get Posts By Categorie 133 | 134 | 获取某一分类下的所有文章 135 | 136 | ###### Request 137 | 138 | ``` 139 | GET /api/categories/:CategorieName.json 140 | ``` 141 | 142 | ###### Response 143 | 144 | 数据格式同 Get Posts By Tag 145 | 146 | ### Get Post By Slug 147 | 148 | 根据文章别名获取文章详细信息 149 | 150 | ###### Request 151 | 152 | ``` 153 | GET /api/articles/:Slug.json 154 | ``` 155 | 156 | ###### Response 157 | 158 | [/api/articles/javascript-advanced-functions.json](http://www.imys.net/api/articles/javascript-advanced-functions.json) 159 | 160 | ### Get Implecit Pages 161 | 162 | 获取来自主题的 Hexo 隐式页面内容,如 About 等。因隐式页面(除 About 等导航栏入口页外)一般在 Hexo 不提供直接访问入口,调用此 API 的开发者需要了解其完整路径,此接口默认关闭。 163 | 164 | 例如: 165 | 166 | ###### Request 167 | 168 | ``` 169 | GET /api/pages/about.json 170 | ``` 171 | 172 | ###### Response 173 | 174 | 格式类似于于 Get Post By Slug。 -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var generator = require('./lib/generator'); 4 | 5 | hexo.extend.generator.register('restful', function(site) { 6 | return generator(Object.assign({}, hexo.config, hexo.theme.config), site); 7 | }); 8 | -------------------------------------------------------------------------------- /lib/generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pagination = require('hexo-pagination'); 4 | var _pick = require('lodash.pick'); 5 | 6 | function filterHTMLTags(str) { 7 | return str ? str 8 | .replace(/\<(?!img|br).*?\>/g, "") 9 | .replace(/\r?\n|\r/g, '') 10 | .replace(//g, ' [Figure] ') : null 11 | } 12 | function fetchCovers(str) { 13 | var temp, 14 | imgURLs = [], 15 | rex = /]+src="?([^"\s]+)"(.*)>/g; 16 | while ( temp = rex.exec( str ) ) { 17 | imgURLs.push( temp[1] ); 18 | } 19 | return imgURLs.length > 0 ? imgURLs : null; 20 | } 21 | function fetchCover(str) { 22 | var covers = fetchCovers(str) 23 | return covers ? covers[0] : null; 24 | } 25 | 26 | module.exports = function (cfg, site) { 27 | 28 | var restful = cfg.hasOwnProperty('restful') ? cfg.restful : 29 | { 30 | site: true, 31 | posts_size: 10, 32 | posts_props: { 33 | title: true, 34 | slug: true, 35 | date: true, 36 | updated: true, 37 | comments: true, 38 | cover: true, 39 | path: true, 40 | raw: false, 41 | excerpt: false, 42 | content: false, 43 | categories: true, 44 | tags: true 45 | }, 46 | categories: true, 47 | use_category_slug: false, 48 | tags: true, 49 | use_tag_slug: false, 50 | post: true, 51 | pages: false, 52 | }, 53 | 54 | posts = site.posts.sort('-date').filter(function (post) { 55 | return post.published; 56 | }), 57 | 58 | posts_props = (function () { 59 | var props = restful.posts_props; 60 | 61 | return function (name, val) { 62 | return props[name] ? (typeof val === 'function' ? val() : val) : null; 63 | } 64 | })(), 65 | 66 | postMap = function (post) { 67 | return { 68 | title: posts_props('title', post.title), 69 | slug: posts_props('slug', post.slug), 70 | date: posts_props('date', post.date), 71 | updated: posts_props('updated', post.updated), 72 | comments: posts_props('comments', post.comments), 73 | path: posts_props('path', 'api/articles/' + post.slug + '.json'), 74 | excerpt: posts_props('excerpt', filterHTMLTags(post.excerpt)), 75 | keywords: posts_props('keywords', cfg.keywords), 76 | // cover: posts_props('cover', fetchCover(post.content)), 77 | cover: posts_props('cover', post.cover || fetchCover(post.content)), 78 | content: posts_props('content', post.content), 79 | raw: posts_props('raw', post.raw), 80 | categories: posts_props('categories', function () { 81 | return post.categories.map(function (cat) { 82 | const name = ( 83 | cfg.restful.use_category_slug && cat.slug 84 | ) ? cat.slug : cat.name; 85 | return { 86 | name: name, 87 | path: 'api/categories/' + name + '.json' 88 | }; 89 | }); 90 | }), 91 | tags: posts_props('tags', function () { 92 | return post.tags.map(function (tag) { 93 | const name = ( 94 | cfg.restful.use_tag_slug && tag.slug 95 | ) ? tag.slug : tag.name; 96 | return { 97 | name: name, 98 | path: 'api/tags/' + name + '.json' 99 | }; 100 | }); 101 | }) 102 | }; 103 | }, 104 | 105 | cateReduce = function (cates, kind) { 106 | return cates.reduce(function (result, item) { 107 | if (!item.length) return result; 108 | 109 | let use_slug = null; 110 | switch (kind) { 111 | case 'categories': 112 | use_slug = cfg.restful.use_category_slug; 113 | break; 114 | case 'tags': 115 | use_slug = cfg.restful.use_tag_slug; 116 | break; 117 | } 118 | 119 | const name = (use_slug && item.slug) ? item.slug : item.name; 120 | 121 | return result.concat(pagination(item.path, posts, { 122 | perPage: 0, 123 | data: { 124 | name: name, 125 | path: 'api/' + kind + '/' + name + '.json', 126 | postlist: item.posts.map(postMap) 127 | } 128 | 129 | })); 130 | }, []); 131 | }, 132 | 133 | catesMap = function (item) { 134 | return { 135 | name: item.data.name, 136 | path: item.data.path, 137 | count: item.data.postlist.length 138 | }; 139 | }, 140 | 141 | cateMap = function (item) { 142 | var itemData = item.data; 143 | return { 144 | path: itemData.path, 145 | data: JSON.stringify({ 146 | name: itemData.name, 147 | postlist: itemData.postlist 148 | }) 149 | }; 150 | }, 151 | 152 | apiData = []; 153 | 154 | 155 | if (restful.site) { 156 | apiData.push({ 157 | path: 'api/site.json', 158 | data: JSON.stringify(restful.site instanceof Array ? _pick(cfg, restful.site) : cfg) 159 | }); 160 | } 161 | 162 | if (restful.categories) { 163 | 164 | var cates = cateReduce(site.categories, 'categories'); 165 | 166 | if (!!cates.length) { 167 | apiData.push({ 168 | path: 'api/categories.json', 169 | data: JSON.stringify(cates.map(catesMap)) 170 | }); 171 | 172 | apiData = apiData.concat(cates.map(cateMap)); 173 | } 174 | 175 | } 176 | 177 | if (restful.tags) { 178 | var tags = cateReduce(site.tags, 'tags'); 179 | 180 | if (tags.length) { 181 | apiData.push({ 182 | path: 'api/tags.json', 183 | data: JSON.stringify(tags.map(catesMap)) 184 | }); 185 | 186 | apiData = apiData.concat(tags.map(cateMap)); 187 | } 188 | 189 | } 190 | 191 | var postlist = posts.map(postMap); 192 | 193 | if (restful.posts_size > 0) { 194 | 195 | var page_posts = [], 196 | i = 0, 197 | len = postlist.length, 198 | ps = restful.posts_size, 199 | pc = Math.ceil(len / ps); 200 | 201 | for (; i < len; i += ps) { 202 | page_posts.push({ 203 | path: 'api/posts/' + Math.ceil((i + 1) / ps) + '.json', 204 | data: JSON.stringify({ 205 | total: len, 206 | pageSize: ps, 207 | pageCount: pc, 208 | data: postlist.slice(i, i + ps) 209 | }) 210 | }); 211 | } 212 | 213 | apiData.push({ 214 | path: 'api/posts.json', 215 | data: page_posts[0].data 216 | }); 217 | 218 | apiData = apiData.concat(page_posts); 219 | 220 | } else { 221 | 222 | apiData.push({ 223 | path: 'api/posts.json', 224 | data: JSON.stringify(postlist) 225 | }); 226 | } 227 | 228 | if (restful.post) { 229 | apiData = apiData.concat(posts.map(function (post) { 230 | var path = 'api/articles/' + post.slug + '.json'; 231 | return { 232 | path: path, 233 | data: JSON.stringify({ 234 | title: post.title, 235 | slug: post.slug, 236 | date: post.date, 237 | updated: post.updated, 238 | comments: post.comments, 239 | path: path, 240 | excerpt: filterHTMLTags(post.excerpt), 241 | covers: fetchCovers(post.content), 242 | keywords: cfg.keyword, 243 | content: post.content, 244 | more: post.more, 245 | categories: post.categories.map(function (cat) { 246 | return { 247 | name: cat.name, 248 | path: 'api/categories/' + cat.name + '.json' 249 | }; 250 | }), 251 | tags: post.tags.map(function (tag) { 252 | return { 253 | name: tag.name, 254 | path: 'api/tags/' + tag.name + '.json' 255 | }; 256 | }) 257 | }) 258 | }; 259 | })); 260 | } 261 | 262 | if (restful.pages) { 263 | apiData = apiData.concat(site.pages.data.map(function (page) { 264 | var safe_title = page.title.replace(/[^a-z0-9]/gi, '-').toLowerCase() 265 | var path = 'api/pages/' + safe_title + '.json'; 266 | 267 | return { 268 | path: path, 269 | data: JSON.stringify({ 270 | title: page.title, 271 | date: page.date, 272 | updated: page.updated, 273 | comments: page.comments, 274 | path: path, 275 | covers: fetchCovers(page.content), 276 | excerpt: filterHTMLTags(page.excerpt), 277 | content: page.content 278 | }) 279 | }; 280 | })); 281 | } 282 | 283 | return apiData; 284 | }; 285 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-generator-restful", 3 | "version": "0.2.3", 4 | "description": "Generate restful json data for Hexo plugins.", 5 | "main": "index.js", 6 | "keywords": ["hexo", "restful", "json", "generator"], 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/yscoder/hexo-generator-restful.git" 10 | }, 11 | "author": "yusen", 12 | "license": "MIT", 13 | "dependencies": { 14 | "hexo-pagination": "^0.1.0", 15 | "lodash.pick": "^4.4.0" 16 | } 17 | } 18 | --------------------------------------------------------------------------------