From a8b004eb64a5548aee352ce4b70ffd18f709ccce Mon Sep 17 00:00:00 2001 From: fishyo <61926874+fishyo@users.noreply.github.com> Date: Thu, 4 Dec 2025 23:59:51 +0800 Subject: [PATCH] feat(route): add supercell game blog routes (#20545) * feat(route): add supercell game blog routes - Add support for 5 supercell games (Clash Royale, Brawl Stars, Clash of Clans, Boom Beach, Hay Day) - Support 12 languages (en, zh, zh-hans, fr, de, id, it, ja, ko, pt, ru, es) - Extract blog posts from __NEXT_DATA__ JSON - Cache full article content with fallback to summary * feat: add Supercell game blog route * fix(supercell): use regex to extract __NEXT_DATA__ JSON, remove cheerio dependency * fix(supercell): use bodyCollection to render content, fix namespace capitalization * fix: improve supercell blog route to extract full content from bodyCollection - Implement renderRichText and renderNodeContent functions to properly parse rich text JSON format - Handle both TextBlock and FeatureBlock types from bodyCollection - Support text formatting marks (bold, italic, underline, code) - Add proper type annotations to handler function and variables - Use Next.js JSON endpoint instead of HTML parsing for better performance - Add error handling with fallback to description if full content extraction fails * feat(supercell): add ImageBlock and CarouselBlock support for multi-game content extraction * fix(supercell): use else-if chain and for-of loop for block handling * refactor(supercell): extract renderBlock function to reduce complexity * fix(supercell): use switch statement for block type handling --- lib/routes/supercell/blog.ts | 256 ++++++++++++++++++++++++++++++ lib/routes/supercell/namespace.ts | 7 + 2 files changed, 263 insertions(+) create mode 100644 lib/routes/supercell/blog.ts create mode 100644 lib/routes/supercell/namespace.ts diff --git a/lib/routes/supercell/blog.ts b/lib/routes/supercell/blog.ts new file mode 100644 index 000000000..9c9730a48 --- /dev/null +++ b/lib/routes/supercell/blog.ts @@ -0,0 +1,256 @@ +import type { Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/:game/blog/:locale?', + categories: ['game'], + example: '/supercell/clashroyale/blog/zh', + parameters: { + game: 'Game name, see below', + locale: 'Language code, see below, English by default', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['supercell.com/en/games/:game/:locale/blog'], + target: '/:game/blog/:locale', + }, + ], + name: 'Game Blog', + maintainers: ['fishyo'], + handler: handler as any, + description: `Supported games + +| Game | Slug | +| ----------------- | ------------- | +| Clash Royale | clashroyale | +| Brawl Stars | brawlstars | +| Clash of Clans | clashofclans | +| Boom Beach | boombeach | +| Hay Day | hayday | + +Language codes + +| Language | Code | +| ------------------ | ------- | +| English | | +| 繁體中文 | zh | +| 简体中文 | zh-hans | +| Français | fr | +| Deutsch | de | +| Indonesia | id | +| Italiano | it | +| 日本語 | ja | +| 한국어 | ko | +| Português | pt | +| Русский | ru | +| Español | es |`, +}; + +const GAME_NAMES = { + clashroyale: 'Clash Royale', + brawlstars: 'Brawl Stars', + clashofclans: 'Clash of Clans', + boombeach: 'Boom Beach', + hayday: 'Hay Day', +}; + +// 解析富文本 JSON 格式 +function renderRichText(json: any): string { + if (!json?.content || !Array.isArray(json.content)) { + return ''; + } + + return json.content + .map((node) => { + switch (node.nodeType) { + case 'paragraph': + return `
${renderNodeContent(node)}
`; + case 'heading-1': + case 'heading-2': + case 'heading-3': + case 'heading-4': + case 'heading-5': + case 'heading-6': { + const level: string = node.nodeType.split('-')[1]; + return `${renderNodeContent(node)}`; + case 'unordered-list': + case 'ordered-list': + return `<${node.nodeType === 'unordered-list' ? 'ul' : 'ol'}>${node.content?.map((item) => `
${text}`;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ return text;
+ }
+ // 递归处理嵌套节点
+ if (item.nodeType === 'paragraph' || item.nodeType === 'list-item') {
+ return renderNodeContent(item);
+ }
+ return '';
+ })
+ .join('');
+}
+
+// 渲染 block 内容
+function renderBlock(block: any): string {
+ const parts: string[] = [];
+
+ switch (block.__typename) {
+ case 'TextBlock':
+ if (block.title) {
+ parts.push(`