From 26ffc44ee72522891b4fdacac15134dfcf9c4859 Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Mon, 12 Jan 2026 22:27:17 +0100 Subject: Rework how tags are working and make them native --- src/content.config.ts | 1 + src/loaders/pleroma.ts | 52 ++++++++++++++++++---- src/pages/micro/[...page].astro | 7 ++- src/pages/micro/tags/[tag]/[...page].astro | 71 ++++++++++++++++++++++++++++++ src/pages/micro/tags/index.astro | 36 +++++++++++++++ src/utils/micro.ts | 20 +++++++++ 6 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 src/pages/micro/tags/[tag]/[...page].astro create mode 100644 src/pages/micro/tags/index.astro 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}`; } @@ -540,6 +538,36 @@ function cleanContent(htmlContent: string): string { return markdown.trim().replace(/\n\s*\n\s*\n/g, "\n\n"); } +/** + * 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(/(? { + 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 = {

- Micro + Micro + + Browse tags + + RSS feed 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; + +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, + }, + }), +}; +--- + + + +

Micro posts about {tag}

+
    + { + page.data.map((note) => ( +
  • + +
  • + )) + } +
+ +
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", +}; +--- + + +

Micro Tags

+
    + { + allTags.map(([tag, val]) => ( +
  • + + #{tag} + + + - {val} Post{val > 1 && "s"} + +
  • + )) + } +
+
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 { 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(), + ), + ].sort((a, b) => b[1] - a[1]); +} -- cgit v1.2.3