diff --git a/lib/routes/joshwcomeau/latest.ts b/lib/routes/joshwcomeau/latest.ts new file mode 100644 index 000000000..248340336 --- /dev/null +++ b/lib/routes/joshwcomeau/latest.ts @@ -0,0 +1,59 @@ +import { Data, Route } from '@/types'; +import { getRelativeUrlList, processList, rootUrl } from './utils'; + +export const route: Route = { + path: '/latest/:category?', + categories: ['programming'], + example: '/joshwcomeau/latest/css', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + parameters: { + category: { + description: 'Category', + options: [ + { value: 'css', label: 'CSS' }, + { value: 'react', label: 'React' }, + { value: 'animation', label: 'Animation' }, + { value: 'javascript', label: 'JavaScript' }, + { value: 'career', label: 'Career' }, + { value: 'blog', label: 'Blog' }, + ], + }, + }, + radar: [ + { + source: ['joshwcomeau.com/'], + target: '/latest', + }, + { + source: ['joshwcomeau.com/:category'], + target: '/latest/:category', + }, + ], + name: 'Articles and Tutorials', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler(ctx) { + const category: string = ctx.req.param('category') || ''; + const currentUrl = category ? `${rootUrl}/${category}` : rootUrl; + const selector = category ? 'div > article > a:first-child' : 'article[data-include-enter-animation="false"] > a:first-child'; + const { heading, urls } = await getRelativeUrlList(currentUrl, selector); + const items = await processList(urls); + const title = category ? `${heading} | ` : ''; + return { + title: `${title}Articles and Tutorials | Josh W. Comeau`, + description: `Friendly tutorials for developers. Focus on ${category ? title : 'React, CSS, Animation, and more!'}`, + link: currentUrl, + item: items, + icon: `${rootUrl}/favicon.png`, + logo: `${rootUrl}/favicon.png`, + } as Data; +} diff --git a/lib/routes/joshwcomeau/namespace.ts b/lib/routes/joshwcomeau/namespace.ts new file mode 100644 index 000000000..f0125e4c9 --- /dev/null +++ b/lib/routes/joshwcomeau/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Josh W Comeau', + url: 'www.joshwcomeau.com', + categories: ['programming'], + lang: 'en', +}; diff --git a/lib/routes/joshwcomeau/popular.ts b/lib/routes/joshwcomeau/popular.ts new file mode 100644 index 000000000..dd5c352ce --- /dev/null +++ b/lib/routes/joshwcomeau/popular.ts @@ -0,0 +1,38 @@ +import { Data, Route } from '@/types'; +import { getRelativeUrlList, processList, rootUrl } from './utils'; + +export const route: Route = { + path: '/popular', + categories: ['programming'], + example: '/joshwcomeau/popular', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['joshwcomeau.com/'], + target: '/popular', + }, + ], + name: 'Popular Content', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler() { + const { urls } = await getRelativeUrlList(rootUrl, 'section > ol > li > a'); + const items = await processList(urls); + return { + title: 'Popular Content | Josh W. Comeau', + description: 'Friendly tutorials for developers. Focus on React, CSS, Animation, and more!', + link: rootUrl, + item: items, + icon: `${rootUrl}/favicon.png`, + logo: `${rootUrl}/favicon.png`, + } as Data; +} diff --git a/lib/routes/joshwcomeau/utils.ts b/lib/routes/joshwcomeau/utils.ts new file mode 100644 index 000000000..90a71a73e --- /dev/null +++ b/lib/routes/joshwcomeau/utils.ts @@ -0,0 +1,61 @@ +import { DataItem } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; + +export const rootUrl = 'https://www.joshwcomeau.com'; + +export async function getRelativeUrlList(url, selector) { + const response = await ofetch(url); + const $ = load(response); + const heading = $('header>h1').text(); + const urls = $(selector) + .toArray() + .map((element) => { + const itemRelativeUrl = $(element).attr('href'); + const cardTitle = $(element).find('span').text(); + return { url: itemRelativeUrl as string, cardTitle }; + }); + return { heading, urls }; +} + +export async function processList(list) { + const listPromise = await Promise.allSettled(list.map(async (item) => await cache.tryGet(`joshwcomeau:${item.url}`, async () => await getPostContent(item)))); + return listPromise.map((item, index) => (item.status === 'fulfilled' ? item.value : ({ title: 'Error Reading Item', link: `${rootUrl}${list[index]?.url}` } as DataItem))); +} + +export async function getPostContent({ url, cardTitle }) { + if (url.startsWith('https')) { + return { + title: cardTitle ?? 'External Content', + description: 'Read it on external Site', + link: url, + } as DataItem; + } + const response = await ofetch(`${rootUrl}${url}`); + const $ = load(response); + const title = $('meta[property="og:title"]').attr('content')?.replace('• Josh W. Comeau', ''); + const summary = $('meta[property="og:description"]').attr('content'); + const author = $('meta[name="author"]').attr('content'); + const dateDiv = $('div[data-parent-layout]'); + const tag = dateDiv.find('dl:first-child > dd > a').text(); + const pubDate = dateDiv.find('dl:first-child > dd:has(span):not(:last-child)').text(); + const updateDate = dateDiv.find('dl:last-child > dd:has(span):not(:last-child)').text(); + const description = $('main > article').html(); + return { + title, + description, + author, + pubDate: processDate(pubDate), + updated: processDate(updateDate), + link: `${rootUrl}${url}`, + content: { html: description, text: summary }, + category: [tag], + } as DataItem; +} + +function processDate(date: string) { + const dateWithSlash = date.trim().replaceAll(' ', '/').replace(',', ''); + return parseDate(dateWithSlash, 'MMMM/Do/YYYY', 'en'); +}