diff --git a/lib/routes-deprecated/quantamagazine/archive.js b/lib/routes-deprecated/quantamagazine/archive.js
deleted file mode 100644
index ad22dc748..000000000
--- a/lib/routes-deprecated/quantamagazine/archive.js
+++ /dev/null
@@ -1,81 +0,0 @@
-const cheerio = require('cheerio');
-const got = require('@/utils/got');
-
-module.exports = async (ctx) => {
- const url = `https://feeder.co/discover/c1d270a6db/quantamagazine-org`;
-
- const res = await got.get(url);
- const $ = cheerio.load(res.data);
- const list = $('.card-title').get();
-
- function batchRemove($, array) {
- for (const element of array) {
- $(element).remove();
- }
- }
-
- function batchRemoveAll($, array) {
- for (const element of array) {
- $(element).each((index, item) => {
- $(item).remove();
- });
- }
- }
-
- const out = await Promise.all(
- list.map(async (item) => {
- const $ = cheerio.load(item);
- const title = $('.discover-feed-item-link').text();
- const address = $('.discover-feed-item-link').attr('href');
- const cache = await ctx.cache.get(address);
- if (cache) {
- return JSON.parse(cache);
- }
- const res = await got.get(address);
- const capture = cheerio.load(res.data);
-
- const infos = ['.post__footer', '.post__title__author-date'];
- batchRemove(capture, infos);
-
- const author = capture('h3.mv05').text();
- const time = capture('.align-c.mb075 > p > em').text();
-
- const unnecessary = ['.header-spacer', '.scale1.mha', '.post__title__author-date', '.post__aside--divider'];
- batchRemove(capture, unnecessary);
-
- const disturbing = ['.hide-on-print', '.post__aside__pullquote', 'aside.post__sidebar.hide'];
- batchRemoveAll(capture, disturbing);
-
- let contents = capture('#postBody').html();
- if (contents !== null) {
- const latex = contents.replaceAll(/\$latex([\S\s]+?)\$/g, '
');
- contents = latex.replaceAll(/
?/g, (omit, src, cap) => {
- const imgUrl = src.replaceAll(/\\([^nu])/g, '$1');
- const img = '

';
-
- const noBS = cap.replaceAll(/\\([^nu])/g, '$1');
- const removeNL = noBS.replaceAll('\\n', '');
- const caption = removeNL.replaceAll(/\\u(\d{1,3}[a-z]\d?|\d{4}?)/g, (omit, s) => String.fromCharCode(Number.parseInt(s, 16)));
- const inset = '
' + img + '' + caption + '' + '';
- return inset;
- });
- }
-
- const single = {
- title,
- author,
- description: contents,
- link: address,
- guid: address,
- pubDate: new Date(time).toUTCString(),
- };
- ctx.cache.set(address, JSON.stringify(single));
- return single;
- })
- );
- ctx.state.data = {
- title: 'Quanta Magazine',
- link: `https://www.quantamagazine.org/`,
- item: out,
- };
-};
diff --git a/lib/routes/quantamagazine/archive.ts b/lib/routes/quantamagazine/archive.ts
new file mode 100644
index 000000000..c9bf893db
--- /dev/null
+++ b/lib/routes/quantamagazine/archive.ts
@@ -0,0 +1,139 @@
+import { load } from 'cheerio';
+
+import type { Route } from '@/types';
+import cache from '@/utils/cache';
+import ofetch from '@/utils/ofetch';
+import { parseDate } from '@/utils/parse-date';
+
+const rootUrl = 'https://www.quantamagazine.org';
+
+const processArticleContent = (html: string | null, articleLink?: string): string => {
+ if (!html) {
+ return '';
+ }
+
+ // Handle LaTeX formulas
+ let processed = html.replaceAll(/\$latex([\S\s]+?)\$/g, '

');
+
+ // Handle embedded images with captions
+ processed = processed.replaceAll(/
?/g, (_match, src, cap) => {
+ const imgUrl = src.replaceAll(/\\([^nu])/g, '$1');
+ const img = `

`;
+
+ const noBS = cap.replaceAll(/\\([^nu])/g, '$1');
+ const removeNL = noBS.replaceAll(String.raw`\n`, '');
+ const caption = removeNL.replaceAll(/\\u(\d{1,3}[a-z]\d?|\d{4}?)/g, (_omit, s) => String.fromCodePoint(Number.parseInt(s, 16)));
+
+ return `
${img}${caption}`;
+ });
+
+ // Handle lottie-player animations
+ // Multiple lottie-players might exist (desktop/mobile versions) - replace all with placeholders first
+ const lottieMatches = [...processed.matchAll(/
]*src="([^"]+)"[^>]*><\/lottie-player>/g)];
+ const uniqueAnimations = new Set();
+
+ // Replace each lottie-player, but track unique animations by filename
+ for (const match of lottieMatches) {
+ const src = match[1];
+ // Extract animation name (without Desktop/Mobile suffix)
+ const animName =
+ src
+ .split('/')
+ .pop()
+ ?.replace(/-(Desktop|Mobile).*\.json$/, '') || 'animation';
+
+ if (uniqueAnimations.has(animName)) {
+ // Duplicate (mobile/desktop variant): just remove it
+ processed = processed.replace(match[0], '');
+ } else {
+ // First occurrence: replace with badge that links to the article
+ uniqueAnimations.add(animName);
+ const linkUrl = articleLink || rootUrl;
+ const badgeImg = 'https://img.shields.io/badge/🎬-View_Interactive_Animation-0066CC?style=for-the-badge';
+ const replacement = `
`;
+ processed = processed.replace(match[0], replacement);
+ }
+ }
+
+ return processed;
+};
+
+export const handler = async (ctx) => {
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
+
+ const apiUrl = `${rootUrl}/wp-json/wp/v2/posts`;
+ const posts = await ofetch(apiUrl, {
+ query: {
+ per_page: limit,
+ page: 1,
+ _embed: 'author',
+ },
+ });
+
+ const items = await Promise.all(
+ posts.map((item) =>
+ cache.tryGet(item.link, async () => {
+ // Get author name from embedded data
+ const authorName = item._embedded?.author?.[0]?.name || '';
+
+ // Fetch full article content from the page
+ const response = await ofetch(item.link, {
+ parseResponse: (txt) => txt,
+ });
+ const $ = load(response);
+
+ // Remove unnecessary elements
+ $('.header-spacer, .scale1.mha, .post__title__author-date, .post__aside--divider').remove();
+ $('.hide-on-print, .post__aside__pullquote, aside.post__sidebar.hide, nav[data-glide-el]').remove();
+ $('.post__footer, .post__title__author-date').remove();
+ // Remove video placeholder images (the poster is already in the video element)
+ $('.iframe-placeholder').remove();
+
+ const contents = processArticleContent($('#postBody').html(), item.link);
+
+ return {
+ title: item.title.rendered,
+ author: authorName,
+ description: contents,
+ link: item.link,
+ guid: item.link,
+ pubDate: parseDate(item.date),
+ };
+ })
+ )
+ );
+
+ return {
+ title: 'Quanta Magazine',
+ link: rootUrl,
+ item: items,
+ };
+};
+
+export const route: Route = {
+ path: '/archive',
+ name: 'Archive',
+ url: 'quantamagazine.org',
+ maintainers: ['emdoe'],
+ handler,
+ example: '/quantamagazine/archive',
+ parameters: {},
+ description: 'Get the latest articles from Quanta Magazine.',
+ categories: ['new-media'],
+
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['quantamagazine.org'],
+ target: '/archive',
+ },
+ ],
+};
diff --git a/lib/routes/quantamagazine/namespace.ts b/lib/routes/quantamagazine/namespace.ts
new file mode 100644
index 000000000..cdd4c7af8
--- /dev/null
+++ b/lib/routes/quantamagazine/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'Quanta Magazine',
+ url: 'quantamagazine.org',
+ lang: 'en',
+};