Skip to content

Commit e6598a0

Browse files
authored
fix(route): AEON (#21030)
* fix aeon * improve layout * remove escape chars * Apply suggestion from TonyRL
1 parent a4db4c2 commit e6598a0

File tree

3 files changed

+164
-69
lines changed

3 files changed

+164
-69
lines changed

lib/routes/aeon/category.ts

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { Route } from '@/types';
22
import ofetch from '@/utils/ofetch';
3-
import { parseDate } from '@/utils/parse-date';
43

5-
import { getBuildId, getData } from './utils';
4+
import { getData } from './utils';
65

76
export const route: Route = {
87
path: '/category/:category',
@@ -38,32 +37,79 @@ export const route: Route = {
3837
handler,
3938
};
4039

40+
const ENDPOINT = 'https://api.aeonmedia.co/graphql';
41+
const LIST_BY_SECTION = /* GraphQL */ `
42+
query getAeonArticlesBySection($section: String!, $sortField: ArticleSortEnum = published_at, $afterCursor: String, $tag: String) {
43+
section(site: aeon, slug: $section) {
44+
slug
45+
title
46+
metaDescription
47+
}
48+
articles(
49+
site: aeon
50+
section: $section
51+
status: [published]
52+
tag: $tag
53+
sort: {field: $sortField, order: desc}
54+
after: $afterCursor
55+
first: 24
56+
) {
57+
nodes {
58+
slug
59+
...aeonArticleCardFragment
60+
}
61+
pageInfo {
62+
hasNextPage
63+
endCursor
64+
}
65+
}
66+
}
67+
68+
fragment aeonArticleCardFragment on Article {
69+
id
70+
title
71+
slug
72+
type
73+
standfirstLong
74+
authors { name }
75+
image { url }
76+
primaryTopic { title }
77+
section { slug }
78+
}
79+
`;
80+
4181
async function handler(ctx) {
4282
const category = ctx.req.param('category').toLowerCase();
4383
const url = `https://aeon.co/category/${category}`;
44-
const buildId = await getBuildId();
45-
const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${category}.json`);
46-
47-
const section = response.pageProps.section;
84+
const response = await ofetch(ENDPOINT, {
85+
method: 'POST',
86+
body: {
87+
operationName: 'getAeonArticlesBySection',
88+
query: LIST_BY_SECTION,
89+
variables: {
90+
section: category,
91+
},
92+
},
93+
});
4894

49-
const list = section.articles.edges.map(({ node }) => ({
95+
const list = response.data.articles.nodes.map((node) => ({
5096
title: node.title,
5197
description: node.standfirstLong,
52-
author: node.authors.map((author) => author.displayName).join(', '),
98+
author: node.authors.map((author) => author.name).join(', '),
5399
link: `https://aeon.co/${node.type}s/${node.slug}`,
54-
pubDate: parseDate(node.createdAt),
55-
category: [node.section.title, ...node.topics.map((topic) => topic.title)],
100+
category: node.primaryTopic.title,
56101
image: node.image.url,
57102
type: node.type,
103+
section: node.section.slug,
58104
slug: node.slug,
59105
}));
60106

61107
const items = await getData(list);
62108

63109
return {
64-
title: `AEON | ${section.title}`,
110+
title: `AEON | ${response.data.section.title}`,
65111
link: url,
66-
description: section.metaDescription,
112+
description: response.data.section.metaDescription,
67113
item: items,
68114
};
69115
}

lib/routes/aeon/type.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,39 @@
11
import type { Route } from '@/types';
22
import ofetch from '@/utils/ofetch';
3-
import { parseDate } from '@/utils/parse-date';
43

5-
import { getBuildId, getData } from './utils';
4+
import { getData } from './utils';
5+
6+
const ENDPOINT = 'https://api.aeonmedia.co/graphql';
7+
const LIST_BY_TYPE = /* GraphQL */ `
8+
query getAeonArticlesByType($type: [ArticleTypeEnum!], $sortField: ArticleSortEnum = published_at, $afterCursor: String, $tag: String) {
9+
articles(
10+
site: aeon
11+
type: $type
12+
status: [published]
13+
tag: $tag
14+
sort: {field: $sortField, order: desc}
15+
after: $afterCursor
16+
first: 12
17+
) {
18+
nodes {
19+
slug
20+
...aeonArticleCardFragment
21+
}
22+
}
23+
}
24+
25+
fragment aeonArticleCardFragment on Article {
26+
id
27+
title
28+
slug
29+
type
30+
standfirstLong
31+
authors { name }
32+
image { url }
33+
primaryTopic { title }
34+
section { slug }
35+
}
36+
`;
637

738
export const route: Route = {
839
path: '/:type',
@@ -43,19 +74,25 @@ async function handler(ctx) {
4374
const type = ctx.req.param('type');
4475
const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1);
4576

46-
const buildId = await getBuildId();
4777
const url = `https://aeon.co/${type}`;
48-
const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${type}.json`);
78+
const response = await ofetch(ENDPOINT, {
79+
method: 'POST',
80+
body: {
81+
query: LIST_BY_TYPE,
82+
variables: { type: [type.slice(0, -1)], sortField: 'published_at' },
83+
operationName: 'getAeonArticlesByType',
84+
},
85+
});
4986

50-
const list = response.pageProps.articles.map((node) => ({
87+
const list = response.data.articles.nodes.map((node) => ({
5188
title: node.title,
5289
description: node.standfirstLong,
53-
author: node.authors.map((author) => author.displayName).join(', '),
54-
link: `https://aeon.co/${node.type}s/${node.slug}`,
55-
pubDate: parseDate(node.createdAt),
56-
category: [node.section.title, ...node.topics.map((topic) => topic.title)],
90+
author: node.authors.map((author) => author.name).join(', '),
91+
link: `https://aeon.co/${type}/${node.slug}`,
92+
category: node.primaryTopic.title,
5793
image: node.image.url,
5894
type: node.type,
95+
section: node.section.slug,
5996
slug: node.slug,
6097
}));
6198

