fix(route): AEON (#21030)
* fix aeon * improve layout * remove escape chars * Apply suggestion from TonyRL
This commit is contained in:
parent
a4db4c217d
commit
e6598a0d91
|
|
@ -1,8 +1,7 @@
|
|||
import type { Route } from '@/types';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
|
||||
import { getBuildId, getData } from './utils';
|
||||
import { getData } from './utils';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/category/:category',
|
||||
|
|
@ -38,32 +37,79 @@ export const route: Route = {
|
|||
handler,
|
||||
};
|
||||
|
||||
const ENDPOINT = 'https://api.aeonmedia.co/graphql';
|
||||
const LIST_BY_SECTION = /* GraphQL */ `
|
||||
query getAeonArticlesBySection($section: String!, $sortField: ArticleSortEnum = published_at, $afterCursor: String, $tag: String) {
|
||||
section(site: aeon, slug: $section) {
|
||||
slug
|
||||
title
|
||||
metaDescription
|
||||
}
|
||||
articles(
|
||||
site: aeon
|
||||
section: $section
|
||||
status: [published]
|
||||
tag: $tag
|
||||
sort: {field: $sortField, order: desc}
|
||||
after: $afterCursor
|
||||
first: 24
|
||||
) {
|
||||
nodes {
|
||||
slug
|
||||
...aeonArticleCardFragment
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment aeonArticleCardFragment on Article {
|
||||
id
|
||||
title
|
||||
slug
|
||||
type
|
||||
standfirstLong
|
||||
authors { name }
|
||||
image { url }
|
||||
primaryTopic { title }
|
||||
section { slug }
|
||||
}
|
||||
`;
|
||||
|
||||
async function handler(ctx) {
|
||||
const category = ctx.req.param('category').toLowerCase();
|
||||
const url = `https://aeon.co/category/${category}`;
|
||||
const buildId = await getBuildId();
|
||||
const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${category}.json`);
|
||||
const response = await ofetch(ENDPOINT, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
operationName: 'getAeonArticlesBySection',
|
||||
query: LIST_BY_SECTION,
|
||||
variables: {
|
||||
section: category,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const section = response.pageProps.section;
|
||||
|
||||
const list = section.articles.edges.map(({ node }) => ({
|
||||
const list = response.data.articles.nodes.map((node) => ({
|
||||
title: node.title,
|
||||
description: node.standfirstLong,
|
||||
author: node.authors.map((author) => author.displayName).join(', '),
|
||||
author: node.authors.map((author) => author.name).join(', '),
|
||||
link: `https://aeon.co/${node.type}s/${node.slug}`,
|
||||
pubDate: parseDate(node.createdAt),
|
||||
category: [node.section.title, ...node.topics.map((topic) => topic.title)],
|
||||
category: node.primaryTopic.title,
|
||||
image: node.image.url,
|
||||
type: node.type,
|
||||
section: node.section.slug,
|
||||
slug: node.slug,
|
||||
}));
|
||||
|
||||
const items = await getData(list);
|
||||
|
||||
return {
|
||||
title: `AEON | ${section.title}`,
|
||||
title: `AEON | ${response.data.section.title}`,
|
||||
link: url,
|
||||
description: section.metaDescription,
|
||||
description: response.data.section.metaDescription,
|
||||
item: items,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,39 @@
|
|||
import type { Route } from '@/types';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
|
||||
import { getBuildId, getData } from './utils';
|
||||
import { getData } from './utils';
|
||||
|
||||
const ENDPOINT = 'https://api.aeonmedia.co/graphql';
|
||||
const LIST_BY_TYPE = /* GraphQL */ `
|
||||
query getAeonArticlesByType($type: [ArticleTypeEnum!], $sortField: ArticleSortEnum = published_at, $afterCursor: String, $tag: String) {
|
||||
articles(
|
||||
site: aeon
|
||||
type: $type
|
||||
status: [published]
|
||||
tag: $tag
|
||||
sort: {field: $sortField, order: desc}
|
||||
after: $afterCursor
|
||||
first: 12
|
||||
) {
|
||||
nodes {
|
||||
slug
|
||||
...aeonArticleCardFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment aeonArticleCardFragment on Article {
|
||||
id
|
||||
title
|
||||
slug
|
||||
type
|
||||
standfirstLong
|
||||
authors { name }
|
||||
image { url }
|
||||
primaryTopic { title }
|
||||
section { slug }
|
||||
}
|
||||
`;
|
||||
|
||||
export const route: Route = {
|
||||
path: '/:type',
|
||||
|
|
@ -43,19 +74,25 @@ async function handler(ctx) {
|
|||
const type = ctx.req.param('type');
|
||||
const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1);
|
||||
|
||||
const buildId = await getBuildId();
|
||||
const url = `https://aeon.co/${type}`;
|
||||
const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${type}.json`);
|
||||
const response = await ofetch(ENDPOINT, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
query: LIST_BY_TYPE,
|
||||
variables: { type: [type.slice(0, -1)], sortField: 'published_at' },
|
||||
operationName: 'getAeonArticlesByType',
|
||||
},
|
||||
});
|
||||
|
||||
const list = response.pageProps.articles.map((node) => ({
|
||||
const list = response.data.articles.nodes.map((node) => ({
|
||||
title: node.title,
|
||||
description: node.standfirstLong,
|
||||
author: node.authors.map((author) => author.displayName).join(', '),
|
||||
link: `https://aeon.co/${node.type}s/${node.slug}`,
|
||||
pubDate: parseDate(node.createdAt),
|
||||
category: [node.section.title, ...node.topics.map((topic) => topic.title)],
|
||||
author: node.authors.map((author) => author.name).join(', '),
|
||||
link: `https://aeon.co/${type}/${node.slug}`,
|
||||
category: node.primaryTopic.title,
|
||||
image: node.image.url,
|
||||
type: node.type,
|
||||
section: node.section.slug,
|
||||
slug: node.slug,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -2,23 +2,36 @@ import { load } from 'cheerio';
|
|||
import { raw } from 'hono/html';
|
||||
import { renderToString } from 'hono/jsx/dom/server';
|
||||
|
||||
import { config } from '@/config';
|
||||
import cache from '@/utils/cache';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
|
||||
export const getBuildId = () =>
|
||||
cache.tryGet(
|
||||
'aeon:buildId',
|
||||
async () => {
|
||||
const response = await ofetch('https://aeon.co');
|
||||
const $ = load(response);
|
||||
const nextData = JSON.parse($('script#__NEXT_DATA__').text());
|
||||
return nextData.buildId;
|
||||
},
|
||||
config.cache.routeExpire,
|
||||
false
|
||||
);
|
||||
const ENDPOINT = 'https://api.aeonmedia.co/graphql';
|
||||
|
||||
const ESSAY = /* GraphQL */ `
|
||||
query getAeonEssay($slug: String!) {
|
||||
essay(slug: $slug) {
|
||||
publishedAt
|
||||
updatedAt
|
||||
authors { name authorBio }
|
||||
audioUrl
|
||||
image { url alt caption }
|
||||
body
|
||||
}
|
||||
}`;
|
||||
|
||||
const VIDEO = /* GraphQL */ `
|
||||
query getAeonVideo($slug: String!, $site: SiteEnum!) {
|
||||
video(slug: $slug, site: $site) {
|
||||
publishedAt
|
||||
updatedAt
|
||||
authors { name authorBio }
|
||||
hoster
|
||||
hosterId
|
||||
credits
|
||||
description
|
||||
}
|
||||
}`;
|
||||
|
||||
const renderVideoDescription = (article) => {
|
||||
let video = article.hosterId;
|
||||
|
|
@ -44,7 +57,7 @@ const renderEssayDescription = ({ banner, authorsBio, content }) =>
|
|||
{banner?.url ? (
|
||||
<figure>
|
||||
<img src={banner.url} alt={banner.alt} />
|
||||
{banner.caption ? <figcaption>{banner.caption}</figcaption> : null}
|
||||
{banner.caption ? <figcaption>{raw(banner.caption)}</figcaption> : null}
|
||||
</figure>
|
||||
) : null}
|
||||
{authorsBio ? raw(authorsBio) : null}
|
||||
|
|
@ -52,54 +65,55 @@ const renderEssayDescription = ({ banner, authorsBio, content }) =>
|
|||
</>
|
||||
);
|
||||
|
||||
const getData = async (list) => {
|
||||
const getJSON = (slug, site) => {
|
||||
const query = site ? VIDEO : ESSAY;
|
||||
const variables = site ? { slug, site } : { slug };
|
||||
const operationName = site ? 'getAeonVideo' : 'getAeonEssay';
|
||||
return ofetch(ENDPOINT, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
operationName,
|
||||
query,
|
||||
variables,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getData = async (list) => {
|
||||
const items = await Promise.all(
|
||||
list.map((item) =>
|
||||
cache.tryGet(item.link, async () => {
|
||||
const buildId = await getBuildId();
|
||||
const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${item.type}s/${item.slug}.json?id=${item.slug}`);
|
||||
|
||||
const data = response.pageProps.article;
|
||||
const type = data.type.toLowerCase();
|
||||
const res = await getJSON(item.slug, item.type === 'video' ? 'aeon' : null);
|
||||
|
||||
const data = res.data[item.type];
|
||||
item.pubDate = parseDate(data.publishedAt);
|
||||
|
||||
if (type === 'video') {
|
||||
if (item.type === 'video') {
|
||||
item.description = renderVideoDescription(data);
|
||||
} else {
|
||||
if (data.audio?.id) {
|
||||
const response = await ofetch('https://api.aeonmedia.co/graphql', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
query: `query getAudio($audioId: ID!) {
|
||||
audio(id: $audioId) {
|
||||
id
|
||||
streamUrl
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
audioId: data.audio.id,
|
||||
},
|
||||
operationName: 'getAudio',
|
||||
},
|
||||
});
|
||||
|
||||
if (data.audioUrl) {
|
||||
delete item.image;
|
||||
item.enclosure_url = response.data.audio.streamUrl;
|
||||
item.enclosure_url = data.audioUrl;
|
||||
item.enclosure_type = 'audio/mpeg';
|
||||
}
|
||||
|
||||
// Besides, it seems that the method based on __NEXT_DATA__
|
||||
// does not include the information of the two-column
|
||||
// images in the article body,
|
||||
// e.g. https://aeon.co/essays/how-to-mourn-a-forest-a-lesson-from-west-papua .
|
||||
// But that's very rare.
|
||||
|
||||
const capture = load(data.body, null, false);
|
||||
const banner = data.image;
|
||||
capture('p.pullquote').remove();
|
||||
|
||||
const authorsBio = data.authors.map((author) => '<p>' + author.name + author.authorBio.replaceAll(/^<p>/g, ' ')).join('');
|
||||
const authorsBio = renderToString(
|
||||
<>
|
||||
<hr />
|
||||
{data.authors.map((author) => (
|
||||
<p>
|
||||
{author.name}
|
||||
{raw(author.authorBio.replaceAll(/^<p>/g, ' '))}
|
||||
</p>
|
||||
))}
|
||||
<hr />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
|
||||
item.description = renderEssayDescription({ banner, authorsBio, content: capture.html() });
|
||||
}
|
||||
|
|
@ -111,5 +125,3 @@ const getData = async (list) => {
|
|||
|
||||
return items;
|
||||
};
|
||||
|
||||
export { getData };
|
||||
|
|
|
|||
Loading…
Reference in New Issue