Integration best practices

Integration best practices

Goodays GEO fragment API serves static files from a CDN, so it is fast and highly available. Your pages should still never depend on a network call to render. The recommendations below keep your website fast and resilient, and make sure crawlers always receive your reviews.

👍

Skip the boilerplate

Our ready-to-use clients for Python, Node.js and PHP implement every recommendation on this page. Copy the client into your project and call its get_fragment function with the external ID of the point of sale.


Recommended values

SettingRecommended valueWhy
Connection timeout1 secondFail fast when the network is unreachable
Total request timeout2 secondsA slow call must never hold your page
Cache duration24 hoursFragments are regenerated every 24 hours
Cache duration for a 4041 hourA newly enabled point of sale appears the same day
Retry delay after error60 secondsDuring an incident, don't wait for a timeout on every page view
Stale copy retention7 daysKeep showing reviews if Goodays is unreachable

1. Call the API from your server

Fetch the fragment on your server and include it in the HTML your server returns.

  • AI crawlers (ChatGPT, Claude, Perplexity, Gemini…) don't execute JavaScript. A fragment injected in the browser is invisible to them.
  • The API doesn't send CORS headers, so browser calls are blocked anyway.
  • Don't load the fragment in an iframe: its content would not be part of your page.

2. Set short timeouts

Always set an explicit timeout. Most HTTP libraries wait indefinitely by default.

requests.get(url, timeout=(1, 2))  # connect, read (seconds)
await fetch(url, { signal: AbortSignal.timeout(2000) }); // whole request, body included
$client->get($url, ['connect_timeout' => 1, 'timeout' => 2]);

3. Cache fragments locally

Don't call the API on every page view. Fragments change once a day at most, so keep each one in a local cache for 24 hours.

When a cached copy expires, revalidate it instead of downloading it again. Send the ETag and Last-Modified values you stored with the request:

GET /clients/style-style/049
If-None-Match: "0x8DF08D1C75AD185"
If-Modified-Since: Wed, 02 Sep 2026 09:08:39 GMT

If the fragment hasn't changed, the API answers 304 Not Modified with an empty body. Keep your copy and reset its 24-hour lifetime.

