diff --git a/docs/README.md b/docs/README.md
index 3095f2e95..aa9baffa6 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1825,3 +1825,19 @@ nearby: 可选 0 或 1,默认 0 为不包括,是否包括临近机场
1. 简单模式,例如「data visualization」,[https://rsshub.app/google/scholar/data+visualization](https://rsshub.app/google/scholar/data+visualization)。
2. 高级模式,前往 [Google Scholar](https://scholar.google.com/schhp?hl=zh-cn&as_sdt=0,5),点击左上角,选择高级搜索并提交查询。此时 URL 应为:[https://scholar.google.com/scholar?as_q=data+visualization&as_epq=&as_oq=&as_eq=&as_occt=any&as_sauthors=&as_publication=&as_ylo=2018&as_yhi=&hl=zh-CN&as_sdt=0%2C5](https://scholar.google.com/scholar?as_q=data+visualization&as_epq=&as_oq=&as_eq=&as_occt=any&as_sauthors=&as_publication=&as_ylo=2018&as_yhi=&hl=zh-CN&as_sdt=0%2C5),复制`https://scholar.google.com/scholar?`后的所有语句作为本路由的查询参数。例子所对应的完整路由为[https://rsshub.app/google/scholar/as_q=data+visualization&as_epq=&as_oq=&as_eq=&as_occt=any&as_sauthors=&as_publication=&as_ylo=2018&as_yhi=&hl=zh-CN&as_sdt=0%2C5](https://rsshub.app/google/scholar/as_q=data+visualization&as_epq=&as_oq=&as_eq=&as_occt=any&as_sauthors=&as_publication=&as_ylo=2018&as_yhi=&hl=zh-CN&as_sdt=0%2C5)。
+
+## Hopper Flight Deals
+
+### Hopper 特价机票
+
+举例: 伦敦希思罗 ✈ 北京首都国际 [https://rsshub.app/hopper/LHR/PEK](https://rsshub.app/hopper/LHR/PEK)
+
+路由: `/hopper/:from/:to?`
+
+参数:
+
+from: 始发地,IATA 国际航空运输协会机场代码
+
+to: 目的地,IATA 国际航空运输协会机场代码,可选,缺省则目的地为`任意城市`
+
+IATA 国际航空运输协会机场代码,参见[维基百科 国际航空运输协会机场代码]()
diff --git a/docs/en/README.md b/docs/en/README.md
index 3839c1a47..24d2c958a 100644
--- a/docs/en/README.md
+++ b/docs/en/README.md
@@ -308,8 +308,11 @@ Eg: [https://rsshub.app/github/issue/DIYgod/RSSHub](https://rsshub.app/github/is
Route: `/github/issue/:user/:repo`
-Parameters: user, username
-Parameters: repo, repo name
+Parameters:
+
+user, username
+
+repo, repo name
## EZTV
@@ -366,7 +369,7 @@ locations: the departing city, consists of an 「ISO 3166-1 country code」 and
1. Origin's ISO 3166-1 country code + city name, eg. `us+new york`, [https://rsshub.app/atfd/us+new york](https://rsshub.app/atfd/us+new%20york)
2. Multiple origins are support via a comma separated string, eg. `us+new york,gb+london`, [https://rsshub.app/atfd/us+new york,gb+london/](https://rsshub.app/atfd/us+new%20york,gb+london/)
-For ISO 3166-1 country codes please refer to [wikipedia ISO_3166-1](https://en.wikipedia.org/wiki/ISO_3166-1)
+For ISO 3166-1 country codes please refer to [Wikipedia ISO_3166-1](https://en.wikipedia.org/wiki/ISO_3166-1)
nearby: whether includes nearby airports, optional value of 0 or 1, default to 0 (exclude nearby airports)
@@ -389,3 +392,19 @@ Parameters: query, query statement which supports「Basic」and「Advanced」mod
1. Basic mode, sample query is the keywords desired, eg.「data visualization」, [https://rsshub.app/google/scholar/data+visualization](https://rsshub.app/google/scholar/data+visualization).
2. Advanced mode, visit [Google Scholar](https://scholar.google.com/schhp?hl=en&as_sdt=0,5), click the top left corner and select「Advanced Search」, fill in your conditions and submit the search. The URL should look like this: [https://scholar.google.com/scholar?as_q=data+visualization&as_epq=&as_oq=&as_eq=&as_occt=any&as_sauthors=&as_publication=&as_ylo=2018&as_yhi=&hl=en&as_sdt=0%2C5](https://scholar.google.com/scholar?as_q=data+visualization&as_epq=&as_oq=&as_eq=&as_occt=any&as_sauthors=&as_publication=&as_ylo=2018&as_yhi=&hl=en&as_sdt=0%2C5), copy everything after `https://scholar.google.com/scholar?` from the URL and use it as the query for this route. The complete URL for the above example should look like this: [https://rsshub.app/google/scholar/as_q=data+visualization&as_epq=&as_oq=&as_eq=&as_occt=any&as_sauthors=&as_publication=&as_ylo=2018&as_yhi=&hl=en&as_sdt=0%2C5](https://rsshub.app/google/scholar/as_q=data+visualization&as_epq=&as_oq=&as_eq=&as_occt=any&as_sauthors=&as_publication=&as_ylo=2018&as_yhi=&hl=en&as_sdt=0%2C5).
+
+## Hopper
+
+### Hopper Flight Deals
+
+Eg: London Heathrow Airport ✈ Beijing Capital International Airport [https://rsshub.app/hopper/LHR/PEK](https://rsshub.app/hopper/LHR/PEK)
+
+Route: `/hopper/:from/:to?`
+
+Parameters:
+
+from, origin airport IATA code
+
+to, destination airport IATA code, optional, if unset the destination will be set to `anywhere`
+
+For airport IATA code please refer to [Wikipedia List of airports by IATA code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_code:_A)
diff --git a/router.js b/router.js
index 0c0c013d9..9a821e86a 100755
--- a/router.js
+++ b/router.js
@@ -414,4 +414,7 @@ router.get('/fir/update/:id', require('./routes/fir/update'));
// Google
router.get('/google/scholar/:query', require('./routes/google/scholar'));
+// Hopper
+router.get('/hopper/:from/:to?', require('./routes/hopper/index'));
+
module.exports = router;
diff --git a/routes/hopper/index.js b/routes/hopper/index.js
new file mode 100644
index 000000000..bdd55a14c
--- /dev/null
+++ b/routes/hopper/index.js
@@ -0,0 +1,77 @@
+const axios = require('../../utils/axios');
+const config = require('../../config');
+const cheerio = require('cheerio');
+
+module.exports = async (ctx) => {
+ const from = ctx.params.from;
+ const to = ctx.params.to || 'Anywhere';
+
+ const url = `https://www.hopper.com/deals/best/from/${from}/to/${to}`;
+ const title = `Hopper - Flights From ${from} to ${to}`;
+
+ const response = await axios({
+ method: 'get',
+ url,
+ headers: {
+ 'User-Agent': config.ua,
+ },
+ params: {
+ pid: 'website',
+ c: 'flexweb',
+ },
+ });
+
+ const $ = cheerio.load(response.data);
+ const list = $('div.prices li a');
+
+ ctx.state.data = {
+ title,
+ link: url,
+ description: title,
+ item: list
+ .map((i, e) => {
+ const item = $(e).attr('href');
+ let reg = new RegExp('destination=(.*?)&', 'g');
+ const destination = reg.exec(item)[1];
+ reg = new RegExp('origin=(.*?)&', 'g');
+ const origin = reg.exec(item)[1];
+ reg = new RegExp('departureDate=(.*?)&', 'g');
+ const departureDate = reg.exec(item)[1];
+ reg = new RegExp('returnDate=(.*?)&', 'g');
+ const returnDate = reg.exec(item)[1];
+
+ const price = $(list[i])
+ .find('.price')
+ .text();
+
+ const title = `${origin} ✈ ${destination} ${getMonthYear(departureDate)} for ${price}`;
+
+ const description = `| From | To | Price |
|---|
|
+ ${origin} | ${destination} | ${price} |
${formatDate(departureDate)} ✈ ${formatDate(returnDate)}`;
+ return {
+ title,
+ description,
+ guid: item,
+ link: item,
+ };
+ })
+ .get(),
+ };
+};
+
+function formatDate(v) {
+ return new Intl.DateTimeFormat('en-gb', {
+ year: 'numeric',
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ }).format(new Date(v));
+}
+
+function getMonthYear(v) {
+ const date = new Date(v);
+ return date.toLocaleString('en-gb', {
+ month: 'long',
+ year: 'numeric',
+ });
+}