{"id":494995,"date":"2026-09-17T09:27:53","date_gmt":"2026-09-17T09:27:53","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=494995"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=494995","title":{"rendered":"Next.js, \u0432\u043d\u0435\u0448\u043d\u0438\u0439 API, Zod, \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0438 controlled fallback"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0412\u043d\u0435\u0448\u043d\u0438\u0439 API \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432\u00a0\u0441\u0432\u043e\u0435\u0439 \u043c\u043e\u0434\u0435\u043b\u0438. \u041d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u0435\u0439, \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f, \u0446\u0435\u043d\u044b \u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0442 provider. UI \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043e\u0431\u044b\u0447\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441\u00a0\u0434\u0440\u0443\u0433\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u044c\u044e. \u0415\u0441\u043b\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0447\u0438\u0442\u0430\u0435\u0442 <code>event._embedded.venues[0].<\/code><a href=\"http:\/\/city.name\" rel=\"noopener nofollow\"><code>city.name<\/code><\/a> \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e, \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u043e\u0442\u0432\u0435\u0442\u0430 \u0440\u0430\u0441\u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u00a0\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443. \u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 API \u0437\u0430\u0442\u0440\u0430\u0433\u0438\u0432\u0430\u0435\u0442 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0438, detail page, search, metadata \u0438 \u0442\u0435\u0441\u0442\u044b.<\/p>\n<p>\u0412\u00a0App Router \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043d\u0430\u00a0\u0441\u0435\u0440\u0432\u0435\u0440\u0435. Server Components \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u0441 <code>fetch<\/code>, \u0430\u00a0\u0441\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 \u043d\u0435\u00a0\u043f\u043e\u043f\u0430\u0434\u0430\u0435\u0442 \u0432\u00a0\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0439 bundle. API key \u043f\u0440\u0438\u00a0\u0442\u0430\u043a\u043e\u0439 \u0441\u0445\u0435\u043c\u0435 \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u0432\u043d\u0435 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430. <a href=\"https:\/\/nextjs.org\/docs\/app\/getting-started\/fetching-data\" rel=\"noopener nofollow\">\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f Next.js<\/a> \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 server\u2011side data fetching \u0434\u043b\u044f\u00a0App Router. \u0412\u00a0\u0443\u0447\u0435\u0431\u043d\u043e\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0435\u00a0<a href=\"https:\/\/eventmap-seo.vercel.app\/en\" rel=\"noopener nofollow\">EventMap<\/a> \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 server API \u0441\u043b\u043e\u0439.<\/p>\n<h3>Server boundary<\/h3>\n<p>\u041a\u043b\u044e\u0447 \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043d\u0435\u00a0\u043d\u0443\u0436\u0435\u043d Client Component. \u0415\u0433\u043e \u0447\u0438\u0442\u0430\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f:<\/p>\n<pre><code class=\"typescript\">export function getTicketmasterApiKey(): string | null {  const key = process.env.TICKETMASTER_API_KEY;  return key &amp;&amp; key.trim().length &gt; 0    ? key    : null;}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u043d\u0435\u00a0\u0438\u043c\u0435\u0435\u0442 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u0430 <code>NEXT_PUBLIC_<\/code>.<\/p>\n<p>URL \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0442\u043e\u0436\u0435 \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u00a0\u0441\u0435\u0440\u0432\u0435\u0440\u0435:<\/p>\n<pre><code class=\"typescript\">const TICKETMASTER_EVENTS_URL =  \"https:\/\/app.ticketmaster.com\/discovery\/v2\/events.json\";export function buildEventsUrl({  apiKey,  locale,  city,  keyword,}: BuildEventsUrlInput): string {  void locale;  const url = new URL(TICKETMASTER_EVENTS_URL);  url.searchParams.set(\"apikey\", apiKey);  url.searchParams.set(\"size\", \"12\");  url.searchParams.set(\"sort\", \"date,asc\");  url.searchParams.set(\"locale\", \"*\");  url.searchParams.set(    \"startDateTime\",    getLiveEventsStartDateTime(),  );  if (city) {    url.searchParams.set(\"city\", city);  }  if (keyword) {    url.searchParams.set(\"keyword\", keyword);  }  return url.toString();}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>UI \u043d\u0435\u00a0\u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 API key \u0438 \u043d\u0435\u00a0\u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442 Ticketmaster URL.<\/p>\n<h3>JSON \u043f\u043e\u0441\u043b\u0435 response.json()<\/h3>\n<p>TypeScript \u043d\u0435\u00a0\u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435, \u043f\u0440\u0438\u0448\u0435\u0434\u0448\u0438\u0435 \u043f\u043e\u00a0\u0441\u0435\u0442\u0438. \u0422\u0438\u043f \u0432\u00a0\u0438\u0441\u0445\u043e\u0434\u043d\u043e\u043c \u043a\u043e\u0434\u0435 \u0438\u0441\u0447\u0435\u0437\u0430\u0435\u0442 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u043c\u043f\u0438\u043b\u044f\u0446\u0438\u0438, \u0430\u00a0\u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 \u043c\u043e\u0436\u0435\u0442 \u0432\u0435\u0440\u043d\u0443\u0442\u044c \u0434\u0440\u0443\u0433\u0443\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443.<\/p>\n<p>\u041f\u043e\u0441\u043b\u0435 <code>response.json()<\/code> \u043f\u0440\u043e\u0435\u043a\u0442 \u0445\u0440\u0430\u043d\u0438\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u043a\u0430\u043a <code>unknown<\/code>:<\/p>\n<pre><code class=\"typescript\">const data: unknown = await response.json();                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0414\u0430\u043b\u044c\u0448\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u043e\u0445\u043e\u0434\u044f\u0442 runtime\u2011\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443.<\/p>\n<p>Zod \u0440\u0430\u0437\u0434\u0435\u043b\u044f\u0435\u0442 \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0439 \u0438 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0447\u0435\u0440\u0435\u0437 <code>safeParse<\/code>. \u041c\u0435\u0442\u043e\u0434 \u043d\u0435\u00a0\u0431\u0440\u043e\u0441\u0430\u0435\u0442 <code>ZodError<\/code>, \u0430\u00a0\u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u044a\u0435\u043a\u0442 \u0441 <code>success<\/code> \u0438 <code>data<\/code>\u00a0\u043b\u0438\u0431\u043e <code>error<\/code>. \u0422\u0430\u043a\u0430\u044f \u0444\u043e\u0440\u043c\u0430 \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 \u0434\u043b\u044f\u00a0source boundary, \u0433\u0434\u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u0432\u00a0\u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439. <a href=\"https:\/\/zod.dev\/basics\" rel=\"noopener nofollow\">\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f Zod<\/a> \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u0442\u043e\u0442\u00a0\u0436\u0435 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 <code>safeParse<\/code>.<\/p>\n<h3>\u0421\u0445\u0435\u043c\u0430 \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f<\/h3>\n<p>\u0421\u0445\u0435\u043c\u0430 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u0435 \u0447\u0430\u0441\u0442\u0438 \u043e\u0442\u0432\u0435\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0443\u0436\u043d\u044b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044e:<\/p>\n<pre><code class=\"typescript\">import { z } from \"zod\";export const TicketmasterEventSchema = z.object({  id: z.string(),  name: z.string(),  info: z.string().optional(),  pleaseNote: z.string().optional(),  url: z.string().optional(),  images: z    .array(      z.object({        url: z.string(),        width: z.number().optional(),        height: z.number().optional(),      }),    )    .optional(),  dates: z    .object({      start: z        .object({          localDate: z.string().optional(),          localTime: z.string().optional(),        })        .optional(),    })    .optional(),  classifications: z    .array(      z.object({        genre: z          .object({            name: z.string().optional(),          })          .optional(),        segment: z          .object({            name: z.string().optional(),          })          .optional(),      }),    )    .optional(),});                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>id<\/code> \u0438 <code>name<\/code> \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b. \u0418\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f, venue, \u0434\u0430\u0442\u0430, classification \u0438 price \u043c\u043e\u0433\u0443\u0442 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c.<\/p>\n<p>\u041e\u0442\u0432\u0435\u0442 search endpoint \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0445\u0435\u043c\u043e\u0439:<\/p>\n<pre><code class=\"typescript\">export const TicketmasterEventsResponseSchema =  z.object({    _embedded: z      .object({        events:          z.array(TicketmasterEventSchema).optional(),      })      .optional(),    page: z      .object({        totalElements: z.number().optional(),        totalPages: z.number().optional(),        number: z.number().optional(),        size: z.number().optional(),      })      .optional(),  });                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Collection response \u0438 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u0438\u043c\u0435\u044e\u0442 \u0440\u0430\u0437\u043d\u044b\u0435 \u0441\u0445\u0435\u043c\u044b.<\/p>\n<h3>safeParse \u043f\u0435\u0440\u0435\u0434 \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439<\/h3>\n<p>Search \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 JSON \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0435\u0433\u043e \u0434\u043e\u00a0\u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u043a\u00a0\u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u043c \u043f\u043e\u043b\u044f\u043c:<\/p>\n<pre><code class=\"typescript\">const data: unknown = await response.json();const parsed =  TicketmasterEventsResponseSchema.safeParse(data);if (!parsed.success) {  console.warn(    \"Ticketmaster search response shape is invalid\",  );  return {    ok: false,    reasonKey: \"invalid-live-response\",  };}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u044b\u0439 JSON \u043d\u0435\u00a0\u043f\u043e\u043f\u0430\u0434\u0430\u0435\u0442 \u0432\u00a0normalizer \u0438 React components.<\/p>\n<p>Detail request \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u0445\u0435\u043c\u0443 \u043e\u0434\u043d\u043e\u0433\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f:<\/p>\n<pre><code class=\"typescript\">const data: unknown = await response.json();const parsed =  TicketmasterEventSchema.safeParse(data);if (!parsed.success) {  console.warn(    \"Ticketmaster detail response shape is invalid\",  );  return null;}return normaliseTicketmasterEvent(parsed.data);                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u0433\u043e <code>safeParse<\/code> TypeScript \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0442\u0438\u043f, \u0432\u044b\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u0438\u0437\u00a0Zod schema.<\/p>\n<h3>\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043c\u043e\u0434\u0435\u043b\u044c<\/h3>\n<p>UI \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u0432\u043e\u0439 \u0442\u0438\u043f:<\/p>\n<pre><code class=\"typescript\">export type EventCardData = {  id: string;  title: string;  city: string;  country: string;  category: string;  dateLabel: string;  venue: string;  description: string;  imageUrl: string;  genreLabel?: string;  priceLabel?: string;  ticketUrl?: string;  timeLabel?: string;};                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412\u00a0\u043d\u0451\u043c \u043d\u0435\u0442 <code>_embedded<\/code>, <code>classifications<\/code>, <code>priceRanges<\/code> \u0438 \u0434\u0440\u0443\u0433\u0438\u0445 Ticketmaster structures.<\/p>\n<p>Normalizer \u0447\u0438\u0442\u0430\u0435\u0442 provider model \u0438 \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442 <code>EventCardData<\/code>:<\/p>\n<pre><code class=\"typescript\">export function normaliseTicketmasterEvent(  ticketmasterEvent: TicketmasterEvent,): EventCardData {  const venue =    ticketmasterEvent._embedded?.venues?.[0];  const classification =    ticketmasterEvent.classifications?.[0];  const category =    classification?.segment?.name;  const genre =    classification?.genre?.name;  const description =    ticketmasterEvent.info?.trim() ||    ticketmasterEvent.pleaseNote?.trim() ||    \"Event details are coming soon.\";  return {    id: ticketmasterEvent.id,    title: ticketmasterEvent.name,    city:      venue?.city?.name ??      \"Unknown city\",    country:      venue?.country?.name ??      \"Unknown country\",    category:      category ??      \"Event\",    dateLabel:      ticketmasterEvent.dates?.start?.localDate ??      \"Date TBA\",    venue:      venue?.name ??      \"Venue TBA\",    description,    imageUrl:      pickBestImage(ticketmasterEvent.images),    genreLabel: genre,    ticketUrl:      ticketmasterEvent.url,    timeLabel:      ticketmasterEvent.dates?.start?.localTime        ?.slice(0, 5),  };}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041a\u0430\u0440\u0442\u043e\u0447\u043a\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u0443\u044e \u0444\u043e\u0440\u043c\u0443 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u043e\u0442\u00a0\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b provider response.<\/p>\n<h3>\u041d\u0435\u043f\u043e\u043b\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435<\/h3>\n<p>\u0412\u043d\u0435\u0448\u043d\u044f\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043c\u043e\u0436\u0435\u0442\u00a0\u0431\u044b\u0442\u044c \u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0439 \u043f\u043e\u00a0schema \u0438 \u043f\u0440\u0438\u00a0\u044d\u0442\u043e\u043c \u043d\u0435\u00a0\u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c venue, description \u0438\u043b\u0438\u00a0image. Schema \u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0442\u0430\u043a\u0438\u0435 \u043f\u043e\u043b\u044f optional. Normalizer \u0440\u0435\u0448\u0430\u0435\u0442, \u0447\u0442\u043e\u00a0\u043f\u043e\u043b\u0443\u0447\u0438\u0442 UI.<\/p>\n<p>Description \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e:<\/p>\n<pre><code class=\"typescript\">const description =  ticketmasterEvent.info?.trim() ||  ticketmasterEvent.pleaseNote?.trim() ||  \"Event details are coming soon.\";                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Venue \u0438 \u0434\u0430\u0442\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u044e\u0442 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0435 fallback:<\/p>\n<pre><code class=\"typescript\">venue:  venue?.name ?? \"Venue TBA\",dateLabel:  ticketmasterEvent.dates?.start?.localDate ??  \"Date TBA\",                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443 \u043d\u0435\u00a0\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0439 \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u0439 provider field.<\/p>\n<h3>\u0412\u044b\u0431\u043e\u0440 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f<\/h3>\n<p>API \u043c\u043e\u0436\u0435\u0442 \u0432\u0435\u0440\u043d\u0443\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439 \u043e\u0434\u043d\u043e\u0433\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u041f\u0435\u0440\u0432\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0432\u00a0\u043c\u0430\u0441\u0441\u0438\u0432\u0435 \u043d\u0435\u00a0\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0435. \u041f\u0440\u043e\u0435\u043a\u0442 \u0444\u0438\u043b\u044c\u0442\u0440\u0443\u0435\u0442 \u043f\u0443\u0441\u0442\u044b\u0435 URL, \u0437\u0430\u0442\u0435\u043c \u0438\u0449\u0435\u0442 \u0448\u0438\u0440\u043e\u043a\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0448\u0438\u0440\u0438\u043d\u043e\u0439 \u043e\u0442 600\u00a0px:<\/p>\n<pre><code class=\"typescript\">export const EVENT_PLACEHOLDER_IMAGE =  \"\/event-placeholder.svg\";export function pickBestImage(  images: TicketmasterImage[] | undefined,): string {  if (!images || images.length === 0) {    return EVENT_PLACEHOLDER_IMAGE;  }  const validImages =    images.filter(      (image) =&gt;        image.url.trim().length &gt; 0,    );  if (validImages.length === 0) {    return EVENT_PLACEHOLDER_IMAGE;  }  const wideImage = validImages    .filter(      (image) =&gt;        typeof image.width === \"number\" &amp;&amp;        image.width &gt;= 600 &amp;&amp;        (image.height ?? 0) &lt;= image.width,    )    .sort(      (a, b) =&gt;        (b.width ?? 0) -        (a.width ?? 0),    )[0];  if (wideImage) {    return wideImage.url;  }  const imagesWithWidth =    validImages.filter(      (image) =&gt;        typeof image.width === \"number\",    );  if (imagesWithWidth.length === 0) {    return validImages[0].url;  }  return (    imagesWithWidth.sort(      (a, b) =&gt;        (b.width ?? 0) -        (a.width ?? 0),    )[0]?.url ??    EVENT_PLACEHOLDER_IMAGE  );}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0435\u0439 \u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438 \u043d\u0435\u0442, UI \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 placeholder.<\/p>\n<h3>HTTP status \u0434\u043e\u00a0Zod<\/h3>\n<p>Zod \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 JSON structure. HTTP status \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u0440\u0430\u043d\u044c\u0448\u0435.<\/p>\n<pre><code class=\"typescript\">const response = await fetch(url, {  next: {    revalidate:      cachePolicy.liveEventsRevalidate,    tags: [      cachePolicy.tags.liveEvents,    ],  },});if (!response.ok) {  console.warn(    \"Ticketmaster search request failed\",    response.status,  );  return {    ok: false,    reasonKey: \"api-error\",  };}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>401<\/code>, <code>429<\/code> \u0438\u043b\u0438 <code>500<\/code> \u043d\u0435\u00a0\u043f\u0435\u0440\u0435\u0434\u0430\u044e\u0442\u0441\u044f \u0432 <code>response.json()<\/code> \u043a\u0430\u043a\u00a0\u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0439 events response.<\/p>\n<p>Ticketmaster \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0438\u0440\u0443\u0435\u0442 quota \u0434\u043b\u044f\u00a0Discovery API \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 HTTP <code>429<\/code>, \u043a\u043e\u0433\u0434\u0430 quota \u043f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u0430. <a href=\"https:\/\/developer.ticketmaster.com\/products-and-docs\/apis\/getting-started\/\" rel=\"noopener nofollow\">\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f Ticketmaster<\/a> \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u044d\u0442\u043e\u0442 \u043e\u0442\u0432\u0435\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e. \u0412\u00a0\u043f\u0440\u043e\u0435\u043a\u0442\u0435 <code>429<\/code> \u043d\u0435\u00a0\u0438\u043c\u0435\u0435\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0439 runtime\u2011\u0432\u0435\u0442\u043a\u0438. \u041b\u044e\u0431\u043e\u0439 <code>!response.ok<\/code> \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432 <code>api-error<\/code>, status \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u0432\u00a0server log.<\/p>\n<h3>\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u0435\u0442\u0438<\/h3>\n<p><code>fetch<\/code> \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c\u0441\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c \u0434\u043e\u00a0\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f HTTP response. \u041e\u0448\u0438\u0431\u043a\u0430 \u043c\u043e\u0436\u0435\u0442 \u0432\u043e\u0437\u043d\u0438\u043a\u043d\u0443\u0442\u044c \u0438 \u0432\u043e\u00a0\u0432\u0440\u0435\u043c\u044f \u0447\u0442\u0435\u043d\u0438\u044f JSON. \u0421\u0435\u0440\u0432\u0435\u0440\u043d\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u043e\u0431\u0430 \u0441\u043b\u0443\u0447\u0430\u044f:<\/p>\n<pre><code class=\"typescript\">try {  const response = await fetch(url, {    next: {      revalidate:        cachePolicy.liveEventsRevalidate,      tags: [        cachePolicy.tags.liveEvents,      ],    },  });  if (!response.ok) {    return {      ok: false,      reasonKey: \"api-error\",    };  }  const data: unknown =    await response.json();  \/\/ parse + normalise} catch {  console.warn(    \"Ticketmaster search request failed before fallback\",  );  return {    ok: false,    reasonKey: \"api-error\",  };}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041e\u0448\u0438\u0431\u043a\u0430 provider \u043d\u0435\u00a0\u0432\u044b\u0445\u043e\u0434\u0438\u0442 \u0438\u0437\u00a0data function \u0432\u00a0Server Component.<\/p>\n<h3>\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 live source<\/h3>\n<p>Search service \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 discriminated union:<\/p>\n<pre><code class=\"typescript\">type LiveSearchResult =  | {      ok: true;      events: EventCardData[];      totalCount: number;      totalPages: number;      currentPage: number;    }  | {      ok: false;      reasonKey:        | \"missing-api-key\"        | \"api-error\"        | \"empty-live-response\"        | \"invalid-live-response\"        | \"empty-normalised-response\";    };                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0423\u0441\u043f\u0435\u0448\u043d\u0430\u044f \u0432\u0435\u0442\u043a\u0430 \u0432\u0441\u0435\u0433\u0434\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0435 <code>EventCardData<\/code>. \u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430\u044f \u0432\u0435\u0442\u043a\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043f\u0440\u0438\u0447\u0438\u043d\u0443 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u043a\u00a0\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c\u0443 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0443.<\/p>\n<h3>\u041f\u0443\u0441\u0442\u043e\u0439 \u043e\u0442\u0432\u0435\u0442<\/h3>\n<p>\u0412\u0430\u043b\u0438\u0434\u043d\u044b\u0439 response \u043c\u043e\u0436\u0435\u0442 \u043d\u0435\u00a0\u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0441\u043e\u0431\u044b\u0442\u0438\u0439:<\/p>\n<pre><code class=\"typescript\">const rawEvents =  (parsed.data._embedded?.events ?? [])    .filter(      isTicketmasterEventOnOrAfterStartDate,    );if (rawEvents.length === 0) {  return {    ok: false,    reasonKey: \"empty-live-response\",  };}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u0443\u0441\u0442\u043e\u0439 \u043c\u0430\u0441\u0441\u0438\u0432 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442\u00a0invalid response. \u041e\u0431\u0430 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043d\u0435\u00a0\u0442\u0440\u0435\u0431\u0443\u044e\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0432\u00a0React component.<\/p>\n<h3>Controlled fallback<\/h3>\n<p>Fallback \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u0432\u00a0\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. Search \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u0442 live source:<\/p>\n<pre><code class=\"typescript\">export async function getSearchPageEvents({  filters,  locale,}: {  filters: SearchFilters;  locale: Locale;}): Promise&lt;SearchPageEventsResult&gt; {  const liveResult =    await searchLiveEvents({      filters,      locale,    });  if (liveResult.ok) {    return {      ...liveResult,      hasPreviousPage:        liveResult.currentPage &gt; 1,      hasNextPage:        liveResult.currentPage &lt;        liveResult.totalPages,      source:        getSourceInfo({          type: \"live\",          reasonKey: \"live-results\",        }),    };  }  const filteredEvents =    await searchControlledEvents({      filters,    });  const pagination =    paginateEvents({      events: filteredEvents,      page: filters.page,    });  return {    ...pagination,    source:      getSourceInfo({        type: \"fallback\",        reasonKey:          liveResult.reasonKey,      }),  };}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Live source \u0438 fallback \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u044e\u0442 \u043e\u0434\u043d\u0443 <code>SearchPageEventsResult<\/code>. React component \u043d\u0435\u00a0\u043c\u0435\u043d\u044f\u0435\u0442 EventCard \u0434\u043b\u044f\u00a0\u0434\u0432\u0443\u0445 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432.<\/p>\n<h3>\u0424\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f controlled source<\/h3>\n<p>Fallback \u043d\u0435\u00a0\u043e\u0431\u044f\u0437\u0430\u043d \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0442\u044c \u043f\u043e\u043b\u043d\u044b\u0439 \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u043a\u0430\u0442\u0430\u043b\u043e\u0433. \u041e\u043d \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441\u00a0\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u044b\u043c \u043d\u0430\u0431\u043e\u0440\u043e\u043c \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f. \u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f, \u0433\u043e\u0440\u043e\u0434 \u0438 query \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u044e\u0442\u0441\u044f \u043a\u00a0controlled events:<\/p>\n<pre><code class=\"typescript\">export async function searchControlledEvents({  filters,}: {  filters: SearchFilters;}): Promise&lt;EventCardData[]&gt; {  return controlledEventCards.filter(    (event) =&gt;      matchesCategory(        event,        filters.category,      ) &amp;&amp;      matchesCity(        event,        filters.city,      ) &amp;&amp;      matchesQuery(        event,        filters.q,      ),  );}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>URL \u043f\u043e\u0438\u0441\u043a\u0430 \u043d\u0435\u00a0\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u0441\u043b\u0435 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u00a0live source \u043d\u0430\u00a0fallback.<\/p>\n<h3>Home \u0438 search<\/h3>\n<p>\u041e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u0430\u044f \u0441\u0445\u0435\u043c\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0430\u00a0\u0433\u043b\u0430\u0432\u043d\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435:<\/p>\n<pre><code class=\"typescript\">export async function getHomeEvents({  locale,}: GetFeaturedEventsInput): Promise&lt;HomeEventsResult&gt; {  const liveEvents =    await getLiveTicketmasterEvents({      locale,    });  if (liveEvents.length &gt; 0) {    return {      source: \"ticketmaster\",      events: liveEvents,    };  }  return {    source: \"controlled\",    events:      await getControlledFeaturedEvents({        locale,      }),  };}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Live response \u0441\u00a0\u0441\u043e\u0431\u044b\u0442\u0438\u044f\u043c\u0438 \u0438\u0434\u0451\u0442 \u0432\u00a0UI. \u041f\u0443\u0441\u0442\u043e\u0439 \u0438\u043b\u0438\u00a0\u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0439 source \u0437\u0430\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f controlled events.<\/p>\n<h3>Detail page<\/h3>\n<p>\u0423\u00a0detail route \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432 \u0434\u0440\u0443\u0433\u043e\u0439. \u041f\u0440\u043e\u0435\u043a\u0442 \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u0438\u0449\u0435\u0442 event \u0441\u0440\u0435\u0434\u0438 controlled data:<\/p>\n<pre><code class=\"typescript\">export async function fetchEventById({  id,  locale,}: {  id: string;  locale: Locale;}): Promise&lt;EventCardData | null&gt; {  const controlledEvent =    await getControlledEventById({      id,      locale,    });  if (controlledEvent) {    return controlledEvent;  }  return fetchLiveTicketmasterEventById({    id,    locale,  });}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0418\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435 controlled IDs \u043d\u0435\u00a0\u0442\u0440\u0435\u0431\u0443\u044e\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0432\u043e\u00a0\u0432\u043d\u0435\u0448\u043d\u0438\u0439 API. \u0415\u0441\u043b\u0438 ID \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0432\u00a0controlled source, \u0441\u0435\u0440\u0432\u0435\u0440 \u043f\u0440\u043e\u0431\u0443\u0435\u0442 live detail endpoint.<\/p>\n<h3>Static params<\/h3>\n<p>Controlled IDs \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0438 \u043f\u0440\u0438\u00a0\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 detail routes:<\/p>\n<pre><code class=\"typescript\">export function getControlledEventIds():  string[] {  return controlledEventCards.map(    (event) =&gt; event.id,  );}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0414\u0430\u043b\u044c\u0448\u0435 locale \u0438 ID \u0444\u043e\u0440\u043c\u0438\u0440\u0443\u044e\u0442 static params:<\/p>\n<pre><code class=\"typescript\">export function getStaticEventParams():  StaticEventParams[] {  const eventIds =    getControlledEventIds();  return locales.flatMap(    (locale) =&gt;      eventIds.map((id) =&gt; ({        locale,        id,      })),  );}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Static detail pages \u0441\u0442\u0440\u043e\u044f\u0442\u0441\u044f \u0438\u0437\u00a0\u043d\u0430\u0431\u043e\u0440\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. Live IDs \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 dynamic params.<\/p>\n<h3>429\u00a0\u0438 build<\/h3>\n<p>\u0414\u043b\u044f\u00a0known detail pages <code>fetchEventById<\/code> \u043d\u0430\u0445\u043e\u0434\u0438\u0442 controlled event \u0434\u043e\u00a0\u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430. \u0422\u0430\u043a\u043e\u0439 route \u043d\u0435\u00a0\u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442\u00a0Ticketmaster response \u043f\u0440\u0438\u00a0\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u044f. Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0434\u0440\u0443\u0433\u0443\u044e \u0441\u0445\u0435\u043c\u0443 \u0438 \u043f\u0440\u043e\u0431\u0443\u0435\u0442 live source. \u0422\u0430\u043c <code>!response.ok<\/code> \u0438 <code>catch<\/code> \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u044e\u0442 \u043f\u0443\u0441\u0442\u043e\u0439 live result, \u043f\u043e\u0441\u043b\u0435 \u0447\u0435\u0433\u043e \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u0442 controlled events. \u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e API \u043d\u0435\u00a0\u0432\u044b\u0431\u0440\u0430\u0441\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0438\u0437\u00a0\u044d\u0442\u0438\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u043d\u0430\u0440\u0443\u0436\u0443. <code>429<\/code> \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 \u0442\u043e\u0442\u00a0\u0436\u0435 \u043a\u043e\u0434, \u0447\u0442\u043e\u00a0\u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 non-2xx responses.<\/p>\n<h3>Cache policy<\/h3>\n<p>Live source \u043d\u0435\u00a0\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u00a0\u043a\u0430\u0436\u0434\u043e\u043c \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0438. \u041f\u0440\u043e\u0435\u043a\u0442 \u0445\u0440\u0430\u043d\u0438\u0442 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u044b \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e:<\/p>\n<pre><code class=\"typescript\">export const cachePolicy = {  liveEventsRevalidate: 300,  liveEventDetailRevalidate: 600,  tags: {    liveEvents: \"live-events\",    liveEventDetail:      \"live-event-detail\",  },} as const;                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Search \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442 policy \u0432\u00a0server\u2011side <code>fetch<\/code>:<\/p>\n<pre><code class=\"typescript\">const response = await fetch(url, {  next: {    revalidate:      cachePolicy.liveEventsRevalidate,    tags: [      cachePolicy.tags.liveEvents,    ],  },});                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Next.js \u0440\u0430\u0441\u0448\u0438\u0440\u044f\u0435\u0442 server\u2011side <code>fetch<\/code> \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043c\u0438 <code>next.revalidate<\/code> \u0438 <code>next.tags<\/code>. <code>revalidate<\/code> \u0437\u0430\u0434\u0430\u0451\u0442 \u0441\u0440\u043e\u043a \u0436\u0438\u0437\u043d\u0438 cached resource, tags \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0434\u043b\u044f\u00a0on\u2011demand revalidation. <a href=\"https:\/\/nextjs.org\/docs\/app\/api-reference\/functions\/fetch\" rel=\"noopener nofollow\">API reference <code>fetch<\/code><\/a> \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u043e\u0431\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430. \u041a\u044d\u0448 \u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0441\u044f \u043a\u00a0live source. Controlled events \u043b\u0435\u0436\u0430\u0442 \u0432\u043d\u0443\u0442\u0440\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u043d\u0435\u00a0\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u0432\u043d\u0435\u0448\u043d\u0438\u0439 fetch.<\/p>\n<h3>Source reason<\/h3>\n<p>Data layer \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u043f\u0440\u0438\u0447\u0438\u043d\u0443 fallback:<\/p>\n<pre><code class=\"typescript\">export type SearchSourceReason =  | \"live-results\"  | \"missing-api-key\"  | \"api-error\"  | \"empty-live-response\"  | \"invalid-live-response\"  | \"empty-normalised-response\"  | \"controlled-fallback\";                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0421\u0435\u0440\u0432\u0438\u0441 \u043c\u043e\u0436\u0435\u0442 \u0440\u0430\u0437\u043b\u0438\u0447\u0438\u0442\u044c \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 API key, HTTP\/network failure, \u043f\u0443\u0441\u0442\u043e\u0439 provider response \u0438 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0443\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443. \u042d\u0442\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043d\u0435\u00a0\u0432\u0445\u043e\u0434\u044f\u0442 \u0432 <code>EventCardData<\/code>.<\/p>\n<h3>Raw data \u0434\u043b\u044f\u00a0\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 Zod<\/h3>\n<p>\u0412\u00a0\u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u0435\u0441\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 controlled \u043d\u0430\u0431\u043e\u0440 raw records. \u0427\u0430\u0441\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e \u0441\u043b\u043e\u043c\u0430\u043d\u0430. \u041e\u0434\u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435\u00a0\u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 <code>name<\/code>:<\/p>\n<pre><code class=\"typescript\">{  id: \"broken-without-name\",  location: {    city: \"Berlin\",    country: \"Germany\",  },  category: \"Design\",  date: {    label: \"30 July\",  },  venue: {    name: \"Design Factory\",  },  description:    \"This raw item should not reach UI.\",  price: \"free\",}                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0414\u0440\u0443\u0433\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 numeric <code>id<\/code>. \u0415\u0449\u0451 \u043e\u0434\u043d\u0430 \u043d\u0435\u00a0\u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 <a href=\"http:\/\/location.city\" rel=\"noopener nofollow\"><code>location.city<\/code><\/a>. \u0412\u0441\u0435 \u043e\u043d\u0438 \u043f\u0440\u043e\u0445\u043e\u0434\u044f\u0442 \u0447\u0435\u0440\u0435\u0437:<\/p>\n<pre><code class=\"typescript\">const parsed =  RawEventSchema.safeParse(rawEvent);if (!parsed.success) {  return [];}return [  normaliseEvent(parsed.data),];                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0417\u0430\u043f\u0438\u0441\u044c, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435\u00a0\u043f\u0440\u043e\u0448\u043b\u0430 schema, \u043d\u0435\u00a0\u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432\u00a0\u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0443.<\/p>\n<h3>Tests \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440\u0430<\/h3>\n<p>\u041d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043e\u0442\u00a0React components. \u041e\u0434\u0438\u043d \u0442\u0435\u0441\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 Ticketmaster event:<\/p>\n<pre><code class=\"typescript\">expect(  normaliseTicketmasterEvent(    ticketmasterEvent,  ),).toEqual({  id: \"tm-1\",  title:    \"Ticketmaster Music Night\",  city: \"London\",  country: \"United Kingdom\",  category: \"Music\",  dateLabel: \"2026-06-12\",  venue: \"Roundhouse\",  description:    \"Live show from Ticketmaster.\",  imageUrl: \"\/tm-wide.jpg\",  genreLabel: \"Rock\",  priceLabel: \"from 25 GBP\",  ticketUrl:    \"https:\/\/example.com\/tickets\",  timeLabel: \"19:30\",});                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0414\u0440\u0443\u0433\u0438\u0435 \u0442\u0435\u0441\u0442\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u044e\u0442 <code>pleaseNote<\/code>, fallback description, time \u0438 image selection.<\/p>\n<h3>Tests \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439<\/h3>\n<p><code>pickBestImage<\/code> \u0438\u043c\u0435\u0435\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u0441\u043b\u0443\u0447\u0430\u0435\u0432:<\/p>\n<pre><code class=\"typescript\">it(  \"returns the placeholder for undefined images\",  () =&gt; {    expect(      pickBestImage(undefined),    ).toBe(      EVENT_PLACEHOLDER_IMAGE,    );  },);it(  \"chooses the widest wide image\",  () =&gt; {    expect(      pickBestImage([        {          url: \"\/small.jpg\",          width: 300,          height: 200,        },        {          url: \"\/wide-600.jpg\",          width: 600,          height: 300,        },        {          url: \"\/wide-1000.jpg\",          width: 1000,          height: 500,        },      ]),    ).toBe(\"\/wide-1000.jpg\");  },);                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412\u044b\u0431\u043e\u0440 \u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u0431\u0435\u0437\u00a0\u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e API.<\/p>\n<h3>\u041a\u043e\u043d\u0442\u0443\u0440 \u0434\u0430\u043d\u043d\u044b\u0445<\/h3>\n<p>Live path \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 \u0447\u0435\u0442\u044b\u0440\u0435 \u0448\u0430\u0433\u0430:<\/p>\n<pre><code class=\"vbscript\">Ticketmaster HTTP response        \u2193      Zod        \u2193   normaliser        \u2193 EventCardData        \u2193       UI                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0439 search path \u0438\u0434\u0451\u0442 \u0447\u0435\u0440\u0435\u0437 controlled source:<\/p>\n<pre><code class=\"vbscript\">missing API keyHTTP error \/ 429network errorinvalid responseempty response        \u2193controlled events        \u2193EventCardData        \u2193       UI                  <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Server Component \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043e\u0434\u0438\u043d \u0442\u0438\u043f \u0434\u0430\u043d\u043d\u044b\u0445 \u0432\u00a0\u043e\u0431\u0435\u0438\u0445 \u0432\u0435\u0442\u043a\u0430\u0445. API key \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u043d\u0430\u00a0\u0441\u0435\u0440\u0432\u0435\u0440\u0435. Provider JSON \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u0434\u043e\u00a0normalizer. Normalizer \u043d\u0435\u00a0\u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442 \u0432\u043d\u0435\u0448\u043d\u044e\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u0432\u00a0\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b. Search \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u00a0controlled source \u043f\u0440\u0438\u00a0\u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u043c live source. Known detail pages \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u0447\u0438\u0442\u0430\u044e\u0442 controlled data. Cache policy \u0437\u0430\u0434\u0430\u0451\u0442\u0441\u044f \u0440\u044f\u0434\u043e\u043c \u0441\u00a0server fetch.<\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1083308\/\">https:\/\/habr.com\/ru\/articles\/1083308\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u0412\u043d\u0435\u0448\u043d\u0438\u0439 API \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432\u00a0\u0441\u0432\u043e\u0435\u0439 \u043c\u043e\u0434\u0435\u043b\u0438. \u041d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u0435\u0439, \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f, \u0446\u0435\u043d\u044b \u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0442 provider. UI \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043e\u0431\u044b\u0447\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441\u00a0\u0434\u0440\u0443\u0433\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u044c\u044e. \u0415\u0441\u043b\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0447\u0438\u0442\u0430\u0435\u0442 event._embedded.venues[0].city.name \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e, \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u043e\u0442\u0432\u0435\u0442\u0430 \u0440\u0430\u0441\u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u00a0\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443. \u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 API \u0437\u0430\u0442\u0440\u0430\u0433\u0438\u0432\u0430\u0435\u0442 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0438, detail page, search, metadata \u0438 \u0442\u0435\u0441\u0442\u044b.\u0412\u00a0App Router \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043d\u0430\u00a0\u0441\u0435\u0440\u0432\u0435\u0440\u0435. Server Components \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u0441 fetch, \u0430\u00a0\u0441\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 \u043d\u0435\u00a0\u043f\u043e\u043f\u0430\u0434\u0430\u0435\u0442 \u0432\u00a0\u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0439 bundle. API key \u043f\u0440\u0438\u00a0\u0442\u0430\u043a\u043e\u0439 \u0441\u0445\u0435\u043c\u0435 \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u0432\u043d\u0435 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430. \u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f Next.js \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 server\u2011side data fetching \u0434\u043b\u044f\u00a0App Router. \u0412\u00a0\u0443\u0447\u0435\u0431\u043d\u043e\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0435\u00a0EventMap \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 server API \u0441\u043b\u043e\u0439.Server boundary\u041a\u043b\u044e\u0447 \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043d\u0435\u00a0\u043d\u0443\u0436\u0435\u043d Client Component. \u0415\u0433\u043e \u0447\u0438\u0442\u0430\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f:export function getTicketmasterApiKey(): string | null {  const key = process.env.TICKETMASTER_API_KEY;  return key &amp;&amp; key.trim().length &gt; 0    ? key    : null;}                  \u041f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u043d\u0435\u00a0\u0438\u043c\u0435\u0435\u0442 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u0430 NEXT_PUBLIC_.URL \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0442\u043e\u0436\u0435 \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u00a0\u0441\u0435\u0440\u0432\u0435\u0440\u0435:const TICKETMASTER_EVENTS_URL =  &#171;https:\/\/app.ticketmaster.com\/discovery\/v2\/events.json&#187;;export function buildEventsUrl({  apiKey,  locale,  city,  keyword,}: BuildEventsUrlInput): string {  void locale;  const url = new URL(TICKETMASTER_EVENTS_URL);  url.searchParams.set(&#171;apikey&#187;, apiKey);  url.searchParams.set(&#171;size&#187;, &#171;12&#187;);  url.searchParams.set(&#171;sort&#187;, &#171;date,asc&#187;);  url.searchParams.set(&#171;locale&#187;, &#171;*&#187;);  url.searchParams.set(    &#171;startDateTime&#187;,    getLiveEventsStartDateTime(),  );  if (city) {    url.searchParams.set(&#171;city&#187;, city);  }  if (keyword) {    url.searchParams.set(&#171;keyword&#187;, keyword);  }  return url.toString();}                  UI \u043d\u0435\u00a0\u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 API key \u0438 \u043d\u0435\u00a0\u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442 Ticketmaster URL.JSON \u043f\u043e\u0441\u043b\u0435 response.json()TypeScript \u043d\u0435\u00a0\u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435, \u043f\u0440\u0438\u0448\u0435\u0434\u0448\u0438\u0435 \u043f\u043e\u00a0\u0441\u0435\u0442\u0438. \u0422\u0438\u043f \u0432\u00a0\u0438\u0441\u0445\u043e\u0434\u043d\u043e\u043c \u043a\u043e\u0434\u0435 \u0438\u0441\u0447\u0435\u0437\u0430\u0435\u0442 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u043c\u043f\u0438\u043b\u044f\u0446\u0438\u0438, \u0430\u00a0\u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 \u043c\u043e\u0436\u0435\u0442 \u0432\u0435\u0440\u043d\u0443\u0442\u044c \u0434\u0440\u0443\u0433\u0443\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443.\u041f\u043e\u0441\u043b\u0435 response.json() \u043f\u0440\u043e\u0435\u043a\u0442 \u0445\u0440\u0430\u043d\u0438\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u043a\u0430\u043a unknown:const data: unknown = await response.json();                  \u0414\u0430\u043b\u044c\u0448\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u043e\u0445\u043e\u0434\u044f\u0442 runtime\u2011\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443.Zod \u0440\u0430\u0437\u0434\u0435\u043b\u044f\u0435\u0442 \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0439 \u0438 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0447\u0435\u0440\u0435\u0437 safeParse. \u041c\u0435\u0442\u043e\u0434 \u043d\u0435\u00a0\u0431\u0440\u043e\u0441\u0430\u0435\u0442 ZodError, \u0430\u00a0\u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u044a\u0435\u043a\u0442 \u0441 success \u0438 data\u00a0\u043b\u0438\u0431\u043e error. \u0422\u0430\u043a\u0430\u044f \u0444\u043e\u0440\u043c\u0430 \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 \u0434\u043b\u044f\u00a0source boundary, \u0433\u0434\u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u0432\u00a0\u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439. \u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f Zod \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u0442\u043e\u0442\u00a0\u0436\u0435 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 safeParse.\u0421\u0445\u0435\u043c\u0430 \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f\u0421\u0445\u0435\u043c\u0430 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u0435 \u0447\u0430\u0441\u0442\u0438 \u043e\u0442\u0432\u0435\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0443\u0436\u043d\u044b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044e:import { z } from &#171;zod&#187;;export const TicketmasterEventSchema = z.object({  id: z.string(),  name: z.string(),  info: z.string().optional(),  pleaseNote: z.string().optional(),  url: z.string().optional(),  images: z    .array(      z.object({        url: z.string(),        width: z.number().optional(),        height: z.number().optional(),      }),    )    .optional(),  dates: z    .object({      start: z        .object({          localDate: z.string().optional(),          localTime: z.string().optional(),        })        .optional(),    })    .optional(),  classifications: z    .array(      z.object({        genre: z          .object({            name: z.string().optional(),          })          .optional(),        segment: z          .object({            name: z.string().optional(),          })          .optional(),      }),    )    .optional(),});                  id \u0438 name \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b. \u0418\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f, venue, \u0434\u0430\u0442\u0430, classification \u0438 price \u043c\u043e\u0433\u0443\u0442 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c.\u041e\u0442\u0432\u0435\u0442 search endpoint \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0445\u0435\u043c\u043e\u0439:export const TicketmasterEventsResponseSchema =  z.object({    _embedded: z      .object({        events:          z.array(TicketmasterEventSchema).optional(),      })      .optional(),    page: z      .object({        totalElements: z.number().optional(),        totalPages: z.number().optional(),        number: z.number().optional(),        size: z.number().optional(),      })      .optional(),  });                  Collection response \u0438 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u0438\u043c\u0435\u044e\u0442 \u0440\u0430\u0437\u043d\u044b\u0435 \u0441\u0445\u0435\u043c\u044b.safeParse \u043f\u0435\u0440\u0435\u0434 \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439Search \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 JSON \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0435\u0433\u043e \u0434\u043e\u00a0\u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u043a\u00a0\u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u043c \u043f\u043e\u043b\u044f\u043c:const data: unknown = await response.json();const parsed =  TicketmasterEventsResponseSchema.safeParse(data);if (!parsed.success) {  console.warn(    &#171;Ticketmaster search response shape is invalid&#187;,  );  return {    ok: false,    reasonKey: &#171;invalid-live-response&#187;,  };}                  \u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u044b\u0439 JSON \u043d\u0435\u00a0\u043f\u043e\u043f\u0430\u0434\u0430\u0435\u0442 \u0432\u00a0normalizer \u0438 React components.Detail request \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u0445\u0435\u043c\u0443 \u043e\u0434\u043d\u043e\u0433\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f:const data: unknown = await response.json();const parsed =  TicketmasterEventSchema.safeParse(data);if (!parsed.success) {  console.warn(    &#171;Ticketmaster detail response shape is invalid&#187;,  );  return null;}return normaliseTicketmasterEvent(parsed.data);                  \u041f\u043e\u0441\u043b\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u0433\u043e safeParse TypeScript \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0442\u0438\u043f, \u0432\u044b\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u0438\u0437\u00a0Zod schema.\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043c\u043e\u0434\u0435\u043b\u044cUI \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u0432\u043e\u0439 \u0442\u0438\u043f:export type EventCardData = {  id: string;  title: string;  city: string;  country: string;  category: string;  dateLabel: string;  venue: string;  description: string;  imageUrl: string;  genreLabel?: string;  priceLabel?: string;  ticketUrl?: string;  timeLabel?: string;};                  \u0412\u00a0\u043d\u0451\u043c \u043d\u0435\u0442 _embedded, classifications, priceRanges \u0438 \u0434\u0440\u0443\u0433\u0438\u0445 Ticketmaster structures.Normalizer \u0447\u0438\u0442\u0430\u0435\u0442 provider model \u0438 \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442 EventCardData:export function normaliseTicketmasterEvent(  ticketmasterEvent: TicketmasterEvent,): EventCardData {  const venue =    ticketmasterEvent._embedded?.venues?.[0];  const classification =    ticketmasterEvent.classifications?.[0];  const category =    classification?.segment?.name;  const genre =    classification?.genre?.name;  const description =    ticketmasterEvent.info?.trim() ||    ticketmasterEvent.pleaseNote?.trim() ||    &#171;Event details are coming soon.&#187;;  return {    id: ticketmasterEvent.id,    title: ticketmasterEvent.name,    city:      venue?.city?.name ??      &#171;Unknown city&#187;,    country:      venue?.country?.name ??      &#171;Unknown country&#187;,    category:      category ??      &#171;Event&#187;,    dateLabel:      ticketmasterEvent.dates?.start?.localDate ??      &#171;Date TBA&#187;,    venue:      venue?.name ??      &#171;Venue TBA&#187;,    description,    imageUrl:      pickBestImage(ticketmasterEvent.images),    genreLabel: genre,    ticketUrl:      ticketmasterEvent.url,    timeLabel:      ticketmasterEvent.dates?.start?.localTime        ?.slice(0, 5),  };}                  \u041a\u0430\u0440\u0442\u043e\u0447\u043a\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u0443\u044e \u0444\u043e\u0440\u043c\u0443 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u043e\u0442\u00a0\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b provider response.\u041d\u0435\u043f\u043e\u043b\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435\u0412\u043d\u0435\u0448\u043d\u044f\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043c\u043e\u0436\u0435\u0442\u00a0\u0431\u044b\u0442\u044c \u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0439 \u043f\u043e\u00a0schema \u0438 \u043f\u0440\u0438\u00a0\u044d\u0442\u043e\u043c \u043d\u0435\u00a0\u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c venue, description \u0438\u043b\u0438\u00a0image. Schema \u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0442\u0430\u043a\u0438\u0435 \u043f\u043e\u043b\u044f optional. Normalizer \u0440\u0435\u0448\u0430\u0435\u0442, \u0447\u0442\u043e\u00a0\u043f\u043e\u043b\u0443\u0447\u0438\u0442 UI.Description \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e:const description =  ticketmasterEvent.info?.trim() ||  ticketmasterEvent.pleaseNote?.trim() ||  &#171;Event details are coming soon.&#187;;                  Venue \u0438 \u0434\u0430\u0442\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u044e\u0442 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0435 fallback:venue:  venue?.name ?? &#171;Venue TBA&#187;,dateLabel:  ticketmasterEvent.dates?.start?.localDate ??  &#171;Date TBA&#187;,                  \u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443 \u043d\u0435\u00a0\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0439 \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u0439 provider field.\u0412\u044b\u0431\u043e\u0440 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044fAPI \u043c\u043e\u0436\u0435\u0442 \u0432\u0435\u0440\u043d\u0443\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439 \u043e\u0434\u043d\u043e\u0433\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u041f\u0435\u0440\u0432\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0432\u00a0\u043c\u0430\u0441\u0441\u0438\u0432\u0435 \u043d\u0435\u00a0\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0435. \u041f\u0440\u043e\u0435\u043a\u0442 \u0444\u0438\u043b\u044c\u0442\u0440\u0443\u0435\u0442 \u043f\u0443\u0441\u0442\u044b\u0435 URL, \u0437\u0430\u0442\u0435\u043c \u0438\u0449\u0435\u0442 \u0448\u0438\u0440\u043e\u043a\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0448\u0438\u0440\u0438\u043d\u043e\u0439 \u043e\u0442 600\u00a0px:export const EVENT_PLACEHOLDER_IMAGE =  &#171;\/event-placeholder.svg&#187;;export function pickBestImage(  images: TicketmasterImage[] | undefined,): string {  if (!images || images.length === 0) {    return EVENT_PLACEHOLDER_IMAGE;  }  const validImages =    images.filter(      (image) =&gt;        image.url.trim().length &gt; 0,    );  if (validImages.length === 0) {    return EVENT_PLACEHOLDER_IMAGE;  }  const wideImage = validImages    .filter(      (image) =&gt;        typeof image.width === &#171;number&#187; &amp;&amp;        image.width &gt;= 600 &amp;&amp;        (image.height ?? 0) &lt;= image.width,    )    .sort(      (a, b) =&gt;        (b.width ?? 0) &#8212;        (a.width ?? 0),    )[0];  if (wideImage) {    return wideImage.url;  }  const imagesWithWidth =    validImages.filter(      (image) =&gt;        typeof image.width === &#171;number&#187;,    );  if (imagesWithWidth.length === 0) {    return validImages[0].url;  }  return (    imagesWithWidth.sort(      (a, b) =&gt;        (b.width ?? 0) &#8212;        (a.width ?? 0),    )[0]?.url ??    EVENT_PLACEHOLDER_IMAGE  );}                  \u0415\u0441\u043b\u0438 \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0435\u0439 \u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438 \u043d\u0435\u0442, UI \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 placeholder.HTTP status \u0434\u043e\u00a0ZodZod \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 JSON structure. HTTP status \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u0440\u0430\u043d\u044c\u0448\u0435.const response = await fetch(url, {  next: {    revalidate:      cachePolicy.liveEventsRevalidate,    tags: [      cachePolicy.tags.liveEvents,    ],  },});if (!response.ok) {  console.warn(    &#171;Ticketmaster search request failed&#187;,    response.status,  );  return {    ok: false,    reasonKey: &#171;api-error&#187;,  };}                  401, 429 \u0438\u043b\u0438 500 \u043d\u0435\u00a0\u043f\u0435\u0440\u0435\u0434\u0430\u044e\u0442\u0441\u044f \u0432 response.json() \u043a\u0430\u043a\u00a0\u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0439 events response.Ticketmaster \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0438\u0440\u0443\u0435\u0442 quota \u0434\u043b\u044f\u00a0Discovery API \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 HTTP 429, \u043a\u043e\u0433\u0434\u0430 quota \u043f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u0430. \u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f Ticketmaster \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u044d\u0442\u043e\u0442 \u043e\u0442\u0432\u0435\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e. \u0412\u00a0\u043f\u0440\u043e\u0435\u043a\u0442\u0435 429 \u043d\u0435\u00a0\u0438\u043c\u0435\u0435\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0439 runtime\u2011\u0432\u0435\u0442\u043a\u0438. \u041b\u044e\u0431\u043e\u0439 !response.ok \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432 api-error, status \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u0432\u00a0server log.\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u0435\u0442\u0438fetch \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c\u0441\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c \u0434\u043e\u00a0\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f HTTP response. \u041e\u0448\u0438\u0431\u043a\u0430 \u043c\u043e\u0436\u0435\u0442 \u0432\u043e\u0437\u043d\u0438\u043a\u043d\u0443\u0442\u044c \u0438 \u0432\u043e\u00a0\u0432\u0440\u0435\u043c\u044f \u0447\u0442\u0435\u043d\u0438\u044f JSON. \u0421\u0435\u0440\u0432\u0435\u0440\u043d\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u043e\u0431\u0430 \u0441\u043b\u0443\u0447\u0430\u044f:try {  const response = await fetch(url, {    next: {      revalidate:        cachePolicy.liveEventsRevalidate,      tags: [        cachePolicy.tags.liveEvents,      ],    },  });  if (!response.ok) {    return {      ok: false,      reasonKey: &#171;api-error&#187;,    };  }  const data: unknown =    await response.json();  \/\/ parse + normalise} catch {  console.warn(    &#171;Ticketmaster search request failed before fallback&#187;,  );  return {    ok: false,    reasonKey: &#171;api-error&#187;,  };}                  \u041e\u0448\u0438\u0431\u043a\u0430 provider \u043d\u0435\u00a0\u0432\u044b\u0445\u043e\u0434\u0438\u0442 \u0438\u0437\u00a0data function \u0432\u00a0Server Component.\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 live sourceSearch service \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 discriminated&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-494995","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/494995","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=494995"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/494995\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=494995"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=494995"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=494995"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}