Přidat siluetu lesa a terč do vzhledu, normalizovat slug
CI / test (push) Successful in 1m8s
CI / build-and-push (push) Successful in 2m16s

Grafika:
- silueta lesa přes celou šířku nad patičkou
- terč v hlavičce mezi značkou a navigací, s červeným středem

Obojí je vložené jako CSS maska. Zdrojová PNG byla černá kresba na bílém
bez alfa kanálu, takže by z nich koukal bílý obdélník a v tmavém motivu
by to bylo nepoužitelné. Převedená maska nese jen průhlednost a barvu
řídí CSS. Skyline se tím zmenšil z 856 kB na 245 kB.

Assety patří do public/, ne do media/ — media je v produkci Docker volume
mimo image, takže by tam soubory nebyly.

Slug se navíc normalizuje sdíleným helperem: vedoucí lomítko vyrábělo
odkaz //kontakt, což prohlížeč čte jako jméno cizího serveru.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 17:26:34 +02:00
co-authored by Claude Opus 5
parent 57f367c219
commit 3f967a2325
11 changed files with 88 additions and 18 deletions
+4
View File
@@ -34,6 +34,7 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
<Link className="znacka" href="/">
Reiner <span>zbraně a střelivo</span>
</Link>
<div className="terc" aria-hidden="true" />
<nav>
<Link href="/">Nabídka</Link>
{stranky.map((stranka) => (
@@ -46,6 +47,9 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
<main>{children}</main>
{/* Silueta lesa uzavírá stránku — přes celou šířku, nad patičkou. */}
<div className="skyline" aria-hidden="true" />
<footer className="paticka">
<p>© {new Date().getFullYear()} Martin Reiner</p>
<p>
+46 -2
View File
@@ -10,6 +10,7 @@
--barva-plocha: #f5f5f4;
--barva-linka: #e2e0dd;
--barva-akcent: #7a5c3e;
--barva-silueta: #dcd7d0;
--sirka: 1100px;
--radius: 6px;
}
@@ -22,6 +23,7 @@
--barva-plocha: #1e1e1e;
--barva-linka: #2e2e2e;
--barva-akcent: #c9a227;
--barva-silueta: #262626;
}
}
@@ -96,11 +98,53 @@ a {
color: var(--barva-text);
}
/*
* Terč a silueta lesa jsou vloženy jako CSS maska, ne jako <img>. Zdrojové
* PNG nesou jen průhlednost, barvu určuje `background-color` — díky tomu
* fungují ve světlém i tmavém motivu a nekouká z nich bílý obdélník.
*/
.terc {
position: relative;
flex: 0 0 auto;
width: 42px;
height: 42px;
background-color: var(--barva-tlumena);
-webkit-mask: url('/terc.png') center / contain no-repeat;
mask: url('/terc.png') center / contain no-repeat;
}
/* Červený střed je jediný barevný akcent v hlavičce — maska ho neumí
přenést, tak ho doplníme. */
.terc::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
border-radius: 50%;
background: #c0392b;
transform: translate(-50%, -50%);
}
.skyline {
height: clamp(60px, 9vw, 120px);
margin-top: 4rem;
background-color: var(--barva-silueta);
-webkit-mask: url('/skyline.png') bottom center / cover no-repeat;
mask: url('/skyline.png') bottom center / cover no-repeat;
}
@media (max-width: 620px) {
.terc {
display: none;
}
}
.paticka {
max-width: var(--sirka);
margin: 4rem auto 0;
margin: 0 auto;
padding: 1.5rem 1.25rem 3rem;
border-top: 1px solid var(--barva-linka);
font-size: 0.85rem;
color: var(--barva-tlumena);
}
+10 -2
View File
@@ -1,5 +1,7 @@
import type { CollectionConfig } from 'payload'
import { vytvorSlug } from '../lib/slug'
/** Statické stránky — o nás, kontakt, otevírací doba. */
export const Pages: CollectionConfig = {
slug: 'pages',
@@ -25,12 +27,18 @@ export const Pages: CollectionConfig = {
name: 'slug',
label: 'URL adresa',
type: 'text',
required: true,
unique: true,
index: true,
admin: {
position: 'sidebar',
description: 'Např. `kontakt` → /kontakt',
description:
'Vyplní se samo z názvu, pokud necháte prázdné. Bez lomítek a diakritiky — ' +
'z „Otevírací doba" vznikne /oteviraci-doba.',
},
hooks: {
// Normalizuje i to, co uživatel napíše ručně — hlavně vede lomítko,
// které by vyrobilo odkaz na cizí server (`//kontakt`).
beforeValidate: [({ value, data }) => vytvorSlug(value || data?.nazev || '')],
},
},
{
+3 -12
View File
@@ -1,5 +1,7 @@
import type { CollectionConfig } from 'payload'
import { vytvorSlug } from '../lib/slug'
/**
* Položka nabídky — zbraň, střelivo, doplněk.
*
@@ -41,9 +43,7 @@ export const Products: CollectionConfig = {
description: 'Vyplní se samo z názvu, pokud necháte prázdné.',
},
hooks: {
beforeValidate: [
({ value, data }) => value || slugify(data?.nazev ?? ''),
],
beforeValidate: [({ value, data }) => vytvorSlug(value || data?.nazev || '')],
},
},
{
@@ -153,12 +153,3 @@ export const Products: CollectionConfig = {
],
}
/** Diakritika pryč, mezery na pomlčky — `CZ 75 SP-01` → `cz-75-sp-01`. */
function slugify(text: string): string {
return text
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
}
+16
View File
@@ -0,0 +1,16 @@
/**
* Převede text na URL adresu: `Otevírací doba` → `oteviraci-doba`.
*
* Snese i to, co člověk do pole běžně napíše — lomítka (`/kontakt`),
* diakritiku, velká písmena i mezery. Právě lomítko je zrádné: uložený
* slug `/kontakt` vyrobí odkaz `href="//kontakt"`, což prohlížeč přečte
* jako jméno cizího serveru, ne jako cestu na tomhle webu.
*/
export function vytvorSlug(text: string): string {
return text
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
}
+2 -2
View File
@@ -212,9 +212,9 @@ export interface Page {
id: number;
nazev: string;
/**
* Např. `kontakt` → /kontakt
* Vyplní se samo z názvu, pokud necháte prázdné. Bez lomítek a diakritiky — z „Otevírací doba" vznikne /oteviraci-doba.
*/
slug: string;
slug?: string | null;
obsah?: {
root: {
type: string;