Build responsive Buch & Bohne website from supplied design
showcase/deploy Live at https://showcase.andrefiedler.de/2608_BuB/

This commit is contained in:
André Fiedler
2026-09-07 17:20:09 +02:00
commit 0712615dfa
37 changed files with 1077 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
.DS_Store
.venv/
node_modules/
dist/
tmp/
output/
.playwright-cli/
docs/superpowers/
+9
View File
@@ -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).
+20
View File
@@ -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';
}
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+27
View File
@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 254"><defs><clipPath id="clip_53">
<path transform="matrix(1,0,0,-1,0,900)" d="M452.348 900 1200 646.206V671.203L525.985 900Z" />
</clipPath>
</defs><path transform="matrix(1,0,0,-1,0,134.74762)" d="M0 0V-23.7L1200 36.564V60.264Z" fill="#fbbb0c" />
<path transform="matrix(1,0,0,-1,0,134.74799)" d="M0 0V-2.6L23.626-22.513 78.499-19.757 51.961 2.609Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,105.8332,129.43299)" d="M0 0 26.538-22.367 81.411-19.611 54.872 2.756Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,214.5783,123.97168)" d="M0 0 26.538-22.367 81.41-19.611 54.872 2.756Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,323.3234,118.510379)" d="M0 0 26.538-22.367 81.41-19.611 54.872 2.756Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,432.0677,113.049198)" d="M0 0 26.538-22.367 81.411-19.611 54.872 2.756Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,540.812,107.58801)" d="M0 0 26.538-22.367 81.411-19.611 54.873 2.756Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,649.5579,102.12671)" d="M0 0 26.538-22.367 81.41-19.611 54.872 2.756Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,758.3023,96.66553)" d="M0 0 26.538-22.367 81.411-19.611 54.873 2.756Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,867.0474,91.20422)" d="M0 0 26.538-22.367 81.411-19.611 54.872 2.756Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,975.7925,85.74292)" d="M0 0 26.538-22.367 81.41-19.611 54.872 2.756Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,1084.5376,80.28168)" d="M0 0 26.538-22.367 81.41-19.611 54.872 2.756Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,452.3481,0)" d="M0 0 747.652-253.794V-228.797L73.637 0Z" fill="#fbbb0c" />
<path transform="matrix(1,0,0,-1,490.7219,0)" d="M0 0 8.561-15.932 60.587-33.593 44.16-3.02 35.263 0Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,602.3868,50.931276)" d="M0 0 52.026-17.661 35.599 12.912-16.427 30.573Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,705.4904,85.9303)" d="M0 0 52.026-17.66 35.599 12.912-16.428 30.573Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,808.5939,120.9292)" d="M0 0 52.026-17.661 35.599 12.912-16.427 30.573Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,911.6972,155.9281)" d="M0 0 52.026-17.66 35.599 12.912-16.428 30.573Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,1014.801,190.92713)" d="M0 0 52.027-17.661 35.599 12.912-16.427 30.573Z" fill="#0c0c0c" />
<path transform="matrix(1,0,0,-1,1117.9052,225.92627)" d="M0 0 52.026-17.661 35.599 12.913-16.427 30.573Z" fill="#0c0c0c" />
<g clip-path="url(#clip_53)">
<path transform="matrix(1,0,0,-1,1231.2191,200.76282)" d="M0 0-33.007-17.735 18.529-113.649 51.536-95.913Z" fill="#0c0c0c" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+93
View File
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="507.62 142 184.76 146.67"><defs /><path transform="matrix(1,0,0,-1,673.2455,163.39783)" d="M0 0-58.484-28.539-58.695-28.588C-58.873-28.588-59.045-28.488-59.128-28.317-59.245-28.078-59.146-27.789-58.906-27.673L-.665 .748C-1.138 1.331-1.552 1.966-1.891 2.656L-62.216-26.782-62.427-26.831C-62.605-26.831-62.776-26.731-62.86-26.56-62.977-26.321-62.877-26.032-62.638-25.916L-2.271 3.542C-2.881 5.155-3.07 6.843-2.861 8.573-3.003 8.502-64.139-21.618-64.139-21.618-64.139-21.618-66.397-22.871-66.675-24.695-67.084-28.322-64.862-31.402-62.74-32.009-61.476-32.362-60.029-32.143-59.354-31.862-53.141-29.084 2.013-1.609 2.013-1.609 1.275-1.146 .6-.608 0 0" fill="#ffffff" />
<path transform="matrix(1,0,0,-1,691.2115,145.61573)" d="M0 0C-.231 1.23-.231 2.412-.018 3.616L-.166 3.603C-.166 3.603-36.995-2.646-40.247-3.205-48.058-4.547-55.871-5.887-63.681-7.234-65.557-7.558-66.121-8.057-66.418-9.606-66.743-11.296-66.077-13.031-64.802-13.82-64.122-14.241-63.402-14.218-62.646-14.086-54.89-12.734-47.131-11.393-39.372-10.053-36.123-9.493-2.544-3.798 .708-3.244L1.168-3.165C.612-2.189 .212-1.131 0 0" fill="#ffffff" />
<path transform="matrix(1,0,0,-1,669.8396,180.38739)" d="M0 0C-.172 .802-.236 1.548-.213 2.348-.314 2.318-.466 2.277-.466 2.277-.466 2.277-16.668-2.962-19.806-3.982-27.343-6.431-34.883-8.879-42.419-11.332-44.229-11.922-44.716-12.496-44.788-14.072-44.867-15.791-43.959-17.413-42.583-18.011-41.85-18.33-41.141-18.204-40.412-17.965-32.93-15.514-25.443-13.073-17.958-10.634-14.822-9.614-1.687-5.359 1.451-4.344L1.966-4.177C1.012-2.95 .331-1.547 0 0" fill="#ffffff" />
<path transform="matrix(1,0,0,-1,679.3229,190.67407)" d="M0 0C-2.351 1.469-5.031 .807-7.477-1.216-7.763-.268-8.071 .714-8.399 1.725-8.545 2.174-9.043 2.414-9.511 2.262L-46.892-9.884C-47.36-10.036-47.622-10.523-47.476-10.971-45.313-17.627-43.161-22.758-41.499-24.802-38.47-28.527-34.62-30.583-29.994-30.967L-29.952-31.14-29.867-31.401C-29.638-32.104-28.884-32.488-28.181-32.26L-11.496-26.838C-10.793-26.61-10.409-25.856-10.637-25.153L-10.722-24.892-10.789-24.727C-8.842-23.05-7.391-21.097-6.438-18.875-3.816-18.367-1.307-16.342 .461-13.513 3.003-9.444 4.368-2.729 0 0M-1.726-12.213C-2.756-13.861-4.091-15.118-5.476-15.81-5.285-14.902-5.159-13.959-5.105-12.977-5.005-11.144-5.595-7.958-6.692-3.943-4.856-2.088-2.914-1.283-1.372-2.246 1.306-3.919 .196-9.137-1.726-12.213" fill="#ffffff" />
<path transform="matrix(1,0,0,-1,652.7086,272.5348)" d="M0 0C0 6.153-3.782 10.036-11.902 10.036-21.03 10.036-25.417 4.993-25.417-2.975-25.417-11.902-20.374-16.138-11.397-16.138-8.019-16.138-4.539-15.482-2.017-14.121L-1.664-7.968-1.866-7.817C-3.782-8.926-6.758-9.733-9.128-9.733-12.456-9.733-14.02-8.624-14.373-4.942H-.404C-.151-3.228 0-1.664 0 0M-9.884-.958H-14.373C-14.221 4.388-13.516 6.001-11.952 6.001-10.389 6.001-9.683 4.135-9.884-.958M-38.63-15.684H-27.081C-27.434-12.356-27.535-9.834-27.535-5.749V.151C-27.535 7.161-29.704 9.935-34.848 9.935-38.025 9.935-40.95 8.573-43.018 6.203L-43.421 9.531H-54.264C-53.961 6.102-53.81 2.875-53.81-.807V-4.69C-53.81-9.128-53.961-12.356-54.314-15.684H-42.665V2.774C-42.009 3.228-41.404 3.429-40.748 3.429-39.235 3.429-38.63 2.37-38.63-1.009ZM-46.749 21.34H-35.15C-35.503 24.921-35.654 27.392-35.654 31.729V37.377C-35.654 44.185-37.924 46.909-43.017 46.909-46.094 46.909-48.716 45.698-50.733 43.631V55.078H-62.584C-62.03 48.724-61.828 43.832-61.828 38.033-61.828 32.132-62.08 26.635-62.584 21.29H-50.733V39.697C-50.178 40.1-49.472 40.403-48.867 40.403-47.304 40.403-46.749 39.394-46.749 35.965ZM-64.45 21.996-64.097 27.745-64.198 27.846C-65.711 27.241-67.022 26.938-68.333 26.938-71.157 26.938-72.771 28.753-72.771 34.755-72.771 40-71.359 41.613-68.686 41.613-67.325 41.613-66.013 41.26-64.601 40.554L-64.5 40.605-64.702 45.9C-66.719 46.556-68.787 46.959-70.905 46.959-79.68 46.959-84.118 42.521-84.118 33.746-84.118 25.677-80.336 20.735-71.309 20.735-69.039 20.735-66.366 21.239-64.45 21.996M-85.782-2.875C-85.782 5.346-90.674 10.036-99.6 10.036-108.476 10.036-113.57 5.346-113.57-2.875-113.57-11.246-108.476-16.138-99.6-16.138-90.725-16.138-85.782-11.196-85.782-2.875M-97.23-3.127C-97.23-9.834-97.886-12.255-99.651-12.255-101.466-12.255-102.122-9.935-102.122-3.177-102.122 3.732-101.567 6.102-99.651 6.102-97.886 6.102-97.23 3.681-97.23-3.127M-97.18 21.29H-85.883C-86.438 24.719-86.64 28.199-86.64 31.628V46.505H-98.239C-97.835 43.227-97.734 39.899-97.734 36.621V28.501C-98.339 27.846-98.945 27.493-99.651 27.493-101.113 27.493-101.769 28.451-101.769 31.729V46.505H-113.368C-112.964 43.378-112.914 40.201-112.914 36.066V30.367C-112.914 23.105-110.796 20.987-105.702 20.987-102.122 20.987-99.449 22.349-97.482 25.122ZM-115.233-6.405C-115.233-1.715-117.956 1.261-123.706 2.118V2.269C-118.41 3.026-116.04 5.547-116.04 9.128-116.04 13.869-119.873 17.096-129.001 17.096H-144.887C-144.382 12.154-144.231 7.06-144.231 1.614-144.231-4.438-144.382-10.641-145.088-15.684H-128.345C-120.075-15.684-115.233-12.406-115.233-6.405M-127.79 7.918C-127.79 4.993-128.95 3.681-131.875 3.681H-132.581V12.507H-132.027C-129.203 12.507-127.79 11.145-127.79 7.918M-127.236-5.547C-127.236-9.38-128.799-10.943-131.976-10.943H-132.581V.101H-131.825C-128.749 .101-127.236-1.715-127.236-5.547M-123.706 39.092V39.243C-118.41 40-116.04 42.521-116.04 46.102-116.04 50.842-119.873 54.07-129.001 54.07H-144.887C-144.382 49.128-144.231 44.034-144.231 38.588-144.231 32.536-144.382 26.333-145.088 21.29H-128.345C-120.075 21.29-115.233 24.568-115.233 30.569-115.233 35.259-117.956 38.234-123.706 39.092M-127.79 44.891C-127.79 41.966-128.95 40.655-131.875 40.655H-132.581V49.481H-132.027C-129.203 49.481-127.79 48.119-127.79 44.891M-127.236 31.426C-127.236 27.594-128.799 26.03-131.976 26.03H-132.581V37.075H-131.825C-128.749 37.075-127.236 35.259-127.236 31.426M-83.967 18.105C-83.412 11.75-83.211 6.859-83.211 1.059-83.211-4.841-83.463-10.338-83.967-15.684H-72.116V2.723C-71.561 3.127-70.855 3.429-70.25 3.429-68.686 3.429-68.132 2.421-68.132-1.009V-15.634H-56.533C-56.886-12.053-57.037-9.582-57.037-5.245V.403C-57.037 7.212-59.306 9.935-64.4 9.935-67.476 9.935-70.098 8.725-72.116 6.657V18.105Z" fill="#ffffff" />
<path transform="matrix(1,0,0,-1,642.92,261.54908)" d="M0 0 6.872 4.635 6.857 4.748C4.045 5.734 1.176 6.804-1.622 7.861-.93 12.928-2.206 16.729-4.408 19.721L-9.004 16.622C-6.791 14.25-5.07 11.608-4.533 8.903L-15.5 12.976C-12.896 16.573-12.152 19.835-14.142 22.786-16.503 26.285-20.651 26.615-25.12 23.601-30.094 20.245-30.892 16.518-28.873 13.525-27.394 11.332-24.918 9.752-21.639 8.529-24.337 4.071-24.68-.331-22.206-3.999-19.05-8.678-13.783-9.848-8.302-6.152-5.73-4.417-3.953-1.869-2.983 1.239ZM-18.084 .621C-19.506 2.729-19.872 5.181-18.87 7.697-15.012 6.251-10.461 4.414-5.513 2.354-6.239 .883-7.291-.195-8.724-1.161-12.139-3.465-15.838-2.709-18.084 .621M-24.056 17.387C-25.109 18.947-24.932 20.231-23.414 21.255-21.77 22.364-19.891 21.669-18.554 19.687-17.332 17.875-16.911 15.705-17.999 13.499-20.924 14.47-22.919 15.701-24.056 17.387" fill="#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

