GunSpec
Rezept

Eine Produktseite bauen

Eine Waffenseite von Anfang bis Ende: vom Namen zur ID, der Datensatz, seine Medien, die Händler, und was bei fehlenden Feldern zu zeigen ist.

Vier Aufrufe. Die letzten drei hängen nicht voneinander ab und laufen deshalb gemeinsam statt nacheinander.

  1. Den Namen auflösenAus einem Slug oder Suchbegriff wird eine Katalog-ID, mit einem Status, auf den Sie reagieren können.
  2. Den Datensatz holenDie vollständigen Daten, Hersteller, Kaliber und Nutzer, dazu die Version, gegen die Sie cachen.
  3. Die Medien holenFotos, Renderings und die Strichzeichnung, jeweils mit eigener Quellenangabe.
  4. Die Angebote holenHändler, die zu diesen Lesern liefern, mit Preisen in kleinster Einheit und gezähltem Ausgangslink.
  5. Das Ergebnis cachenETag und Version danebenlegen und die Seite aus dem Cache ausliefern, bis sich eines davon bewegt.

Mit dem TypeScript-SDK geschrieben, und er läuft auf Ihrem Server, sodass der Schlüssel den Browser nie erreicht. Schlüssel im Produktivbetrieb behandelt das. Der Auflöseschritt lohnt zweimaliges Lesen, und Erst auflösen, dann lesen sagt warum.

load-firearm-page.ts
javascript
// One product page. Four calls, all cacheable, none of them// depending on an answer the previous one did not already give.export async function loadFirearmPage(query, region) {  // 1. A name becomes an id. Never search-and-take-the-first-hit.  const { data: resolved } = await gunspec.firearms.resolve(query)  if (resolved.status !== 'resolved') return { notFound: true, resolved }   const id = resolved.firearmId   // 2. The record, the media and the sellers are independent of each  //    other, so they go together rather than one after another.  const [firearm, media, offers] = await Promise.all([    gunspec.firearms.get(id),    gunspec.firearms.listMedia(id),    // Sellers who ship where this visitor is; the rest are noise.    gunspec.firearms.getOffers(id, { region }),  ])   return {    firearm: firearm.data,    // Fall back down the kinds rather than rendering a gap: a    // photograph does not exist for every record, and the line-art    // silhouette does for most of them and scales to any size.    hero: ['photo', 'render', 'silhouette'].reduce(      (found, kind) => found ?? media.data.find((m) => m.kind === kind),      undefined,    ),    // priceCents is an integer in minor units. Never parse it as a    // float, and format it in the currency the offer names.    offers: offers.data.map((offer) => ({      ...offer,      price: new Intl.NumberFormat(locale, { style: 'currency', currency: offer.currency })        .format(offer.priceCents / 100),      // Send readers through the counted link, not straight to      // the shop: it is what lets a seller see their placement work.      href: `${API}/v1/out/${offer.clickId}`,    })),    // Store these two beside your copy: `version` says whether the    // record changed, the ETag revalidates it for nothing.    version: firearm.data.version,  }}

Vier Anfragen bei kaltem Cache, null bei warmem. Eine Seite, die stattdessen vier Anfragen pro Aufruf verbraucht, ist der häufigste Weg, einen Tarif zu sprengen.

  • Cachen Sie das Ergebnis des Loaders, nicht die Einzelaufrufe: es ist die Seite, die Sie ausliefern, und ein Vergleich von version invalidiert sie. Caching nennt die Lebensdauern.
  • Revalidieren Sie mit dem gespeicherten ETag statt neu zu laden. Ein 304 kostet nichts vom Limit, wie Im Tarif bleiben erklärt.
  • Rendern Sie aus dem Cache, während Sie revalidieren. Niemand sollte auf unsere Latenz warten, um Daten zu sehen, die sich seit Dienstag nicht geändert haben.

Ein fehlendes Feld ist null. Das heißt, wir haben es nicht belegt, nicht dass der Wert null ist. 0 mm für eine unbekannte Lauflänge zu drucken ist der eine Fehler, den Leser nicht erkennen und nicht verzeihen. Datenqualität berichtet, wie gut jeder Katalog abgedeckt ist.

spec-rows.ts
javascript
// A spec sheet that does not invent what it does not have.function rows(firearm) {  return SPEC_FIELDS    // A null is "we have not sourced this", not zero. Printing 0 mm    // for an unknown barrel length is the one mistake a reader    // cannot detect and will not forgive.    .filter((field) => firearm[field.key] != null)    .map((field) => ({ label: field.label, value: field.format(firearm[field.key]) }))} // Confidence is about the record, not any one figure. Show the// sources the record carries beside the numbers, and link the// report route so a reader who knows better can tell us.const showSources = firearm.dataConfidence < 0.8 && firearm.sources?.length > 0

Assets kommen mit den Bedingungen, unter denen sie nutzbar sind; die Medienseite listet die Arten, Rechteinhaber die Bedingungen. Zwei Regeln gehören hierher.

  • Rendern Sie das credit, das ein Asset trägt. Das ist keine Zierde, sondern die Bedingung der Auslieferung.
  • Schicken Sie Leser über /v1/out/{clickId} zum Händler statt direkt in den Shop, damit der Besuch dem Händler zugeordnet wird, dessen Angebot Sie gezeigt haben.
  • Gehen Sie die Arten abwärts durch (Foto, Rendering, Strichzeichnung), statt eine Lücke zu rendern. Die meisten Datensätze haben eine Zeichnung, nicht alle ein Foto.