diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2026-01-12 22:27:17 +0100 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2026-01-12 22:27:17 +0100 |
| commit | 26ffc44ee72522891b4fdacac15134dfcf9c4859 (patch) | |
| tree | 0a4014f93f35b348f9e5062904e17d724f228a69 | |
| parent | 686ccbfd2ed51723f4df79ba0b976e0f5fafce2f (diff) | |
Rework how tags are working and make them native
| -rw-r--r-- | src/content.config.ts | 1 | ||||
| -rw-r--r-- | src/loaders/pleroma.ts | 52 | ||||
| -rw-r--r-- | src/pages/micro/[...page].astro | 7 | ||||
| -rw-r--r-- | src/pages/micro/tags/[tag]/[...page].astro | 71 | ||||
| -rw-r--r-- | src/pages/micro/tags/index.astro | 36 | ||||
| -rw-r--r-- | src/utils/micro.ts | 20 |
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}`} + > + #{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]); +} |
