Skip to content

admin/dorfarchiv

PublicMirror

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>
Levin Keller authored 1 parent a50f10acommit 3dc8f353899c33f97e1fa306f43dcc9c767f0b17Browse files

Showing 8 changed files with 130 additions and 1 deletion.

Modifieddocusaurus.config.ts+3 -0
@@ -1,6 +1,7 @@
11import type * as Preset from '@docusaurus/preset-classic'
22import type { Config } from '@docusaurus/types'
33import { themes as prismThemes } from 'prism-react-renderer'
4added: import remarkImageFigcaption from './src/plugins/remark-image-figcaption.mjs'
45
56// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
67
@@ -54,6 +55,7 @@ const config: Config = {
5455 sidebarPath: './sidebars.ts',
5556 editUrl:
5657 'https://github.com/Dorfpflege-Rossing/dorfarchiv/edit/main/',
58added: beforeDefaultRemarkPlugins: [remarkImageFigcaption],
5759 },
5860 blog: {
5961 path: 'berichte',
@@ -69,6 +71,7 @@ const config: Config = {
6971 blogDescription: 'Berichte und Geschichten aus Rössing',
7072 blogSidebarTitle: 'Alle Berichte',
7173 blogSidebarCount: 'ALL',
74added: beforeDefaultRemarkPlugins: [remarkImageFigcaption],
7275 },
7376 theme: {
7477 customCss: './src/css/custom.css',
Modifiedpackage-lock.json

The file is too large to show.

Modifiedpackage.json+3 -1
@@ -21,9 +21,11 @@
2121 "@docusaurus/preset-classic": "^3.8.1",
2222 "@mdx-js/react": "^3.0.0",
2323 "clsx": "^2.0.0",
24added: "exifreader": "^4.32.0",
2425 "prism-react-renderer": "^2.3.0",
2526 "react": "^19.0.0",
26deleted: "react-dom": "^19.0.0"
27added: "react-dom": "^19.0.0",
28added: "unist-util-visit": "^5.0.0"
2729 },
2830 "devDependencies": {
2931 "@anthropic-ai/claude-code": "^2.0.10",
Addedsrc/components/ImageFigure/index.js+23 -0
@@ -0,0 +1,23 @@
1added: import styles from './styles.module.css';
2added:
3added: export default function ImageFigure({ src, alt, artist, copyright, rights, children }) {
4added: const metadata = [];
5added: if (artist) metadata.push(`Foto: ${artist}`);
6added: if (copyright) metadata.push(copyright);
7added:
8added: const caption = metadata.join(' | ');
9added:
10added: return (
11added: <figure className={styles.imageWithCaption}>
12added: {children ? children : <img src={src} alt={alt} loading="lazy" decoding="async" />}
13added: <figcaption className={styles.imageCaption}>
14added: {caption}
15added: {rights && (
16added: <>
17added: {' '}(<a href={rights} target="_blank" rel="noopener noreferrer">Lizenz</a>)
18added: </>
19added: )}
20added: </figcaption>
21added: </figure>
22added: );
23added: }
Addedsrc/components/ImageFigure/styles.module.css+20 -0
@@ -0,0 +1,20 @@
1added: .imageWithCaption {
2added: margin: 2rem auto;
3added: text-align: center;
4added: max-width: 100%;
5added: }
6added:
7added: .imageWithCaption img {
8added: display: block;
9added: margin: 0 auto;
10added: max-width: 100%;
11added: height: auto;
12added: }
13added:
14added: .imageCaption {
15added: margin-top: 0.5rem;
16added: font-size: 0.875rem;
17added: color: var(--ifm-color-emphasis-600);
18added: font-style: italic;
19added: text-align: center;
20added: }
Modifiedsrc/css/custom.css+1 -0
@@ -28,3 +28,4 @@
2828 --ifm-color-primary-lightest: #4fddbf;
2929 --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
3030}
31added:
Addedsrc/plugins/remark-image-figcaption.mjs+73 -0
@@ -0,0 +1,73 @@
1added: import {visit, SKIP} from 'unist-util-visit';
2added: import path from 'node:path';
3added: import fs from 'node:fs';
4added: import ExifReader from 'exifreader';
5added:
6added: /**
7added: * Remark plugin to wrap images with figure/figcaption based on EXIF metadata
8added: * Works with MDX by creating JSX elements
9added: */
10added: export default function remarkImageFigcaption() {
11added: return (tree, file) => {
12added: visit(tree, 'image', (node, index, parent) => {
13added: try {
14added: // Skip if already processed
15added: if (node.data?.processed) return;
16added:
17added: const imageSrc = node.url;
18added: if (!imageSrc || imageSrc.startsWith('http')) return;
19added:
20added: // Get absolute path to image
21added: const fileDir = path.dirname(file.path);
22added: let imagePath;
23added:
24added: if (imageSrc.startsWith('/')) {
25added: imagePath = path.join(process.cwd(), 'static', imageSrc);
26added: } else {
27added: imagePath = path.join(fileDir, imageSrc);
28added: }
29added:
30added: // Skip if doesn't exist
31added: if (!fs.existsSync(imagePath)) return;
32added:
33added: // Read EXIF data
34added: const tags = ExifReader.load(fs.readFileSync(imagePath));
35added:
36added: const artist = tags.Artist?.description || tags.Creator?.description;
37added: const copyright = tags.Copyright?.description;
38added: const rights = tags.rights?.description || tags.Rights?.description || tags['Rights Usage Terms']?.description;
39added:
40added: // Only wrap if we have metadata
41added: if (!artist && !copyright && !rights) return;
42added:
43added: // Mark the node as processed to avoid infinite loops
44added: node.data = node.data || {};
45added: node.data.processed = true;
46added:
47added: // Create an MDX JSX element node to use our React component
48added: // Pass the original image as a child so it gets processed correctly by Docusaurus
49added: const jsxNode = {
50added: type: 'mdxJsxFlowElement',
51added: name: 'ImageFigure',
52added: attributes: [
53added: {type: 'mdxJsxAttribute', name: 'artist', value: artist || ''},
54added: {type: 'mdxJsxAttribute', name: 'copyright', value: copyright || ''},
55added: {type: 'mdxJsxAttribute', name: 'rights', value: rights || ''}
56added: ],
57added: children: [node] // Pass the original image node as child
58added: };
59added:
60added: // Replace the image node with the JSX component
61added: parent.children[index] = jsxNode;
62added:
63added: console.log('[remark-figcaption] Wrapped image:', imageSrc);
64added:
65added: // Skip visiting the newly added node
66added: return [SKIP, index];
67added:
68added: } catch (error) {
69added: console.warn(`[remark-figcaption] Failed to process image:`, error.message);
70added: }
71added: });
72added: };
73added: }
Addedsrc/theme/MDXComponents.js+7 -0
@@ -0,0 +1,7 @@
1added: import MDXComponents from '@theme-original/MDXComponents';
2added: import ImageFigure from '@site/src/components/ImageFigure';
3added:
4added: export default {
5added: ...MDXComponents,
6added: ImageFigure,
7added: };