diff --git a/docs/other.md b/docs/other.md
index 595e1921a9..2732fb6df7 100644
--- a/docs/other.md
+++ b/docs/other.md
@@ -270,6 +270,26 @@ type 为 all 时,category 参数不支持 cost 和 free
+## wikiHow
+
+### 首页
+
+
+
+### 分类目录
+
+
+
+顶级目录分类可在目录分类页[查看](https://zh.wikihow.com/Special:CategoryListing), 支持二级目录
+
+类型
+
+| 所有 | 推荐 |
+| ---- | ---- |
+| all | rec |
+
+
+
## ZAKER
### source
@@ -1041,6 +1061,24 @@ type 为 all 时,category 参数不支持 cost 和 free
+## 正版中国
+
+### 分类列表
+
+
+
+类型
+
+| 全部文章 | 永久免费 | 限时折扣 | 限时免费 | PC | Mac | Android | UWP |
+| -------- | -------- | -------- | -------- | --- | --- | ------- | --- |
+| all | 311 | 309 | 310 | 8 | 50 | 17 | 312 |
+
+
+
+### 搜索
+
+
+
## 中国大学 MOOC(慕课)
### 最新
diff --git a/lib/router.js b/lib/router.js
index b89cbddee7..7971bc588e 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -1545,4 +1545,12 @@ router.get('/ff14/ff14_zh/:type', require('./routes/ff14/ff14_zh'));
router.get('/xuetangx/course/:cid/:type', require('./routes/xuetangx/course_info'));
router.get('/xuetangx/course/list/:mode/:credential/:status/:type?', require('./routes/xuetangx/course_list'));
+// wikihow
+router.get('/wikihow/index', require('./routes/wikihow/index.js'));
+router.get('/wikihow/category/:category/:type', require('./routes/wikihow/category.js'));
+
+// 正版中国
+router.get('/getitfree/category/:category?', require('./routes/getitfree/category.js'));
+router.get('/getitfree/search/:keyword?', require('./routes/getitfree/search.js'));
+
module.exports = router;
diff --git a/lib/routes/getitfree/category.js b/lib/routes/getitfree/category.js
new file mode 100644
index 0000000000..9ecb18b1c0
--- /dev/null
+++ b/lib/routes/getitfree/category.js
@@ -0,0 +1,59 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const utils = require('./utils');
+
+module.exports = async (ctx) => {
+ const { category = 'all' } = ctx.params;
+ const baseUrl = `https://getitfree.cn`;
+ const categoryToLabel = {
+ '8': 'PC',
+ '17': 'Android',
+ '50': 'Mac',
+ '309': '限时折扣',
+ '310': '限时免费',
+ '311': '永久免费',
+ '312': 'UWP',
+ all: '全部文章',
+ };
+
+ let item = [];
+ if (category === 'all') {
+ const res = await got(baseUrl);
+ const $ = cheerio.load(res.data);
+ $('#page-content .top-content .top-slide > .item')
+ .toArray()
+ .forEach((ele) => {
+ const $item = cheerio.load(ele);
+ const infoNode = $item('.slider-image > a');
+ const title = infoNode.attr('title');
+ const link = infoNode.attr('href');
+ const thumbnail = infoNode.attr('style').replace(/background-image:url\(|\)/g, '');
+ const deadlineStr = utils.getDeadlineStr($item, '.countDownCont');
+ const description = [title, deadlineStr, ``].filter((str) => !!str).join('
');
+ item.push({
+ title,
+ link,
+ description,
+ });
+ });
+ item = item.concat(utils.parseListItem($, '.main-content'));
+ } else {
+ const res = await got(baseUrl, {
+ query: {
+ action: 'fa_load_postlist',
+ paged: 1,
+ category,
+ },
+ });
+ const $ = cheerio.load(res.data);
+ item = utils.parseListItem($, '.ajax-load-con');
+ }
+
+ const categoryLabel = categoryToLabel[category];
+ ctx.state.data = {
+ title: `正版中国 - ${categoryLabel}`,
+ description: `正版中国 - ${categoryLabel}`,
+ link: baseUrl,
+ item,
+ };
+};
diff --git a/lib/routes/getitfree/search.js b/lib/routes/getitfree/search.js
new file mode 100644
index 0000000000..a419195cda
--- /dev/null
+++ b/lib/routes/getitfree/search.js
@@ -0,0 +1,25 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const qs = require('query-string');
+const utils = require('./utils');
+
+module.exports = async (ctx) => {
+ const { keyword } = ctx.params;
+ const link = 'https://getitfree.cn';
+ const query = {
+ s: keyword,
+ };
+ const res = await got(link, {
+ query,
+ });
+ const $ = cheerio.load(res.data);
+
+ const item = utils.parseListItem($, '#page-content');
+
+ ctx.state.data = {
+ title: `正版中国搜索 - ${keyword}`,
+ description: `正版中国搜索 - ${keyword}`,
+ link: `${link}?${qs.stringify(query)}`,
+ item,
+ };
+};
diff --git a/lib/routes/getitfree/utils.js b/lib/routes/getitfree/utils.js
new file mode 100644
index 0000000000..3273141e17
--- /dev/null
+++ b/lib/routes/getitfree/utils.js
@@ -0,0 +1,47 @@
+const cheerio = require('cheerio');
+
+const getDeadlineStr = ($, countDownNodeSelector) => {
+ const countDownNode = $(countDownNodeSelector);
+ let deadlineStr = '';
+ if (countDownNode.text().includes('活动已经结束')) {
+ return '截至日期: 活动已经结束';
+ }
+ const countdownCodeStr = countDownNode.children('script').html() || '';
+ const arr = /ShowCountDown\("\d*"\s*,\s*([\d,]+)\s*\)/.exec(countdownCodeStr.trim());
+ if (arr) {
+ const dateStr = arr[1].replace(/,/g, (...args) => {
+ const index = args[args.length - 2];
+ return index === 10 ? ' ' : index < 10 ? '-' : ':';
+ });
+ deadlineStr = `截至日期: ${new Date(dateStr).toUTCString()}`;
+ }
+ return deadlineStr;
+};
+
+const parseListItem = ($, listSelector) =>
+ $(`${listSelector} .content-box`)
+ .map((_, ele) => {
+ const $item = cheerio.load(ele);
+ const infoNode = $item('.posts-default-title > h2 > a');
+ const title = infoNode.attr('title');
+ const link = infoNode.attr('href');
+ const pubDateStr = $item('.posts-default-info .icon-time').text();
+ const pubDate = new Date(pubDateStr).toUTCString();
+ const thumbnail = $item('.posts-default-img img').attr('data-original');
+ const deadlineStr = getDeadlineStr($item, '.countDownCont');
+ const digest = $item('.posts-default-content > .posts-text')
+ .text()
+ .trim();
+ return {
+ title,
+ link,
+ pubDate,
+ description: [title, deadlineStr, digest, `
`].filter((str) => !!str).join('
'),
+ };
+ })
+ .get();
+
+module.exports = {
+ getDeadlineStr,
+ parseListItem,
+};
diff --git a/lib/routes/wikihow/category.js b/lib/routes/wikihow/category.js
new file mode 100644
index 0000000000..2da08f83ae
--- /dev/null
+++ b/lib/routes/wikihow/category.js
@@ -0,0 +1,34 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+
+module.exports = async (ctx) => {
+ const { category, type = 'all' } = ctx.params;
+ const link = encodeURI(`https://zh.wikihow.com/Category:${category}`);
+ const res = await got(link);
+ const $ = cheerio.load(res.data);
+
+ const itemsSelector = type === 'all' ? '#cat_featured .thumbnail' : '#cat_all .thumbnail';
+ const item = $(itemsSelector)
+ .map((_, ele) => {
+ const $item = cheerio.load(ele);
+ const title = '如何' + $item('.text span').text();
+ const link = $item('a').attr('href');
+ const thumbnail = $item('img').attr('data-src');
+ return {
+ title,
+ description: `
+ ${title}
+
+ `,
+ link,
+ };
+ })
+ .get();
+
+ ctx.state.data = {
+ title: `wikiHow - ${category}`,
+ description: `wikiHow - ${category}`,
+ link,
+ item,
+ };
+};
diff --git a/lib/routes/wikihow/index.js b/lib/routes/wikihow/index.js
new file mode 100644
index 0000000000..4af318427d
--- /dev/null
+++ b/lib/routes/wikihow/index.js
@@ -0,0 +1,32 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+
+module.exports = async (ctx) => {
+ const link = `https://zh.wikihow.com/${encodeURIComponent('首页')}`;
+ const res = await got(link);
+ const $ = cheerio.load(res.data);
+
+ const item = $('#fa_container td.image_map')
+ .map((_, ele) => {
+ const $item = cheerio.load(ele);
+ const title = '如何' + $item('.text span').text();
+ const link = $item('.thumbnail > a').attr('href');
+ const thumbnail = $item('.thumbnail img').attr('data-src');
+ return {
+ title,
+ description: `
+ ${title}
+
+ `,
+ link,
+ };
+ })
+ .get();
+
+ ctx.state.data = {
+ title: 'wikiHow - 首页',
+ description: 'wikiHow - 首页',
+ link,
+ item,
+ };
+};