Choose a cache shared by your application instances. An in-memory cache is enough for a single process. With several workers or servers, use your shared cache (Redis, Memcached, your framework's cache) so they all reuse the same copy. PHP doesn't keep memory between requests, so use APCu, a filesystem cache or Redis.

Use one cache key per universe and point of sale, for example goodays-geo:{universe_id}:{external_id}.

Refresh each point of sale once. When a cached copy expires under heavy traffic, let a single request refresh it while the others wait for that result or keep serving the current copy.


4. Fail gracefully

Your page must render even if Goodays is unreachable.

  • Only insert valid responses. Only use a response with status 200, a text/html content type and a non-empty body. Error responses come with an XML body that must never reach your page.
  • Serve a stale copy on error. On a timeout, network error or 5xx response, keep serving the last valid fragment, for up to 7 days.
  • Back off after an error. Don't call the API again for 60 seconds, otherwise every page view waits for the timeout during an incident.
  • Without any copy, render the page without the reviews block. Hide the reviews section and its title. Don't show an error message.
  • Cache 404 responses for one hour. A 404 means the universe ID or the external ID is unknown. Caching it avoids calling the API on every page view of that point of sale.

The complete logic, as implemented by our ready-to-use clients:

get_fragment(external_id):
  entry = cache.get("goodays-geo:{universe_id}:{external_id}")
  if entry is fresh: return entry.html

  GET https://geo.goodays.co/clients/{universe_id}/{external_id}
      timeout 1 s connect / 2 s total
      If-None-Match: entry.etag, If-Modified-Since: entry.last_modified

  200 + text/html  → store html, ETag, Last-Modified, fresh for 24 h → return html
  304              → mark entry fresh for 24 h                       → return entry.html
  404              → remember "no fragment" for 1 h                  → return nothing
  error / timeout  → entry exists?  mark fresh for 60 s              → return entry.html
                     otherwise      remember "no fragment" for 60 s  → return nothing

  Valid entries are kept 7 days, to be served as stale copies.

5. Prefer a background refresh for high-traffic or static sites

Instead of fetching fragments while serving pages, you can refresh them in the background:

  • Scheduled job: once a day, fetch the fragment of each point of sale and store it in your database or CMS. Your pages then read it locally, with no network call at all.
  • Static site generators and ISR (Next.js, Nuxt, Astro, Gatsby…): fetch the fragment at build or render time and regenerate pages every 24 hours. If a call fails, keep the previous version of the page rather than publishing one without reviews.

The same rules apply: set timeouts, only store valid responses, and keep the previous fragment when a refresh fails.


6. Insert the fragment correctly

  • Output it unescaped. Template engines escape variables by default. Use |safe (Jinja, Django), <%- (EJS), {{{ }}} (Handlebars), dangerouslySetInnerHTML (React SSR), {!! !!} (Blade) or |raw (Twig).
  • Don't sanitize it. HTML sanitizers such as WordPress wp_kses_post() remove the JSON-LD <script>, the inline <style> block, the script of the "See more reviews" button and the stylesheet <link>.
  • Keep the whole fragment and its order: the stylesheet <link>, the inline <style> block, the <section> (including the inline script before the reviews) and the JSON-LD <script>. Don't edit or reorder its content, or defer the inline script: it collapses the list before the reviews are first displayed.
  • One fragment per page, on the page dedicated to that place, near its address and opening hours. The widget already includes a "Customer reviews" heading; the place name remains in the JSON-LD.
  • Serve your page as UTF-8. The fragment is UTF-8 encoded. Some HTTP libraries assume another encoding when the Content-Type header has no charset: with Python requests, use response.content.decode("utf-8") rather than response.text.

Without JavaScript. All reviews remain visible and the inactive toggle button is hidden, provided the styles are loaded and allowed by your site. With JavaScript enabled, the first 5 reviews are initially displayed and the button reveals the rest.


7. Avoid duplicate structured data

The fragment contains a JSON-LD block with the AggregateRating of the point of sale and its reviews. If your page already declares ratings for the same place, from another review widget or a hand-written schema, remove them. Conflicting ratings for the same business confuse search engines and AI assistants.

You can keep your other structured data, such as Organization, BreadcrumbList or Product.


8. Security

  • Build the URL from your own data. Take the external ID from your own point of sale data, never directly from the query string or a form field.
  • URL-encode the parameters. Use encodeURIComponent (JavaScript), urllib.parse.quote (Python) or rawurlencode (PHP).
  • Always use HTTPS and keep certificate verification enabled.
  • Content Security Policy (CSP). The fragment loads a stylesheet from cdn.goodays.co and includes inline styles and a short inline script. If your website enforces a restrictive CSP, contact your Goodays project contact before integration so we can review compatibility and agree on the appropriate setup.

Checklist

  • The API is called from your server, never from the browser.
  • Requests have a 1-second connection timeout and a 2-second total timeout.
  • Fragments are cached for 24 hours in a cache shared by your instances.
  • Expired fragments are revalidated with If-None-Match / If-Modified-Since.
  • Only 200 + text/html responses are inserted.
  • On errors, the last valid fragment is served and retries are spaced by 60 seconds.
  • Without a fragment, the page renders without the reviews block.
  • The fragment is output unescaped and unmodified, in the initial HTML.
  • The fragment is kept whole and in its original order, and its inline script is not deferred.
  • No other AggregateRating markup exists for the same point of sale.
  • The external ID comes from your own data and is URL-encoded.
  • If your site enforces a restrictive CSP, compatibility has been reviewed with your Goodays project contact.
  • A crawler request returns the fragment and its JSON-LD.