Skip to content

Commit 7ae15a8

Browse files
authored
feat(route): add XJTLU news route (#20966)
* feat(route): add XJTLU news route * feat(route): update feed title format * refactor(route/xjtlu): move "all" category into categories object * fix(route/xjtlu): remove unnecessary category parameter from radar rules * fix(route/xjtlu): replace generic error handling with custom one
1 parent 2e756e0 commit 7ae15a8

File tree

2 files changed

+177
-0
lines changed

2 files changed

+177
-0
lines changed

lib/routes/xjtlu/namespace.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Namespace } from '@/types';
2+
3+
export const namespace: Namespace = {
4+
name: "Xi'an Jiaotong-Liverpool University",
5+
url: 'www.xjtlu.edu.cn',
6+
categories: ['university'],
7+
lang: 'en',
8+
zh: {
9+
name: '西交利物浦大学',
10+
},
11+
};

lib/routes/xjtlu/news.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { load } from 'cheerio';
2+
3+
import InvalidParameterError from '@/errors/types/invalid-parameter';
4+
import type { Route } from '@/types';
5+
import cache from '@/utils/cache';
6+
import ofetch from '@/utils/ofetch';
7+
import { parseDate } from '@/utils/parse-date';
8+
9+
const categories = {
10+
all: {
11+
en: { name: 'All', path: 'about/news/all-news' },
12+
zh: { name: '全部', path: 'about/news/all-news' },
13+
},
14+
anniversary: {
15+
en: { name: 'XJTLU 20th Anniversary', path: 'news/xjtlu-20th-anniversary-en' },
16+
zh: { name: '西浦20周年', path: 'news/xjtlu-20th-anniversary' },
17+
},
18+
technology: {
19+
en: { name: 'Science and Technology', path: 'news/science-and-technology' },
20+
zh: { name: '科技', path: 'news/technology' },
21+
},
22+
business: {
23+
en: { name: 'Business', path: 'news/business' },
24+
zh: { name: '商业管理', path: 'news_category/topictopic1680' },
25+
},
26+
environment: {
27+
en: { name: 'Built Environment', path: 'news/built-environment' },
28+
zh: { name: '建筑环境', path: 'news_category/topictopic1670' },
29+
},
30+
humanities: {
31+
en: { name: 'Humanities and Social Sciences', path: 'news/humanities-and-social-sciences' },
32+
zh: { name: '人文社科', path: 'news_category/topictopic1672' },
33+
},
34+
community: {
35+
en: { name: 'Community', path: 'news/community' },
36+
zh: { name: '校园与社区', path: 'news_category/topictopic1679' },
37+
},
38+
about: {
39+
en: { name: 'About XJTLU', path: 'news/about-xjtlu' },
40+
zh: { name: '要闻聚焦', path: 'news_category/%E8%A6%81%E9%97%BB%E8%81%9A%E7%84%A6' },
41+
},
42+
stories: {
43+
en: { name: 'XJTLU Stories', path: 'news/xjtlu-stories' },
44+
zh: { name: '招生专区', path: 'news_category/topictopic4683' },
45+
},
46+
};
47+
48+
const handler = async (ctx) => {
49+
const lang = ctx.req.param('lang') ?? 'en';
50+
const category = ctx.req.param('category') ?? 'all';
51+
52+
// Validate language parameter
53+
if (lang !== 'en' && lang !== 'zh') {
54+
throw new InvalidParameterError('Invalid language parameter. Use "en" or "zh".');
55+
}
56+
57+
// Validate category parameter
58+
if (!categories[category]) {
59+
throw new InvalidParameterError(`Invalid category: ${category}. Please refer to the category table in the documentation.`);
60+
}
61+
62+
// Build the list URL based on category
63+
const baseUrl = `https://www.xjtlu.edu.cn/${lang}`;
64+
const categoryPath = categories[category][lang].path;
65+
const listUrl = `${baseUrl}/${categoryPath}`;
66+
67+
// Fetch the news list page
68+
const response = await ofetch(listUrl);
69+
const $ = load(response);
70+
71+
// Extract article cards from the page
72+
const list = $('.card-group-3 .card.up-down-card, .content .card.up-down-card')
73+
.toArray()
74+
.map((item) => {
75+
const $item = $(item);
76+
const link = $item.find('.a-links-block').attr('href');
77+
const title = $item.find('.card-title').text().trim();
78+
const categoryTags = $item
79+
.find('.tag-group .tag')
80+
.toArray()
81+
.map((tag) => $(tag).text().trim())
82+
.filter((text) => text.length > 0);
83+
84+
return {
85+
title,
86+
link,
87+
category: categoryTags,
88+
};
89+
})
90+
.filter((item) => item.link);
91+
92+
// Fetch full article content for each item
93+
const items = await Promise.all(
94+
list.map((item) =>
95+
cache.tryGet(item.link!, async () => {
96+
const articleResponse = await ofetch(item.link!);
97+
const $article = load(articleResponse);
98+
99+
const fullContent = $article('.post_content').html();
100+
const articleDate = $article('.edited-view .date, p.date').first().text().trim();
101+
102+
// Parse date based on language
103+
// English: "21 Jan 2026" with 'en' locale for month name recognition
104+
// Chinese: "2026年01月20日" - numeric format, no locale needed
105+
const pubDate = articleDate ? parseDate(articleDate, lang === 'en' ? 'DD MMM YYYY' : 'YYYY年MM月DD日', lang === 'en' ? 'en' : undefined) : undefined;
106+
107+
return {
108+
title: item.title,
109+
link: item.link!,
110+
category: item.category,
111+
description: fullContent || undefined,
112+
pubDate,
113+
};
114+
})
115+
)
116+
);
117+
118+
const categoryTitle = categories[category][lang].name;
119+
const iconUrl = 'https://www.xjtlu.edu.cn/favicon.ico';
120+
121+
return {
122+
title: `XJTLU ${categoryTitle}${lang === 'en' ? ' News' : '新闻'}`,
123+
link: listUrl,
124+
description: lang === 'en' ? "Official news from Xi'an Jiaotong-Liverpool University" : '西交利物浦大学官方新闻',
125+
image: iconUrl,
126+
icon: iconUrl,
127+
logo: iconUrl,
128+
item: items,
129+
lang: lang === 'en' ? 'en' : 'zh-CN',
130+
};
131+
};
132+
133+
export const route: Route = {
134+
path: '/news/:lang?/:category?',
135+
categories: ['university'],
136+
example: '/xjtlu/news/en/technology',
137+
url: 'www.xjtlu.edu.cn/en/news',
138+
parameters: {
139+
lang: 'Language, `en` or `zh`, default: `en`',
140+
category: 'Category, see table below, default: `all`',
141+
},
142+
features: {
143+
requireConfig: false,
144+
requirePuppeteer: false,
145+
antiCrawler: false,
146+
supportBT: false,
147+
supportPodcast: false,
148+
supportScihub: false,
149+
},
150+
radar: [
151+
{
152+
source: ['www.xjtlu.edu.cn/:lang/about/news/all-news', 'www.xjtlu.edu.cn/:lang/news'],
153+
target: '/news/:lang',
154+
},
155+
],
156+
name: 'News',
157+
maintainers: ['Circloud'],
158+
handler,
159+
description: `Categories:
160+
161+
| Category | English Name | Chinese Name |
162+
| -------- | ------------ | ------------ |
163+
${Object.entries(categories)
164+
.map(([key, value]) => `| \`${key}\` | ${value.en.name} | ${value.zh.name} |`)
165+
.join('\n')}`,
166+
};

0 commit comments

Comments
 (0)