Compare commits
3
Commits
71ffedfaa4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09208c762f | ||
|
|
a495bba165 | ||
|
|
bfcfd14625 |
No files matched your search
+2
-2
@@ -1,9 +1,9 @@
|
||||
# Build the static site, then serve it with nginx. Two stages, so the image that
|
||||
# runs carries no toolchain — about 50 MB instead of about 400.
|
||||
FROM node:20-bookworm-slim AS build
|
||||
FROM node:22-bookworm-slim AS build
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci --no-audit --no-fund || npm install --no-audit --no-fund
|
||||
RUN npm ci --no-audit --no-fund
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
|
||||
+11
@@ -1,5 +1,16 @@
|
||||
# Access log without the client address.
|
||||
#
|
||||
# The default format writes $remote_addr and X-Forwarded-For, so a static page
|
||||
# that needs no personal data at all was storing the IP of everyone who looked
|
||||
# at it. This format keeps what is useful for operating the site — when, what,
|
||||
# which status, how big, from where, with what — and drops the one field that
|
||||
# identifies a person. Data minimisation is cheaper than a retention policy.
|
||||
log_format noip '[$time_local] "$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent"';
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
access_log /var/log/nginx/access.log noip;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
Generated
+1407
-3219
File diff suppressed because it is too large.
Load diff
+3
-3
@@ -10,9 +10,9 @@
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/space-grotesk": "^5.1.0",
|
||||
"@fontsource/space-mono": "^5.1.0",
|
||||
"astro": "^5.5.0",
|
||||
"@fontsource/space-grotesk": "^5.3.0",
|
||||
"@fontsource/space-mono": "^5.3.0",
|
||||
"astro": "^7.3.8",
|
||||
"pdf-lib": "^1.17.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
import '@fontsource/space-grotesk/400.css';
|
||||
import '@fontsource/space-grotesk/500.css';
|
||||
import '@fontsource/space-grotesk/700.css';
|
||||
import '@fontsource/space-mono/400.css';
|
||||
import '@fontsource/space-mono/700.css';
|
||||
import '../styles/tokens.css';
|
||||
import '../styles/site.css';
|
||||
import type { LegalPage } from '../lib/legal';
|
||||
|
||||
interface Props {
|
||||
page: LegalPage;
|
||||
lang: 'en' | 'de';
|
||||
canonical: string;
|
||||
otherHref: string;
|
||||
otherLabel: string;
|
||||
backHref: string;
|
||||
backLabel: string;
|
||||
siblingHref: string;
|
||||
siblingLabel: string;
|
||||
}
|
||||
const { page, lang, canonical, otherHref, otherLabel, backHref, backLabel, siblingHref, siblingLabel } = Astro.props;
|
||||
const home = lang === 'de' ? '/de/' : '/';
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={lang}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#EAEAE3" />
|
||||
<title>{page.title} · Klarbild</title>
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="canonical" href={canonical} />
|
||||
</head>
|
||||
<body>
|
||||
<header class="top">
|
||||
<div class="shell top-in">
|
||||
<a class="brand" href={home}><span class="regmark"></span>klarbild</a>
|
||||
<nav>
|
||||
<a href={backHref}>{backLabel}</a>
|
||||
<a class="langswitch" href={otherHref} hreflang={otherLabel.toLowerCase()}>{otherLabel}</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="legal shell">
|
||||
<span class="kicker">{page.updated}</span>
|
||||
<h1>{page.title}</h1>
|
||||
{page.intro && <p class="lead">{page.intro}</p>}
|
||||
|
||||
{page.blocks.map((b) => (
|
||||
<section class="lblock">
|
||||
{b.h && <h2>{b.h}</h2>}
|
||||
{b.rows && (
|
||||
<dl class="lrows">
|
||||
{b.rows.map(([k, v]) => (<><dt>{k}</dt><dd>{v}</dd></>))}
|
||||
</dl>
|
||||
)}
|
||||
{b.p && <p>{b.p}</p>}
|
||||
{b.html && <p set:html={b.html} />}
|
||||
</section>
|
||||
))}
|
||||
|
||||
<p class="lsibling"><a href={siblingHref}>{siblingLabel}</a></p>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="shell foot-in">
|
||||
<a class="brand" href={home}><span class="regmark"></span>klarbild</a>
|
||||
<a href={backHref}>{backLabel}</a>
|
||||
<a href={siblingHref}>{siblingLabel}</a>
|
||||
<a href="https://github.com/tillheidrich/klarbild" rel="noopener">GitHub</a>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,6 +12,7 @@ interface Props { copy: Copy; canonical: string }
|
||||
const { copy: c, canonical } = Astro.props;
|
||||
|
||||
const GITHUB = 'https://github.com/tillheidrich/klarbild';
|
||||
const RAW = 'https://raw.githubusercontent.com/tillheidrich/klarbild/main';
|
||||
const SITE = 'https://klarbild-demo.heidrich-digital.de';
|
||||
|
||||
// The output sizes the preset builder offers, straight from the application's
|
||||
@@ -354,9 +355,10 @@ KLARBILD_MODULES=all</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<pre>git clone {GITHUB}.git
|
||||
cd klarbild
|
||||
cp .env.example .env <span class="c">{c.install.comment1}</span>
|
||||
<pre>mkdir klarbild && cd klarbild
|
||||
curl -fsSLO {RAW}/docker-compose.yaml
|
||||
curl -fsSL {RAW}/.env.example -o .env
|
||||
nano .env <span class="c">{c.install.comment1}</span>
|
||||
docker compose up -d
|
||||
|
||||
docker compose logs app <span class="c">{c.install.comment2}</span></pre>
|
||||
@@ -380,6 +382,8 @@ docker compose logs app <span class="c">{c.install.comment2}</span></pre>
|
||||
<a class="brand" href={c.htmlLang === 'de' ? '/de/' : '/'}><span class="regmark"></span>klarbild</a>
|
||||
<a href={GITHUB} rel="noopener">GitHub</a>
|
||||
<a href={`${GITHUB}/blob/main/LICENSE`} rel="noopener">AGPL-3.0</a>
|
||||
<a href={c.footer.imprintHref}>{c.footer.imprint}</a>
|
||||
<a href={c.footer.privacyHref}>{c.footer.privacy}</a>
|
||||
<a href={c.otherHref}>{c.otherLabel === 'DE' ? 'Deutsch' : 'English'}</a>
|
||||
<span class="fine">
|
||||
{c.footer.fine} <a href="https://heidrich-digital.de" rel="noopener">Heidrich Digital</a>.
|
||||
|
||||
+23
-15
@@ -85,7 +85,7 @@ export interface Copy {
|
||||
};
|
||||
install: { kicker: string; h2: string; p1: string; p2: string; cta1: string; cta2: string; comment1: string; comment2: string; stack: string };
|
||||
privacy: { kicker: string; h2: string; lead: string; body: string };
|
||||
footer: { fine: string };
|
||||
footer: { fine: string; imprint: string; privacy: string; imprintHref: string; privacyHref: string };
|
||||
}
|
||||
|
||||
const TASKS_EN = [
|
||||
@@ -184,7 +184,7 @@ export const EN: Copy = {
|
||||
stats: [
|
||||
{ n: '210.00', t: '× 297.00 mm on the page when you ask for A4 — measured, not assumed' },
|
||||
{ n: '0', t: 'outbound requests from the print studio. It is arithmetic, sharp and pdf-lib' },
|
||||
{ n: '124', t: 'tests over the layout engine, the naming scheme and the path checks' },
|
||||
{ n: '130', t: 'tests over the layout engine, the naming scheme and the path checks' },
|
||||
{ n: '1', t: 'container and a database for the default install' },
|
||||
],
|
||||
},
|
||||
@@ -246,7 +246,7 @@ export const EN: Copy = {
|
||||
lang: {
|
||||
kicker: 'Two languages',
|
||||
h2: 'English by default, German all the way through.',
|
||||
lead: 'Not a half-translated interface: 1,067 entries, covering every translatable string in the source. The English sentence is the translation key, so a missing entry falls back to readable English rather than to print.sheet.title.',
|
||||
lead: 'Not a half-translated interface: 1,095 entries, covering every translatable string in the source. The English sentence is the translation key, so a missing entry falls back to readable English rather than to print.sheet.title.',
|
||||
body: 'One environment variable sets the default; visitors switch with the button in the corner and the choice sticks. Adding a third language means copying one JSON file and translating its values.',
|
||||
caption: 'The same screen, ?lang=de.',
|
||||
},
|
||||
@@ -262,13 +262,13 @@ export const EN: Copy = {
|
||||
},
|
||||
install: {
|
||||
kicker: 'Install',
|
||||
h2: 'Four lines, then a password in the log.',
|
||||
p1: 'The default compose file runs the application and a PostgreSQL, with files on a volume. No object store, no API key, no account anywhere. A second compose file adds MinIO and turns every module on, for people who want that.',
|
||||
p2: 'There are no built-in accounts. The first boot creates one administrator — from ADMIN_USERNAME and ADMIN_PASSWORD if you set them, and otherwise with a generated password printed to the log exactly once.',
|
||||
h2: 'Two files, then a password in the log.',
|
||||
p1: 'The default compose file runs the application and a PostgreSQL, with files on a volume. No object store, no API key, no account anywhere. A second compose file adds MinIO and turns every module on, for people who want that. Nothing to clone and nothing to build: the compose file pulls a prebuilt image for amd64 and arm64, so a Raspberry Pi 4 or 5 runs it too, and an update is docker compose pull.',
|
||||
p2: 'There are no built-in accounts. The first boot creates one administrator — from ADMIN_USERNAME and ADMIN_PASSWORD if you set them, and otherwise with a generated password printed to the log exactly once. No picture at hand for the first try? The print studio has a sample photo one click away.',
|
||||
cta1: 'Source on GitHub', cta2: 'Releases',
|
||||
comment1: '# four values at the top; the file says which',
|
||||
comment2: '# your admin password, printed once',
|
||||
stack: 'Astro on Node, PostgreSQL, sharp and pdf-lib. About 13,700 lines across 117 files. Sixteen forward-only migrations that run themselves on boot. Type check, tests and a secret scan run in CI on every push.',
|
||||
stack: 'Astro on Node, PostgreSQL, sharp and pdf-lib. About 13,800 lines across 115 files. Seventeen forward-only migrations that run themselves on boot. Type check, tests and a secret scan run in CI on every push.',
|
||||
},
|
||||
privacy: {
|
||||
kicker: 'Why self-hosted',
|
||||
@@ -276,7 +276,11 @@ export const EN: Copy = {
|
||||
lead: 'The print studio never makes an outbound request at all. With the image module on, only what you hand the model leaves the building, and the key is yours.',
|
||||
body: 'Share links are built to be careful rather than convenient: they expire after thirty days by default, can be revoked, can carry a passphrase, and are noindex. Access hangs on the link rather than on the picture, so guessing an image id returns a 404. The view counter records when and with what, never who — the address is hashed with a server secret and the link id, one row per visitor per hour, deleted after ninety days.',
|
||||
},
|
||||
footer: { fine: 'A demo page. The layout engine is real; the pictures are outlines. Built by' },
|
||||
footer: {
|
||||
fine: 'A demo page. The layout engine is real; the pictures are outlines. Built by',
|
||||
imprint: 'Imprint', privacy: 'Privacy',
|
||||
imprintHref: '/imprint/', privacyHref: '/privacy/',
|
||||
},
|
||||
};
|
||||
|
||||
export const DE: Copy = {
|
||||
@@ -296,7 +300,7 @@ export const DE: Copy = {
|
||||
stats: [
|
||||
{ n: '210,00', t: '× 297,00 mm auf der Seite, wenn du A4 sagst — nachgemessen, nicht angenommen' },
|
||||
{ n: '0', t: 'Anfragen nach außen aus dem Druckstudio. Es ist Rechnen, sharp und pdf-lib' },
|
||||
{ n: '124', t: 'Tests über Layout-Engine, Namensschema und Pfadprüfungen' },
|
||||
{ n: '130', t: 'Tests über Layout-Engine, Namensschema und Pfadprüfungen' },
|
||||
{ n: '1', t: 'Container und eine Datenbank für die Standardinstallation' },
|
||||
],
|
||||
},
|
||||
@@ -358,7 +362,7 @@ export const DE: Copy = {
|
||||
lang: {
|
||||
kicker: 'Zwei Sprachen',
|
||||
h2: 'Englisch als Standard, Deutsch vollständig daneben.',
|
||||
lead: 'Keine halb übersetzte Oberfläche: 1.067 Einträge, jeder übersetzbare String im Quelltext ist abgedeckt. Der englische Satz ist der Schlüssel — fehlt ein Eintrag, steht da lesbares Englisch und nicht print.sheet.title.',
|
||||
lead: 'Keine halb übersetzte Oberfläche: 1.095 Einträge, jeder übersetzbare String im Quelltext ist abgedeckt. Der englische Satz ist der Schlüssel — fehlt ein Eintrag, steht da lesbares Englisch und nicht print.sheet.title.',
|
||||
body: 'Eine Umgebungsvariable setzt den Standard; Besucher schalten mit dem Knopf in der Ecke um, und die Wahl bleibt. Eine dritte Sprache heißt: eine JSON-Datei kopieren und ihre Werte übersetzen.',
|
||||
caption: 'Derselbe Bildschirm, ?lang=de.',
|
||||
},
|
||||
@@ -374,13 +378,13 @@ export const DE: Copy = {
|
||||
},
|
||||
install: {
|
||||
kicker: 'Installieren',
|
||||
h2: 'Vier Zeilen, dann steht ein Passwort im Log.',
|
||||
p1: 'Die Standard-Compose-Datei startet die Anwendung und ein PostgreSQL, Dateien liegen auf einem Volume. Kein Objektspeicher, kein API-Schlüssel, kein Konto irgendwo. Eine zweite Compose-Datei nimmt MinIO dazu und schaltet alle Module an — für die, die das wollen.',
|
||||
p2: 'Es gibt keine eingebauten Konten. Der erste Start legt genau einen Administrator an — aus ADMIN_USERNAME und ADMIN_PASSWORD, wenn du sie setzt, sonst mit einem erzeugten Passwort, das genau einmal ins Log geschrieben wird.',
|
||||
h2: 'Zwei Dateien, dann steht ein Passwort im Log.',
|
||||
p1: 'Die Standard-Compose-Datei startet die Anwendung und ein PostgreSQL, Dateien liegen auf einem Volume. Kein Objektspeicher, kein API-Schlüssel, kein Konto irgendwo. Eine zweite Compose-Datei nimmt MinIO dazu und schaltet alle Module an — für die, die das wollen. Nichts klonen, nichts bauen: Die Compose-Datei holt ein fertiges Image für amd64 und arm64, ein Raspberry Pi 4 oder 5 reicht also auch, und ein Update ist docker compose pull.',
|
||||
p2: 'Es gibt keine eingebauten Konten. Der erste Start legt genau einen Administrator an — aus ADMIN_USERNAME und ADMIN_PASSWORD, wenn du sie setzt, sonst mit einem erzeugten Passwort, das genau einmal ins Log geschrieben wird. Kein Bild zur Hand für den ersten Versuch? Das Druckstudio hat ein Beispielfoto, einen Klick entfernt.',
|
||||
cta1: 'Quellcode auf GitHub', cta2: 'Releases',
|
||||
comment1: '# vier Werte ganz oben; die Datei sagt, welche',
|
||||
comment2: '# dein Admin-Passwort, einmalig ausgegeben',
|
||||
stack: 'Astro auf Node, PostgreSQL, sharp und pdf-lib. Rund 13.700 Zeilen in 117 Dateien. Sechzehn vorwärts-nur-Migrationen, die beim Start selbst laufen. Typprüfung, Tests und ein Secret-Scan laufen in der CI bei jedem Push.',
|
||||
stack: 'Astro auf Node, PostgreSQL, sharp und pdf-lib. Rund 13.800 Zeilen in 115 Dateien. Siebzehn vorwärts-nur-Migrationen, die beim Start selbst laufen. Typprüfung, Tests und ein Secret-Scan laufen in der CI bei jedem Push.',
|
||||
},
|
||||
privacy: {
|
||||
kicker: 'Warum selbst gehostet',
|
||||
@@ -388,7 +392,11 @@ export const DE: Copy = {
|
||||
lead: 'Das Druckstudio macht überhaupt keine Anfrage nach außen. Mit eingeschaltetem Bildmodul verlässt nur das Haus, was du dem Modell gibst — und der Schlüssel gehört dir.',
|
||||
body: 'Teilen-Links sind auf Vorsicht gebaut, nicht auf Bequemlichkeit: Sie laufen standardmäßig nach dreißig Tagen ab, lassen sich widerrufen, können ein Zugangswort tragen und sind noindex. Der Zugriff hängt am Link, nicht am Bild — eine geratene Bildkennung ergibt 404. Der Zähler hält fest, wann und womit, nie wer: Die Adresse wird mit einem Server-Geheimnis und der Link-Kennung gehasht, eine Zeile je Besucher und Stunde, gelöscht nach neunzig Tagen.',
|
||||
},
|
||||
footer: { fine: 'Eine Demoseite. Der Layout-Kern ist echt, die Bilder sind Umrisse. Gebaut von' },
|
||||
footer: {
|
||||
fine: 'Eine Demoseite. Der Layout-Kern ist echt, die Bilder sind Umrisse. Gebaut von',
|
||||
imprint: 'Impressum', privacy: 'Datenschutz',
|
||||
imprintHref: '/de/impressum/', privacyHref: '/de/datenschutz/',
|
||||
},
|
||||
};
|
||||
|
||||
export const COPY: Record<Lang, Copy> = { en: EN, de: DE };
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Imprint and privacy notice, both languages.
|
||||
*
|
||||
* The facts here are checked, not assumed: the hosting details and the
|
||||
* processing agreement come from the Art. 28 contract with Hetzner, the server
|
||||
* location from the provider's own API (Nuremberg, DE), and the "no third-party
|
||||
* requests, no cookies, no storage" statement from a measurement of the live
|
||||
* page. The German version is the authoritative one; the English one is a
|
||||
* courtesy translation.
|
||||
*
|
||||
* This is a plain notice written from verified facts, not legal advice.
|
||||
*/
|
||||
|
||||
export interface LegalBlock { h?: string; p?: string; html?: string; rows?: [string, string][] }
|
||||
export interface LegalPage { title: string; intro?: string; updated: string; blocks: LegalBlock[] }
|
||||
|
||||
const OWNER = {
|
||||
firm: 'Till Heidrich – Content Creation',
|
||||
street: 'Rotdornweg 17',
|
||||
city: '21255 Tostedt',
|
||||
country: 'Deutschland',
|
||||
phone: '0157 73953819',
|
||||
mail: 'mail@tillheidrich.de',
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ imprint */
|
||||
|
||||
export const IMPRINT_DE: LegalPage = {
|
||||
title: 'Impressum',
|
||||
updated: 'Stand: 15. September 2026',
|
||||
blocks: [
|
||||
{ h: 'Angaben gemäß § 5 DDG', rows: [
|
||||
['Anbieter', OWNER.firm],
|
||||
['Anschrift', `${OWNER.street}, ${OWNER.city}, ${OWNER.country}`],
|
||||
['Telefon', OWNER.phone],
|
||||
['E-Mail', OWNER.mail],
|
||||
] },
|
||||
{ h: 'Umsatzsteuer',
|
||||
p: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet. Eine Umsatzsteuer-Identifikationsnummer nach § 27a UStG besteht daher nicht.' },
|
||||
{ h: 'Redaktionell verantwortlich (§ 18 Abs. 2 MStV)',
|
||||
p: `Till Heidrich, ${OWNER.street}, ${OWNER.city}` },
|
||||
{ h: 'Was diese Seite ist',
|
||||
p: 'Eine Produkt- und Demoseite für Klarbild, eine quelloffene Software unter der GNU AGPL-3.0. Sie verkauft nichts, sammelt keine Anfragen und schließt keine Verträge. Der Quellcode liegt öffentlich auf GitHub.' },
|
||||
{ h: 'EU-Streitschlichtung',
|
||||
html: 'Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung bereit: <a href="https://ec.europa.eu/consumers/odr" rel="noopener">ec.europa.eu/consumers/odr</a>. Wir sind nicht bereit und nicht verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen (§ 36 VSBG).' },
|
||||
{ h: 'Haftung für Inhalte',
|
||||
p: 'Als Diensteanbieter sind wir für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich (§ 7 Abs. 1 DDG). Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich; bei Bekanntwerden entsprechender Rechtsverletzungen entfernen wir diese Inhalte umgehend.' },
|
||||
{ h: 'Haftung für Links',
|
||||
p: 'Diese Seite verlinkt auf externe Seiten Dritter, auf deren Inhalte wir keinen Einfluss haben. Für diese fremden Inhalte kann keine Gewähr übernommen werden; verantwortlich ist stets der jeweilige Anbieter oder Betreiber. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße geprüft, rechtswidrige Inhalte waren nicht erkennbar. Eine dauerhafte inhaltliche Kontrolle ohne konkrete Anhaltspunkte einer Rechtsverletzung ist nicht zumutbar; bei Bekanntwerden von Rechtsverletzungen entfernen wir solche Links umgehend.' },
|
||||
{ h: 'Urheberrecht',
|
||||
p: 'Die Software Klarbild steht unter der GNU Affero General Public License, Version 3 — sie darf unter deren Bedingungen genutzt, verändert und weitergegeben werden. Die Texte, Abbildungen und das Gestaltungsraster dieser Seite unterliegen dem deutschen Urheberrecht. Beiträge Dritter sind als solche gekennzeichnet.' },
|
||||
],
|
||||
};
|
||||
|
||||
export const IMPRINT_EN: LegalPage = {
|
||||
title: 'Imprint',
|
||||
updated: 'Last updated: 15 September 2026',
|
||||
intro: 'German law requires this notice. The German version is the authoritative one; this is a courtesy translation.',
|
||||
blocks: [
|
||||
{ h: 'Information pursuant to § 5 DDG', rows: [
|
||||
['Provider', OWNER.firm],
|
||||
['Address', `${OWNER.street}, ${OWNER.city}, Germany`],
|
||||
['Phone', OWNER.phone],
|
||||
['Email', OWNER.mail],
|
||||
] },
|
||||
{ h: 'VAT',
|
||||
p: 'Under § 19 of the German VAT Act no VAT is charged, and there is therefore no VAT identification number under § 27a.' },
|
||||
{ h: 'Responsible for editorial content (§ 18 (2) MStV)',
|
||||
p: `Till Heidrich, ${OWNER.street}, ${OWNER.city}, Germany` },
|
||||
{ h: 'What this page is',
|
||||
p: 'A product and demo page for Klarbild, open-source software under the GNU AGPL-3.0. It sells nothing, collects no enquiries and concludes no contracts. The source is public on GitHub.' },
|
||||
{ h: 'Online dispute resolution',
|
||||
html: 'The European Commission provides a platform for online dispute resolution: <a href="https://ec.europa.eu/consumers/odr" rel="noopener">ec.europa.eu/consumers/odr</a>. We are neither willing nor obliged to take part in dispute resolution proceedings before a consumer arbitration board.' },
|
||||
{ h: 'Liability for content and links',
|
||||
p: 'We are responsible for our own content under the general laws. We are not obliged to monitor third-party information we transmit or store, nor to investigate circumstances indicating unlawful activity. This page links to external sites whose content we do not control; responsibility rests with their respective operators. We remove content or links promptly once we become aware of an actual infringement.' },
|
||||
{ h: 'Copyright',
|
||||
p: 'The Klarbild software is licensed under the GNU Affero General Public License, version 3, and may be used, modified and redistributed under its terms. The text, images and layout of this page are protected by German copyright law.' },
|
||||
],
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ privacy */
|
||||
|
||||
export const PRIVACY_DE: LegalPage = {
|
||||
title: 'Datenschutzerklärung',
|
||||
updated: 'Stand: 15. September 2026',
|
||||
intro: 'Diese Seite ist eine statische Produktseite. Sie setzt keine Cookies, bindet nichts von Dritten ein, misst nichts und speichert nichts in Ihrem Browser. Was unten steht, ist nachgemessen, nicht angenommen.',
|
||||
blocks: [
|
||||
{ h: 'Verantwortlicher', rows: [
|
||||
['Name', OWNER.firm],
|
||||
['Anschrift', `${OWNER.street}, ${OWNER.city}, ${OWNER.country}`],
|
||||
['Telefon', OWNER.phone],
|
||||
['E-Mail', OWNER.mail],
|
||||
] },
|
||||
{ h: 'Was diese Seite nicht tut',
|
||||
html: 'Kein Cookie — auch kein „technisch notwendiges". Kein Analysewerkzeug, keine Reichweitenmessung, kein Pixel, kein Einwilligungsbanner, weil es nichts gibt, worin einzuwilligen wäre. Keine Schriftart, kein Skript und kein Bild von einer fremden Domain: die Schriften sind mitgeliefert, alles andere liegt auf demselben Server. Nachgeprüft am 15.09.2026 mit einem Browser, der jede ausgehende Anfrage mitschreibt — sie gingen ausnahmslos an <code>klarbild-demo.heidrich-digital.de</code>. Kein Kontaktformular, keine Anmeldung, kein Newsletter.' },
|
||||
{ h: 'Der Bogen- und der Vorlagen-Baukasten',
|
||||
p: 'Beide rechnen vollständig in Ihrem Browser. Es gibt hinter dieser Seite kein Programm, das Bilder entgegennimmt. Das PDF, das der Bogen-Baukasten anbietet, entsteht ebenfalls in Ihrem Browser und wird nirgendwohin übertragen. Ihre Eingaben — Papier, Maß, Anzahl — verlassen das Gerät nicht und werden auch dort nicht gespeichert.' },
|
||||
{ h: 'Hosting und Auftragsverarbeitung',
|
||||
html: 'Die Seite läuft auf einem Server der <strong>Hetzner Online GmbH</strong>, Industriestr. 25, 91710 Gunzenhausen. Standort des Servers ist <strong>Nürnberg, Deutschland</strong>. Mit Hetzner besteht ein <strong>Vertrag über die Auftragsverarbeitung nach Art. 28 DSGVO</strong>. Die vertraglich vereinbarte Verarbeitung findet ausschließlich in der EU beziehungsweise im EWR statt; eine Verlagerung in ein Drittland bedürfte unserer vorherigen Zustimmung. Hetzner hat einen Datenschutzbeauftragten bestellt (<code>data-protection@hetzner.com</code>) und ist verpflichtet, uns Datenschutzverletzungen unverzüglich zu melden. Die Rechenzentren sind nach ISO/IEC 27001 zertifiziert und verfügen über ein BSI-C5-Typ-2-Testat.' },
|
||||
{ h: 'Zugriffsdaten',
|
||||
html: 'Damit eine Seite ausgeliefert werden kann, muss Ihr Gerät eine Verbindung zum Server aufbauen; dabei ist Ihre IP-Adresse technisch unvermeidbar im Spiel. Der Webserver dieser Seite <strong>protokolliert sie nicht</strong>: das Zugriffsprotokoll enthält Zeitpunkt, angefragte Datei, Statuscode, übertragene Größe, Verweisseite und Browserkennung — die Adressfelder sind aus dem Protokollformat entfernt. Der vorgelagerte Reverse-Proxy verarbeitet Verbindungsdaten, soweit das für die Auslieferung und die Abwehr von Angriffen erforderlich ist. Diese Daten werden nicht ausgewertet, nicht mit anderen Daten zusammengeführt und nicht zur Identifizierung von Personen verwendet. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO; das berechtigte Interesse liegt im sicheren Betrieb der Seite.' },
|
||||
{ h: 'Links auf andere Seiten',
|
||||
p: 'Diese Seite verlinkt auf GitHub (GitHub, Inc., ein Unternehmen von Microsoft) und auf heidrich-digital.de. Solange Sie nicht klicken, entsteht keine Verbindung dorthin — es sind gewöhnliche Links, keine eingebetteten Inhalte. Sobald Sie klicken, gilt die Datenschutzerklärung der jeweiligen Seite. Bei GitHub ist dabei eine Übermittlung in die USA möglich.' },
|
||||
{ h: 'Ihre Rechte',
|
||||
p: 'Sie haben das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18), Datenübertragbarkeit (Art. 20) und Widerspruch gegen eine Verarbeitung auf Grundlage berechtigter Interessen (Art. 21). Wenden Sie sich dafür an die oben genannte Adresse. In der Praxis liegt hier allerdings nichts, wozu wir Auskunft geben könnten — es werden keine personenbezogenen Daten gespeichert, die einer Person zuzuordnen wären.' },
|
||||
{ h: 'Beschwerderecht',
|
||||
p: 'Sie können sich bei einer Datenschutz-Aufsichtsbehörde beschweren. Für uns zuständig ist die Landesbeauftragte für den Datenschutz Niedersachsen, Prinzenstraße 5, 30159 Hannover.' },
|
||||
{ h: 'Änderungen',
|
||||
p: 'Ändert sich etwas an der Seite, ändert sich diese Erklärung mit. Das Datum oben sagt, wann sie zuletzt angefasst wurde.' },
|
||||
],
|
||||
};
|
||||
|
||||
export const PRIVACY_EN: LegalPage = {
|
||||
title: 'Privacy',
|
||||
updated: 'Last updated: 15 September 2026',
|
||||
intro: 'This is a static product page. It sets no cookies, embeds nothing from anyone else, measures nothing and stores nothing in your browser. What follows was measured, not assumed. The German version is the authoritative one.',
|
||||
blocks: [
|
||||
{ h: 'Controller', rows: [
|
||||
['Name', OWNER.firm],
|
||||
['Address', `${OWNER.street}, ${OWNER.city}, Germany`],
|
||||
['Phone', OWNER.phone],
|
||||
['Email', OWNER.mail],
|
||||
] },
|
||||
{ h: 'What this page does not do',
|
||||
html: 'No cookie — not even a “strictly necessary” one. No analytics, no pixel, no consent banner, because there is nothing to consent to. No font, script or image from another domain: the fonts ship with the page and everything else sits on the same server. Checked on 15 September 2026 with a browser that records every outgoing request — all of them went to <code>klarbild-demo.heidrich-digital.de</code>. No contact form, no sign-up, no newsletter.' },
|
||||
{ h: 'The sheet and preset builders',
|
||||
p: 'Both run entirely in your browser. There is no program behind this page that accepts images. The PDF the sheet builder offers is assembled in your browser and transmitted nowhere. Your inputs — paper, size, count — never leave the device, and are not stored on it either.' },
|
||||
{ h: 'Hosting and data processing agreement',
|
||||
html: 'The page runs on a server operated by <strong>Hetzner Online GmbH</strong>, Industriestr. 25, 91710 Gunzenhausen, Germany. The server is located in <strong>Nuremberg, Germany</strong>. A <strong>data processing agreement under Art. 28 GDPR</strong> is in place with Hetzner. The processing covered by that contract takes place exclusively within the EU/EEA; moving it to a third country would require our prior consent. Hetzner has appointed a data protection officer (<code>data-protection@hetzner.com</code>) and is obliged to report data breaches to us without undue delay. The data centres are certified to ISO/IEC 27001 and hold a BSI C5 Type 2 attestation.' },
|
||||
{ h: 'Access data',
|
||||
html: 'Delivering a page requires your device to open a connection, which technically involves your IP address. This site\'s web server <strong>does not log it</strong>: the access log records the time, the file requested, the status code, the size, the referrer and the browser string — the address fields have been removed from the log format. The reverse proxy in front of it processes connection data as far as delivery and attack mitigation require. That data is not analysed, not combined with anything else and not used to identify anyone. The legal basis is Art. 6(1)(f) GDPR; the legitimate interest is operating the site securely.' },
|
||||
{ h: 'Links to other sites',
|
||||
p: 'This page links to GitHub (GitHub, Inc., a Microsoft company) and to heidrich-digital.de. Until you click, no connection is made — these are ordinary links, not embedded content. Once you click, the privacy notice of that site applies; with GitHub this may involve a transfer to the United States.' },
|
||||
{ h: 'Your rights',
|
||||
p: 'You have the right of access (Art. 15 GDPR), rectification (Art. 16), erasure (Art. 17), restriction (Art. 18), portability (Art. 20) and to object to processing based on legitimate interests (Art. 21). Write to the address above. In practice there is nothing here to give access to — no personal data attributable to a person is stored.' },
|
||||
{ h: 'Right to complain',
|
||||
p: 'You may lodge a complaint with a supervisory authority. Ours is the Data Protection Commissioner of Lower Saxony, Prinzenstraße 5, 30159 Hannover, Germany.' },
|
||||
{ h: 'Changes',
|
||||
p: 'If the page changes, this notice changes with it. The date above says when it was last touched.' },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import Legal from '../../components/Legal.astro';
|
||||
import { PRIVACY_DE } from '../../lib/legal';
|
||||
---
|
||||
<Legal page={PRIVACY_DE} lang="de"
|
||||
canonical="https://klarbild-demo.heidrich-digital.de/de/datenschutz/"
|
||||
otherHref="/privacy/" otherLabel="EN"
|
||||
backHref="/de/" backLabel="← Zurück zu Klarbild"
|
||||
siblingHref="/de/impressum/" siblingLabel="Impressum →" />
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import Legal from '../../components/Legal.astro';
|
||||
import { IMPRINT_DE } from '../../lib/legal';
|
||||
---
|
||||
<Legal page={IMPRINT_DE} lang="de"
|
||||
canonical="https://klarbild-demo.heidrich-digital.de/de/impressum/"
|
||||
otherHref="/imprint/" otherLabel="EN"
|
||||
backHref="/de/" backLabel="← Zurück zu Klarbild"
|
||||
siblingHref="/de/datenschutz/" siblingLabel="Datenschutz →" />
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import Legal from '../components/Legal.astro';
|
||||
import { IMPRINT_EN } from '../lib/legal';
|
||||
---
|
||||
<Legal page={IMPRINT_EN} lang="en"
|
||||
canonical="https://klarbild-demo.heidrich-digital.de/imprint/"
|
||||
otherHref="/de/impressum/" otherLabel="DE"
|
||||
backHref="/" backLabel="← Back to Klarbild"
|
||||
siblingHref="/privacy/" siblingLabel="Privacy →" />
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import Legal from '../components/Legal.astro';
|
||||
import { PRIVACY_EN } from '../lib/legal';
|
||||
---
|
||||
<Legal page={PRIVACY_EN} lang="en"
|
||||
canonical="https://klarbild-demo.heidrich-digital.de/privacy/"
|
||||
otherHref="/de/datenschutz/" otherLabel="DE"
|
||||
backHref="/" backLabel="← Back to Klarbild"
|
||||
siblingHref="/imprint/" siblingLabel="Imprint →" />
|
||||
@@ -245,3 +245,24 @@ footer .fine { font-size: 0.8rem; color: var(--ink-soft); margin-left: auto; }
|
||||
.mod-after { display: grid; gap: 22px; margin-top: 26px; align-items: start; }
|
||||
@media (min-width: 880px) { .mod-after { grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr); gap: 40px; } }
|
||||
.stackline { font-size: .84rem; color: var(--ink-soft); margin-top: 14px; }
|
||||
|
||||
/* ---------------------------------------------------------------- legal pages */
|
||||
|
||||
.legal { position: relative; z-index: 2; max-width: 74ch;
|
||||
padding: clamp(40px, 7vw, 76px) 0 clamp(48px, 8vw, 96px); }
|
||||
.legal h1 { font-size: clamp(2rem, 5vw, 3rem); margin-bottom: 8px; }
|
||||
.legal .lead { margin: 18px 0 0; }
|
||||
.lblock { margin-top: clamp(28px, 4vw, 40px); }
|
||||
.lblock h2 { font-size: clamp(1.1rem, 2vw, 1.3rem); margin-bottom: 10px; }
|
||||
.lblock p { color: var(--ink-soft); line-height: 1.7; margin: 0 0 .9em; }
|
||||
.lblock p:last-child { margin-bottom: 0; }
|
||||
.lblock code { background: var(--card); border: 1px solid var(--line);
|
||||
border-radius: 3px; padding: 1px 5px; font-size: .86em; }
|
||||
.lrows { display: grid; gap: 1px; margin: 0 0 14px; background: var(--line);
|
||||
border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; }
|
||||
@media (min-width: 520px) { .lrows { grid-template-columns: 12rem minmax(0, 1fr); } }
|
||||
.lrows dt { background: var(--card); padding: 9px 12px; font-family: var(--font-mono);
|
||||
font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-soft);
|
||||
display: flex; align-items: center; }
|
||||
.lrows dd { background: var(--card); padding: 9px 12px; margin: 0; font-size: .95rem; }
|
||||
.lsibling { margin-top: clamp(32px, 5vw, 48px); padding-top: 20px; border-top: 1px solid var(--line); }
|
||||
Reference in new issue
Block a user