summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--astro.config.ts2
-rw-r--r--package.json3
-rw-r--r--pnpm-lock.yaml9
-rw-r--r--src/loaders/pleroma.ts22
-rw-r--r--src/plugins/rehype-image-captions.ts78
-rw-r--r--src/utils/markdown.ts21
6 files changed, 127 insertions, 8 deletions
diff --git a/astro.config.ts b/astro.config.ts
index 792a5f2..335e744 100644
--- a/astro.config.ts
+++ b/astro.config.ts
@@ -15,6 +15,7 @@ import rehypeUnwrapImages from "rehype-unwrap-images";
// Remark plugins
import remarkDirective from "remark-directive"; /* Handle ::: directives as nodes */
import robotsConfig from "./robots-txt.config";
+import { rehypeImageCaptions } from "./src/plugins/rehype-image-captions"; /* Add image captions on hover */
import { remarkAdmonitions } from "./src/plugins/remark-admonitions"; /* Add admonitions */
import { expressiveCodeOptions, siteConfig } from "./src/site.config";
@@ -64,6 +65,7 @@ export default defineConfig({
],
markdown: {
rehypePlugins: [
+ rehypeImageCaptions,
rehypeHeadingIds,
[rehypeAutolinkHeadings, { behavior: "wrap", properties: { className: ["not-prose"] } }],
[
diff --git a/package.json b/package.json
index b1fdb22..8f7a4d2 100644
--- a/package.json
+++ b/package.json
@@ -37,8 +37,11 @@
"medium-zoom": "^1.1.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-external-links": "^3.0.0",
+ "rehype-stringify": "^10.0.1",
"rehype-unwrap-images": "^1.0.0",
"remark-directive": "^4.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.1.2",
"sanitize-html": "^2.17.0",
"satori": "0.15.2",
"satori-html": "^0.3.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2e47431..b196d43 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -68,12 +68,21 @@ importers:
rehype-external-links:
specifier: ^3.0.0
version: 3.0.0
+ rehype-stringify:
+ specifier: ^10.0.1
+ version: 10.0.1
rehype-unwrap-images:
specifier: ^1.0.0
version: 1.0.0
remark-directive:
specifier: ^4.0.0
version: 4.0.0
+ remark-parse:
+ specifier: ^11.0.0
+ version: 11.0.0
+ remark-rehype:
+ specifier: ^11.1.2
+ version: 11.1.2
sanitize-html:
specifier: ^2.17.0
version: 2.17.0
diff --git a/src/loaders/pleroma.ts b/src/loaders/pleroma.ts
index d633aa0..766e9ee 100644
--- a/src/loaders/pleroma.ts
+++ b/src/loaders/pleroma.ts
@@ -531,6 +531,18 @@ function buildImageGridHtml(attachments: PleromaMediaAttachment[]): string {
if (count === 1 && imageAttachments[0]) {
const attachment = imageAttachments[0];
const description = attachment.description || "Image";
+ // Only wrap with figcaption if we have descriptive alt text
+ const hasDescriptiveAlt =
+ description && description.trim() && description.toLowerCase() !== "image";
+
+ if (hasDescriptiveAlt) {
+ return `
+<figure class="image-with-alt group relative mt-4 mb-4">
+<img src="${attachment.url}" alt="${description}" class="w-full cursor-zoom-in rounded-lg border border-gray-200 object-cover transition-colors hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600" loading="lazy" data-zoomable />
+<figcaption class="pointer-events-none absolute bottom-0 left-0 right-0 rounded-b-lg bg-black/70 px-3 py-2 text-sm text-white opacity-0 transition-opacity group-hover:opacity-100">${description}</figcaption>
+</figure>`;
+ }
+
return `
<div class="mt-4 mb-4">
<img src="${attachment.url}" alt="${description}" class="w-full cursor-zoom-in rounded-lg border border-gray-200 object-cover transition-colors hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600" loading="lazy" data-zoomable />
@@ -542,9 +554,19 @@ function buildImageGridHtml(attachments: PleromaMediaAttachment[]): string {
const images = imageAttachments
.map((attachment, index) => {
const description = attachment.description || "Image";
+ const hasDescriptiveAlt =
+ description && description.trim() && description.toLowerCase() !== "image";
// For odd count, the last image spans all columns
const isLastImage = index === count - 1;
const spanClass = isOdd && isLastImage ? "sm:col-span-2" : "";
+
+ if (hasDescriptiveAlt) {
+ return `<figure class="image-with-alt group relative h-fit ${spanClass}">
+<img src="${attachment.url}" alt="${description}" class="w-full max-h-96 object-cover cursor-zoom-in rounded-lg border border-gray-200 transition-colors hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600" loading="lazy" data-zoomable />
+<figcaption class="pointer-events-none absolute bottom-0 left-0 right-0 rounded-b-lg bg-black/70 px-3 py-2 text-sm text-white opacity-0 transition-opacity group-hover:opacity-100">${description}</figcaption>
+</figure>`;
+ }
+
return `<img src="${attachment.url}" alt="${description}" class="w-full max-h-96 object-cover cursor-zoom-in rounded-lg border border-gray-200 transition-colors hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600 ${spanClass}" loading="lazy" data-zoomable />`;
})
.join("\n");
diff --git a/src/plugins/rehype-image-captions.ts b/src/plugins/rehype-image-captions.ts
new file mode 100644
index 0000000..61bdd84
--- /dev/null
+++ b/src/plugins/rehype-image-captions.ts
@@ -0,0 +1,78 @@
+import type { Element, Root } from "hast";
+import { h } from "hastscript";
+import type { Plugin } from "unified";
+import { visit } from "unist-util-visit";
+
+/**
+ * Rehype plugin that wraps images with figure/figcaption to display alt text on hover.
+ * Skips images without alt text or with generic alt text like "Image".
+ */
+export const rehypeImageCaptions: Plugin<[], Root> = () => {
+ return (tree: Root) => {
+ visit(tree, "element", (node: Element, index, parent) => {
+ // Only process img elements that have a parent and an index
+ if (node.tagName !== "img" || !parent || index === undefined) {
+ return;
+ }
+
+ // Get the alt text
+ const alt = node.properties?.alt;
+ if (!alt || typeof alt !== "string") {
+ return;
+ }
+
+ // Skip generic or empty alt text
+ const trimmedAlt = alt.trim();
+ if (!trimmedAlt || trimmedAlt.toLowerCase() === "image") {
+ return;
+ }
+
+ // Preserve all existing properties including data-zoomable for medium-zoom
+ const imgProperties = { ...node.properties };
+
+ // Create the figcaption element with tailwind classes for hover overlay
+ const figcaption = h(
+ "figcaption",
+ {
+ class: [
+ "pointer-events-none",
+ "absolute",
+ "bottom-0",
+ "left-0",
+ "right-0",
+ "rounded-b-lg",
+ "bg-black/70",
+ "px-3",
+ "py-2",
+ "text-sm",
+ "text-white",
+ "opacity-0",
+ "transition-opacity",
+ "group-hover:opacity-100",
+ ].join(" "),
+ },
+ [{ type: "text", value: trimmedAlt }],
+ );
+
+ // Create the figure wrapper with group class for hover trigger
+ const figure = h(
+ "figure",
+ {
+ class: "image-with-alt group relative",
+ },
+ [
+ {
+ type: "element",
+ tagName: "img",
+ properties: imgProperties,
+ children: [],
+ },
+ figcaption,
+ ],
+ );
+
+ // Replace the img with the figure in the parent
+ parent.children[index] = figure;
+ });
+ };
+};
diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts
index 946d8d8..eaac6e7 100644
--- a/src/utils/markdown.ts
+++ b/src/utils/markdown.ts
@@ -1,11 +1,16 @@
-import { marked } from "marked";
-
-marked.setOptions({
- breaks: true,
- gfm: true,
-});
+import rehypeStringify from "rehype-stringify";
+import remarkParse from "remark-parse";
+import remarkRehype from "remark-rehype";
+import { unified } from "unified";
+import { rehypeImageCaptions } from "@/plugins/rehype-image-captions";
export function markdownToHtml(markdown: string): string {
- const html = marked.parse(markdown);
- return typeof html === "string" ? html : "";
+ const result = unified()
+ .use(remarkParse)
+ .use(remarkRehype, { allowDangerousHtml: true })
+ .use(rehypeImageCaptions)
+ .use(rehypeStringify, { allowDangerousHtml: true })
+ .processSync(markdown);
+
+ return String(result);
}