Перейти к содержимому

Публичные страницы

Эта страница переведена с английского. Читать страницу на английском

Экраны, помеченные public, может открыть любой, в том числе посетители, которые никогда не входили: посадочная страница, прайс-лист, справочный центр. Zeyn отдаёт их так, чтобы их было дёшево доставлять, а кэшам и поисковым системам было удобно с ними работать.

Без cookie для посетителей

Публичный экран, открытый запросом без сессии, не устанавливает cookie. Страница несёт пустой CSRF-токен, поэтому каждый посетитель с тем же языком и темой получает одни и те же байты.

Среда выполнения в браузере запрашивает токен через GET /_zeyn/csrf непосредственно перед первым вызовом, которому он нужен (форма, действие, выбор языка). Этот вызов устанавливает cookie zeyn_csrf и никогда не кэшируется. Каждый POST проверяется точно так же, как на любой другой странице: без подходящего токена он отклоняется с кодом 403, и ничто не повторяется молча.

Вошедшие пользователи, как и раньше, всегда получают cookie и токен на странице.

HTTP-кэширование

#[screen(route = "/pricing", public, cache = 300)]
pub fn pricing() -> Screen {
    Screen::new("Pricing")
}

cache = N позволяет браузерам и общим кэшам хранить страницу N секунд (от 1 до 86400). Для запроса без сессии страница тогда получает:

ЗаголовокЗначение
Cache-Controlpublic, max-age=300
ETagСтрогий тег по отрисованному телу
Varyx-zeyn-partial, Accept-Language, Cookie

Запрос с подходящим If-None-Match получает 304 Not Modified без тела. Экраны, загружаемые навигацией внутри приложения, кэшируются так же, со своим тегом.

Запрос с сессией никогда не получает публичных заголовков кэша: страница отправляется с Cache-Control: private, no-store. cache на экране, который не public, не компилируется.

Экран по-прежнему строится и отрисовывается для каждого запроса, дошедшего до сервера; тег экономит передачу, а кэш перед приложением экономит сам запрос. Не помещайте в кэшируемые экраны ничего, что зависит от посетителя: время суток, счётчик, случайный выбор.

#[screen(route = "/pricing", public, cache = 300)]
pub fn pricing() -> Screen {
    Screen::new(t!("pricing.title"))
        .description(t!("pricing.description"))
        .canonical("/pricing")
        .og_image("https://cdn.example.com/pricing.png")
        .robots(Robots::Index)
        .json_ld(serde_json::json!({
            "@context": "https://schema.org",
            "@type": "Product",
            "name": "Team plan",
        }))
}
МетодТеги head
.description(text)<meta name="description"> и og:description на языке пользователя
.canonical(url)<link rel="canonical"> и og:url
.og_image(url)og:image
.robots(Robots::Index) или Robots::NoIndex<meta name="robots">
.json_ld(value)<script type="application/ld+json">

Каждое значение экранируется. JSON-LD записывается с <, > и & в виде <, > и &, поэтому значение, содержащее </script>, не может закрыть элемент; браузер его никогда не выполняет, а политика безопасности содержимого остаётся такой же строгой, как на любой другой странице. Пути приложения в адресных свойствах JSON-LD (url, @id, image, logo, item и подобных) на любой глубине записываются абсолютными с App::site_url, как ожидают поисковые системы.

URL должны быть адресами https или путями приложения, начинающимися с /. Всё остальное (http:, //host, javascript:, пробелы, кавычки) отбрасывается с предупреждением в журнале. Пути приложения записываются после URL сайта и базового пути:

App::new(t!("app.name")).site_url("https://shop.example.com")

Без site_url теги head сохраняют путь приложения, который браузеры разрешают, а некоторые краулеры нет.

Навигация внутри приложения обновляет заголовок и все эти теги без загрузки страницы.

Sitemap

#[action(public)]
pub async fn site_pages(part: SitemapPart, db: Db) -> Result<Vec<SitemapUrl>> {
    let rows = db.products_after(part.offset, part.limit).await?;
    Ok(rows.into_iter().map(|p| SitemapUrl::new(format!("/products/{}", p.slug)).lastmod(p.changed)).collect())
}

Feature::new("site").sitemap(site_pages)

/sitemap.xml запрашивает у действия по 1000 URL за раз (part.offset, part.limit) и передаёт XML потоком, поэтому большой сайт никогда не занимает память целиком. До 50 000 URL это один urlset. Сверх этого это индекс карт сайта из /sitemaps/1.xml, /sitemaps/2.xml и так далее, в каждой по 50 000 URL. Действие должно быть public и возвращать URL в стабильном порядке. Для путей приложения нужен App::site_url, потому что карта сайта содержит абсолютные URL; записи, которые не являются адресами https или путями приложения, отбрасываются. Перечисляйте только публичные страницы: поисковые системы не могут открыть остальные. Карта сайта есть у одной функции (feature) на приложение.

robots.txt и llms.txt

App::new(t!("app.name"))
    .robots_txt("User-agent: *\nAllow: /\nSitemap: https://shop.example.com/sitemap.xml\n")
    .llms_txt(include_str!("../llms.txt"))

Оба файла отдаются всем как обычный текст.