Přeskočit na obsah

Menu, přesměrování, jazyky

Tři věci, které rozhodují o tvaru adres na webu. Všechny tři drží patro a vykonává je vaše šablona — proto na ně balík má hotové kusy, které stačí zapojit.

Strom menu přijde ze serveru rozřešený: každá položka má hotovou adresu a položky, jejichž cíl na webu k vidění není, ze stromu vypadly i s podstromem. Nemusíte tedy počítat nic.

---
import Menu from "@patro/render/Menu.astro"
const menu = await client.getMenu("hlavni", locale)
---
<Menu menu={menu} />

null se vykreslí jako nic. Web bez navigace je pořád web; web, který kvůli chybějícímu menu spadne, není.

Když si chcete navigaci nakreslit po svém (jiné rozvržení, rozbalování, megamenu), vezměte si jen její čistou půlku:

---
import { buildMenuLinks } from "@patro/render"
const menu = await client.getMenu("hlavni", locale)
const links = menu === null ? [] : buildMenuLinks(menu.items)
---
<ul>
{links.map((link) => (
<li>
<a href={link.href} class:list={["odkaz", link.classes]} {...link.attrs}>
{link.label}
</a>
</li>
))}
</ul>

buildMenuLinks udělá z rozřešeného stromu položky připravené k vykreslení. Dvě věci na tom stojí za pozornost:

  • attrs nese jen atributy, které položka opravdu má. Prázdná hodnota z administrace se do HTML nedostane, takže ve zdroji nesvítí title="" a rozprostřít je jde bez podmínek. rel="noopener" se přidá samo k položkám otevíraným v novém okně.
  • classes stojí vedle attrs schválně. Jsou to třídy, které zadal správce v administraci, a musíte je slepit se svými. Kdyby jely uvnitř attrs, rozprostření by vám přepsalo vlastní class a odkaz by přišel o veškerý vzhled právě u těch položek, kterým správce třídu přidal.

Které menu je to hlavní

Sekce “Které menu je to hlavní”

Web nemusí jméno menu znát dopředu (a je lepší, když ho neví — jinak stačí, aby se menu v administraci jmenovalo jinak, a navigace tiše zmizí). Na výběr je hotový mechanismus i s odkládací pamětí na isolate:

import { createMainMenuCache } from "@patro/render"
const cache = createMainMenuCache({
client: getPatroClient, // továrna, ne hotový klient
preferred: env.PUBLIC_MENU_NAME, // volitelné přání, ne podmínka
onError: (error) => console.error("patro: navigace — ", error),
})
const menu = await cache.load(locale, background)

Bez preferred si vybere z toho, co web má (a přeskočí prázdné menu). Vyplnit ho má smysl teprve tehdy, když je menu víc a je potřeba říct, které je nahoře.

Paměť má smysl, protože navigace se kreslí na každé stránce — bez ní stojí každý požadavek dvě volání navíc. background je ctx.waitUntil, kterým se obnovení odsune mimo cestu odpovědi návštěvníkovi; na Astru ho vytáhnete z Astro.locals pomocníkem backgroundOf.

Pravidla drží patro (redaktor je zadává v administraci), ale vyhodnocuje je váš web — patro do provozu nevidí. Celý mechanismus je hotový: paměť mapy, vyhodnocení přesných shod i vzorů, hlášení nenalezených adres zpátky.

src/middleware.ts
import { defineMiddleware } from "astro:middleware"
import { createRedirectMiddleware } from "@patro/render"
import { getPatroClient } from "./lib/patro"
export const onRequest = defineMiddleware(
createRedirectMiddleware({
client: getPatroClient,
onError: (error) => {
console.error("patro: přesměrování — ", error)
},
}),
)

To je celé. client je funkce, ne hotový klient — middleware vzniká jednou při načtení modulu, kdežto env se na Cloudflare čte až za běhu požadavku.

