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 boilerplateOur 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_fragmentfunction with the external ID of the point of sale.
Recommended values
| Setting | Recommended value | Why |
|---|---|---|
| Connection timeout | 1 second | Fail fast when the network is unreachable |
| Total request timeout | 2 seconds | A slow call must never hold your page |
| Cache duration | 24 hours | Fragments are regenerated every 24 hours |
| Cache duration for a 404 | 1 hour | A newly enabled point of sale appears the same day |
| Retry delay after error | 60 seconds | During an incident, don't wait for a timeout on every page view |
| Stale copy retention | 7 days | Keep 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 GMTIf 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, atext/htmlcontent 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
5xxresponse, 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
404means 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-Typeheader has no charset: with Pythonrequests, useresponse.content.decode("utf-8")rather thanresponse.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) orrawurlencode(PHP). - Always use HTTPS and keep certificate verification enabled.
- Content Security Policy (CSP). The fragment loads a stylesheet from
cdn.goodays.coand 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/htmlresponses 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
AggregateRatingmarkup 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.
Updated about 1 hour ago