+429
View File
@@ -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;
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.
+28
View File
@@ -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.
+15
View File
@@ -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.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><rect width="128" height="128" rx="20" fill="#982249" /><svg viewBox="600 136 99 94" x="12" y="12" width="104" height="104"><defs /><path transform="matrix(1,0,0,-1,673.2455,163.39783)" d="M0 0-58.484-28.539-58.695-28.588C-58.873-28.588-59.045-28.488-59.128-28.317-59.245-28.078-59.146-27.789-58.906-27.673L-.665 .748C-1.138 1.331-1.552 1.966-1.891 2.656L-62.216-26.782-62.427-26.831C-62.605-26.831-62.776-26.731-62.86-26.56-62.977-26.321-62.877-26.032-62.638-25.916L-2.271 3.542C-2.881 5.155-3.07 6.843-2.861 8.573-3.003 8.502-64.139-21.618-64.139-21.618-64.139-21.618-66.397-22.871-66.675-24.695-67.084-28.322-64.862-31.402-62.74-32.009-61.476-32.362-60.029-32.143-59.354-31.862-53.141-29.084 2.013-1.609 2.013-1.609 1.275-1.146 .6-.608 0 0" fill="#ffffff" />
<path transform="matrix(1,0,0,-1,691.2115,145.61573)" d="M0 0C-.231 1.23-.231 2.412-.018 3.616L-.166 3.603C-.166 3.603-36.995-2.646-40.247-3.205-48.058-4.547-55.871-5.887-63.681-7.234-65.557-7.558-66.121-8.057-66.418-9.606-66.743-11.296-66.077-13.031-64.802-13.82-64.122-14.241-63.402-14.218-62.646-14.086-54.89-12.734-47.131-11.393-39.372-10.053-36.123-9.493-2.544-3.798 .708-3.244L1.168-3.165C.612-2.189 .212-1.131 0 0" fill="#ffffff" />
<path transform="matrix(1,0,0,-1,669.8396,180.38739)" d="M0 0C-.172 .802-.236 1.548-.213 2.348-.314 2.318-.466 2.277-.466 2.277-.466 2.277-16.668-2.962-19.806-3.982-27.343-6.431-34.883-8.879-42.419-11.332-44.229-11.922-44.716-12.496-44.788-14.072-44.867-15.791-43.959-17.413-42.583-18.011-41.85-18.33-41.141-18.204-40.412-17.965-32.93-15.514-25.443-13.073-17.958-10.634-14.822-9.614-1.687-5.359 1.451-4.344L1.966-4.177C1.012-2.95 .331-1.547 0 0" fill="#ffffff" />
<path transform="matrix(1,0,0,-1,679.3229,190.67407)" d="M0 0C-2.351 1.469-5.031 .807-7.477-1.216-7.763-.268-8.071 .714-8.399 1.725-8.545 2.174-9.043 2.414-9.511 2.262L-46.892-9.884C-47.36-10.036-47.622-10.523-47.476-10.971-45.313-17.627-43.161-22.758-41.499-24.802-38.47-28.527-34.62-30.583-29.994-30.967L-29.952-31.14-29.867-31.401C-29.638-32.104-28.884-32.488-28.181-32.26L-11.496-26.838C-10.793-26.61-10.409-25.856-10.637-25.153L-10.722-24.892-10.789-24.727C-8.842-23.05-7.391-21.097-6.438-18.875-3.816-18.367-1.307-16.342 .461-13.513 3.003-9.444 4.368-2.729 0 0M-1.726-12.213C-2.756-13.861-4.091-15.118-5.476-15.81-5.285-14.902-5.159-13.959-5.105-12.977-5.005-11.144-5.595-7.958-6.692-3.943-4.856-2.088-2.914-1.283-1.372-2.246 1.306-3.919 .196-9.137-1.726-12.213" fill="#ffffff" />
</svg></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+150
View File
@@ -0,0 +1,150 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Buch &amp; Bohne e. V. – Buchladen &amp; Lesecafé in Oelsnitz</title>
<meta name="description" content="Buch &amp; Bohne e. V.: Ihr Buchladen und Lesecafé am Markt 5 in Oelsnitz. Unsere Website wird überarbeitet – besuchen und unterstützen Sie unser Café!">
<meta name="author" content="Buch &amp; Bohne e. V.">
<meta name="copyright" content="Buch &amp; Bohne e. V.">
<meta name="robots" content="index, follow, max-image-preview:large">
<meta name="theme-color" content="#982249">
<meta name="color-scheme" content="dark">
<meta name="format-detection" content="telephone=no">
<link rel="canonical" href="https://showcase.andrefiedler.de/2608_BuB/">
<link rel="sitemap" type="application/xml" href="./sitemap.xml">
<meta property="og:type" content="website">
<meta property="og:locale" content="de_DE">
<meta property="og:site_name" content="Buch &amp; Bohne e. V.">
<meta property="og:title" content="Buch &amp; Bohne – Buchladen &amp; Lesecafé in Oelsnitz">
<meta property="og:description" content="Unsere Website wird überarbeitet. Besuchen Sie uns am Markt 5 in Oelsnitz – und werden Sie ein Teil von Buch &amp; Bohne!">
<meta property="og:url" content="https://showcase.andrefiedler.de/2608_BuB/">
<meta property="og:image" content="https://showcase.andrefiedler.de/2608_BuB/assets/social-preview.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:type" content="image/jpeg">
<meta property="og:image:alt" content="Buch &amp; Bohne: Unsere Website wird überarbeitet. Werden Sie Teil unseres Buchladens und Lesecafés!">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Buch &amp; Bohne – Buchladen &amp; Lesecafé in Oelsnitz">
<meta name="twitter:description" content="Besuchen und unterstützen Sie unser Lesecafé am Markt 5 in Oelsnitz.">
<meta name="twitter:image" content="https://showcase.andrefiedler.de/2608_BuB/assets/social-preview.jpg">
<meta name="twitter:image:alt" content="Buch &amp; Bohne – Buchladen und Lesecafé in Oelsnitz">
<link rel="icon" href="./favicon.ico" sizes="16x16 32x32 48x48">
<link rel="icon" href="./favicon.svg" type="image/svg+xml" sizes="any">
<link rel="apple-touch-icon" href="./assets/apple-touch-icon.png" sizes="180x180">
<link rel="manifest" href="./site.webmanifest">
<link rel="preload" href="./assets/fonts/roboto-italic-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="./assets/fonts/roboto-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="./assets/styles.css">
<script type="module" src="./assets/app.mjs"></script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": ["BookStore", "CafeOrCoffeeShop"],
"@id": "https://showcase.andrefiedler.de/2608_BuB/#buch-und-bohne",
"name": "Buch & Bohne e. V.",
"url": "https://showcase.andrefiedler.de/2608_BuB/",
"description": "Buchladen und Lesecafé am Markt 5 in Oelsnitz.",
"logo": "https://showcase.andrefiedler.de/2608_BuB/assets/icon-512.png",
"image": "https://showcase.andrefiedler.de/2608_BuB/assets/social-preview.jpg",
"telephone": "+49 37421 23633",
"email": "lesecafe@buchundbohne.de",
"address": {
"@type": "PostalAddress",
"streetAddress": "Markt 5",
"addressLocality": "Oelsnitz",
"addressCountry": "DE"
},
"contactPoint": {
"@type": "ContactPoint",
"name": "Lilly Möbius",
"contactType": "Ansprechpartnerin",
"telephone": "+49 173 8022126",
"email": "lesecafe@buchundbohne.de",
"availableLanguage": "de"
},
"sameAs": ["https://www.instagram.com/buch_und_bohne_/"]
},
{
"@type": "WebSite",
"@id": "https://showcase.andrefiedler.de/2608_BuB/#website",
"name": "Buch & Bohne e. V.",
"url": "https://showcase.andrefiedler.de/2608_BuB/",
"inLanguage": "de",
"publisher": {"@id": "https://showcase.andrefiedler.de/2608_BuB/#buch-und-bohne"}
},
{
"@type": "WebPage",
"@id": "https://showcase.andrefiedler.de/2608_BuB/#webpage",
"url": "https://showcase.andrefiedler.de/2608_BuB/",
"name": "Buch & Bohne e. V. – Buchladen & Lesecafé in Oelsnitz",
"inLanguage": "de",
"isPartOf": {"@id": "https://showcase.andrefiedler.de/2608_BuB/#website"},
"about": {"@id": "https://showcase.andrefiedler.de/2608_BuB/#buch-und-bohne"},
"author": {"@id": "https://showcase.andrefiedler.de/2608_BuB/#buch-und-bohne"}
}
]
}
</script>
</head>
<body>
<a class="skip-link" href="#inhalt">Zum Inhalt</a>
<main class="page" id="inhalt">
<img class="logo" src="./assets/logo.svg" alt="Buch &amp; Bohne" width="185" height="147" fetchpriority="high">
<section class="announcement" aria-labelledby="announcement-title">
<h1 id="announcement-title">Aktuell überarbeiten wir unsere Website.</h1>
<p>
<span class="design-line">Bleiben Sie gespannt. Und schauen Sie in der Zwischenzeit</span>
<span class="design-line">gerne bei uns im Buchladen und Lesecafé</span>
<span class="design-line">in Oelsnitz am Markt 5 vorbei.</span>
</p>
</section>
<section class="support" aria-labelledby="support-title">
<h2 class="support-title" id="support-title">
<span class="visually-hidden">Werden Sie ein Teil von „Buch &amp; Bohne“!</span>
<img src="./assets/support-heading.svg" alt="" width="454" height="48">
</h2>
<p class="support-options">
<span class="design-line">Ob als Vereinsmitglied, Kuchen- oder Kaffeepate</span>
<span class="design-line">oder durch eine finanzielle Unterstützung – jeder Beitrag zählt.</span>
<span class="design-line">… und macht unser Café möglich.</span>
</p>
<p class="thanks">Vielen Dank für Ihre Unterstützung!</p>
</section>
<section class="contact" aria-labelledby="contact-title">
<h2 id="contact-title">Ihre Ansprechpartnerin</h2>
<address>
<p class="contact-person"><strong>Lilly Möbius,</strong> Buch &amp; Bohne e. V.</p>
<p class="contact-channels">
<span class="phone-line"><span class="label">Telefon</span> <a href="tel:+493742123633">037421 23633</a><span class="phone-separator"> . </span><span class="mobile-phone"><span class="label">mobil</span> <a href="tel:+491738022126">0173 8022126</a></span></span>
<span class="email-line"><span class="label">E-Mail</span> <a href="mailto:lesecafe@buchundbohne.de">lesecafe@buchundbohne.de</a></span>
</p>
<p class="instagram"><span class="label">Instagram:</span> <a href="https://www.instagram.com/buch_und_bohne_/">@buch_und_bohne_</a></p>
</address>
</section>
<section class="donations" aria-labelledby="bank-title">
<div class="bank">
<h2 id="bank-title">Kontodaten</h2>
<p class="account-holder"><strong>Buch &amp; Bohne e. V.</strong><br>Volksbank Vogtland-Saale-Orla eG</p>
<dl class="account-details">
<div><dt>IBAN:</dt><dd><button class="copy-iban" type="button" data-iban="DE04870958245044385904" title="IBAN kopieren" aria-label="IBAN DE04 8709 5824 5044 3859 04 kopieren">DE04 8709 5824 5044 3859 04</button></dd></div>
<div><dt>BIC:</dt><dd>GENODEF1PL1</dd></div>
</dl>
</div>
<div class="paypal">
<a class="qr-link" href="https://paypal.me/buchundbohne" aria-label="Buch &amp; Bohne über PayPal unterstützen">
<img src="./assets/paypal-qr.png" alt="PayPal-QR-Code für Buch &amp; Bohne" width="468" height="469">
</a>
<a class="paypal-address" href="https://paypal.me/buchundbohne"><span class="label">paypal.me</span>/buchundbohne</a>
</div>
</section>
<p class="visually-hidden" id="copy-status" role="status" aria-live="polite"></p>
</main>
</body>
</html>
+15
View File
@@ -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"
}
}
}
}
+9
View File
@@ -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"}
}
+6
View File
@@ -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
+13
View File
@@ -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)}`);
+150
View File
@@ -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.')
+3
View File
@@ -0,0 +1,3 @@
{
"publish": "dist"
}
+17
View File
@@ -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"}
]
}
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://showcase.andrefiedler.de/2608_BuB/</loc>
<lastmod>2026-09-07</lastmod>
</url>
</urlset>