diff --git a/lib/routes/wired/namespace.ts b/lib/routes/wired/namespace.ts new file mode 100644 index 0000000000000..95e58c7f5dff1 --- /dev/null +++ b/lib/routes/wired/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'WIRED', + url: 'www.wired.com', + categories: ['traditional-media'], +}; diff --git a/lib/routes/wired/tag.ts b/lib/routes/wired/tag.ts new file mode 100644 index 0000000000000..8347d7d199d14 --- /dev/null +++ b/lib/routes/wired/tag.ts @@ -0,0 +1,97 @@ +import { Route } from '@/types'; + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; +import { Item } from './types'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/tag/:tag', + example: '/wired/tag/facebook', + parameters: { tag: 'Tag name' }, + radar: [ + { + source: ['www.wired.com/tag/:tag/'], + }, + ], + name: 'Tags', + maintainers: ['Naiqus'], + handler, +}; + +async function handler(ctx) { + const baseUrl = 'https://www.wired.com'; + const { tag } = ctx.req.param() as { tag: string }; + const link = `${baseUrl}/tag/${tag}/`; + + const response = await ofetch(link); + const $ = cheerio.load(response); + const preloadedState = JSON.parse( + $('script:contains("window.__PRELOADED_STATE__")') + .text() + .match(/window\.__PRELOADED_STATE__ = (.*);/)?.[1] ?? '{}' + ); + + const list = (preloadedState.transformed.tag.items as Item[]).map((item) => ({ + title: item.dangerousHed, + description: item.dangerousDek, + link: `${baseUrl}${item.url}`, + pubDate: parseDate(item.date), + author: item.contributors.author.items.map((author) => author.name).join(', '), + category: [item.rubric.name], + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = cheerio.load(response); + const preloadedState = JSON.parse( + $('script:contains("window.__PRELOADED_STATE__")') + .text() + .match(/window\.__PRELOADED_STATE__ = (.*);/)?.[1] ?? '{}' + ); + + const headerLeadAsset = $('div[data-testid*="ContentHeaderLeadAsset"]'); + headerLeadAsset.find('button').remove(); + // false postive: 'some' does not exist on type 'Cheerio' + // eslint-disable-next-line unicorn/prefer-array-some + if (headerLeadAsset.find('video')) { + headerLeadAsset.find('video').attr('src', $('link[rel="preload"][as="video"]').attr('href')); + headerLeadAsset.find('video').attr('controls', ''); + headerLeadAsset.find('video').attr('preload', 'metadata'); + headerLeadAsset.find('video').removeAttr('autoplay'); + } + + const content = $('.body__inner-container') + .toArray() + .map((el) => { + const $el = $(el); + $el.find('noscript').each((_, el) => { + const $e = $(el); + $e.replaceWith($e.html() || ''); + }); + + return $el.html(); + }) + .join(''); + + item.description = ($('div[class^=ContentHeaderDek]').prop('outerHTML') || '') + headerLeadAsset.prop('outerHTML') + content; + + item.category = [...new Set([...item.category, ...preloadedState.transformed.article.tagCloud.tags.map((t: { tag: string }) => t.tag)])]; + + return item; + }) + ) + ); + + return { + title: preloadedState.transformed['head.title'], + description: preloadedState.transformed['head.description'], + link, + image: `${baseUrl}${preloadedState.transformed.logo.sources.sm.url}`, + language: 'en', + item: items, + }; +} diff --git a/lib/routes/wired/types.ts b/lib/routes/wired/types.ts new file mode 100644 index 0000000000000..7d92b1258bfa3 --- /dev/null +++ b/lib/routes/wired/types.ts @@ -0,0 +1,81 @@ +interface ImageSource { + aspectRatio: string; + width: number; + url: string; + srcset: string; +} + +interface Image { + contentType: string; + id: string; + altText: string; + credit: string; + caption: string; + metaData: string; + modelName: string; + sources: { + sm: ImageSource; + md: ImageSource; + lg: ImageSource; + xl: ImageSource; + xxl: ImageSource; + }; + segmentedSources: { + sm: ImageSource[]; + lg: ImageSource[]; + }; + showImageWithoutLink: boolean; + isLazy: boolean; + brandDetail: { + brandIcon: string; + brandName: string; + }; +} + +interface Contributor { + author: { + items: { name: string }[]; + }; + photographer: { + items: any[]; + }; +} + +interface Rubric { + name: string; + url: string; +} + +interface RecircMostPopular { + contentType: string; + dangerousHed: string; + dangerousDek: string; + url: string; + rubric: { name: string }; + tout: Image; + image: Image; + contributors: { + author: { + brandName: string; + brandSlug: string; + preamble: string; + items: { name: string }[]; + }; + }; +} + +export interface Item { + contributors: Contributor; + contentType: string; + date: string; + dangerousDek: string; + dangerousHed: string; + id: string; + image: Image; + imageLabels: any[]; + hasNoFollowOnSyndicated: boolean; + rating: string; + rubric: Rubric; + url: string; + recircMostPopular: RecircMostPopular[]; +}