diff --git a/docs/new-media.md b/docs/new-media.md
index a42c29f9a6..921a6d858a 100644
--- a/docs/new-media.md
+++ b/docs/new-media.md
@@ -537,6 +537,20 @@ Supported sub-sites:
+## 報道者
+
+### 最新
+
+
+
+### 摄影
+
+
+
+### 分类
+
+
+
## 本地宝
### 焦点资讯
diff --git a/lib/router.js b/lib/router.js
index bea4b8a0f0..806b3a15f0 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -96,6 +96,11 @@ router.get('/bangumi/group/:id', require('./routes/bangumi/group/topic'));
router.get('/bangumi/subject/:id', require('./routes/bangumi/subject'));
router.get('/bangumi/user/blog/:id', require('./routes/bangumi/user/blog'));
+// 報道者
+router.get('/twreporter/newest', require('./routes/twreporter/newest'));
+router.get('/twreporter/photography', require('./routes/twreporter/photography'));
+router.get('/twreporter/category/:cid', require('./routes/twreporter/category'));
+
// 微博
router.get('/weibo/user/:uid/:routeParams?', require('./routes/weibo/user'));
router.get('/weibo/keyword/:keyword/:routeParams?', require('./routes/weibo/keyword'));
diff --git a/lib/routes/twreporter/category.js b/lib/routes/twreporter/category.js
new file mode 100644
index 0000000000..aef0e8a086
--- /dev/null
+++ b/lib/routes/twreporter/category.js
@@ -0,0 +1,36 @@
+const cheerio = require('cheerio');
+const got = require('@/utils/got');
+
+const fetch = require('./fetch_article');
+
+module.exports = async (ctx) => {
+ const baseURL = 'https://www.twreporter.org';
+ const url = baseURL + `/categories/${ctx.params.cid}`;
+ const res = await got.get(url);
+ const $ = cheerio.load(res.data);
+ const list = $('.lnKPLr').get();
+ const category = $('.kCfkTU').text();
+
+ const out = await Promise.all(
+ list.map(async (item) => {
+ const $ = cheerio.load(item);
+ const address = baseURL + $('a').attr('href');
+ const title = $('.list-item__Title-sc-1dx5lew-5').text();
+ const cache = await ctx.cache.get(address);
+ if (cache) {
+ return Promise.resolve(JSON.parse(cache));
+ }
+
+ const single = await fetch(address);
+ single.title = title;
+
+ ctx.cache.set(address, JSON.stringify(single));
+ return Promise.resolve(single);
+ })
+ );
+ ctx.state.data = {
+ title: `報道者 | ${category}`,
+ link: url,
+ item: out,
+ };
+};
diff --git a/lib/routes/twreporter/fetch_article.js b/lib/routes/twreporter/fetch_article.js
new file mode 100644
index 0000000000..ff94f5ddc4
--- /dev/null
+++ b/lib/routes/twreporter/fetch_article.js
@@ -0,0 +1,66 @@
+const cheerio = require('cheerio');
+const got = require('@/utils/got');
+
+module.exports = async function fetch(address) {
+ const res = await got.get(address);
+ const capture = cheerio.load(res.data);
+ capture('.gIMvvS').remove();
+
+ let metaInfoBox = capture('.iRdvCH')
+ .filter((index) => index === 0)
+ .get();
+
+ // For photography
+ if (metaInfoBox.length === 0) {
+ metaInfoBox = capture('.dPQBaW')
+ .filter((index) => index === 0)
+ .get();
+ }
+
+ const acquire = cheerio.load(metaInfoBox);
+ let time = acquire('.gimsRe').text();
+
+ // For `photography`
+ if (!time) {
+ time = acquire('.kHluJP').text();
+ }
+ // # Author(s) of the article
+ //
+ // There exists two formats for this section.
+ // 1. 文字 /...` with `摄影 / ..` in separate line, or
+ // 2. just a simple line starts with `文/ ...`
+ // For the first condition, we use a array
+ // to record the list of author(s), and use
+ // `;` to connect two lines.
+ const authors = [];
+
+ if (acquire('.loxoWO').text() === '') {
+ for (const item of acquire('.flciyI').get()) {
+ const $ = cheerio.load(item);
+ const job = $('.hGsNtm').text();
+ // An article may have multiple authors
+ const name = [];
+ for (const item of $('.cidPTd > a').get()) {
+ const $ = cheerio.load(item);
+ name.push($('.fJSaZP').text());
+ }
+
+ const author = job + '/' + name.join(',');
+ authors.push(author);
+ }
+ } else {
+ authors.push(acquire('.loxoWO').text());
+ }
+ const author = authors.join(';');
+
+ // contents = cover photo + italic intro + text
+ const contents = '' + capture('.hxFBKc').html() + '
' + capture('.irqyDp').html();
+
+ return {
+ author: author,
+ description: contents,
+ link: address,
+ guid: address,
+ pubDate: new Date(time).toUTCString(),
+ };
+};
diff --git a/lib/routes/twreporter/newest.js b/lib/routes/twreporter/newest.js
new file mode 100644
index 0000000000..b9ab37cd68
--- /dev/null
+++ b/lib/routes/twreporter/newest.js
@@ -0,0 +1,34 @@
+const cheerio = require('cheerio');
+const got = require('@/utils/got');
+
+const fetch = require('./fetch_article');
+
+module.exports = async (ctx) => {
+ const url = 'https://www.twreporter.org';
+ const res = await got.get(url);
+ const $ = cheerio.load(res.data);
+ const list = $('.gKMjSz').get();
+
+ const out = await Promise.all(
+ list.map(async (item) => {
+ const $ = cheerio.load(item);
+ const address = url + $('a').attr('href');
+ const title = $('.dpNivU').text();
+ const cache = await ctx.cache.get(address);
+ if (cache) {
+ return Promise.resolve(JSON.parse(cache));
+ }
+
+ const single = await fetch(address);
+ single.title = title;
+
+ ctx.cache.set(address, JSON.stringify(single));
+ return Promise.resolve(single);
+ })
+ );
+ ctx.state.data = {
+ title: `報道者 | 最新`,
+ link: url,
+ item: out,
+ };
+};
diff --git a/lib/routes/twreporter/photography.js b/lib/routes/twreporter/photography.js
new file mode 100644
index 0000000000..bb6c3e5a89
--- /dev/null
+++ b/lib/routes/twreporter/photography.js
@@ -0,0 +1,57 @@
+const cheerio = require('cheerio');
+const got = require('@/utils/got');
+
+const fetch = require('./fetch_article');
+
+module.exports = async (ctx) => {
+ const baseURL = 'https://www.twreporter.org';
+ const url = baseURL + `/photography`;
+ const res = await got.get(url);
+ const $ = cheerio.load(res.data);
+ const coverList = $('.WPJvn').get();
+ const commonList = $('.eVNsZf').get();
+
+ const coverView = await Promise.all(
+ coverList.map(async (item) => {
+ const $ = cheerio.load(item);
+ const address = baseURL + $('li > a').attr('href');
+ const title = $('.gRCDdm').text();
+
+ const cache = await ctx.cache.get(address);
+ if (cache) {
+ return Promise.resolve(JSON.parse(cache));
+ }
+
+ const single = await fetch(address);
+ single.title = title;
+
+ ctx.cache.set(address, JSON.stringify(single));
+ return Promise.resolve(single);
+ })
+ );
+
+ const listView = await Promise.all(
+ commonList.map(async (item) => {
+ const $ = cheerio.load(item);
+ const address = baseURL + $('li > a').attr('href');
+ const title = $('.etJLWI').text();
+
+ const cache = await ctx.cache.get(address);
+ if (cache) {
+ return Promise.resolve(JSON.parse(cache));
+ }
+
+ const single = await fetch(address);
+ single.title = title;
+
+ ctx.cache.set(address, JSON.stringify(single));
+ return Promise.resolve(single);
+ })
+ );
+
+ ctx.state.data = {
+ title: `報道者 | 影像`,
+ link: url,
+ item: coverView.concat(listView),
+ };
+};