If you can’t (or don’t want to) change your backend templates, this integration lets you add Enhancely JSON‑LD using:
- one JS snippet you paste into GTM (or
<head>) - one small proxy endpoint on your domain (keeps your Enhancely API key secret)
- Never put
ENHANCELY_API_KEYin GTM or client-side code.
GTM runs in the browser. Anything in it is public. - You must host a proxy endpoint on your domain:
POST https://YOUR_DOMAIN/_enhancely/jsonld
- Production security: configure
ALLOWED_HOSTSon your proxy/worker to prevent abuse.
- Client snippet:
enhancely-js.js - Proxy templates:
- Cloudflare Worker:
proxy/cloudflare/worker.mjs - Node (dockerized):
proxy/node/server.js
- Cloudflare Worker:
- Example pages:
- Local demo:
example/index.html(editproxyUrl) - Same-domain server demo:
example/server.html(uses/_enhancely/jsonld) - “Processing” demo:
example/processing.html(exercises 201/202 retry flow)
- Local demo:
- Deploy the proxy
- Use Cloudflare Worker or your own server (Node/PHP/etc.)
- Set
ENHANCELY_API_KEYserver-side - Restrict allowed hosts (recommended)
- Paste the snippet
- GTM Custom HTML tag (recommended) or site
<head> - Set
proxyUrl
- Verify
- Proxy returns JSON‑LD + ETag
- Page has exactly one JSON‑LD
<script>in<head> - Reload shows 304/412 behavior (ETag revalidation)
You need:
- Endpoint:
POST /_enhancely/jsonld - Request body:
{ "url": "https://yourdomain.com/some/page" } - Optional header:
If-None-Match: <etag>(no quotes)
The proxy must:
- Keep
ENHANCELY_API_KEYserver-side - Call Enhancely:
POST https://app.enhancely.ai/api/v1/jsonld - Forward
If-None-Match - Return:
- 200 with JSON
{ status, etag, jsonld }when ready - 304 or 412 (empty body) when not modified
- 200 with JSON
Deploy proxy/cloudflare/worker.mjs.
Env vars:
ENHANCELY_API_KEY=...
ENHANCELY_API_URL=https://app.enhancely.ai/api/v1
ALLOWED_HOSTS=example.com,www.example.comRoute it so it serves /_enhancely/jsonld on the same domain as your site.
Minimal Worker deploy (example):
- Create a Worker and set a route for
https://YOUR_DOMAIN/_enhancely/jsonld* - Set secrets/vars:
ENHANCELY_API_KEYas a secretENHANCELY_API_URLas a variable (optional)ALLOWED_HOSTSas a variable (recommended)
See proxy/cloudflare/README.md for a wrangler-based deploy walkthrough.
Use the provided Node Dockerfile:
proxy/node/Dockerfile
You can run it with any Docker platform, behind your existing reverse proxy (recommended for same-domain routing).
Hetzner VPS note (common setup):
- Run the container on the server (e.g.
127.0.0.1:8787) - In your web server (nginx/Caddy/Apache), route
/_enhancely/jsonldto that local port so the public URL stays same-domain:- nginx:
location = /_enhancely/jsonld {
proxy_pass http://127.0.0.1:8787/_enhancely/jsonld;
}If you can’t add a route, you can still use a port URL as proxyUrl (less ideal):
proxyUrl: "https://YOUR_DOMAIN:8787/_enhancely/jsonld"
Env example:
proxy/node/.env.example
Important: if you deploy via rsync, exclude .env:
rsync -av --delete --exclude '.env' --exclude '.DS_Store' enhancely-js/ YOUR_SERVER:/path/to/enhancely-js/Node Docker quickstart:
# build
docker build -t enhancely-jsonld-proxy ./proxy/node
# run (create your own ./proxy/node/.env first)
docker run --rm -p 8787:8787 --env-file ./proxy/node/.env enhancely-jsonld-proxyNotes:
- Node runtime: the container uses Node 20 (fetch is built-in).
- Security: set
ALLOWED_HOSTSfor production. For legacy behavior withoutALLOWED_HOSTS, you can setALLOW_SAME_HOST_FALLBACK=1(not recommended).
Create a Custom HTML tag and paste:
<script>
window.EnhancelyJsonldConfig = {
proxyUrl: "https://YOUR_DOMAIN/_enhancely/jsonld",
// Optional:
// debug: true,
// observeSpa: true,
// If your site differentiates pages/sites via query params (e.g. ?lang=de or ?site=foo),
// list them here so the correct schema is fetched per variant:
// includeQueryParams: ["lang", "site"],
};
</script>
<script>
/* Paste the contents of enhancely-js.js here */
</script>Paste the same config + snippet as early as possible in <head>.
curl -sS -i -X POST 'https://YOUR_DOMAIN/_enhancely/jsonld' \
-H 'Content-Type: application/json' \
-d '{"url":"https://YOUR_DOMAIN/some/page"}' | head -n 25Expected:
HTTP 200- JSON includes
status: 200,etag, andjsonldwith@context
Copy the returned etag (it should have no quotes) and re-run:
curl -sS -i -X POST 'https://YOUR_DOMAIN/_enhancely/jsonld' \
-H 'Content-Type: application/json' \
-H 'If-None-Match: PASTE_ETAG_HERE' \
-d '{"url":"https://YOUR_DOMAIN/some/page"}' | head -n 25Expected:
HTTP 304orHTTP 412- empty body
Open your page and confirm <head> contains exactly one:
<script type="application/ld+json" id="enhancely-jsonld">
If debug: true, you’ll also see [enhancely] logs in the console.
Set via window.EnhancelyJsonldConfig before loading/pasting enhancely-js.js:
- proxyUrl (string, required): Proxy endpoint URL (recommended same-domain), e.g.
"/_enhancely/jsonld"or"https://example.com/_enhancely/jsonld". - scriptId (string, default
enhancely-jsonld): The injected<script>element id. - storage (
localStorage|sessionStorage|none, defaultlocalStorage): Where to cache{etag,jsonld,ts}. - cachePrefix (string, default
enhancely:jsonld:): Storage key prefix. - revalidateAfterMs (number, default 300000): Cache freshness window. Fresh cache injects without a network call.
- timeoutMs (number, default 4000): Network timeout for the proxy request.
- debug (boolean, default
false): Logs"[enhancely]"debug lines. - includeQueryParams (string[], default
[]): Query params to keep in the canonical URL identity. All other query params are stripped. - observeSpa (boolean, default
false): Re-run on SPA navigation (pushState/replaceState/popstate). - processingRetries (number, default 4): Retry count when status is
201/202. - processingRetryDelayMs (number, default 4000): Delay between retries.
- Cache is stored in browser storage (default
localStorage) per canonical URL. - On first ready fetch, it caches
{ etag, jsonld, ts }. - On later loads:
- if cache is “fresh” (default 5 minutes), it injects immediately with no network call
- if cache is “stale”, it revalidates with
If-None-Match - on
304/412, it keeps the cached JSON‑LD
Sometimes Enhancely needs time to crawl a fresh URL and returns 201/202 first.
This snippet:
- does not inject the 201/202 “problem” payload
- retries a few times (configurable) and only injects once real JSON‑LD is ready (
status: 200and has@context)
-
Proxy returns
500 Missing ENHANCELY_API_KEY- Your server env isn’t set (or the container wasn’t recreated after changing
.env) - If using Docker: recreate/restart the proxy container after changing env vars
- Your server env isn’t set (or the container wasn’t recreated after changing
-
Proxy returns
500 Missing ALLOWED_HOSTS- Set
ALLOWED_HOSTS=example.com,www.example.comon the proxy/worker (recommended for production) - For local dev, use
localhostor explicitly setALLOW_SAME_HOST_FALLBACK=1(not recommended)
- Set
-
Proxy returns
403 URL host not allowed- Add your domain to
ALLOWED_HOSTS(or you’re testing a different host than your site)
- Add your domain to
-
No
<script id="enhancely-jsonld">appears- Confirm
proxyUrlis correct and reachable - Turn on
debug: trueand check console logs - If your site is an SPA, enable
observeSpa: true
- Confirm
-
SEO tooling doesn’t see the JSON‑LD
- Client-injected JSON‑LD can be less reliable than SSR. Validate with Google Rich Results Test and Search Console.
- Keep the proxy same-domain when possible (avoid CORS complexity).
- Configure
ALLOWED_HOSTSto prevent the endpoint being used to generate JSON-LD for arbitrary domains. - Add basic rate limiting at your edge / reverse proxy (especially for public endpoints).