diff --git a/lib/routes/psyche/namespace.ts b/lib/routes/psyche/namespace.ts
new file mode 100644
index 000000000..ece31fee1
--- /dev/null
+++ b/lib/routes/psyche/namespace.ts
@@ -0,0 +1,6 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'Psyche',
+ url: 'psyche.co',
+};
diff --git a/lib/routes/psyche/templates/essay.art b/lib/routes/psyche/templates/essay.art
new file mode 100644
index 000000000..5a2fab892
--- /dev/null
+++ b/lib/routes/psyche/templates/essay.art
@@ -0,0 +1,3 @@
+
+{{@ authorsBio }}
+{{@ content}}
\ No newline at end of file
diff --git a/lib/routes/psyche/templates/video.art b/lib/routes/psyche/templates/video.art
new file mode 100644
index 000000000..d1f546c39
--- /dev/null
+++ b/lib/routes/psyche/templates/video.art
@@ -0,0 +1,10 @@
+{{ set video = article.hosterId }}
+{{ if article.hoster === 'vimeo' }}
+ {{ set video = "https://player.vimeo.com/video/" + video + "?dnt=1"}}
+{{ else if article.hoster == 'youtube' }}
+ {{ set video = "https://www.youtube-nocookie.com/embed/" + video }}
+{{ /if }}
+
+
+{{@ article.credits}}
+{{@ article.description}}
diff --git a/lib/routes/psyche/topic.ts b/lib/routes/psyche/topic.ts
new file mode 100644
index 000000000..d273aff6d
--- /dev/null
+++ b/lib/routes/psyche/topic.ts
@@ -0,0 +1,46 @@
+import { Route } from '@/types';
+import { load } from 'cheerio';
+import { ofetch } from 'ofetch';
+import { getData } from './utils';
+
+export const route: Route = {
+ path: '/topic/:topic',
+ categories: ['new-media'],
+ example: '/psyche/topic/therapeia',
+ parameters: { topic: 'Topic' },
+ radar: [
+ {
+ source: ['psyche.co/:topic'],
+ },
+ ],
+ name: 'Topics',
+ maintainers: ['emdoe'],
+ handler,
+ description: 'Supported categories: Therapeia, Eudaimonia, and Poiesis.',
+};
+
+async function handler(ctx) {
+ const url = `https://psyche.co/${ctx.req.param('topic')}`;
+ const response = await ofetch(url);
+ const $ = load(response);
+
+ const data = JSON.parse($('script#__NEXT_DATA__').text());
+ const articles = data.props.pageProps.articles;
+ const prefix = `https://psyche.co/_next/data/${data.buildId}`;
+ const list = Object.keys(articles).flatMap((type) =>
+ articles[type].edges.map((item) => ({
+ title: item.node.title,
+ link: `https://psyche.co/${type}/${item.node.slug}`,
+ json: `${prefix}/${type}/${item.node.slug}.json`,
+ }))
+ );
+
+ const items = await getData(list);
+
+ return {
+ title: `Psyche | ${data.props.pageProps.section.title}`,
+ link: url,
+ description: data.props.pageProps.section.metaDescription,
+ item: items,
+ };
+}
diff --git a/lib/routes/psyche/type.ts b/lib/routes/psyche/type.ts
new file mode 100644
index 000000000..6647dd87a
--- /dev/null
+++ b/lib/routes/psyche/type.ts
@@ -0,0 +1,53 @@
+import { Route } from '@/types';
+import { load } from 'cheerio';
+import { getData } from './utils';
+import { ofetch } from 'ofetch';
+
+export const route: Route = {
+ path: '/type/:type',
+ categories: ['new-media'],
+ example: '/psyche/type/ideas',
+ parameters: { type: 'Type' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['psyche.co/:type'],
+ },
+ ],
+ name: 'Types',
+ maintainers: ['emdoe'],
+ handler,
+ description: `Supported types: Ideas, Guides, and Films.`,
+};
+
+async function handler(ctx) {
+ const type = ctx.req.param('type');
+ const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1);
+
+ const url = `https://psyche.co/${type}`;
+ const response = await ofetch(url);
+ const $ = load(response);
+
+ const data = JSON.parse($('script#__NEXT_DATA__').text());
+ const prefix = `https://psyche.co/_next/data/${data.buildId}`;
+ const list = data.props.pageProps.articles.map((item) => ({
+ title: item.title,
+ link: `${url}/${item.slug}`,
+ json: `${prefix}/${type}/${item.slug}.json`,
+ }));
+
+ const items = await getData(list);
+
+ return {
+ title: `Psyche | ${capitalizedType}`,
+ link: url,
+ item: items,
+ };
+}
diff --git a/lib/routes/psyche/utils.ts b/lib/routes/psyche/utils.ts
new file mode 100644
index 000000000..7e0612949
--- /dev/null
+++ b/lib/routes/psyche/utils.ts
@@ -0,0 +1,107 @@
+import { getCurrentPath } from '@/utils/helpers';
+const __dirname = getCurrentPath(import.meta.url);
+
+import cache from '@/utils/cache';
+import { ofetch } from 'ofetch';
+import { load } from 'cheerio';
+import { art } from '@/utils/render';
+import path from 'node:path';
+
+const getImageById = async (id) => {
+ const response = await ofetch('https://api.aeonmedia.co/graphql', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: 'query getImageById($id: ID!) { image(id: $id) { id url alt caption width height } }',
+ variables: { id, site: 'Aeon' },
+ operationName: 'getImageById',
+ }),
+ });
+
+ return response.data.image.url;
+};
+
+function format(article) {
+ const type = article.type.toLowerCase();
+
+ let block = '';
+ let banner = '';
+ let authorsBio = '';
+
+ switch (type) {
+ case 'film':
+ block = art(path.join(__dirname, 'templates/video.art'), { article });
+
+ break;
+
+ case 'guide': {
+ banner = article.imageSquare?.url;
+ authorsBio = article.authors.map((author) => author.bio).join(' ');
+
+ const sectionNames = ['Need To Know', 'What To Do', 'Key Points', 'Learn More', 'Links & Books'];
+ const sections = Object.keys(article).filter((key) => key.startsWith('section') && key !== 'section');
+ const content = sections
+ .map((section) => {
+ const capture = load(article[section]);
+ capture('p.pullquote').remove();
+ const sectionName = sectionNames.shift();
+ return `