Bug: Only index.astro passed errorPage={filterError}, missing:
- index.astro unavailable (503) — Dataset оставался на error page
- spots/[id].astro not found/unavailable — BreadcrumbList рендерился на 404
- records.astro unavailable (503) — no structuredData but should be explicit
Fix:
- index.astro: errorPage={filterError || unavailable}
- spots/[id].astro: errorPage={!spot || unavailable}
- records.astro: errorPage={unavailable}
- report.astro: не нужен (structuredData не передаётся)
Verification:
- Astro build: 0 errors
- Error pages (422, 503, 404) skip structuredData
- Normal pages include structuredData
- noindex still works for robots meta tag
47 lines
4.8 KiB
Plaintext
47 lines
4.8 KiB
Plaintext
---
|
|
import Layout from "../../layouts/Layout.astro";
|
|
import SourceBadge from "../../components/SourceBadge.astro";
|
|
import CoordinateRadar from "../../components/CoordinateRadar.astro";
|
|
import ActivityTimeline from "../../components/ActivityTimeline.astro";
|
|
import CatchList from "../../components/CatchList.astro";
|
|
import { activityLevel, api, ApiError, plural, type Activity, type Catch, type PaginatedActivity, type Spot } from "../../lib/api";
|
|
const { id } = Astro.params;
|
|
let spot: Spot | null = null, catches: Catch[] = [], activity: Activity | null = null, unavailable = false;
|
|
let timeline: { start: string; end: string; count: number }[] = [];
|
|
try {
|
|
const readable = id?.match(/^(.+)-(-?\d+)x(-?\d+)$/);
|
|
const spotResult = readable
|
|
? await api<Spot>(`/api/v1/spots/resolve?waterbody=${encodeURIComponent(readable[1])}&x=${readable[2]}&y=${readable[3]}`)
|
|
: await api<Spot>(`/api/v1/spots/${id}`);
|
|
if (!readable) return Astro.redirect(`/spots/${spotResult.waterbody_slug}-${spotResult.x}x${spotResult.y}`, 301);
|
|
const [catchResult, activityPaginated] = await Promise.all([api<Catch[]>(`/api/v1/spots/${spotResult.id}/catches`), api<PaginatedActivity>(`/api/v1/activity?hours=24&waterbody=${encodeURIComponent(spotResult.waterbody_slug)}&limit=100`)]);
|
|
spot = spotResult; catches = catchResult; activity = activityPaginated.items.find(item => item.spot_id === spotResult.id) ?? null;
|
|
timeline = await api<typeof timeline>(`/api/v1/spots/${spotResult.id}/timeline`);
|
|
} catch (error) {
|
|
unavailable = true;
|
|
Astro.response.status = error instanceof ApiError && [404, 422].includes(error.status) ? 404 : 503;
|
|
if (Astro.response.status === 503) Astro.response.headers.set("Retry-After", "60");
|
|
}
|
|
const level = activity ? activityLevel(activity.activity_score) : null;
|
|
const spotDescription = spot ? `Свежие уловы и активность на точке ${spot.x}:${spot.y}, ${spot.waterbody}: рыба, вес, приманки и источники данных.` : "Данные точки ловли Russian Fishing 4.";
|
|
const breadcrumbs = spot ? { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: [
|
|
{ "@type": "ListItem", position: 1, name: "Сейчас клюёт", item: "https://rf4spotter.ru/" },
|
|
{ "@type": "ListItem", position: 2, name: `${spot.waterbody} ${spot.x}:${spot.y}`, item: `https://rf4spotter.ru/spots/${spot.waterbody_slug}-${spot.x}x${spot.y}` },
|
|
] } : null;
|
|
---
|
|
<Layout title={spot ? `Точка ${spot.x}:${spot.y}, ${spot.waterbody} — RF4 Spotter` : "Точка не найдена — RF4 Spotter"} description={spotDescription} noindex={!spot} structuredData={breadcrumbs} errorPage={!spot || unavailable}>
|
|
<a class="back" href="/">← Все активные точки</a>
|
|
{unavailable || !spot ? <div class="state"><h1>Точка недоступна</h1><p>API не ответил или такой точки нет.</p></div> : <>
|
|
<section class="spot-hero"><div><span class="eyebrow">{spot.waterbody}</span><h1>Точка {spot.x}:{spot.y}</h1><p>{spot.description}</p></div><CoordinateRadar x={spot.x} y={spot.y}/></section>
|
|
<div class="periods"><div><strong>{spot.catches_24h}</strong><span>за 24 часа</span></div><div><strong>{spot.catches_3d}</strong><span>за 3 дня</span></div><div><strong>{spot.catches_7d}</strong><span>за 7 дней</span></div></div>
|
|
<ActivityTimeline buckets={timeline}/>
|
|
<section class="detail-grid">
|
|
<div>
|
|
<div class="section-heading"><h2>Последние уловы</h2></div>
|
|
{catches.length ? <CatchList catches={catches}/> : <div class="state compact-state"><h2>Уловов пока нет</h2><p>Для этой точки нет одобренных наблюдений.</p></div>}
|
|
</div>
|
|
<aside><span class="eyebrow">Оценка за 24 часа</span>{activity && level ? <><h2>{activity.activity_score} / 100</h2><strong data-activity-level={level.short}>{level.description}</strong><div class="source-strip">{activity.sources.map(source => <SourceBadge source={source}/>)}</div><p>{activity.explanation}</p><p class="note">Основано на {activity.catches} {plural(activity.catches, ["наблюдении", "наблюдениях", "наблюдениях"])} от {activity.unique_players} {plural(activity.unique_players, ["игрока", "игроков", "игроков"])}. Уверенность: {activity.confidence_score}%.</p></> : <p>За последние 24 часа данных для расчёта нет.</p>}<span class="eyebrow">Лучшие приманки</span>{spot.top_baits.length ? <ol>{spot.top_baits.map(name => <li>{name}</li>)}</ol> : <p>Недостаточно данных.</p>}<p class="note">Учитываются только одобренные наблюдения.</p></aside>
|
|
</section>
|
|
</>}
|
|
</Layout>
|