lib/routes/aeon/utils.tsx

Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,36 @@ import { load } from 'cheerio';
22
import { raw } from 'hono/html';
33
import { renderToString } from 'hono/jsx/dom/server';
44

5-
import { config } from '@/config';
65
import cache from '@/utils/cache';
76
import ofetch from '@/utils/ofetch';
87
import { parseDate } from '@/utils/parse-date';
98

10-
export const getBuildId = () =>
11-
cache.tryGet(
12-
'aeon:buildId',
13-
async () => {
14-
const response = await ofetch('https://aeon.co');
15-
const $ = load(response);
16-
const nextData = JSON.parse($('script#__NEXT_DATA__').text());
17-
return nextData.buildId;
18-
},
19-
config.cache.routeExpire,
20-
false
21-
);
9+
const ENDPOINT = 'https://api.aeonmedia.co/graphql';
10+
11+
const ESSAY = /* GraphQL */ `
12+
query getAeonEssay($slug: String!) {
13+
essay(slug: $slug) {
14+
publishedAt
15+
updatedAt
16+
authors { name authorBio }
17+
audioUrl
18+
image { url alt caption }
19+
body
20+
}
21+
}`;
22+
23+
const VIDEO = /* GraphQL */ `
24+
query getAeonVideo($slug: String!, $site: SiteEnum!) {
25+
video(slug: $slug, site: $site) {
26+
publishedAt
27+
updatedAt
28+
authors { name authorBio }
29+
hoster
30+
hosterId
31+
credits
32+
description
33+
}
34+
}`;
2235

2336
const renderVideoDescription = (article) => {
2437
let video = article.hosterId;
@@ -44,62 +57,63 @@ const renderEssayDescription = ({ banner, authorsBio, content }) =>
4457
{banner?.url ? (
4558
<figure>
4659
<img src={banner.url} alt={banner.alt} />
47-
{banner.caption ? <figcaption>{banner.caption}</figcaption> : null}
60+
{banner.caption ? <figcaption>{raw(banner.caption)}</figcaption> : null}
4861
</figure>
4962
) : null}
5063
{authorsBio ? raw(authorsBio) : null}
5164
{content ? raw(content) : null}
5265
</>
5366
);
5467

55-
const getData = async (list) => {
68+
const getJSON = (slug, site) => {
69+
const query = site ? VIDEO : ESSAY;
70+
const variables = site ? { slug, site } : { slug };
71+
const operationName = site ? 'getAeonVideo' : 'getAeonEssay';
72+
return ofetch(ENDPOINT, {
73+
method: 'POST',
74+
body: {
75+
operationName,
76+
query,
77+
variables,
78+
},
79+
});
80+
};
81+
82+
export const getData = async (list) => {
5683
const items = await Promise.all(
5784
list.map((item) =>
5885
cache.tryGet(item.link, async () => {
59-
const buildId = await getBuildId();
60-
const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${item.type}s/${item.slug}.json?id=${item.slug}`);
61-
62-
const data = response.pageProps.article;
63-
const type = data.type.toLowerCase();
86+
const res = await getJSON(item.slug, item.type === 'video' ? 'aeon' : null);
6487

88+
const data = res.data[item.type];
6589
item.pubDate = parseDate(data.publishedAt);
6690

67-
if (type === 'video') {
91+
if (item.type === 'video') {
6892
item.description = renderVideoDescription(data);
6993
} else {
70-
if (data.audio?.id) {
71-
const response = await ofetch('https://api.aeonmedia.co/graphql', {
72-
method: 'POST',
73-
body: {
74-
query: `query getAudio($audioId: ID!) {
75-
audio(id: $audioId) {
76-
id
77-
streamUrl
78-
}
79-
}`,
80-
variables: {
81-
audioId: data.audio.id,
82-
},
83-
operationName: 'getAudio',
84-
},
85-
});
86-
94+
if (data.audioUrl) {
8795
delete item.image;
88-
item.enclosure_url = response.data.audio.streamUrl;
96+
item.enclosure_url = data.audioUrl;
8997
item.enclosure_type = 'audio/mpeg';
9098
}
9199

92-
// Besides, it seems that the method based on __NEXT_DATA__
93-
// does not include the information of the two-column
94-
// images in the article body,
95-
// e.g. https://aeon.co/essays/how-to-mourn-a-forest-a-lesson-from-west-papua .
96-
// But that's very rare.
97-
98100
const capture = load(data.body, null, false);
99101
const banner = data.image;
100102
capture('p.pullquote').remove();
101103

102-
const authorsBio = data.authors.map((author) => '<p>' + author.name + author.authorBio.replaceAll(/^<p>/g, ' ')).join('');
104+
const authorsBio = renderToString(
105+
<>
106+
<hr />
107+
{data.authors.map((author) => (
108+
<p>
109+
{author.name}
110+
{raw(author.authorBio.replaceAll(/^<p>/g, ' '))}
111+
</p>
112+
))}
113+
<hr />
114+
<br />
115+
</>
116+
);
103117

104118
item.description = renderEssayDescription({ banner, authorsBio, content: capture.html() });
105119
}
@@ -111,5 +125,3 @@ const getData = async (list) => {
111125

112126
return items;
113127
};
114-
115-
export { getData };

0 commit comments

Comments
 (0)