commit 0712615dfa4e65d9f85ede70c1ea17302051e4ce Author: André Fiedler Date: Mon Sep 7 17:20:09 2026 +0200 Build responsive Buch & Bohne website from supplied design diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb84fdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +.venv/ +node_modules/ +dist/ +tmp/ +output/ +.playwright-cli/ +docs/superpowers/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..11f63de --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Buch & Bohne + +Die Website für **Buch & Bohne e. V.**, Buchladen und Lesecafé am Markt 5 in Oelsnitz. + +[Website öffnen](https://showcase.andrefiedler.de/2608_BuB/) + +Umsetzung der bereitgestellten Gestaltung mit responsivem Layout, lokal eingebundenen Schriften, Kontaktlinks und Unterstützung per Überweisung oder PayPal. + +Hinweise zur Pflege stehen in [docs/implementation.md](docs/implementation.md). diff --git a/assets/app.mjs b/assets/app.mjs new file mode 100644 index 0000000..a94d281 --- /dev/null +++ b/assets/app.mjs @@ -0,0 +1,20 @@ +const copyButton = document.querySelector('.copy-iban'); +const status = document.querySelector('#copy-status'); + +copyButton?.addEventListener('click', async () => { + status.textContent = ''; + + try { + await navigator.clipboard.writeText(copyButton.dataset.iban); + status.textContent = 'Die IBAN wurde in die Zwischenablage kopiert.'; + copyButton.title = 'IBAN kopiert'; + } catch { + const range = document.createRange(); + range.selectNodeContents(copyButton); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + status.textContent = 'Bitte kopieren Sie die markierte IBAN.'; + copyButton.title = 'Bitte die markierte IBAN kopieren'; + } +}); diff --git a/assets/apple-touch-icon.png b/assets/apple-touch-icon.png new file mode 100644 index 0000000..faecc27 Binary files /dev/null and b/assets/apple-touch-icon.png differ diff --git a/assets/background-1200.webp b/assets/background-1200.webp new file mode 100644 index 0000000..518b478 Binary files /dev/null and b/assets/background-1200.webp differ diff --git a/assets/background-2400.webp b/assets/background-2400.webp new file mode 100644 index 0000000..d76b5ab Binary files /dev/null and b/assets/background-2400.webp differ diff --git a/assets/background-mobile.webp b/assets/background-mobile.webp new file mode 100644 index 0000000..8d2a2e5 Binary files /dev/null and b/assets/background-mobile.webp differ diff --git a/assets/construction-tape.svg b/assets/construction-tape.svg new file mode 100644 index 0000000..c63f0ab --- /dev/null +++ b/assets/construction-tape.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/fonts/OFL.txt b/assets/fonts/OFL.txt new file mode 100644 index 0000000..65a3057 --- /dev/null +++ b/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/roboto-italic-latin.woff2 b/assets/fonts/roboto-italic-latin.woff2 new file mode 100644 index 0000000..20e8d25 Binary files /dev/null and b/assets/fonts/roboto-italic-latin.woff2 differ diff --git a/assets/fonts/roboto-latin.woff2 b/assets/fonts/roboto-latin.woff2 new file mode 100644 index 0000000..aa2ddfc Binary files /dev/null and b/assets/fonts/roboto-latin.woff2 differ diff --git a/assets/icon-192.png b/assets/icon-192.png new file mode 100644 index 0000000..6ed3b69 Binary files /dev/null and b/assets/icon-192.png differ diff --git a/assets/icon-512.png b/assets/icon-512.png new file mode 100644 index 0000000..228483b Binary files /dev/null and b/assets/icon-512.png differ diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 0000000..540c945 --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/paypal-qr.png b/assets/paypal-qr.png new file mode 100644 index 0000000..f339b38 Binary files /dev/null and b/assets/paypal-qr.png differ diff --git a/assets/social-preview.jpg b/assets/social-preview.jpg new file mode 100644 index 0000000..f095d34 Binary files /dev/null and b/assets/social-preview.jpg differ diff --git a/assets/styles.css b/assets/styles.css new file mode 100644 index 0000000..b91696a --- /dev/null +++ b/assets/styles.css @@ -0,0 +1,429 @@ +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url("./fonts/roboto-latin.woff2") format("woff2"); +} + +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400 700; + font-display: swap; + src: url("./fonts/roboto-italic-latin.woff2") format("woff2"); +} + +:root { + color-scheme: dark; + --color-burgundy: #982249; + --color-white: #fff; + --color-cream: #faf5eb; + --color-sand: #e5cd9c; + --color-announcement: #f2e6cc; + --color-focus: #ffc300; + /* One design unit is one point on the supplied 1200 × 900 layout. */ + --unit: 0.0833333333vw; + --font-body: Roboto, system-ui, sans-serif; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + min-inline-size: 280px; + background: var(--color-burgundy); + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; +} + +body { + position: relative; + isolation: isolate; + margin: 0; + color: var(--color-white); + font-family: var(--font-body); + font-synthesis: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body::before { + position: absolute; + z-index: -1; + inset: 0 0 auto; + min-block-size: 100svh; + block-size: calc(900 * var(--unit)); + background-image: image-set(url("./background-1200.webp") 1x, url("./background-2400.webp") 2x); + background-size: 100% auto; + background-repeat: no-repeat; + background-position: center top; + content: ""; +} + +.page { + display: flex; + flex-direction: column; + align-items: center; + min-block-size: max(100svh, calc(900 * var(--unit))); + padding: calc(142 * var(--unit)) calc(24 * var(--unit)) calc(56 * var(--unit)); + text-align: center; +} + +h1, +h2, +p, +dl, +dd { + margin: 0; +} + +img { + display: block; + max-inline-size: 100%; + block-size: auto; +} + +a { + color: inherit; + text-decoration: none; + text-underline-offset: 0.2em; +} + +a:hover, +.copy-iban:hover { + text-decoration: underline; +} + +a:focus-visible, +button:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 4px; + border-radius: 2px; +} + +strong { + font-weight: 700; +} + +.logo { + inline-size: calc(184.76 * var(--unit)); + block-size: calc(146.67 * var(--unit)); +} + +.announcement { + margin-block-start: calc(31.4 * var(--unit)); + color: var(--color-cream); + font-style: italic; + font-size: calc(15 * var(--unit)); + line-height: calc(22 * var(--unit)); + letter-spacing: 0.005em; +} + +.announcement h1 { + margin-block-end: calc(8.64 * var(--unit)); + color: var(--color-announcement); + font-size: calc(19 * var(--unit)); + font-weight: 700; + line-height: calc(23 * var(--unit)); + letter-spacing: 0.01em; +} + +.design-line { + display: block; +} + +.support { + margin-block-start: calc(18.25 * var(--unit)); + color: var(--color-cream); + font-size: calc(15 * var(--unit)); + line-height: calc(22 * var(--unit)); + font-style: italic; + letter-spacing: 0.005em; +} + +.support-title { + inline-size: calc(454.02 * var(--unit)); + margin-inline: auto; + transform: translateX(calc(-9.22 * var(--unit))); +} + +.support-title img { + inline-size: 100%; + aspect-ratio: 454.02 / 47.9; +} + +.support-options { + margin-block-start: calc(5.63 * var(--unit)); + font-weight: 700; +} + +.thanks { + margin-block-start: calc(9 * var(--unit)); +} + +.contact { + margin-block-start: calc(22.63 * var(--unit)); + font-size: calc(10 * var(--unit)); + line-height: calc(14 * var(--unit)); + letter-spacing: 0.005em; +} + +.contact h2, +.bank h2 { + color: var(--color-sand); + font-size: inherit; + font-weight: 700; + letter-spacing: 0.015em; +} + +address { + font-style: normal; +} + +.contact-person, +.contact-channels { + margin-block-start: calc(12 * var(--unit)); +} + +.phone-line, +.email-line { + display: block; +} + +.instagram { + margin-block-start: calc(10 * var(--unit)); +} + +.label { + color: var(--color-sand); +} + +.donations { + display: grid; + grid-template-columns: calc(185 * var(--unit)) calc(119 * var(--unit)); + gap: calc(24 * var(--unit)); + margin-block-start: calc(19.92 * var(--unit)); + text-align: start; + font-size: calc(10 * var(--unit)); + line-height: calc(14 * var(--unit)); + letter-spacing: 0.01em; +} + +.bank { + padding-block-start: calc(14.24 * var(--unit)); +} + +.account-holder { + margin-block-start: calc(12 * var(--unit)); +} + +.account-holder strong { + font-weight: 600; +} + +.account-details { + margin-block-start: calc(14 * var(--unit)); +} + +.account-details > div { + display: grid; + grid-template-columns: calc(29 * var(--unit)) 1fr; +} + +.copy-iban { + display: inline; + margin: 0; + border: 0; + padding: 0; + background: transparent; + color: inherit; + font: inherit; + letter-spacing: inherit; + white-space: nowrap; + cursor: copy; + user-select: all; +} + +.qr-link { + display: block; + inline-size: calc(85.54 * var(--unit)); +} + +.paypal-address { + display: inline-block; + margin-block-start: calc(10.86 * var(--unit)); + white-space: nowrap; +} + +.visually-hidden { + position: absolute; + inline-size: 1px; + block-size: 1px; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +.skip-link { + position: absolute; + z-index: 1; + inset-block-start: 1rem; + inset-inline-start: 1rem; + padding: 0.75rem 1rem; + background: var(--color-burgundy); + transform: translateY(-200%); +} + +.skip-link:focus { + transform: none; +} + +@media (width <= 1024px) { + :root { + --unit: 1px; + } + + body::before { + block-size: 100%; + background-image: url("./background-mobile.webp"); + background-size: cover; + } + + body::after { + position: absolute; + z-index: -1; + inset: 0 0 auto; + block-size: 254px; + background: url("./construction-tape.svg") center top / 1200px 254px no-repeat; + content: ""; + } + + .page { + min-block-size: 100svh; + padding: 154px 24px 48px; + } + + .logo { + inline-size: 185px; + block-size: auto; + } + + .announcement, + .support { + max-inline-size: 620px; + inline-size: 100%; + font-size: 16px; + line-height: 1.5; + } + + .announcement { + margin-block-start: 30px; + } + + .announcement h1 { + margin-block-end: 16px; + font-size: 21px; + line-height: 1.3; + } + + .support { + margin-block-start: 28px; + } + + .support-title { + max-inline-size: 100%; + transform: none; + } + + .support-options { + margin-block-start: 12px; + } + + .thanks { + margin-block-start: 14px; + } + + .contact { + margin-block-start: 30px; + font-size: 14px; + line-height: 1.6; + } + + .contact-channels a, + .instagram a { + display: inline-block; + padding-block: 4px; + } + + .donations { + grid-template-columns: minmax(0, 260px) 170px; + gap: 28px; + margin-block-start: 30px; + font-size: 14px; + line-height: 1.5; + } + + .account-details > div { + grid-template-columns: 40px 1fr; + } + + .qr-link { + inline-size: 124px; + } + + .copy-iban { + padding-block: 4px; + margin-block: -4px; + } +} + +@media (width <= 600px) { + .page { + padding-inline: 20px; + } + + .design-line { + display: inline; + } + + .mobile-phone { + display: block; + } + + .phone-separator { + display: none; + } + + .donations { + grid-template-columns: minmax(0, 1fr); + gap: 26px; + inline-size: min(100%, 288px); + } + + .bank { + padding-block-start: 0; + } + + .paypal { + justify-self: center; + text-align: center; + } + + .qr-link { + inline-size: 140px; + margin-inline: auto; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + } +} diff --git a/assets/support-heading.svg b/assets/support-heading.svg new file mode 100644 index 0000000..b15b96a --- /dev/null +++ b/assets/support-heading.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/2608_BuB_website_baustelle.idml b/design/2608_BuB_website_baustelle.idml new file mode 100644 index 0000000..ef6440c Binary files /dev/null and b/design/2608_BuB_website_baustelle.idml differ diff --git a/design/2608_BuB_website_baustelle.indd b/design/2608_BuB_website_baustelle.indd new file mode 100644 index 0000000..78ba927 Binary files /dev/null and b/design/2608_BuB_website_baustelle.indd differ diff --git a/design/2608_BuB_website_baustelle.pdf b/design/2608_BuB_website_baustelle.pdf new file mode 100644 index 0000000..61f45f4 Binary files /dev/null and b/design/2608_BuB_website_baustelle.pdf differ diff --git a/design/Links/image0.jpeg b/design/Links/image0.jpeg new file mode 100644 index 0000000..d9bd8ec Binary files /dev/null and b/design/Links/image0.jpeg differ diff --git a/design/Links/mitarbeiter_header_2.jpg b/design/Links/mitarbeiter_header_2.jpg new file mode 100644 index 0000000..80f44e1 Binary files /dev/null and b/design/Links/mitarbeiter_header_2.jpg differ diff --git a/design/Links/titelbild_sponsorenmappe_2.tif b/design/Links/titelbild_sponsorenmappe_2.tif new file mode 100644 index 0000000..056c69d Binary files /dev/null and b/design/Links/titelbild_sponsorenmappe_2.tif differ diff --git a/docs/implementation.md b/docs/implementation.md new file mode 100644 index 0000000..9b32189 --- /dev/null +++ b/docs/implementation.md @@ -0,0 +1,28 @@ +# Implementation and maintenance + +The supplied 1200 × 900 PDF and its IDML source define the desktop composition. All original design files are preserved under `design/`. The photographic backdrop and construction tape are extracted together to preserve the print export's crop, tint and shadows. The logo and Lumios Marker heading retain their original outlines as SVG artwork; all other content is semantic, selectable HTML in `index.html`. + +CSS lives in `assets/styles.css`; all colors are CSS custom properties. Layout scales with the design above 1024 px. At smaller sizes the type becomes larger and wraps; at 600 px the bank and PayPal details stack in their original reading order. The mobile background separates the supplied tape from the photograph so that it remains above the logo while the photo fills a tall page. `assets/app.mjs` only enhances the IBAN with clipboard support and an accessible selection fallback. Contact and payment links work without JavaScript. + +## Preview and deployment + +Requires Node 20.11 or newer. There are no npm dependencies and no third-party page requests. + +```sh +npm run build +python3 -m http.server 8080 --directory dist --bind 127.0.0.1 +``` + +`scripts/build.mjs` copies only explicit public files to `dist/`. Source PDFs, InDesign files, original TIFFs, scripts and documentation are never in the public artifact. `showcase.json` selects `dist` for the existing organization deployer. A push to `showcase/2608_BuB` on `main` deploys to `https://showcase.andrefiedler.de/2608_BuB/`; the `showcase/deploy` commit status reports completion. + +## Artwork and fonts + +Generated web assets are committed. Recreate them only when the design changes, using `scripts/prepare-assets.py` with PyMuPDF, Pillow, fontTools (WOFF support) and Poppler's `pdftoppm`. Its PDF object/group assertions deliberately target this exact supplied design. The script downloads the matching [Roboto 3.015 release](https://github.com/googlefonts/roboto-3-classic/releases/tag/v3.015), fixes the width axis to 100, keeps weights 400–700, and subsets Latin and punctuation. The accompanying [OFL license](../assets/fonts/OFL.txt) is bundled. The commercial handwritten font is not distributed; its original supplied heading is retained as outlined artwork. + +Favicon SVG, ICO, Apple touch icon and manifest icons are derived from the original book-and-cup symbol. The PayPal QR code is extracted from the supplied PDF and reduced to a 64-color PNG; its encoded payload and geometry are preserved and verified by decoding. The social preview is a crop of the supplied design. + +## Search metadata + +The document contains German title/description/author information, canonical and social metadata, a local social image, and JSON-LD for the business, website and page. Only facts present in the design are used; no opening hours, ratings or registration details are invented. The canonical and all structured URLs use the requested showcase path. Update these together with `sitemap.xml`, `robots.txt` and the manifest ID when moving to a different domain. + +The single canonical page is listed in `sitemap.xml`. Submit `https://showcase.andrefiedler.de/2608_BuB/sitemap.xml` in the site's search-engine webmaster accounts when available. The local `robots.txt` is supplied for hosting completeness, but crawlers only use `/robots.txt` at the domain root. The shared host currently has no root robots or sitemap; this permits crawling and does not automatically advertise this sitemap. A host administrator can add this sitemap URL to the shared root robots file. Search indexing itself is outside the deployment workflow. diff --git a/docs/verification.md b/docs/verification.md new file mode 100644 index 0000000..d1a2518 --- /dev/null +++ b/docs/verification.md @@ -0,0 +1,15 @@ +# Verification + +Local checks on 2026-09-07: + +- Compared the rendered page against the supplied PDF at 1200 × 900, retaining artwork positions and correcting body text baselines. Inspected 1800 px desktop and 320, 390 and 768 px mobile/tablet screenshots. +- Browser checks at 320, 390, 600, 768, 1024, 1200 and 1800 px: document width equals viewport width; all images load. +- No failed or third-party resource requests in the completed page. +- Clipboard action copies exactly `DE04870958245044385904` and announces success through the live status region. +- Keyboard navigation begins with the visible-on-focus skip link. +- With JavaScript disabled, the page content and seven native links remain available without overflow at 390 px. +- macOS Vision decodes the optimized PayPal QR as the original managed PayPal QR URL: `https://www.paypal.com/qrcodes/managed/d49af904-76e1-4181-9e35-064be97b4f66?utm_source=payandgetpaid`. +- Syntax checks pass for both `.mjs` files. Local asset references, unique IDs, fragment links, JSON-LD, sitemap XML, manifest icons and production-file exclusions pass. +- `npm run build` produces an approximately 807 kB static artifact, including social-preview and icon assets that are not requested during normal page rendering. + +For each published revision, check the `showcase/deploy` status on its Gitea commit and compare the served files with `dist`. The repository is public by request. The organization listing is public so anonymous visitors can access it; the existing `hello-world` and `vite-example` repositories retain their private visibility. diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..1c992cb Binary files /dev/null and b/favicon.ico differ diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..264201f --- /dev/null +++ b/favicon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..0f65206 --- /dev/null +++ b/index.html @@ -0,0 +1,150 @@ + + + + + + Buch & Bohne e. V. – Buchladen & Lesecafé in Oelsnitz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+

Aktuell überarbeiten wir unsere Website.

+

+ Bleiben Sie gespannt. Und schauen Sie in der Zwischenzeit + gerne bei uns im Buchladen und Lesecafé + in Oelsnitz am Markt 5 vorbei. +

+
+ +
+

+ Werden Sie ein Teil von „Buch & Bohne“! + +

+

+ Ob als Vereinsmitglied, Kuchen- oder Kaffeepate + oder durch eine finanzielle Unterstützung – jeder Beitrag zählt. + … und macht unser Café möglich. +

+

Vielen Dank für Ihre Unterstützung!

+
+ +
+

Ihre Ansprechpartnerin

+
+

Lilly Möbius, Buch & Bohne e. V.

+

+ Telefon 037421 23633 . mobil 0173 8022126 + +

+

Instagram: @buch_und_bohne_

+
+
+ +
+
+

Kontodaten

+ + +
+ +
+

+
+ + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..40061f8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15 @@ +{ + "name": "buch-und-bohne", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "buch-und-bohne", + "version": "1.0.0", + "engines": { + "node": ">=20.11" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d40e078 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "buch-und-bohne", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "Buch & Bohne e. V. – Buchladen und Lesecafé in Oelsnitz", + "scripts": {"build": "node scripts/build.mjs"}, + "engines": {"node": ">=20.11"} +} diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..9dd819a --- /dev/null +++ b/robots.txt @@ -0,0 +1,6 @@ +# For standalone hosting, serve this file at the origin's /robots.txt. +# The showcase site is in a subdirectory; its crawling policy is set by the host. +User-agent: * +Allow: /2608_BuB/ + +Sitemap: https://showcase.andrefiedler.de/2608_BuB/sitemap.xml diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..77d171e --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,13 @@ +import { cp, mkdir, rm } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +const root = new URL('../', import.meta.url); +const output = new URL('dist/', root); +const publicFiles = ['index.html', 'assets', 'favicon.ico', 'favicon.svg', 'sitemap.xml', 'robots.txt', 'site.webmanifest']; + +await rm(output, { recursive: true, force: true }); +await mkdir(output, { recursive: true }); +for (const filename of publicFiles) { + await cp(new URL(filename, root), new URL(filename, output), { recursive: true }); +} +console.log(`Static site built at ${fileURLToPath(output)}`); diff --git a/scripts/prepare-assets.py b/scripts/prepare-assets.py new file mode 100644 index 0000000..668f452 --- /dev/null +++ b/scripts/prepare-assets.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Derive web artwork from the supplied PDF; never redraw the brand assets. + +Development only: uv run --with pymupdf --with pillow --with 'fonttools[woff]' +scripts/prepare-assets.py. Generated assets are committed; deployment needs only Node. +""" +from copy import deepcopy +from io import BytesIO +from pathlib import Path +import re +import subprocess +import urllib.request +import xml.etree.ElementTree as ET +from zipfile import ZipFile + +import pymupdf +from PIL import Image +from fontTools import subset +from fontTools.ttLib import TTFont +from fontTools.varLib.instancer import instantiateVariableFont + +ROOT = Path(__file__).resolve().parents[1] +ASSETS = ROOT / 'assets' +FONTS = ASSETS / 'fonts' +TMP = ROOT / 'tmp' / 'assets' +for directory in [ASSETS, FONTS, TMP]: + directory.mkdir(parents=True, exist_ok=True) + +SVG = 'http://www.w3.org/2000/svg' +XLINK = 'http://www.w3.org/1999/xlink' +ET.register_namespace('', SVG) +ET.register_namespace('xlink', XLINK) +pdf = pymupdf.open(ROOT / 'design/2608_BuB_website_baustelle.pdf') +page = pdf[0] +source = ET.fromstring(page.get_svg_image(text_as_path=True)) +definitions = {element.get('id'): element for element in source.iter() if element.get('id')} + + +def artwork(elements, viewbox): + root = ET.Element(f'{{{SVG}}}svg', {'viewBox': ' '.join(map(str, viewbox))}) + defs = ET.SubElement(root, f'{{{SVG}}}defs') + required = set() + + def dependencies(element): + text = ET.tostring(element, encoding='unicode') + refs = re.findall(r'url\(#([^)]*)\)', text) + refs += re.findall(r'(?:xlink:)?href="#([^"]*)"', text) + for ref in refs: + if ref not in required: + required.add(ref) + dependencies(definitions[ref]) + + for element in elements: + dependencies(element) + for key, value in definitions.items(): + if key in required: + defs.append(deepcopy(value)) + root.extend(deepcopy(element) for element in elements) + return ET.tostring(root, encoding='unicode') + + +def raster(svg, width): + document = pymupdf.open(stream=svg.encode(), filetype='svg') + vector = pymupdf.open('pdf', document.convert_to_pdf()) + pix = vector[0].get_pixmap(matrix=pymupdf.Matrix(width / vector[0].rect.width, + width / vector[0].rect.width), alpha=True) + return Image.open(BytesIO(pix.tobytes('png'))) + + +# These drawing groups are verified against the supplied one-page PDF. +# The print export flattens photos, tint and tape shadows into clipped tiles. +# Remove only text, wholly enclosed logo/brush vectors, and the QR resource. +# Poppler preserves the original print color conversion and clipping exactly. +assert len(source[50]) == 6 and source[504].get('data-text') == 'W' +background_pdf = pymupdf.open(ROOT / 'design/2608_BuB_website_baustelle.pdf') +background_page = background_pdf[0] +background_page.add_redact_annot(background_page.rect, fill=False, cross_out=False) +background_page.apply_redactions(images=0, graphics=0, text=0) +for bounds in [(506, 141, 694, 290), (363, 435, 819, 485)]: + background_page.add_redact_annot(pymupdf.Rect(*bounds), fill=False, cross_out=False) +background_page.apply_redactions(images=0, graphics=1, text=1) +background_page.delete_image(48) +background_pdf.save(TMP / 'background.pdf') +subprocess.run(['pdftoppm', '-scale-to', '2400', '-singlefile', '-png', + str(TMP / 'background.pdf'), str(TMP / 'background')], check=True) +background = Image.open(TMP / 'background.png').convert('RGB') +for width in [1200, 2400]: + background.resize((width, width * 3 // 4), Image.Resampling.LANCZOS).save( + ASSETS / f'background-{width}.webp', quality=91, method=6) + +# On tall screens the photo fills the page and the tape stays above the logo. +background.crop((0, 340, 2400, 1800)).resize((1200, 730), Image.Resampling.LANCZOS).save( + ASSETS / 'background-mobile.webp', quality=89, method=6) +tape = artwork([*list(source)[12:24], *list(source)[41:50]], (0, 0, 1200, 254)) +tape = tape.replace('#fdc200', '#fbbb0c').replace('#020102', '#0c0c0c') +(ASSETS / 'construction-tape.svg').write_text(tape) + +logo_paths = list(source[50]) +logo = artwork(logo_paths, (507.62, 142, 184.76, 146.67)) +(ASSETS / 'logo.svg').write_text(logo) +heading_glyphs = [element for element in source + if element.get(f'{{{XLINK}}}href', '').startswith('#font_63_')] +# Match the PDF's sRGB print rendering, as used for the photographic backdrop. +for element in source[503].iter(): + if element.get('fill'): + element.set('fill', '#ef3a57') +heading = artwork([source[503], *heading_glyphs], (363.77, 435.92, 454.02, 47.90)) +(ASSETS / 'support-heading.svg').write_text(heading) + +qr = pdf.extract_image(48) +Image.open(BytesIO(qr['image'])).convert('RGB').quantize(colors=64).save(ASSETS / 'paypal-qr.png', optimize=True) + +mark = artwork(logo_paths[:4], (600, 136, 99, 94)) +mark_root = ET.fromstring(mark) +mark_root.set('x', '12') +mark_root.set('y', '12') +mark_root.set('width', '104') +mark_root.set('height', '104') +icon = ET.Element(f'{{{SVG}}}svg', {'viewBox': '0 0 128 128'}) +ET.SubElement(icon, f'{{{SVG}}}rect', {'width': '128', 'height': '128', 'rx': '20', 'fill': '#982249'}) +icon.append(mark_root) +icon_svg = ET.tostring(icon, encoding='unicode') +(ROOT / 'favicon.svg').write_text(icon_svg) +icon_image = raster(icon_svg, 512) +for filename, size in [('apple-touch-icon.png', 180), ('icon-192.png', 192), ('icon-512.png', 512)]: + icon_image.resize((size, size), Image.Resampling.LANCZOS).save(ASSETS / filename, optimize=True) +icon_image.save(ROOT / 'favicon.ico', sizes=[(16, 16), (32, 32), (48, 48)]) + +preview = page.get_pixmap(clip=pymupdf.Rect(0, 100, 1200, 730), alpha=False) +Image.open(BytesIO(preview.tobytes('png'))).save(ASSETS / 'social-preview.jpg', quality=90, optimize=True) + +archive = ROOT / 'tmp/fonts/Roboto_v3.015.zip' +archive.parent.mkdir(parents=True, exist_ok=True) +if not archive.exists(): + urllib.request.urlretrieve('https://github.com/googlefonts/roboto-3-classic/releases/download/v3.015/Roboto_v3.015.zip', archive) +with ZipFile(archive) as zipped: + for name, filename in [('Roboto', 'roboto-latin.woff2'), ('Roboto-Italic', 'roboto-italic-latin.woff2')]: + font = TTFont(BytesIO(zipped.read(f'web/split/{name}[wdth,wght].ttf'))) + options = subset.Options() + options.flavor = 'woff2' + subsetter = subset.Subsetter(options=options) + subsetter.populate(unicodes=[*range(0x20, 0x100), *range(0x2000, 0x2070), 0x20AC, 0xFFFD]) + subsetter.subset(font) + font = instantiateVariableFont(font, {'wdth': 100, 'wght': (400, 700)}, inplace=True) + font.flavor = 'woff2' + font.save(FONTS / filename) +urllib.request.urlretrieve('https://raw.githubusercontent.com/googlefonts/roboto-3-classic/v3.015/OFL.txt', FONTS / 'OFL.txt') +license_file = FONTS / 'OFL.txt' +license_file.write_text('\n'.join(line.rstrip() for line in license_file.read_text().splitlines()) + '\n') +print('Prepared original artwork, QR code, logo icons and local Roboto 3.015 fonts.') diff --git a/showcase.json b/showcase.json new file mode 100644 index 0000000..6bce7fe --- /dev/null +++ b/showcase.json @@ -0,0 +1,3 @@ +{ + "publish": "dist" +} diff --git a/site.webmanifest b/site.webmanifest new file mode 100644 index 0000000..331bad2 --- /dev/null +++ b/site.webmanifest @@ -0,0 +1,17 @@ +{ + "id": "/2608_BuB/", + "name": "Buch & Bohne e. V. – Buchladen & Lesecafé", + "short_name": "Buch & Bohne", + "description": "Buchladen und Lesecafé am Markt 5 in Oelsnitz.", + "lang": "de", + "dir": "ltr", + "start_url": "./", + "scope": "./", + "display": "browser", + "background_color": "#982249", + "theme_color": "#982249", + "icons": [ + {"src": "./assets/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any"}, + {"src": "./assets/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any"} + ] +} diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..a506263 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,7 @@ + + + + https://showcase.andrefiedler.de/2608_BuB/ + 2026-09-07 + +