onError je jediné místo, kde je výpadek vidět: chybu při stahování mapy ani při odesílání hlášení middleware nepropustí do odpovědi (stránka se má vykreslit tak jako tak), takže bez něj zmizí beze stopy.

Volitelně jdou doladit lhůty (ttlMs, maxAgeMs, flushIntervalMs); výchozí hodnoty jsou zvolené tak, aby běžná stránka nesahala na síť vůbec.

⚠️ Vyhodnocuje se jen GET a HEAD

Sekce “⚠️ Vyhodnocuje se jen GET a HEAD”

Zapamatujte si to dřív, než budete hledat, proč přesměrování „nefunguje“ na formuláři. Přesměrovaný POST znamená ztracená data: prohlížeč po 301 i 302 požadavek zopakuje GETem, tedy bez těla.

Zápisové cesty vaší šablony (odeslání komentáře, vlastní formulář) tedy tímhle middlewarem procházejí bez zásahu, a je to správně.

Middleware si střádá nenalezené adresy a zásahy pravidel a posílá je dávkou z pozadí. Díky tomu vidí redaktor v administraci, na které neexistující adresy lidé chodí, a může na ně pravidlo založit. Nemusíte kvůli tomu dělat nic — je to součást téhož onRequest.

Konvence je výchozí jazyk bez prefixu, ostatní s prefixem:

Adresa Jazyk Slug
/o-nas výchozí o-nas
/en/about en about
/en en — (jazykový index)
/cs/o-nas 404

Poslední řádek je vědomý: výchozí jazyk prefix nemá, takže /cs/… se jako slug nenajde. Prefix se uzná jedině pro jazyk, který je v tenant.locales a není výchozí.

Seznam jazyků i výchozí jazyk přijdou z client.getTenant(), takže je šablona nemá napsané u sebe. Pravidlo samotné je hotové v @patro/shared a nepište si ho po svém — musí totiž vyjít stejně jako adresy, které skládá server do menu:

import { entryRouteFromPath, localeHrefPrefix, entryHref } from "@patro/shared"
const tenant = await client.getTenant()
// adresa → { locale, slug }
const { locale, slug } = entryRouteFromPath(
Astro.params.slug ?? "",
tenant.locales,
tenant.defaultLocale,
)
// zpátky: slug → adresa
const href = entryHref("o-nas", locale, tenant.defaultLocale)
// prefix pro odkazy uvnitř jazyka ("" nebo "/en")
const prefix = localeHrefPrefix(locale, tenant.defaultLocale)

Slug může mít víc úseků (sluzby/uklid) a lomítka v něm jsou součástí cesty, ne oddělovač k zakódování — proto na něj nesahejte encodeURIComponentem jako na celek. entryHref to řeší po segmentech.

Jeden catch-all pokryje všechno:

src/pages/index.astro → kořen ve výchozím jazyce
src/pages/[...slug].astro → /en, /en/about, /o-nas, /hledani

Kořen má vlastní soubor proto, že specifičtější routa má v Astru přednost — / tedy do catch-allu nespadne.

Co ještě chodí z konfigurace webu

Sekce “Co ještě chodí z konfigurace webu”

Vedle jazyků nese getTenant() tři nastavení, která rozhodují o adresách, a ve slovníku adres nejsou (ten je obsahový):

Klíč K čemu
bylineRouteSlug první úsek adresy autora — /autor/jan-novak
searchRouteSlug první úsek adresy hledání — /hledani?q=…
timezone v jakém pásmu web vypisuje časy

Časové pásmo stojí za jednu poznámku navíc: stránka se vykresluje na serveru a ten běží v UTC, takže bez explicitně zadaného pásma by se návštěvníkovi (a redaktorovi v náhledu naplánované stránky) ukázal posunutý čas. Na formátování je v @patro/shared funkce formatInTimeZone.

Titulní stránku řeší client.getFrontSlug(locale) — vrátí slug, na který má kořen vnitřně přesměrovat (Astro.rewrite), nebo null, když titulka nastavená není a kořen má ukázat výpis.