summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/FormattedDate.astro2
-rw-r--r--src/components/blog/PostPreview.astro2
-rw-r--r--src/components/blog/TOC.astro2
-rw-r--r--src/components/blog/webmentions/Comments.astro2
-rw-r--r--src/components/layout/Header.astro2
-rw-r--r--src/components/note/Note.astro2
-rw-r--r--src/content.config.ts16
-rw-r--r--src/layouts/Base.astro4
-rw-r--r--src/loaders/pleroma.ts375
-rw-r--r--src/pages/index.astro2
-rw-r--r--src/pages/micro/[...page].astro23
-rw-r--r--src/pages/micro/[...slug].astro18
-rw-r--r--src/pages/micro/rss.xml.ts22
-rw-r--r--src/pages/og-image/[...slug].png.ts8
-rw-r--r--src/pages/posts/[...page].astro6
-rw-r--r--src/pages/posts/[...slug].astro2
-rw-r--r--src/pages/rss.xml.ts2
-rw-r--r--src/pages/tags/[tag]/[...page].astro6
-rw-r--r--src/plugins/remark-admonitions.ts4
-rw-r--r--src/site.config.ts5
-rw-r--r--src/utils/date.ts4
-rw-r--r--src/utils/micro.ts26
-rw-r--r--src/utils/webmentions.ts2
23 files changed, 490 insertions, 47 deletions
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<Tag extends HTMLTag> = 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";
---
<header class="group relative mb-28 flex items-center sm:ps-18" id="main-header">
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<Tag extends HTMLTag> = 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(/<span class="[^"]*mention[^"]*"[^>]*>/gi, "") // Remove mention spans but keep content
+ .replace(/<\/span>/gi, "")
+ .replace(/<span class="[^"]*hashtag[^"]*"[^>]*>/gi, "") // Remove hashtag spans but keep content
+ .replace(/<span class="[^"]*ellipsis[^"]*"[^>]*>.*?<\/span>/gi, "") // Remove ellipsis
+ .replace(/<span class="[^"]*invisible[^"]*"[^>]*>.*?<\/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("<feed") ||
+ xmlContent.includes('xmlns="http://www.w3.org/2005/Atom"');
+ logger.info(`Detected feed type: ${isAtomFeed ? "Atom" : "RSS"}`);
+
+ let validEntries: AtomEntry[] = [];
+
+ if (isAtomFeed) {
+ // Process as Atom feed
+ const entries = parseAtomFeed(xmlContent);
+ logger.info(`Parsed ${entries.length} entries from Atom feed`);
+
+ validEntries = entries.filter((entry) => !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: `<p>${cleanedContent.replace(/\n\n/g, "</p><p>")}</p>`,
+ },
+ });
+
+ 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: `<p>${cleanedContent.replace(/\n\n/g, "</p><p>")}</p>`,
+ },
+ });
+
+ 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<CollectionEntry<"note">>;
+ page: Page<CollectionEntry<"note"> | 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<AdmonitionType>(["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<MicroEntry[]> {
+ 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;