diff --git a/lib/routes/30secondsofcode/utils.ts b/lib/routes/30secondsofcode/utils.ts index 9ffbab8b5045b9..c3e88d9705366b 100644 --- a/lib/routes/30secondsofcode/utils.ts +++ b/lib/routes/30secondsofcode/utils.ts @@ -48,7 +48,6 @@ async function processItem({ link: articleLink, date }) { category: tags, image: `${rootUrl}${image}`, banner: `${rootUrl}${image}`, - language: 'en-us', } as DataItem; }); } diff --git a/lib/routes/css-tricks/articles.ts b/lib/routes/css-tricks/articles.ts new file mode 100644 index 00000000000000..f40c93c2569a76 --- /dev/null +++ b/lib/routes/css-tricks/articles.ts @@ -0,0 +1,48 @@ +import { Data, Route, ViewType } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import { processWithWp } from './utils'; +export const route: Route = { + path: '/articles', + view: ViewType.Articles, + categories: ['programming'], + example: '/articles', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['css-tricks.com/category/articles/'], + target: '/articles', + }, + ], + name: 'Articles', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler() { + // const category = ctx.req.param('category') ?? ''; + // const subCategory = ctx.req.param('subCategory') ?? ''; + + const rootUrl = 'https://css-tricks.com'; + const currentUrl = `${rootUrl}/category/articles/`; + const response = await ofetch(currentUrl); + const $ = load(response); + const articleCards = $('article.article-card').toArray(); + const items = await processWithWp(articleCards); + return { + title: 'Articles - CSS-Tricks', + description: 'Latest Articles - CSS-Tricks', + link: currentUrl, + item: items, + language: 'en', + logo: `${rootUrl}/favicon.ico`, + icon: `${rootUrl}/favicon.ico`, + } as Data; +} diff --git a/lib/routes/css-tricks/fresh.ts b/lib/routes/css-tricks/fresh.ts new file mode 100644 index 00000000000000..4ddf7786b02f6e --- /dev/null +++ b/lib/routes/css-tricks/fresh.ts @@ -0,0 +1,40 @@ +import { Data, Route, ViewType } from '@/types'; +import { extractMiniCards, processCards, rootUrl } from './utils'; +export const route: Route = { + path: '/fresh', + view: ViewType.Articles, + categories: ['programming'], + example: '/fresh', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['css-tricks.com'], + target: '/fresh', + }, + ], + name: 'Fresh From the Almanac', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler() { + const popularCards = await extractMiniCards('body > div.page-wrap > section.post-sliders > div:nth-child(4) article.mini-card.module.module-article'); + // Can't use wordPress API, these post Id's aren't available in the response + const items = await processCards(popularCards, true); + return { + title: 'Fresh From the Almanac', + description: 'Properties, selectors, rules, and functions!', + link: rootUrl, + item: items, + language: 'en', + logo: `${rootUrl}/favicon.ico`, + icon: `${rootUrl}/favicon.ico`, + } as Data; +} diff --git a/lib/routes/css-tricks/guides.ts b/lib/routes/css-tricks/guides.ts new file mode 100644 index 00000000000000..79a85c350e3d26 --- /dev/null +++ b/lib/routes/css-tricks/guides.ts @@ -0,0 +1,39 @@ +import { Data, Route, ViewType } from '@/types'; +import { extractMiniCards, processWithWp, rootUrl } from './utils'; +export const route: Route = { + path: '/guides', + view: ViewType.Articles, + categories: ['programming'], + example: '/guides', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['css-tricks.com'], + target: '/guides', + }, + ], + name: 'CSS Guides', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler() { + const guideCards = await extractMiniCards('body > div.page-wrap > section.post-sliders > div:nth-child(3) article.mini-card.module.module-article'); + const items = await processWithWp(guideCards, true); + return { + title: 'Latest CSS Guides', + description: 'Dive deep into CSS features and concepts', + link: rootUrl, + item: items, + language: 'en', + logo: `${rootUrl}/favicon.ico`, + icon: `${rootUrl}/favicon.ico`, + } as Data; +} diff --git a/lib/routes/css-tricks/namespace.ts b/lib/routes/css-tricks/namespace.ts new file mode 100644 index 00000000000000..248fe9ee439632 --- /dev/null +++ b/lib/routes/css-tricks/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'CSS-Tricks', + url: 'css-tricks.com', + categories: ['programming'], + lang: 'en', +}; diff --git a/lib/routes/css-tricks/popular.ts b/lib/routes/css-tricks/popular.ts new file mode 100644 index 00000000000000..af4209872561cb --- /dev/null +++ b/lib/routes/css-tricks/popular.ts @@ -0,0 +1,39 @@ +import { Data, Route, ViewType } from '@/types'; +import { extractMiniCards, processWithWp, rootUrl } from './utils'; +export const route: Route = { + path: '/popular', + view: ViewType.Articles, + categories: ['programming'], + example: '/popular', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['css-tricks.com'], + target: '/popular', + }, + ], + name: 'Popular this month', + maintainers: ['Rjnishant530'], + handler, +}; + +async function handler() { + const popularCards = await extractMiniCards('div.popular-articles > div.mini-card-grid article.mini-card.module.module-article'); + const items = await processWithWp(popularCards, true); + return { + title: 'Popular this month', + description: 'Popular CSS articles this month', + link: rootUrl, + item: items, + language: 'en', + logo: `${rootUrl}/favicon.ico`, + icon: `${rootUrl}/favicon.ico`, + } as Data; +} diff --git a/lib/routes/css-tricks/utils.ts b/lib/routes/css-tricks/utils.ts new file mode 100644 index 00000000000000..22f06442bc40d8 --- /dev/null +++ b/lib/routes/css-tricks/utils.ts @@ -0,0 +1,141 @@ +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import { DataItem } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; + +export const rootUrl = 'https://css-tricks.com'; +type Card = { + id: string; + title: string; + link: string; + thumbnail: string; +}; + +export async function extractMiniCards(selector) { + const response = await ofetch(rootUrl); + const $ = load(response); + return $(selector).toArray(); +} + +function extractCardsInfo(cards) { + return cards.map((card) => { + const $ = load(card); + const id = $(card).attr('id'); + const thumbnail = $(card).find('div.article-thumbnail-wrap > a >img').attr('src'); + const article = $('div.article-article'); + const title = article.find('h2 > a').text(); + const link = article.find('h2 > a').attr('href'); + return { + id, + title, + link, + thumbnail, + } as Card; + }); +} + +function extractMiniCardsInfo(cards) { + return cards.map((card) => { + const $ = load(card); + const id = $(card).attr('id')?.replace('mini-', ''); + const thumbnail = ''; + const title = $('h3.mini-card-title').find('a:not(.aal_anchor)').text(); + const link = $('h3.mini-card-title').find('a:not(.aal_anchor)').attr('href'); + return { + id, + title, + link, + thumbnail, + } as Card; + }); +} + +export async function processWithWp(cards, mini: boolean = false) { + const cardsWithInfo = mini ? extractMiniCardsInfo(cards) : extractCardsInfo(cards); + const ids = cardsWithInfo.map((card: Card) => card.id.replace('post-', '')); + const allPosts = await ofetch(`${rootUrl}/wp-json/wp/v2/posts?include=${ids.join(',')}&_embed&per_page=${ids.length}`); + // To maintain the ID/post Order + const idMappedPost = Object.fromEntries(allPosts.map((post) => [post.id, post])); + return ids.map((id) => extractPostDetails(idMappedPost[id])); +} + +export async function processCards(cards, mini: boolean = false) { + const cardsWithInfo = mini ? extractMiniCardsInfo(cards) : extractCardsInfo(cards); + const cardsPromise = await Promise.allSettled(cardsWithInfo.map(async (card: Card) => await fetchCardDetails(card))); + return cardsPromise.filter((card) => card.status === 'fulfilled').map((card) => card.value as DataItem); +} + +export async function fetchCardDetails(card: Card) { + return await cache.tryGet(`css-tricks:${card.id}`, async () => { + const response = await ofetch(card.link); + const $ = load(response); + const tags = $('meta[property="article:tag"]') + ?.toArray() + .map((tag) => $(tag).attr('content')); + const date = $('meta[property="article:published_time"]').attr('content') || ''; + const updateDate = $('meta[property="article:modified_time"]').attr('content') || ''; + const summary = $('meta[property="description"]').attr('content') || ''; + const authorUrl = $('header.mega-header').find('div.author-row > a').attr('href'); + const authorAvatar = $('header.mega-header').find('header.mega-header div.author-row > a >img').attr('src'); + const authorName = $('header.mega-header').find('header.mega-header div.author-row > div > a.author-name').text(); + const content = $('div.article-content').html() || ''; + return { + title: card.title, + link: card.link, + description: content, + banner: card.thumbnail, + image: card.thumbnail, + pubDate: parseDate(date), + updated: parseDate(updateDate), + author: [ + { + name: authorName || '', + url: authorUrl || '', + avatar: authorAvatar || '', + }, + ], + content: { + html: content, + text: summary, + }, + category: tags, + } as DataItem; + }); +} + +function extractPostDetails(data) { + const title = data.title.rendered; + const link = data.link; + const content = data.content.rendered; + const summary = data.excerpt.rendered; + const date = data.date_gmt; + const updateDate = data.modified_gmt; + const author = data._embedded.author; + const authorName = author[0]?.name; + const authorUrl = author[0]?.link; + const authorAvatar = author[0]?.avatar_urls['48']; + const thumbnail = data._embedded['wp:featuredmedia']?.[0]?.source_url; + const tags = data._embedded['wp:term']?.[1]?.map((tag) => tag.name); + return { + title, + link, + description: content, + banner: thumbnail, + image: thumbnail, + pubDate: parseDate(date), + updated: parseDate(updateDate), + author: [ + { + name: authorName || '', + url: authorUrl || '', + avatar: authorAvatar || '', + }, + ], + content: { + html: content, + text: summary, + }, + category: tags, + } as DataItem; +}