summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/content.config.ts1
-rw-r--r--src/loaders/pleroma.ts52
-rw-r--r--src/pages/micro/[...page].astro7
-rw-r--r--src/pages/micro/tags/[tag]/[...page].astro71
-rw-r--r--src/pages/micro/tags/index.astro36
-rw-r--r--src/utils/micro.ts20
6 files changed, 178 insertions, 9 deletions
diff --git a/src/content.config.ts b/src/content.config.ts
index bee2f8c..e7729e3 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -49,6 +49,7 @@ const micro = defineCollection({
publishDate: z.date().or(z.string().transform((val) => new Date(val))),
sourceUrl: z.string().optional(),
language: z.string().optional(),
+ tags: z.array(z.string()).default([]).transform(removeDupsAndLowerCase),
author: z
.object({
username: z.string(),
diff --git a/src/loaders/pleroma.ts b/src/loaders/pleroma.ts
index 0fd839f..975f57e 100644
--- a/src/loaders/pleroma.ts
+++ b/src/loaders/pleroma.ts
@@ -317,11 +317,9 @@ ${imageAttachments
// Join segments with horizontal rule separator
let content = segments.join("\n\n---\n\n");
- // Append consolidated tags at the end as markdown links
+ // Append consolidated tags at the end as plain hashtags
if (allTags.size > 0) {
- // Get the instance URL from the first post to construct tag URLs
- const instanceUrl = chain[0]?.account.url.split("/@")[0] || "https://social.craftknight.com";
- const tagLine = [...allTags].map((t) => `[#${t}](${instanceUrl}/tag/${t})`).join(" ");
+ const tagLine = [...allTags].map((t) => `#${t}`).join(" ");
content = `${content}\n\n${tagLine}`;
}
@@ -541,6 +539,36 @@ function cleanContent(htmlContent: string): string {
}
/**
+ * Replace all hashtags in content with internal tag links
+ * Handles both plain #hashtags and existing markdown links [#tag](url)
+ * Returns modified content and extracted tags array
+ */
+function replaceHashtagsWithLinks(content: string): {
+ content: string;
+ tags: string[];
+} {
+ const tags: string[] = [];
+
+ // First, replace existing markdown hashtag links: [#tag](any-url)
+ let modifiedContent = content.replace(/\[#(\w+)\]\([^)]+\)/g, (_match, tag) => {
+ tags.push(tag.toLowerCase());
+ return `[#${tag}](/micro/tags/${tag.toLowerCase()})`;
+ });
+
+ // Then, replace plain #hashtags (not already in markdown link format)
+ // Negative lookbehind to avoid matching hashtags already in [#tag] format
+ modifiedContent = modifiedContent.replace(/(?<!\[)#(\w+)(?!\])/g, (_match, tag) => {
+ tags.push(tag.toLowerCase());
+ return `[#${tag}](/micro/tags/${tag.toLowerCase()})`;
+ });
+
+ return {
+ content: modifiedContent,
+ tags: [...new Set(tags)], // Deduplicate
+ };
+}
+
+/**
* Replace Pleroma notice links with internal links when the post exists in our collection
* Handles both markdown links and plain URLs
*/
@@ -654,6 +682,7 @@ export function pleromaLoader(config: PleromaFeedConfig): Loader {
let attachments: Array<{ url: string; type: string }>;
let postId: string;
let sourceUrl: string;
+ let tags: string[];
// Check if this is a thread starter and thread merging is enabled
if (config.mergeThreads !== false && isThreadStarter(content)) {
@@ -667,15 +696,21 @@ export function pleromaLoader(config: PleromaFeedConfig): Loader {
// Merge thread content
const merged = mergeThreadContent(chain);
- cleanedContent = merged.content;
- cleanedContent = replacePleromaLinks(cleanedContent, instanceUrl, allPostIds);
+ const { content: contentWithTags, tags: extractedTags } = replaceHashtagsWithLinks(
+ merged.content,
+ );
+ tags = extractedTags;
+ cleanedContent = replacePleromaLinks(contentWithTags, instanceUrl, allPostIds);
attachments = merged.attachments;
postId = status.id;
sourceUrl = status.url;
} else {
// Process as single post
- cleanedContent = cleanContent(content);
- cleanedContent = replacePleromaLinks(cleanedContent, instanceUrl, allPostIds);
+ const rawContent = cleanContent(content);
+ const { content: contentWithTags, tags: extractedTags } =
+ replaceHashtagsWithLinks(rawContent);
+ tags = extractedTags;
+ cleanedContent = replacePleromaLinks(contentWithTags, instanceUrl, allPostIds);
postId = status.id;
sourceUrl = status.url;
@@ -711,6 +746,7 @@ export function pleromaLoader(config: PleromaFeedConfig): Loader {
author,
attachments,
language: status.language || undefined,
+ tags,
},
body: cleanedContent,
rendered: {
diff --git a/src/pages/micro/[...page].astro b/src/pages/micro/[...page].astro
index edfecab..8e7e814 100644
--- a/src/pages/micro/[...page].astro
+++ b/src/pages/micro/[...page].astro
@@ -51,7 +51,12 @@ const paginationProps = {
<PageLayout meta={meta}>
<section>
<h1 class="title mb-6 flex items-center gap-3">
- Micro <a class="text-accent" href="/micro/rss.xml" target="_blank">
+ Micro
+ <a class="text-accent" href="/micro/tags/" title="Browse micro tags">
+ <span class="sr-only">Browse tags</span>
+ <Icon aria-hidden="true" class="h-6 w-6" focusable="false" name="mdi:tag-multiple" />
+ </a>
+ <a class="text-accent" href="/micro/rss.xml" target="_blank">
<span class="sr-only">RSS feed</span>
<Icon aria-hidden="true" class="h-6 w-6" focusable="false" name="mdi:rss" />
</a>
diff --git a/src/pages/micro/tags/[tag]/[...page].astro b/src/pages/micro/tags/[tag]/[...page].astro
new file mode 100644
index 0000000..3f78663
--- /dev/null
+++ b/src/pages/micro/tags/[tag]/[...page].astro
@@ -0,0 +1,71 @@
+---
+import { getCollection } from "astro:content";
+import type { GetStaticPaths, InferGetStaticPropsType } 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 { getUniqueMicroTags, sortMicroEntries } from "@/utils/micro";
+
+export const getStaticPaths = (async ({ paginate }) => {
+ const allMicroPosts = await getCollection("micro");
+ const sortedPosts = sortMicroEntries(allMicroPosts);
+ const uniqueTags = getUniqueMicroTags(sortedPosts);
+
+ return uniqueTags.flatMap((tag) => {
+ const postsWithTag = sortedPosts.filter((post) => post.data.tags?.includes(tag));
+ return paginate(postsWithTag, {
+ pageSize: 10,
+ params: { tag },
+ });
+ });
+}) satisfies GetStaticPaths;
+
+type Props = InferGetStaticPropsType<typeof getStaticPaths>;
+
+const { page } = Astro.props as Props;
+const { tag } = Astro.params;
+
+const meta = {
+ description: `View all micro posts with the tag - ${tag}`,
+ title: `Micro posts about ${tag}`,
+};
+
+const paginationProps = {
+ ...(page.url.prev && {
+ prevUrl: {
+ text: "← Previous Page",
+ url: page.url.prev,
+ },
+ }),
+ ...(page.url.next && {
+ nextUrl: {
+ text: "Next Page →",
+ url: page.url.next,
+ },
+ }),
+};
+---
+
+<PageLayout meta={meta}>
+ <nav class="mb-8" aria-label="Breadcrumbs">
+ <ul class="flex items-center">
+ <li class="flex items-center">
+ <a class="text-accent" href="/micro/tags/">Micro Tags</a>
+ <Icon aria-hidden="true" name="mdi:chevron-right" class="mx-1.5" />
+ </li>
+ <li aria-current="page" class=""><span aria-hidden="true">#</span>{tag}</li>
+ </ul>
+ </nav>
+ <h1 class="title capitalize">Micro posts about {tag}</h1>
+ <ul class="mt-6 space-y-8 text-start">
+ {
+ page.data.map((note) => (
+ <li class="">
+ <Note note={note} as="h2" isPreview />
+ </li>
+ ))
+ }
+ </ul>
+ <Pagination {...paginationProps} />
+</PageLayout>
diff --git a/src/pages/micro/tags/index.astro b/src/pages/micro/tags/index.astro
new file mode 100644
index 0000000..8d39b8a
--- /dev/null
+++ b/src/pages/micro/tags/index.astro
@@ -0,0 +1,36 @@
+---
+import { getCollection } from "astro:content";
+import PageLayout from "@/layouts/Base.astro";
+import { getUniqueMicroTagsWithCount } from "@/utils/micro";
+
+const allMicroPosts = await getCollection("micro");
+const allTags = getUniqueMicroTagsWithCount(allMicroPosts);
+
+const meta = {
+ description: "A list of all the topics I've written about in my micro posts",
+ title: "Micro Tags",
+};
+---
+
+<PageLayout meta={meta}>
+ <h1 class="title mb-6">Micro Tags</h1>
+ <ul class="space-y-6">
+ {
+ allTags.map(([tag, val]) => (
+ <li class="flex items-center gap-x-2">
+ <a
+ class="cactus-link inline-block"
+ data-astro-prefetch
+ href={`/micro/tags/${tag}/`}
+ title={`View micro posts with the tag: ${tag}`}
+ >
+ &#35;{tag}
+ </a>
+ <span class="inline-block">
+ - {val} Post{val > 1 && "s"}
+ </span>
+ </li>
+ ))
+ }
+ </ul>
+</PageLayout>
diff --git a/src/utils/micro.ts b/src/utils/micro.ts
index 51d336b..7f06b41 100644
--- a/src/utils/micro.ts
+++ b/src/utils/micro.ts
@@ -18,3 +18,23 @@ export async function getAllMicroPosts(): Promise<MicroEntry[]> {
return [];
}
}
+
+/** Extract all tags from micro posts */
+export function getAllMicroTags(entries: MicroEntry[]): string[] {
+ return entries.flatMap((entry) => entry.data.tags ?? []);
+}
+
+/** Get unique tags from micro posts */
+export function getUniqueMicroTags(entries: MicroEntry[]): string[] {
+ return [...new Set(getAllMicroTags(entries))];
+}
+
+/** Get unique tags with counts from micro posts */
+export function getUniqueMicroTagsWithCount(entries: MicroEntry[]): [string, number][] {
+ return [
+ ...getAllMicroTags(entries).reduce(
+ (acc, t) => acc.set(t, (acc.get(t) ?? 0) + 1),
+ new Map<string, number>(),
+ ),
+ ].sort((a, b) => b[1] - a[1]);
+}