From f100d259d2ffebe61fef56ea3964f6d534d598c8 Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Thu, 3 Jul 2025 13:46:07 +0300 Subject: Initial pleroma pull support --- astro.config.ts | 2 +- package.json | 3 + pnpm-lock.yaml | 26 ++ src/components/FormattedDate.astro | 2 +- src/components/blog/PostPreview.astro | 2 +- src/components/blog/TOC.astro | 2 +- src/components/blog/webmentions/Comments.astro | 2 +- src/components/layout/Header.astro | 2 +- src/components/note/Note.astro | 2 +- src/content.config.ts | 16 +- src/layouts/Base.astro | 4 +- src/loaders/pleroma.ts | 375 +++++++++++++++++++++++++ src/pages/index.astro | 2 +- src/pages/micro/[...page].astro | 23 +- src/pages/micro/[...slug].astro | 18 +- src/pages/micro/rss.xml.ts | 22 +- src/pages/og-image/[...slug].png.ts | 8 +- src/pages/posts/[...page].astro | 6 +- src/pages/posts/[...slug].astro | 2 +- src/pages/rss.xml.ts | 2 +- src/pages/tags/[tag]/[...page].astro | 6 +- src/plugins/remark-admonitions.ts | 4 +- src/site.config.ts | 5 +- src/utils/date.ts | 4 +- src/utils/micro.ts | 26 ++ src/utils/webmentions.ts | 2 +- 26 files changed, 520 insertions(+), 48 deletions(-) create mode 100644 src/loaders/pleroma.ts create mode 100644 src/utils/micro.ts diff --git a/astro.config.ts b/astro.config.ts index 287a6de..01bfd96 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -33,7 +33,7 @@ export default defineConfig({ webmanifest({ // See: https://github.com/alextim/astro-lib/blob/main/packages/astro-webmanifest/README.md name: siteConfig.title, - short_name: "Astro_Cactus", // optional + short_name: "Dawid_Rycerz", // optional description: siteConfig.description, lang: siteConfig.lang, icon: "public/icon.svg", // the source for generating favicon & icons diff --git a/package.json b/package.json index 7467313..e0fc731 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "astro-robots-txt": "^1.0.0", "astro-webmanifest": "^1.0.0", "cssnano": "^7.0.7", + "fast-xml-parser": "^5.2.5", "hastscript": "^9.0.0", "mdast-util-directive": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", @@ -37,6 +38,7 @@ "satori": "0.15.2", "satori-html": "^0.3.2", "sharp": "^0.34.2", + "turndown": "^7.2.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, @@ -49,6 +51,7 @@ "@tailwindcss/typography": "^0.5.16", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", + "@types/turndown": "^5.0.5", "autoprefixer": "^10.4.21", "pagefind": "^1.3.0", "prettier": "^3.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a486cb0..10d65ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: cssnano: specifier: ^7.0.7 version: 7.0.7(postcss@8.5.3) + fast-xml-parser: + specifier: ^5.2.5 + version: 5.2.5 hastscript: specifier: ^9.0.0 version: 9.0.1 @@ -77,6 +80,9 @@ importers: sharp: specifier: ^0.34.2 version: 0.34.2 + turndown: + specifier: ^7.2.0 + version: 7.2.0 unified: specifier: ^11.0.5 version: 11.0.5 @@ -108,6 +114,9 @@ importers: '@types/mdast': specifier: ^4.0.4 version: 4.0.4 + '@types/turndown': + specifier: ^5.0.5 + version: 5.0.5 autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.3) @@ -612,6 +621,9 @@ packages: '@mdx-js/mdx@3.1.0': resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1031,6 +1043,9 @@ packages: '@types/tar@6.1.13': resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==} + '@types/turndown@5.0.5': + resolution: {integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2934,6 +2949,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turndown@7.2.0: + resolution: {integrity: sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==} + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -3840,6 +3858,8 @@ snapshots: - acorn - supports-color + '@mixmark-io/domino@2.2.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4195,6 +4215,8 @@ snapshots: '@types/node': 22.15.21 minipass: 4.2.8 + '@types/turndown@5.0.5': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -6703,6 +6725,10 @@ snapshots: tslib@2.8.1: {} + turndown@7.2.0: + dependencies: + '@mixmark-io/domino': 2.2.0 + type-fest@4.41.0: {} typesafe-path@0.2.2: {} diff --git a/src/components/FormattedDate.astro b/src/components/FormattedDate.astro index aba7f4d..45de273 100644 --- a/src/components/FormattedDate.astro +++ b/src/components/FormattedDate.astro @@ -1,6 +1,6 @@ --- -import { getFormattedDate } from "@/utils/date"; import type { HTMLAttributes } from "astro/types"; +import { getFormattedDate } from "@/utils/date"; type Props = HTMLAttributes<"time"> & { date: Date; diff --git a/src/components/blog/PostPreview.astro b/src/components/blog/PostPreview.astro index fc1a9a3..cbe747e 100644 --- a/src/components/blog/PostPreview.astro +++ b/src/components/blog/PostPreview.astro @@ -1,7 +1,7 @@ --- import type { CollectionEntry } from "astro:content"; -import FormattedDate from "@/components/FormattedDate.astro"; import type { HTMLTag, Polymorphic } from "astro/types"; +import FormattedDate from "@/components/FormattedDate.astro"; type Props = Polymorphic<{ as: Tag }> & { post: CollectionEntry<"post">; diff --git a/src/components/blog/TOC.astro b/src/components/blog/TOC.astro index 6649546..2a45124 100644 --- a/src/components/blog/TOC.astro +++ b/src/components/blog/TOC.astro @@ -1,6 +1,6 @@ --- -import { generateToc } from "@/utils/generateToc"; import type { MarkdownHeading } from "astro"; +import { generateToc } from "@/utils/generateToc"; import TOCHeading from "./TOCHeading.astro"; interface Props { diff --git a/src/components/blog/webmentions/Comments.astro b/src/components/blog/webmentions/Comments.astro index 5177d57..af14bd0 100644 --- a/src/components/blog/webmentions/Comments.astro +++ b/src/components/blog/webmentions/Comments.astro @@ -1,7 +1,7 @@ --- import { Image } from "astro:assets"; -import type { WebmentionsChildren } from "@/types"; import { Icon } from "astro-icon/components"; +import type { WebmentionsChildren } from "@/types"; interface Props { mentions: WebmentionsChildren[]; diff --git a/src/components/layout/Header.astro b/src/components/layout/Header.astro index 65ea5cc..e7a9a1d 100644 --- a/src/components/layout/Header.astro +++ b/src/components/layout/Header.astro @@ -2,7 +2,7 @@ import Search from "@/components/Search.astro"; import ThemeToggle from "@/components/ThemeToggle.astro"; import { menuLinks } from "@/site.config"; -import {siteConfig} from "../../site.config"; +import { siteConfig } from "../../site.config"; ---
diff --git a/src/components/note/Note.astro b/src/components/note/Note.astro index d96cb6d..b6a11c8 100644 --- a/src/components/note/Note.astro +++ b/src/components/note/Note.astro @@ -1,7 +1,7 @@ --- import { type CollectionEntry, render } from "astro:content"; -import FormattedDate from "@/components/FormattedDate.astro"; import type { HTMLTag, Polymorphic } from "astro/types"; +import FormattedDate from "@/components/FormattedDate.astro"; type Props = Polymorphic<{ as: Tag }> & { note: CollectionEntry<"note">; diff --git a/src/content.config.ts b/src/content.config.ts index 271b472..5930884 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -1,5 +1,6 @@ import { defineCollection, z } from "astro:content"; import { glob } from "astro/loaders"; +import { pleromaLoader } from "./loaders/pleroma"; function removeDupsAndLowerCase(array: string[]) { return [...new Set(array.map((str) => str.toLowerCase()))]; @@ -47,6 +48,19 @@ const note = defineCollection({ }), }); +const micro = defineCollection({ + loader: pleromaLoader({ + instanceUrl: "https://social.craftknight.com", + username: "dawid", + maxPosts: 50, + feedType: "atom", + }), + schema: baseSchema.extend({ + description: z.string().optional(), + publishDate: z.date().or(z.string().transform((val) => new Date(val))), + }), +}); + const tag = defineCollection({ loader: glob({ base: "./src/content/tag", pattern: "**/*.{md,mdx}" }), schema: z.object({ @@ -55,4 +69,4 @@ const tag = defineCollection({ }), }); -export const collections = { post, note, tag }; +export const collections = { post, note, tag, micro }; diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 09d7727..41a56dd 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -1,9 +1,9 @@ --- import BaseHead from "@/components/BaseHead.astro"; -import SkipLink from "@/components/SkipLink.astro"; -import ThemeProvider from "@/components/ThemeProvider.astro"; import Footer from "@/components/layout/Footer.astro"; import Header from "@/components/layout/Header.astro"; +import SkipLink from "@/components/SkipLink.astro"; +import ThemeProvider from "@/components/ThemeProvider.astro"; import { siteConfig } from "@/site.config"; import type { SiteMeta } from "@/types"; diff --git a/src/loaders/pleroma.ts b/src/loaders/pleroma.ts new file mode 100644 index 0000000..dc6a05c --- /dev/null +++ b/src/loaders/pleroma.ts @@ -0,0 +1,375 @@ +import type { Loader } from "astro/loaders"; +import { XMLParser } from "fast-xml-parser"; +import TurndownService from "turndown"; + +interface PleromaFeedConfig { + instanceUrl: string; + username: string; + maxPosts?: number; + feedType?: "rss" | "atom"; +} + +interface RssItem { + guid: string; + title: string; + description: string; + pubDate: string; + link: string; + category?: string | string[]; + "activity:object-type"?: string; + "activity:verb"?: string; + "thr:in-reply-to"?: { + "@_ref": string; + }; +} + +interface RssFeed { + rss: { + channel: { + title: string; + description: string; + link: string; + item?: RssItem | RssItem[]; + }; + }; +} + +interface AtomEntry { + id: string; + title: string; + content: { + "#text": string; + "@_type": string; + }; + published: string; + updated: string; + link: { + "@_href": string; + "@_rel": string; + "@_type": string; + }[]; + author: { + name: string; + uri: string; + }; + category?: { + "@_term": string; + }[]; + "activity:object-type"?: string; + "activity:verb"?: string; + "thr:in-reply-to"?: { + "@_ref": string; + }; +} + +interface AtomFeed { + feed: { + title: string; + id: string; + updated: string; + entry?: AtomEntry | AtomEntry[]; + }; +} + +function parseAtomFeed(xmlContent: string): AtomEntry[] { + const parser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: "@_", + parseAttributeValue: true, + }); + + const result: AtomFeed = parser.parse(xmlContent); + + if (!result.feed?.entry) { + return []; + } + + // Handle both single entry and array of entries + const entries = Array.isArray(result.feed.entry) ? result.feed.entry : [result.feed.entry]; + + return entries; +} + +function parseRssFeed(xmlContent: string): RssItem[] { + const parser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: "@_", + parseAttributeValue: true, + }); + + try { + const result: RssFeed = parser.parse(xmlContent); + + if (!result.rss?.channel?.item) { + console.log("RSS structure:", JSON.stringify(result, null, 2)); + return []; + } + + // Handle both single item and array of items + const items = Array.isArray(result.rss.channel.item) + ? result.rss.channel.item + : [result.rss.channel.item]; + + return items; + } catch (error) { + console.error("Failed to parse RSS feed:", error); + console.log("XML content length:", xmlContent.length); + console.log("XML preview:", xmlContent.substring(0, 1000)); + return []; + } +} + +function isFilteredPostAtom(entry: AtomEntry): boolean { + // Filter out boosts/reblogs + if (entry["activity:verb"] === "http://activitystrea.ms/schema/1.0/share") { + return true; + } + + // Filter out replies + if (entry["thr:in-reply-to"]) { + return true; + } + + // Filter out NSFW/sensitive content + if (entry.category) { + const categories = Array.isArray(entry.category) ? entry.category : [entry.category]; + const hasNsfwTag = categories.some( + (cat) => + cat["@_term"]?.toLowerCase().includes("nsfw") || + cat["@_term"]?.toLowerCase().includes("sensitive"), + ); + if (hasNsfwTag) { + return true; + } + } + + return false; +} + +function isFilteredPostRss(item: RssItem): boolean { + // Filter out boosts/reblogs + if (item["activity:verb"] === "http://activitystrea.ms/schema/1.0/share") { + return true; + } + + // Filter out replies + if (item["thr:in-reply-to"]) { + return true; + } + + // Filter out NSFW/sensitive content + if (item.category) { + const categories = Array.isArray(item.category) ? item.category : [item.category]; + const hasNsfwTag = categories.some( + (cat) => cat?.toLowerCase().includes("nsfw") || cat?.toLowerCase().includes("sensitive"), + ); + if (hasNsfwTag) { + return true; + } + } + + return false; +} + +function cleanContent(htmlContent: string): string { + const turndownService = new TurndownService({ + headingStyle: "atx", + codeBlockStyle: "fenced", + }); + + // Remove or replace common Pleroma/Mastodon elements + const cleanedContent = htmlContent + .replace(/]*>/gi, "") // Remove mention spans but keep content + .replace(/<\/span>/gi, "") + .replace(/]*>/gi, "") // Remove hashtag spans but keep content + .replace(/]*>.*?<\/span>/gi, "") // Remove ellipsis + .replace(/]*>.*?<\/span>/gi, ""); // Remove invisible text + + // Convert to markdown + const markdown = turndownService.turndown(cleanedContent); + + // Clean up extra whitespace + return markdown.trim().replace(/\n\s*\n\s*\n/g, "\n\n"); +} + +function extractTitle(content: string): string { + // Extract first line or first sentence as title + const firstLine = content.split("\n")[0]; + if (!firstLine) return "Micro post"; + + const firstSentence = firstLine.split(/[.!?]/)[0]; + if (!firstSentence) return "Micro post"; + + // Limit title length and clean it up + const title = (firstSentence.length > 60 ? `${firstSentence.substring(0, 57)}...` : firstSentence) + .replace(/[#*_`]/g, "") // Remove markdown formatting + .trim(); + + return title || "Micro post"; +} + +export function pleromaLoader(config: PleromaFeedConfig): Loader { + return { + name: "pleroma-loader", + load: async ({ store, logger }) => { + try { + const { instanceUrl, username, maxPosts = 20 } = config; + // Use RSS URL that redirects to Atom - this bypasses some access restrictions + const feedUrl = `${instanceUrl}/users/${username}.rss`; + + logger.info(`Fetching Pleroma feed from: ${feedUrl}`); + + // Add retry logic for network issues + let response: Response | undefined; + let lastError: unknown; + + for (let attempt = 1; attempt <= 3; attempt++) { + try { + logger.info(`Attempt ${attempt} to fetch feed...`); + + // Create timeout controller + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + + response = await fetch(feedUrl, { + headers: { + "User-Agent": "Astro Blog (pleroma-loader)", + }, + redirect: "follow", // Follow redirects + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (response.ok) { + break; // Success, exit retry loop + } + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } catch (error) { + lastError = error; + logger.warn(`Attempt ${attempt} failed: ${error}`); + + if (attempt < 3) { + logger.info("Retrying in 2 seconds..."); + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } + } + + if (!response || !response.ok) { + logger.warn(`Failed to fetch Pleroma feed after 3 attempts. Last error: ${lastError}`); + logger.info("Continuing without Pleroma posts..."); + store.clear(); + return; + } + + const xmlContent = await response.text(); + logger.info(`Received XML content length: ${xmlContent.length}`); + + // Auto-detect if it's Atom or RSS based on content + const isAtomFeed = + xmlContent.includes(" !isFilteredPostAtom(entry)).slice(0, maxPosts); + + logger.info(`After filtering: ${validEntries.length} valid posts`); + + // Clear existing entries + store.clear(); + + // Process each Atom entry + for (const entry of validEntries) { + try { + const content = entry.content?.["#text"] || ""; + const cleanedContent = cleanContent(content); + const title = extractTitle(cleanedContent); + + // Extract post ID from the entry ID + const postId = entry.id.split("/").pop() || entry.id; + + // Create note entry + store.set({ + id: `pleroma-${postId}`, + data: { + title, + description: + cleanedContent.substring(0, 160) + (cleanedContent.length > 160 ? "..." : ""), + publishDate: new Date(entry.published), + }, + body: cleanedContent, + rendered: { + html: `

${cleanedContent.replace(/\n\n/g, "

")}

`, + }, + }); + + logger.info(`Processed post: ${title.substring(0, 50)}...`); + } catch (error) { + logger.warn(`Failed to process entry ${entry.id}: ${error}`); + } + } + } else { + // Process as RSS feed + const items = parseRssFeed(xmlContent); + logger.info(`Parsed ${items.length} items from RSS feed`); + + const validRssItems = items.filter((item) => !isFilteredPostRss(item)).slice(0, maxPosts); + + logger.info(`After filtering: ${validRssItems.length} valid posts`); + + // Clear existing entries + store.clear(); + + // Process each RSS item + for (const item of validRssItems) { + try { + const content = item.description || ""; + const cleanedContent = cleanContent(content); + const title = extractTitle(cleanedContent); + + // Extract post ID from the GUID or link + const postId = + item.guid?.split("/").pop() || + (typeof item.link === "string" ? item.link.split("/").pop() : null) || + Math.random().toString(36); + + // Create note entry + store.set({ + id: `pleroma-${postId}`, + data: { + title, + description: + cleanedContent.substring(0, 160) + (cleanedContent.length > 160 ? "..." : ""), + publishDate: new Date(item.pubDate), + }, + body: cleanedContent, + rendered: { + html: `

${cleanedContent.replace(/\n\n/g, "

")}

`, + }, + }); + + logger.info(`Processed post: ${title.substring(0, 50)}...`); + } catch (error) { + logger.warn(`Failed to process RSS item ${item.guid}: ${error}`); + } + } + } + + logger.info(`Successfully loaded ${validEntries.length} Pleroma posts`); + } catch (error) { + logger.warn(`Pleroma loader failed: ${error}`); + logger.info("Continuing build without Pleroma posts..."); + // Don't throw error to prevent build failure + store.clear(); + } + }, + }; +} diff --git a/src/pages/index.astro b/src/pages/index.astro index 4b813da..f3aac47 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,8 +1,8 @@ --- import { type CollectionEntry, getCollection } from "astro:content"; -import SocialList from "@/components/SocialList.astro"; import PostPreview from "@/components/blog/PostPreview.astro"; import Note from "@/components/note/Note.astro"; +import SocialList from "@/components/SocialList.astro"; import { getAllPosts } from "@/data/post"; import PageLayout from "@/layouts/Base.astro"; import { collectionDateSort } from "@/utils/date"; diff --git a/src/pages/micro/[...page].astro b/src/pages/micro/[...page].astro index 08f5fd3..b4e3e07 100644 --- a/src/pages/micro/[...page].astro +++ b/src/pages/micro/[...page].astro @@ -1,20 +1,31 @@ --- import { type CollectionEntry, getCollection } from "astro:content"; -import Pagination from "@/components/Paginator.astro"; +import type { GetStaticPaths, Page } from "astro"; +import { Icon } from "astro-icon/components"; import Note from "@/components/note/Note.astro"; +import Pagination from "@/components/Paginator.astro"; import PageLayout from "@/layouts/Base.astro"; import { collectionDateSort } from "@/utils/date"; -import type { GetStaticPaths, Page } from "astro"; -import { Icon } from "astro-icon/components"; export const getStaticPaths = (async ({ paginate }) => { const MAX_MICRO_PER_PAGE = 10; - const allMicro = await getCollection("note"); - return paginate(allMicro.sort(collectionDateSort), { pageSize: MAX_MICRO_PER_PAGE }); + + // Get both local notes and Pleroma posts + const [allNotes, allMicro] = await Promise.all([ + getCollection("note"), + getCollection("micro").catch(() => []), // Fallback to empty array if micro collection fails + ]); + + // Combine and sort all micro posts + const allMicroPosts = [...allNotes, ...allMicro].sort( + (a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(), + ); + + return paginate(allMicroPosts, { pageSize: MAX_MICRO_PER_PAGE }); }) satisfies GetStaticPaths; interface Props { - page: Page>; + page: Page | CollectionEntry<"micro">>; uniqueTags: string[]; } diff --git a/src/pages/micro/[...slug].astro b/src/pages/micro/[...slug].astro index 2ce847d..54f6234 100644 --- a/src/pages/micro/[...slug].astro +++ b/src/pages/micro/[...slug].astro @@ -1,16 +1,22 @@ --- import { getCollection } from "astro:content"; - +import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; import Note from "@/components/note/Note.astro"; import PageLayout from "@/layouts/Base.astro"; -import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; // if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr export const getStaticPaths = (async () => { - const allNotes = await getCollection("note"); - return allNotes.map((note) => ({ - params: { slug: note.id }, - props: { note }, + // Get both local notes and Pleroma posts + const [allNotes, allMicro] = await Promise.all([ + getCollection("note"), + getCollection("micro").catch(() => []), // Fallback to empty array if micro collection fails + ]); + + const allPosts = [...allNotes, ...allMicro]; + + return allPosts.map((post) => ({ + params: { slug: post.id }, + props: { note: post }, // Keep 'note' name for compatibility with existing component })); }) satisfies GetStaticPaths; diff --git a/src/pages/micro/rss.xml.ts b/src/pages/micro/rss.xml.ts index 7311319..0827ccb 100644 --- a/src/pages/micro/rss.xml.ts +++ b/src/pages/micro/rss.xml.ts @@ -1,18 +1,28 @@ import { getCollection } from "astro:content"; -import { siteConfig } from "@/site.config"; import rss from "@astrojs/rss"; +import { siteConfig } from "@/site.config"; export const GET = async () => { - const micro = await getCollection("note"); + // Get both local notes and Pleroma posts + const [allNotes, allMicro] = await Promise.all([ + getCollection("note"), + getCollection("micro").catch(() => []), // Fallback to empty array if micro collection fails + ]); + + // Combine and sort all micro posts + const allMicroPosts = [...allNotes, ...allMicro].sort( + (a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(), + ); return rss({ title: siteConfig.title, description: siteConfig.description, site: import.meta.env.SITE, - items: micro.map((note) => ({ - title: note.data.title, - pubDate: note.data.publishDate, - link: `micro/${note.id}/`, + items: allMicroPosts.map((post) => ({ + title: post.data.title, + pubDate: post.data.publishDate, + link: `micro/${post.id}/`, + description: post.data.description, })), }); }; diff --git a/src/pages/og-image/[...slug].png.ts b/src/pages/og-image/[...slug].png.ts index a4982d8..f58316c 100644 --- a/src/pages/og-image/[...slug].png.ts +++ b/src/pages/og-image/[...slug].png.ts @@ -1,12 +1,12 @@ +import { Resvg } from "@resvg/resvg-js"; +import type { APIContext, InferGetStaticPropsType } from "astro"; +import satori, { type SatoriOptions } from "satori"; +import { html } from "satori-html"; import RobotoMonoBold from "@/assets/roboto-mono-700.ttf"; import RobotoMono from "@/assets/roboto-mono-regular.ttf"; import { getAllPosts } from "@/data/post"; import { siteConfig } from "@/site.config"; import { getFormattedDate } from "@/utils/date"; -import { Resvg } from "@resvg/resvg-js"; -import type { APIContext, InferGetStaticPropsType } from "astro"; -import satori, { type SatoriOptions } from "satori"; -import { html } from "satori-html"; const ogOptions: SatoriOptions = { // debug: true, diff --git a/src/pages/posts/[...page].astro b/src/pages/posts/[...page].astro index 495fc7b..e07bc29 100644 --- a/src/pages/posts/[...page].astro +++ b/src/pages/posts/[...page].astro @@ -1,12 +1,12 @@ --- import type { CollectionEntry } from "astro:content"; -import Pagination from "@/components/Paginator.astro"; +import type { GetStaticPaths, Page } from "astro"; +import { Icon } from "astro-icon/components"; import PostPreview from "@/components/blog/PostPreview.astro"; +import Pagination from "@/components/Paginator.astro"; import { getAllPosts, getUniqueTags, groupPostsByYear } from "@/data/post"; import PageLayout from "@/layouts/Base.astro"; import { collectionDateSort } from "@/utils/date"; -import type { GetStaticPaths, Page } from "astro"; -import { Icon } from "astro-icon/components"; export const getStaticPaths = (async ({ paginate }) => { const MAX_POSTS_PER_PAGE = 10; diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro index ca9c491..02047bd 100644 --- a/src/pages/posts/[...slug].astro +++ b/src/pages/posts/[...slug].astro @@ -1,8 +1,8 @@ --- import { render } from "astro:content"; +import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; import { getAllPosts } from "@/data/post"; import PostLayout from "@/layouts/BlogPost.astro"; -import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; // if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr export const getStaticPaths = (async () => { diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index 1c305af..8a6525d 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -1,6 +1,6 @@ +import rss from "@astrojs/rss"; import { getAllPosts } from "@/data/post"; import { siteConfig } from "@/site.config"; -import rss from "@astrojs/rss"; export const GET = async () => { const posts = await getAllPosts(); diff --git a/src/pages/tags/[tag]/[...page].astro b/src/pages/tags/[tag]/[...page].astro index 56923fb..93ea3be 100644 --- a/src/pages/tags/[tag]/[...page].astro +++ b/src/pages/tags/[tag]/[...page].astro @@ -1,12 +1,12 @@ --- import { render } from "astro:content"; -import Pagination from "@/components/Paginator.astro"; +import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; +import { Icon } from "astro-icon/components"; import PostPreview from "@/components/blog/PostPreview.astro"; +import Pagination from "@/components/Paginator.astro"; import { getAllPosts, getTagMeta, getUniqueTags } from "@/data/post"; import PageLayout from "@/layouts/Base.astro"; import { collectionDateSort } from "@/utils/date"; -import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; -import { Icon } from "astro-icon/components"; export const getStaticPaths = (async ({ paginate }) => { const allPosts = await getAllPosts(); diff --git a/src/plugins/remark-admonitions.ts b/src/plugins/remark-admonitions.ts index 89c8be9..1f718ca 100644 --- a/src/plugins/remark-admonitions.ts +++ b/src/plugins/remark-admonitions.ts @@ -1,5 +1,4 @@ -import type { AdmonitionType } from "@/types"; -import { type Properties, h as _h } from "hastscript"; +import { h as _h, type Properties } from "hastscript"; import type { Node, Paragraph as P, Parent, PhrasingContent, Root } from "mdast"; import type { Directives, LeafDirective, TextDirective } from "mdast-util-directive"; import { directiveToMarkdown } from "mdast-util-directive"; @@ -7,6 +6,7 @@ import { toMarkdown } from "mdast-util-to-markdown"; import { toString as mdastToString } from "mdast-util-to-string"; import type { Plugin } from "unified"; import { visit } from "unist-util-visit"; +import type { AdmonitionType } from "@/types"; // Supported admonition types const Admonitions = new Set(["tip", "note", "important", "caution", "warning"]); diff --git a/src/site.config.ts b/src/site.config.ts index 5abb1ae..a09953d 100644 --- a/src/site.config.ts +++ b/src/site.config.ts @@ -1,5 +1,5 @@ -import type { SiteConfig } from "@/types"; import type { AstroExpressiveCodeOptions } from "astro-expressive-code"; +import type { SiteConfig } from "@/types"; export const siteConfig: SiteConfig = { // Used as both a meta property (src/components/BaseHead.astro L:31 + L:49) & the generated satori png (src/pages/og-image/[slug].png.ts) @@ -14,7 +14,8 @@ export const siteConfig: SiteConfig = { }, }, // Used as the default description meta property and webmanifest description - description: "DevOps consulting and web development services. Specializing in CI/CD, Kubernetes, AWS, and modern web technologies.", + description: + "DevOps consulting and web development services. Specializing in CI/CD, Kubernetes, AWS, and modern web technologies.", // HTML lang property, found in src/layouts/Base.astro L:18 & astro.config.ts L:48 lang: "en-US", // Meta property, found in src/components/BaseHead.astro L:42 diff --git a/src/utils/date.ts b/src/utils/date.ts index fb943a5..b919810 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -16,8 +16,8 @@ export function getFormattedDate( } export function collectionDateSort( - a: CollectionEntry<"post" | "note">, - b: CollectionEntry<"post" | "note">, + a: CollectionEntry<"post" | "note" | "micro">, + b: CollectionEntry<"post" | "note" | "micro">, ) { return b.data.publishDate.getTime() - a.data.publishDate.getTime(); } diff --git a/src/utils/micro.ts b/src/utils/micro.ts new file mode 100644 index 0000000..7344850 --- /dev/null +++ b/src/utils/micro.ts @@ -0,0 +1,26 @@ +import type { CollectionEntry } from "astro:content"; + +export type MicroEntry = CollectionEntry<"note">; + +export function sortMicroEntries(entries: MicroEntry[]): MicroEntry[] { + return entries.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime()); +} + +export async function getAllMicroPosts(): Promise { + const { getCollection } = await import("astro:content"); + + const notes = await getCollection("note"); + + // Try to get micro posts if available, otherwise just use notes + try { + const microPosts = await getCollection("micro"); + const allMicroPosts: (CollectionEntry<"note"> | CollectionEntry<"micro">)[] = [ + ...notes, + ...microPosts, + ]; + return sortMicroEntries(allMicroPosts as MicroEntry[]); + } catch (error) { + console.warn("Micro collection not available, using notes only:", error); + return sortMicroEntries(notes); + } +} diff --git a/src/utils/webmentions.ts b/src/utils/webmentions.ts index c9f62b7..8edfd90 100644 --- a/src/utils/webmentions.ts +++ b/src/utils/webmentions.ts @@ -1,5 +1,5 @@ -import * as fs from "node:fs"; import { WEBMENTION_API_KEY } from "astro:env/server"; +import * as fs from "node:fs"; import type { WebmentionsCache, WebmentionsChildren, WebmentionsFeed } from "@/types"; const DOMAIN = import.meta.env.SITE; -- cgit v1.2.3