Mirror of https://forge.levinkeller.de/levin/dorfarchiv.git
feat: Automatische EXIF-Metadaten-Anzeige für Bilder
Implementiert ein Plugin-System, das automatisch EXIF-Metadaten (Künstler, Copyright, Lizenz) aus Bildern extrahiert und als Bildunterschriften anzeigt. ## Änderungen: - Remark-Plugin zur EXIF-Extraktion aus Bildern beim Build - React-Komponente für figure/figcaption mit Metadaten - Automatische Anzeige von Lizenz-Links aus EXIF-Rights-Feld - Server-side Rendering (kein Client-JavaScript nötig) ## Technische Details: - Plugin läuft vor MDX-Verarbeitung (beforeDefaultRemarkPlugins) - Nutzt exifreader für EXIF-Daten - Erstellt MDX JSX-Komponenten für saubere Integration - Bild-URLs werden korrekt von Docusaurus-Webpack verarbeitet 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Showing 8 changed files with 130 additions and 1 deletion.
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | import type * as Preset from '@docusaurus/preset-classic' |
| 2 | 2 | import type { Config } from '@docusaurus/types' |
| 3 | 3 | import { themes as prismThemes } from 'prism-react-renderer' |
| 4 | added: import remarkImageFigcaption from './src/plugins/remark-image-figcaption.mjs' | |
| 4 | 5 | |
| 5 | 6 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) |
| 6 | 7 | |
| @@ -54,6 +55,7 @@ const config: Config = { | ||
| 54 | 55 | sidebarPath: './sidebars.ts', |
| 55 | 56 | editUrl: |
| 56 | 57 | 'https://github.com/Dorfpflege-Rossing/dorfarchiv/edit/main/', |
| 58 | added: beforeDefaultRemarkPlugins: [remarkImageFigcaption], | |
| 57 | 59 | }, |
| 58 | 60 | blog: { |
| 59 | 61 | path: 'berichte', |
| @@ -69,6 +71,7 @@ const config: Config = { | ||
| 69 | 71 | blogDescription: 'Berichte und Geschichten aus Rössing', |
| 70 | 72 | blogSidebarTitle: 'Alle Berichte', |
| 71 | 73 | blogSidebarCount: 'ALL', |
| 74 | added: beforeDefaultRemarkPlugins: [remarkImageFigcaption], | |
| 72 | 75 | }, |
| 73 | 76 | theme: { |
| 74 | 77 | customCss: './src/css/custom.css', |
The file is too large to show.
| @@ -21,9 +21,11 @@ | ||
| 21 | 21 | "@docusaurus/preset-classic": "^3.8.1", |
| 22 | 22 | "@mdx-js/react": "^3.0.0", |
| 23 | 23 | "clsx": "^2.0.0", |
| 24 | added: "exifreader": "^4.32.0", | |
| 24 | 25 | "prism-react-renderer": "^2.3.0", |
| 25 | 26 | "react": "^19.0.0", |
| 26 | deleted: "react-dom": "^19.0.0" | |
| 27 | added: "react-dom": "^19.0.0", | |
| 28 | added: "unist-util-visit": "^5.0.0" | |
| 27 | 29 | }, |
| 28 | 30 | "devDependencies": { |
| 29 | 31 | "@anthropic-ai/claude-code": "^2.0.10", |
| @@ -0,0 +1,23 @@ | ||
| 1 | added: import styles from './styles.module.css'; | |
| 2 | added: | |
| 3 | added: export default function ImageFigure({ src, alt, artist, copyright, rights, children }) { | |
| 4 | added: const metadata = []; | |
| 5 | added: if (artist) metadata.push(`Foto: ${artist}`); | |
| 6 | added: if (copyright) metadata.push(copyright); | |
| 7 | added: | |
| 8 | added: const caption = metadata.join(' | '); | |
| 9 | added: | |
| 10 | added: return ( | |
| 11 | added: <figure className={styles.imageWithCaption}> | |
| 12 | added: {children ? children : <img src={src} alt={alt} loading="lazy" decoding="async" />} | |
| 13 | added: <figcaption className={styles.imageCaption}> | |
| 14 | added: {caption} | |
| 15 | added: {rights && ( | |
| 16 | added: <> | |
| 17 | added: {' '}(<a href={rights} target="_blank" rel="noopener noreferrer">Lizenz</a>) | |
| 18 | added: </> | |
| 19 | added: )} | |
| 20 | added: </figcaption> | |
| 21 | added: </figure> | |
| 22 | added: ); | |
| 23 | added: } | |
| @@ -0,0 +1,20 @@ | ||
| 1 | added: .imageWithCaption { | |
| 2 | added: margin: 2rem auto; | |
| 3 | added: text-align: center; | |
| 4 | added: max-width: 100%; | |
| 5 | added: } | |
| 6 | added: | |
| 7 | added: .imageWithCaption img { | |
| 8 | added: display: block; | |
| 9 | added: margin: 0 auto; | |
| 10 | added: max-width: 100%; | |
| 11 | added: height: auto; | |
| 12 | added: } | |
| 13 | added: | |
| 14 | added: .imageCaption { | |
| 15 | added: margin-top: 0.5rem; | |
| 16 | added: font-size: 0.875rem; | |
| 17 | added: color: var(--ifm-color-emphasis-600); | |
| 18 | added: font-style: italic; | |
| 19 | added: text-align: center; | |
| 20 | added: } | |
| @@ -28,3 +28,4 @@ | ||
| 28 | 28 | --ifm-color-primary-lightest: #4fddbf; |
| 29 | 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); |
| 30 | 30 | } |
| 31 | added: | |
| @@ -0,0 +1,73 @@ | ||
| 1 | added: import {visit, SKIP} from 'unist-util-visit'; | |
| 2 | added: import path from 'node:path'; | |
| 3 | added: import fs from 'node:fs'; | |
| 4 | added: import ExifReader from 'exifreader'; | |
| 5 | added: | |
| 6 | added: /** | |
| 7 | added: * Remark plugin to wrap images with figure/figcaption based on EXIF metadata | |
| 8 | added: * Works with MDX by creating JSX elements | |
| 9 | added: */ | |
| 10 | added: export default function remarkImageFigcaption() { | |
| 11 | added: return (tree, file) => { | |
| 12 | added: visit(tree, 'image', (node, index, parent) => { | |
| 13 | added: try { | |
| 14 | added: // Skip if already processed | |
| 15 | added: if (node.data?.processed) return; | |
| 16 | added: | |
| 17 | added: const imageSrc = node.url; | |
| 18 | added: if (!imageSrc || imageSrc.startsWith('http')) return; | |
| 19 | added: | |
| 20 | added: // Get absolute path to image | |
| 21 | added: const fileDir = path.dirname(file.path); | |
| 22 | added: let imagePath; | |
| 23 | added: | |
| 24 | added: if (imageSrc.startsWith('/')) { | |
| 25 | added: imagePath = path.join(process.cwd(), 'static', imageSrc); | |
| 26 | added: } else { | |
| 27 | added: imagePath = path.join(fileDir, imageSrc); | |
| 28 | added: } | |
| 29 | added: | |
| 30 | added: // Skip if doesn't exist | |
| 31 | added: if (!fs.existsSync(imagePath)) return; | |
| 32 | added: | |
| 33 | added: // Read EXIF data | |
| 34 | added: const tags = ExifReader.load(fs.readFileSync(imagePath)); | |
| 35 | added: | |
| 36 | added: const artist = tags.Artist?.description || tags.Creator?.description; | |
| 37 | added: const copyright = tags.Copyright?.description; | |
| 38 | added: const rights = tags.rights?.description || tags.Rights?.description || tags['Rights Usage Terms']?.description; | |
| 39 | added: | |
| 40 | added: // Only wrap if we have metadata | |
| 41 | added: if (!artist && !copyright && !rights) return; | |
| 42 | added: | |
| 43 | added: // Mark the node as processed to avoid infinite loops | |
| 44 | added: node.data = node.data || {}; | |
| 45 | added: node.data.processed = true; | |
| 46 | added: | |
| 47 | added: // Create an MDX JSX element node to use our React component | |
| 48 | added: // Pass the original image as a child so it gets processed correctly by Docusaurus | |
| 49 | added: const jsxNode = { | |
| 50 | added: type: 'mdxJsxFlowElement', | |
| 51 | added: name: 'ImageFigure', | |
| 52 | added: attributes: [ | |
| 53 | added: {type: 'mdxJsxAttribute', name: 'artist', value: artist || ''}, | |
| 54 | added: {type: 'mdxJsxAttribute', name: 'copyright', value: copyright || ''}, | |
| 55 | added: {type: 'mdxJsxAttribute', name: 'rights', value: rights || ''} | |
| 56 | added: ], | |
| 57 | added: children: [node] // Pass the original image node as child | |
| 58 | added: }; | |
| 59 | added: | |
| 60 | added: // Replace the image node with the JSX component | |
| 61 | added: parent.children[index] = jsxNode; | |
| 62 | added: | |
| 63 | added: console.log('[remark-figcaption] Wrapped image:', imageSrc); | |
| 64 | added: | |
| 65 | added: // Skip visiting the newly added node | |
| 66 | added: return [SKIP, index]; | |
| 67 | added: | |
| 68 | added: } catch (error) { | |
| 69 | added: console.warn(`[remark-figcaption] Failed to process image:`, error.message); | |
| 70 | added: } | |
| 71 | added: }); | |
| 72 | added: }; | |
| 73 | added: } | |
| @@ -0,0 +1,7 @@ | ||
| 1 | added: import MDXComponents from '@theme-original/MDXComponents'; | |
| 2 | added: import ImageFigure from '@site/src/components/ImageFigure'; | |
| 3 | added: | |
| 4 | added: export default { | |
| 5 | added: ...MDXComponents, | |
| 6 | added: ImageFigure, | |
| 7 | added: }; | |