Repository files navigation

smartive Utilities

A collection of general purpose utilities and helpers for web projects.

Installation

npm install @smartive/utils

The root export (@smartive/utils) stays dependency-free. Optional peer dependencies are only required when you import the corresponding subpath.

Utilities

classNames

Cleans and joins an array of class names (strings and numbers), filtering out undefined and boolean values.

import{classNames}from'@smartive/utils';constclassName=classNames('btn',isActive&&'btn-active',42,undefined,'btn-primary');// Result: "btn btn-active 42 btn-primary"

getTelLink

Converts a phone number into a tel: link by removing non-digit characters (except + for international numbers).

import{getTelLink}from'@smartive/utils';constlink=getTelLink('+1 (555) 123-4567');// Result: "tel:+15551234567"

@smartive/utils/http

Framework-agnostic HTTP helpers for token checks, open-redirect protection, and CORS headers.

import{isSafeRelativePath,isValidToken,withCORS}from'@smartive/utils/http';isValidToken(request.headers.get('Webhook-Token'),process.env.CACHE_INVALIDATION_SECRET_TOKEN);isSafeRelativePath('/relative/path');withCORS({status: 401});

No environment variables are required by this subpath; callers pass secrets explicitly.

@smartive/utils/datocms

Typed GraphQL client factory wrapping @datocms/cda-client.

npm install @datocms/cda-client
import{createDatoClient,queryDatoCMS}from'@smartive/utils/datocms';// Default client (reads env vars)constdata=awaitqueryDatoCMS({document: MyDocument,includeDrafts: true});// Or configure explicitlyconstquery=createDatoClient({apiToken: process.env.DATOCMS_API_TOKEN,revalidate: 60*60,});
Env varPurpose
DATOCMS_API_TOKENRead-only CDA token (draft-capable when needed)
DATOCMS_ENVIRONMENTOptional X-Environment header
NEXT_DATOCMS_BASE_EDITING_URLEnables Content Link headers when querying drafts

Config passed to createDatoClient always wins over environment variables.

@smartive/utils/datocms/next

The same client, backed by Next.js Cache Components instead of the fetch data cache. Queries are wrapped in use cache, tagged with a deterministic query ID, and given a cacheLife profile — so a DatoCMS publish can invalidate exactly the affected queries instead of purging the whole app.

Requires Next.js >= 16 with cacheComponents: true. Without that flag, use @smartive/utils/datocms.

npm install @datocms/cda-client @neondatabase/serverless
// lib/dato.tsimport{createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';import{createCachedDatoClient}from'@smartive/utils/datocms/next';exportconststore=createNeonCacheTagStore();exportconstqueryDatoCMS=createCachedDatoClient({ store });
// app/api/invalidate-cache-tags/route.tsimport{createCacheTagInvalidationHandler}from'@smartive/utils/next';import{store}from'@/lib/dato';exportconst{POST,GET,OPTIONS}=createCacheTagInvalidationHandler({ store });

Point a DatoCMS cache tags invalidation webhook at that POST route, with the secret in the Webhook-Token header.

There is deliberately no default client: one without a store looks like it works but can never be invalidated, so the store is a required, visible decision. Omitting it is still supported for local development — every entry then falls back to the short unstored lifetime.

cacheLife profiles

ProfileDefaultWhen it applies
cached'days'The tag mapping persisted, so the webhook can reach the entry
unstored'minutes'The mapping could not be stored — the entry must expire on its own
draft'seconds'Draft mode is enabled (Next writes nothing to the cache regardless)

Override per client via profiles, or per query via cacheProfile. A per-query override is ignored when the mapping did not persist, so it can never extend the lifetime of an entry the webhook cannot reach.

Draft mode

draftMode().isEnabled is read inside the cached scope, which Next.js permits. While draft mode is on, cached functions re-execute per request and nothing is written to the cache, so includeDrafts no longer needs threading through your data layer.

Pass includeDrafts: true (or skipCache: true) explicitly only when you need a guaranteed fresh read — preview slug resolution, for example. That bypasses the cached scope entirely.

@smartive/utils/cache-tags

The query-ID ⇄ DatoCMS cache-tag mapping used by @smartive/utils/datocms/next. Zero dependencies; no Next.js import.

import{buildQueryId,createMemoryCacheTagStore,parseXCacheTagsResponseHeader}from'@smartive/utils/cache-tags';

createMemoryCacheTagStore() is for tests, local development, and single-instance deployments only — on a serverless platform each instance would see a different subset of the mapping. It also accepts { configured: false } and { failing: true } for exercising fail-soft paths.

@smartive/utils/cache-tags/neon

import{cacheTagStoreSchemaSql,createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';conststore=createNeonCacheTagStore({onError: (error)=>captureException(error)});

Create the table once per Neon branch:

psql "$CACHETAGS_POSTGRES_URL" -c "$(node -e "import('@smartive/utils/cache-tags/neon').then((m) => console.log(m.cacheTagStoreSchemaSql()))")"

The store never throws: every method fails soft to a documented fallback and arms a 30-second backoff, because a store outage must degrade cache lifetimes rather than fail a page render. Writes report a boolean, and lookups return null on failure versus [] for "nothing matched" — that distinction is what drives the cacheLife choice and the webhook's 503-vs-200 response.

Isolate the store per DatoCMS environment (a Neon branch per deployment environment). The query ID already includes the resolved environment, but a table shared between apps also needs a tagPrefix.

Env varPurpose
CACHETAGS_POSTGRES_URLNeon connection string for the mapping table

One synthetic tag per query, rather than one tag per DatoCMS record, is deliberate: Vercel caps a cache entry at 128 tags.

@smartive/utils/next

Next.js App Router helpers for draft mode, DatoCMS web previews, and cache revalidation.

npm install next
// app/api/draft/enable/route.tsimport{createDraftHandlers}from'@smartive/utils/next';exportconst{enable: GET}=createDraftHandlers();// app/api/draft/preview-links/route.tsimport{createWebPreviewsHandler}from'@smartive/utils/next';exportconst{OPTIONS,POST}=createWebPreviewsHandler({baseUrl: 'https://example.com/api/draft',resolvePreviewUrl: async({ item, itemType })=>{if(itemType.attributes.api_key==='page')return`/${item.attributes.slug}`;returnnull;},});// app/api/revalidate-path/route.tsimport{createRevalidateHandler}from'@smartive/utils/next';exportconstPOST=createRevalidateHandler({paths: ['/sitemap.xml']});

Draft enable/disable and web-preview links use the url query parameter for redirect targets (e.g. /api/draft/enable?url=/page&token=…).

createRevalidateHandler revalidates '/' with 'layout' — the whole app — on every call. For per-query invalidation, use createCacheTagInvalidationHandler with a cache-tag store instead. createCacheTagInvalidateAllHandler covers the manual full reset (a missed webhook, or a change to the query-ID format).

Env varPurpose
DRAFT_SECRET_TOKENAuthorizes draft enable/disable and web-previews
CACHE_INVALIDATION_SECRET_TOKENAuthorizes the revalidate and cache-tag webhooks (Webhook-Token)

Migrating from @smartive/datocms-utils

This package was previously published as @smartive/datocms-utils. With 4.0.0 it was renamed to @smartive/utils and the old cache-tag utilities were removed.

  • classNames and getTelLink are unchanged — only the import specifier needs to be updated.
  • DatoCMS / Next.js helpers live under @smartive/utils/http, @smartive/utils/datocms, and @smartive/utils/next.

Cache tags

The cache-tag utilities are back, rebuilt for Next.js Cache Components. You no longer need to stay on @smartive/datocms-utils@3.

Removed in 4.0.0Replacement
CacheTagsProvider (interface)CacheTagStore from @smartive/utils/cache-tags
NeonCacheTagsProvidercreateNeonCacheTagStore (/cache-tags/neon)
NoopCacheTagsProvidercreateMemoryCacheTagStore (/cache-tags)
RedisCacheTagsProviderNot ported — open an issue if you need it
generateQueryIdbuildQueryIddifferent output format, see below
parseXCacheTagsResponseHeaderUnchanged, now from @smartive/utils/cache-tags
CacheTag (branded type)Plain string
CacheTagsInvalidateWebhookUnchanged, plus an isCacheTagsInvalidateWebhook guard

Behavioural changes worth knowing:

  • Query IDs changed format.buildQueryId emits <operationName>-<hash16> rather than a bare sha1, so every stored mapping from v3 is unreachable. Run the invalidate-all handler once after upgrading to clear them.
  • Stores never throw. The throwOnError option is gone: a store outage must degrade cache lifetimes, not fail a render. onError remains, for telemetry.
  • Return types carry more signal.storeQueryCacheTags returns boolean and queriesReferencingCacheTags returns string[] | null, which is what lets the client pick a cacheLife profile and the webhook distinguish 503 from 200.
  • buildQueryId needs no graphql dependency. It hashes the document structurally, ignoring loc, so graphql-tag output is stable across unrelated edits in the same file.

License

MIT © smartive AG

About

A set of utilities and helpers to work with DatoCMS in a Next.js project.

Resources

Stars

0 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

smartive Utilities

A collection of general purpose utilities and helpers for web projects.

Installation

npm install @smartive/utils

The root export (@smartive/utils) stays dependency-free. Optional peer dependencies are only required when you import the corresponding subpath.

Utilities

classNames

Cleans and joins an array of class names (strings and numbers), filtering out undefined and boolean values.

import{classNames}from'@smartive/utils';constclassName=classNames('btn',isActive&&'btn-active',42,undefined,'btn-primary');// Result: "btn btn-active 42 btn-primary"

getTelLink

Converts a phone number into a tel: link by removing non-digit characters (except + for international numbers).

import{getTelLink}from'@smartive/utils';constlink=getTelLink('+1 (555) 123-4567');// Result: "tel:+15551234567"

@smartive/utils/http

Framework-agnostic HTTP helpers for token checks, open-redirect protection, and CORS headers.

import{isSafeRelativePath,isValidToken,withCORS}from'@smartive/utils/http';isValidToken(request.headers.get('Webhook-Token'),process.env.CACHE_INVALIDATION_SECRET_TOKEN);isSafeRelativePath('/relative/path');withCORS({status: 401});

No environment variables are required by this subpath; callers pass secrets explicitly.

@smartive/utils/datocms

Typed GraphQL client factory wrapping @datocms/cda-client.

npm install @datocms/cda-client
import{createDatoClient,queryDatoCMS}from'@smartive/utils/datocms';// Default client (reads env vars)constdata=awaitqueryDatoCMS({document: MyDocument,includeDrafts: true});// Or configure explicitlyconstquery=createDatoClient({apiToken: process.env.DATOCMS_API_TOKEN,revalidate: 60*60,});
Env varPurpose
DATOCMS_API_TOKENRead-only CDA token (draft-capable when needed)
DATOCMS_ENVIRONMENTOptional X-Environment header
NEXT_DATOCMS_BASE_EDITING_URLEnables Content Link headers when querying drafts

Config passed to createDatoClient always wins over environment variables.

@smartive/utils/datocms/next

The same client, backed by Next.js Cache Components instead of the fetch data cache. Queries are wrapped in use cache, tagged with a deterministic query ID, and given a cacheLife profile — so a DatoCMS publish can invalidate exactly the affected queries instead of purging the whole app.

Requires Next.js >= 16 with cacheComponents: true. Without that flag, use @smartive/utils/datocms.

npm install @datocms/cda-client @neondatabase/serverless
// lib/dato.tsimport{createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';import{createCachedDatoClient}from'@smartive/utils/datocms/next';exportconststore=createNeonCacheTagStore();exportconstqueryDatoCMS=createCachedDatoClient({ store });
// app/api/invalidate-cache-tags/route.tsimport{createCacheTagInvalidationHandler}from'@smartive/utils/next';import{store}from'@/lib/dato';exportconst{POST,GET,OPTIONS}=createCacheTagInvalidationHandler({ store });

Point a DatoCMS cache tags invalidation webhook at that POST route, with the secret in the Webhook-Token header.

There is deliberately no default client: one without a store looks like it works but can never be invalidated, so the store is a required, visible decision. Omitting it is still supported for local development — every entry then falls back to the short unstored lifetime.

cacheLife profiles

ProfileDefaultWhen it applies
cached'days'The tag mapping persisted, so the webhook can reach the entry
unstored'minutes'The mapping could not be stored — the entry must expire on its own
draft'seconds'Draft mode is enabled (Next writes nothing to the cache regardless)

Override per client via profiles, or per query via cacheProfile. A per-query override is ignored when the mapping did not persist, so it can never extend the lifetime of an entry the webhook cannot reach.

Draft mode

draftMode().isEnabled is read inside the cached scope, which Next.js permits. While draft mode is on, cached functions re-execute per request and nothing is written to the cache, so includeDrafts no longer needs threading through your data layer.

Pass includeDrafts: true (or skipCache: true) explicitly only when you need a guaranteed fresh read — preview slug resolution, for example. That bypasses the cached scope entirely.

@smartive/utils/cache-tags

The query-ID ⇄ DatoCMS cache-tag mapping used by @smartive/utils/datocms/next. Zero dependencies; no Next.js import.

import{buildQueryId,createMemoryCacheTagStore,parseXCacheTagsResponseHeader}from'@smartive/utils/cache-tags';

createMemoryCacheTagStore() is for tests, local development, and single-instance deployments only — on a serverless platform each instance would see a different subset of the mapping. It also accepts { configured: false } and { failing: true } for exercising fail-soft paths.

@smartive/utils/cache-tags/neon

import{cacheTagStoreSchemaSql,createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';conststore=createNeonCacheTagStore({onError: (error)=>captureException(error)});

Create the table once per Neon branch:

psql "$CACHETAGS_POSTGRES_URL" -c "$(node -e "import('@smartive/utils/cache-tags/neon').then((m) => console.log(m.cacheTagStoreSchemaSql()))")"

The store never throws: every method fails soft to a documented fallback and arms a 30-second backoff, because a store outage must degrade cache lifetimes rather than fail a page render. Writes report a boolean, and lookups return null on failure versus [] for "nothing matched" — that distinction is what drives the cacheLife choice and the webhook's 503-vs-200 response.

Isolate the store per DatoCMS environment (a Neon branch per deployment environment). The query ID already includes the resolved environment, but a table shared between apps also needs a tagPrefix.

Env varPurpose
CACHETAGS_POSTGRES_URLNeon connection string for the mapping table

One synthetic tag per query, rather than one tag per DatoCMS record, is deliberate: Vercel caps a cache entry at 128 tags.

@smartive/utils/next

Next.js App Router helpers for draft mode, DatoCMS web previews, and cache revalidation.

npm install next
// app/api/draft/enable/route.tsimport{createDraftHandlers}from'@smartive/utils/next';exportconst{enable: GET}=createDraftHandlers();// app/api/draft/preview-links/route.tsimport{createWebPreviewsHandler}from'@smartive/utils/next';exportconst{OPTIONS,POST}=createWebPreviewsHandler({baseUrl: 'https://example.com/api/draft',resolvePreviewUrl: async({ item, itemType })=>{if(itemType.attributes.api_key==='page')return`/${item.attributes.slug}`;returnnull;},});// app/api/revalidate-path/route.tsimport{createRevalidateHandler}from'@smartive/utils/next';exportconstPOST=createRevalidateHandler({paths: ['/sitemap.xml']});

Draft enable/disable and web-preview links use the url query parameter for redirect targets (e.g. /api/draft/enable?url=/page&token=…).

createRevalidateHandler revalidates '/' with 'layout' — the whole app — on every call. For per-query invalidation, use createCacheTagInvalidationHandler with a cache-tag store instead. createCacheTagInvalidateAllHandler covers the manual full reset (a missed webhook, or a change to the query-ID format).

Env varPurpose
DRAFT_SECRET_TOKENAuthorizes draft enable/disable and web-previews
CACHE_INVALIDATION_SECRET_TOKENAuthorizes the revalidate and cache-tag webhooks (Webhook-Token)

Migrating from @smartive/datocms-utils

This package was previously published as @smartive/datocms-utils. With 4.0.0 it was renamed to @smartive/utils and the old cache-tag utilities were removed.

  • classNames and getTelLink are unchanged — only the import specifier needs to be updated.
  • DatoCMS / Next.js helpers live under @smartive/utils/http, @smartive/utils/datocms, and @smartive/utils/next.

Cache tags

The cache-tag utilities are back, rebuilt for Next.js Cache Components. You no longer need to stay on @smartive/datocms-utils@3.

Removed in 4.0.0Replacement
CacheTagsProvider (interface)CacheTagStore from @smartive/utils/cache-tags
NeonCacheTagsProvidercreateNeonCacheTagStore (/cache-tags/neon)
NoopCacheTagsProvidercreateMemoryCacheTagStore (/cache-tags)
RedisCacheTagsProviderNot ported — open an issue if you need it
generateQueryIdbuildQueryIddifferent output format, see below
parseXCacheTagsResponseHeaderUnchanged, now from @smartive/utils/cache-tags
CacheTag (branded type)Plain string
CacheTagsInvalidateWebhookUnchanged, plus an isCacheTagsInvalidateWebhook guard

Behavioural changes worth knowing:

  • Query IDs changed format.buildQueryId emits <operationName>-<hash16> rather than a bare sha1, so every stored mapping from v3 is unreachable. Run the invalidate-all handler once after upgrading to clear them.
  • Stores never throw. The throwOnError option is gone: a store outage must degrade cache lifetimes, not fail a render. onError remains, for telemetry.
  • Return types carry more signal.storeQueryCacheTags returns boolean and queriesReferencingCacheTags returns string[] | null, which is what lets the client pick a cacheLife profile and the webhook distinguish 503 from 200.
  • buildQueryId needs no graphql dependency. It hashes the document structurally, ignoring loc, so graphql-tag output is stable across unrelated edits in the same file.

License

MIT © smartive AG

About

A set of utilities and helpers to work with DatoCMS in a Next.js project.

Resources

Stars

0 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

smartive Utilities

A collection of general purpose utilities and helpers for web projects.

Installation

npm install @smartive/utils

The root export (@smartive/utils) stays dependency-free. Optional peer dependencies are only required when you import the corresponding subpath.

Utilities

classNames

Cleans and joins an array of class names (strings and numbers), filtering out undefined and boolean values.

import{classNames}from'@smartive/utils';constclassName=classNames('btn',isActive&&'btn-active',42,undefined,'btn-primary');// Result: "btn btn-active 42 btn-primary"

getTelLink

Converts a phone number into a tel: link by removing non-digit characters (except + for international numbers).

import{getTelLink}from'@smartive/utils';constlink=getTelLink('+1 (555) 123-4567');// Result: "tel:+15551234567"

@smartive/utils/http

Framework-agnostic HTTP helpers for token checks, open-redirect protection, and CORS headers.

import{isSafeRelativePath,isValidToken,withCORS}from'@smartive/utils/http';isValidToken(request.headers.get('Webhook-Token'),process.env.CACHE_INVALIDATION_SECRET_TOKEN);isSafeRelativePath('/relative/path');withCORS({status: 401});

No environment variables are required by this subpath; callers pass secrets explicitly.

@smartive/utils/datocms

Typed GraphQL client factory wrapping @datocms/cda-client.

npm install @datocms/cda-client
import{createDatoClient,queryDatoCMS}from'@smartive/utils/datocms';// Default client (reads env vars)constdata=awaitqueryDatoCMS({document: MyDocument,includeDrafts: true});// Or configure explicitlyconstquery=createDatoClient({apiToken: process.env.DATOCMS_API_TOKEN,revalidate: 60*60,});
Env varPurpose
DATOCMS_API_TOKENRead-only CDA token (draft-capable when needed)
DATOCMS_ENVIRONMENTOptional X-Environment header
NEXT_DATOCMS_BASE_EDITING_URLEnables Content Link headers when querying drafts

Config passed to createDatoClient always wins over environment variables.

@smartive/utils/datocms/next

The same client, backed by Next.js Cache Components instead of the fetch data cache. Queries are wrapped in use cache, tagged with a deterministic query ID, and given a cacheLife profile — so a DatoCMS publish can invalidate exactly the affected queries instead of purging the whole app.

Requires Next.js >= 16 with cacheComponents: true. Without that flag, use @smartive/utils/datocms.

npm install @datocms/cda-client @neondatabase/serverless
// lib/dato.tsimport{createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';import{createCachedDatoClient}from'@smartive/utils/datocms/next';exportconststore=createNeonCacheTagStore();exportconstqueryDatoCMS=createCachedDatoClient({ store });
// app/api/invalidate-cache-tags/route.tsimport{createCacheTagInvalidationHandler}from'@smartive/utils/next';import{store}from'@/lib/dato';exportconst{POST,GET,OPTIONS}=createCacheTagInvalidationHandler({ store });

Point a DatoCMS cache tags invalidation webhook at that POST route, with the secret in the Webhook-Token header.

There is deliberately no default client: one without a store looks like it works but can never be invalidated, so the store is a required, visible decision. Omitting it is still supported for local development — every entry then falls back to the short unstored lifetime.

cacheLife profiles

ProfileDefaultWhen it applies
cached'days'The tag mapping persisted, so the webhook can reach the entry
unstored'minutes'The mapping could not be stored — the entry must expire on its own
draft'seconds'Draft mode is enabled (Next writes nothing to the cache regardless)

Override per client via profiles, or per query via cacheProfile. A per-query override is ignored when the mapping did not persist, so it can never extend the lifetime of an entry the webhook cannot reach.

Draft mode

draftMode().isEnabled is read inside the cached scope, which Next.js permits. While draft mode is on, cached functions re-execute per request and nothing is written to the cache, so includeDrafts no longer needs threading through your data layer.

Pass includeDrafts: true (or skipCache: true) explicitly only when you need a guaranteed fresh read — preview slug resolution, for example. That bypasses the cached scope entirely.

@smartive/utils/cache-tags

The query-ID ⇄ DatoCMS cache-tag mapping used by @smartive/utils/datocms/next. Zero dependencies; no Next.js import.

import{buildQueryId,createMemoryCacheTagStore,parseXCacheTagsResponseHeader}from'@smartive/utils/cache-tags';

createMemoryCacheTagStore() is for tests, local development, and single-instance deployments only — on a serverless platform each instance would see a different subset of the mapping. It also accepts { configured: false } and { failing: true } for exercising fail-soft paths.

@smartive/utils/cache-tags/neon

import{cacheTagStoreSchemaSql,createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';conststore=createNeonCacheTagStore({onError: (error)=>captureException(error)});

Create the table once per Neon branch:

psql "$CACHETAGS_POSTGRES_URL" -c "$(node -e "import('@smartive/utils/cache-tags/neon').then((m) => console.log(m.cacheTagStoreSchemaSql()))")"

The store never throws: every method fails soft to a documented fallback and arms a 30-second backoff, because a store outage must degrade cache lifetimes rather than fail a page render. Writes report a boolean, and lookups return null on failure versus [] for "nothing matched" — that distinction is what drives the cacheLife choice and the webhook's 503-vs-200 response.

Isolate the store per DatoCMS environment (a Neon branch per deployment environment). The query ID already includes the resolved environment, but a table shared between apps also needs a tagPrefix.

Env varPurpose
CACHETAGS_POSTGRES_URLNeon connection string for the mapping table

One synthetic tag per query, rather than one tag per DatoCMS record, is deliberate: Vercel caps a cache entry at 128 tags.

@smartive/utils/next

Next.js App Router helpers for draft mode, DatoCMS web previews, and cache revalidation.

npm install next
// app/api/draft/enable/route.tsimport{createDraftHandlers}from'@smartive/utils/next';exportconst{enable: GET}=createDraftHandlers();// app/api/draft/preview-links/route.tsimport{createWebPreviewsHandler}from'@smartive/utils/next';exportconst{OPTIONS,POST}=createWebPreviewsHandler({baseUrl: 'https://example.com/api/draft',resolvePreviewUrl: async({ item, itemType })=>{if(itemType.attributes.api_key==='page')return`/${item.attributes.slug}`;returnnull;},});// app/api/revalidate-path/route.tsimport{createRevalidateHandler}from'@smartive/utils/next';exportconstPOST=createRevalidateHandler({paths: ['/sitemap.xml']});

Draft enable/disable and web-preview links use the url query parameter for redirect targets (e.g. /api/draft/enable?url=/page&token=…).

createRevalidateHandler revalidates '/' with 'layout' — the whole app — on every call. For per-query invalidation, use createCacheTagInvalidationHandler with a cache-tag store instead. createCacheTagInvalidateAllHandler covers the manual full reset (a missed webhook, or a change to the query-ID format).

Env varPurpose
DRAFT_SECRET_TOKENAuthorizes draft enable/disable and web-previews
CACHE_INVALIDATION_SECRET_TOKENAuthorizes the revalidate and cache-tag webhooks (Webhook-Token)

Migrating from @smartive/datocms-utils

This package was previously published as @smartive/datocms-utils. With 4.0.0 it was renamed to @smartive/utils and the old cache-tag utilities were removed.

  • classNames and getTelLink are unchanged — only the import specifier needs to be updated.
  • DatoCMS / Next.js helpers live under @smartive/utils/http, @smartive/utils/datocms, and @smartive/utils/next.

Cache tags

The cache-tag utilities are back, rebuilt for Next.js Cache Components. You no longer need to stay on @smartive/datocms-utils@3.

Removed in 4.0.0Replacement
CacheTagsProvider (interface)CacheTagStore from @smartive/utils/cache-tags
NeonCacheTagsProvidercreateNeonCacheTagStore (/cache-tags/neon)
NoopCacheTagsProvidercreateMemoryCacheTagStore (/cache-tags)
RedisCacheTagsProviderNot ported — open an issue if you need it
generateQueryIdbuildQueryIddifferent output format, see below
parseXCacheTagsResponseHeaderUnchanged, now from @smartive/utils/cache-tags
CacheTag (branded type)Plain string
CacheTagsInvalidateWebhookUnchanged, plus an isCacheTagsInvalidateWebhook guard

Behavioural changes worth knowing:

  • Query IDs changed format.buildQueryId emits <operationName>-<hash16> rather than a bare sha1, so every stored mapping from v3 is unreachable. Run the invalidate-all handler once after upgrading to clear them.
  • Stores never throw. The throwOnError option is gone: a store outage must degrade cache lifetimes, not fail a render. onError remains, for telemetry.
  • Return types carry more signal.storeQueryCacheTags returns boolean and queriesReferencingCacheTags returns string[] | null, which is what lets the client pick a cacheLife profile and the webhook distinguish 503 from 200.
  • buildQueryId needs no graphql dependency. It hashes the document structurally, ignoring loc, so graphql-tag output is stable across unrelated edits in the same file.

License

MIT © smartive AG

About

A set of utilities and helpers to work with DatoCMS in a Next.js project.

Resources

Stars

0 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

smartive Utilities

A collection of general purpose utilities and helpers for web projects.

Installation

npm install @smartive/utils

The root export (@smartive/utils) stays dependency-free. Optional peer dependencies are only required when you import the corresponding subpath.

Utilities

classNames

Cleans and joins an array of class names (strings and numbers), filtering out undefined and boolean values.

import{classNames}from'@smartive/utils';constclassName=classNames('btn',isActive&&'btn-active',42,undefined,'btn-primary');// Result: "btn btn-active 42 btn-primary"

getTelLink

Converts a phone number into a tel: link by removing non-digit characters (except + for international numbers).

import{getTelLink}from'@smartive/utils';constlink=getTelLink('+1 (555) 123-4567');// Result: "tel:+15551234567"

@smartive/utils/http

Framework-agnostic HTTP helpers for token checks, open-redirect protection, and CORS headers.

import{isSafeRelativePath,isValidToken,withCORS}from'@smartive/utils/http';isValidToken(request.headers.get('Webhook-Token'),process.env.CACHE_INVALIDATION_SECRET_TOKEN);isSafeRelativePath('/relative/path');withCORS({status: 401});

No environment variables are required by this subpath; callers pass secrets explicitly.

@smartive/utils/datocms

Typed GraphQL client factory wrapping @datocms/cda-client.

npm install @datocms/cda-client
import{createDatoClient,queryDatoCMS}from'@smartive/utils/datocms';// Default client (reads env vars)constdata=awaitqueryDatoCMS({document: MyDocument,includeDrafts: true});// Or configure explicitlyconstquery=createDatoClient({apiToken: process.env.DATOCMS_API_TOKEN,revalidate: 60*60,});
Env varPurpose
DATOCMS_API_TOKENRead-only CDA token (draft-capable when needed)
DATOCMS_ENVIRONMENTOptional X-Environment header
NEXT_DATOCMS_BASE_EDITING_URLEnables Content Link headers when querying drafts

Config passed to createDatoClient always wins over environment variables.

@smartive/utils/datocms/next

The same client, backed by Next.js Cache Components instead of the fetch data cache. Queries are wrapped in use cache, tagged with a deterministic query ID, and given a cacheLife profile — so a DatoCMS publish can invalidate exactly the affected queries instead of purging the whole app.

Requires Next.js >= 16 with cacheComponents: true. Without that flag, use @smartive/utils/datocms.

npm install @datocms/cda-client @neondatabase/serverless
// lib/dato.tsimport{createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';import{createCachedDatoClient}from'@smartive/utils/datocms/next';exportconststore=createNeonCacheTagStore();exportconstqueryDatoCMS=createCachedDatoClient({ store });
// app/api/invalidate-cache-tags/route.tsimport{createCacheTagInvalidationHandler}from'@smartive/utils/next';import{store}from'@/lib/dato';exportconst{POST,GET,OPTIONS}=createCacheTagInvalidationHandler({ store });

Point a DatoCMS cache tags invalidation webhook at that POST route, with the secret in the Webhook-Token header.

There is deliberately no default client: one without a store looks like it works but can never be invalidated, so the store is a required, visible decision. Omitting it is still supported for local development — every entry then falls back to the short unstored lifetime.

cacheLife profiles

ProfileDefaultWhen it applies
cached'days'The tag mapping persisted, so the webhook can reach the entry
unstored'minutes'The mapping could not be stored — the entry must expire on its own
draft'seconds'Draft mode is enabled (Next writes nothing to the cache regardless)

Override per client via profiles, or per query via cacheProfile. A per-query override is ignored when the mapping did not persist, so it can never extend the lifetime of an entry the webhook cannot reach.

Draft mode

draftMode().isEnabled is read inside the cached scope, which Next.js permits. While draft mode is on, cached functions re-execute per request and nothing is written to the cache, so includeDrafts no longer needs threading through your data layer.

Pass includeDrafts: true (or skipCache: true) explicitly only when you need a guaranteed fresh read — preview slug resolution, for example. That bypasses the cached scope entirely.

@smartive/utils/cache-tags

The query-ID ⇄ DatoCMS cache-tag mapping used by @smartive/utils/datocms/next. Zero dependencies; no Next.js import.

import{buildQueryId,createMemoryCacheTagStore,parseXCacheTagsResponseHeader}from'@smartive/utils/cache-tags';

createMemoryCacheTagStore() is for tests, local development, and single-instance deployments only — on a serverless platform each instance would see a different subset of the mapping. It also accepts { configured: false } and { failing: true } for exercising fail-soft paths.

@smartive/utils/cache-tags/neon

import{cacheTagStoreSchemaSql,createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';conststore=createNeonCacheTagStore({onError: (error)=>captureException(error)});

Create the table once per Neon branch:

psql "$CACHETAGS_POSTGRES_URL" -c "$(node -e "import('@smartive/utils/cache-tags/neon').then((m) => console.log(m.cacheTagStoreSchemaSql()))")"

The store never throws: every method fails soft to a documented fallback and arms a 30-second backoff, because a store outage must degrade cache lifetimes rather than fail a page render. Writes report a boolean, and lookups return null on failure versus [] for "nothing matched" — that distinction is what drives the cacheLife choice and the webhook's 503-vs-200 response.

Isolate the store per DatoCMS environment (a Neon branch per deployment environment). The query ID already includes the resolved environment, but a table shared between apps also needs a tagPrefix.

Env varPurpose
CACHETAGS_POSTGRES_URLNeon connection string for the mapping table

One synthetic tag per query, rather than one tag per DatoCMS record, is deliberate: Vercel caps a cache entry at 128 tags.

@smartive/utils/next

Next.js App Router helpers for draft mode, DatoCMS web previews, and cache revalidation.

npm install next
// app/api/draft/enable/route.tsimport{createDraftHandlers}from'@smartive/utils/next';exportconst{enable: GET}=createDraftHandlers();// app/api/draft/preview-links/route.tsimport{createWebPreviewsHandler}from'@smartive/utils/next';exportconst{OPTIONS,POST}=createWebPreviewsHandler({baseUrl: 'https://example.com/api/draft',resolvePreviewUrl: async({ item, itemType })=>{if(itemType.attributes.api_key==='page')return`/${item.attributes.slug}`;returnnull;},});// app/api/revalidate-path/route.tsimport{createRevalidateHandler}from'@smartive/utils/next';exportconstPOST=createRevalidateHandler({paths: ['/sitemap.xml']});

Draft enable/disable and web-preview links use the url query parameter for redirect targets (e.g. /api/draft/enable?url=/page&token=…).

createRevalidateHandler revalidates '/' with 'layout' — the whole app — on every call. For per-query invalidation, use createCacheTagInvalidationHandler with a cache-tag store instead. createCacheTagInvalidateAllHandler covers the manual full reset (a missed webhook, or a change to the query-ID format).

Env varPurpose
DRAFT_SECRET_TOKENAuthorizes draft enable/disable and web-previews
CACHE_INVALIDATION_SECRET_TOKENAuthorizes the revalidate and cache-tag webhooks (Webhook-Token)

Migrating from @smartive/datocms-utils

This package was previously published as @smartive/datocms-utils. With 4.0.0 it was renamed to @smartive/utils and the old cache-tag utilities were removed.

  • classNames and getTelLink are unchanged — only the import specifier needs to be updated.
  • DatoCMS / Next.js helpers live under @smartive/utils/http, @smartive/utils/datocms, and @smartive/utils/next.

Cache tags

The cache-tag utilities are back, rebuilt for Next.js Cache Components. You no longer need to stay on @smartive/datocms-utils@3.

Removed in 4.0.0Replacement
CacheTagsProvider (interface)CacheTagStore from @smartive/utils/cache-tags
NeonCacheTagsProvidercreateNeonCacheTagStore (/cache-tags/neon)
NoopCacheTagsProvidercreateMemoryCacheTagStore (/cache-tags)
RedisCacheTagsProviderNot ported — open an issue if you need it
generateQueryIdbuildQueryIddifferent output format, see below
parseXCacheTagsResponseHeaderUnchanged, now from @smartive/utils/cache-tags
CacheTag (branded type)Plain string
CacheTagsInvalidateWebhookUnchanged, plus an isCacheTagsInvalidateWebhook guard

Behavioural changes worth knowing:

  • Query IDs changed format.buildQueryId emits <operationName>-<hash16> rather than a bare sha1, so every stored mapping from v3 is unreachable. Run the invalidate-all handler once after upgrading to clear them.
  • Stores never throw. The throwOnError option is gone: a store outage must degrade cache lifetimes, not fail a render. onError remains, for telemetry.
  • Return types carry more signal.storeQueryCacheTags returns boolean and queriesReferencingCacheTags returns string[] | null, which is what lets the client pick a cacheLife profile and the webhook distinguish 503 from 200.
  • buildQueryId needs no graphql dependency. It hashes the document structurally, ignoring loc, so graphql-tag output is stable across unrelated edits in the same file.

License

MIT © smartive AG

About

A set of utilities and helpers to work with DatoCMS in a Next.js project.

Resources

Stars

0 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

smartive Utilities

A collection of general purpose utilities and helpers for web projects.

Installation

npm install @smartive/utils

The root export (@smartive/utils) stays dependency-free. Optional peer dependencies are only required when you import the corresponding subpath.

Utilities

classNames

Cleans and joins an array of class names (strings and numbers), filtering out undefined and boolean values.

import{classNames}from'@smartive/utils';constclassName=classNames('btn',isActive&&'btn-active',42,undefined,'btn-primary');// Result: "btn btn-active 42 btn-primary"

getTelLink

Converts a phone number into a tel: link by removing non-digit characters (except + for international numbers).

import{getTelLink}from'@smartive/utils';constlink=getTelLink('+1 (555) 123-4567');// Result: "tel:+15551234567"

@smartive/utils/http

Framework-agnostic HTTP helpers for token checks, open-redirect protection, and CORS headers.

import{isSafeRelativePath,isValidToken,withCORS}from'@smartive/utils/http';isValidToken(request.headers.get('Webhook-Token'),process.env.CACHE_INVALIDATION_SECRET_TOKEN);isSafeRelativePath('/relative/path');withCORS({status: 401});

No environment variables are required by this subpath; callers pass secrets explicitly.

@smartive/utils/datocms

Typed GraphQL client factory wrapping @datocms/cda-client.

npm install @datocms/cda-client
import{createDatoClient,queryDatoCMS}from'@smartive/utils/datocms';// Default client (reads env vars)constdata=awaitqueryDatoCMS({document: MyDocument,includeDrafts: true});// Or configure explicitlyconstquery=createDatoClient({apiToken: process.env.DATOCMS_API_TOKEN,revalidate: 60*60,});
Env varPurpose
DATOCMS_API_TOKENRead-only CDA token (draft-capable when needed)
DATOCMS_ENVIRONMENTOptional X-Environment header
NEXT_DATOCMS_BASE_EDITING_URLEnables Content Link headers when querying drafts

Config passed to createDatoClient always wins over environment variables.

@smartive/utils/datocms/next

The same client, backed by Next.js Cache Components instead of the fetch data cache. Queries are wrapped in use cache, tagged with a deterministic query ID, and given a cacheLife profile — so a DatoCMS publish can invalidate exactly the affected queries instead of purging the whole app.

Requires Next.js >= 16 with cacheComponents: true. Without that flag, use @smartive/utils/datocms.

npm install @datocms/cda-client @neondatabase/serverless
// lib/dato.tsimport{createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';import{createCachedDatoClient}from'@smartive/utils/datocms/next';exportconststore=createNeonCacheTagStore();exportconstqueryDatoCMS=createCachedDatoClient({ store });
// app/api/invalidate-cache-tags/route.tsimport{createCacheTagInvalidationHandler}from'@smartive/utils/next';import{store}from'@/lib/dato';exportconst{POST,GET,OPTIONS}=createCacheTagInvalidationHandler({ store });

Point a DatoCMS cache tags invalidation webhook at that POST route, with the secret in the Webhook-Token header.

There is deliberately no default client: one without a store looks like it works but can never be invalidated, so the store is a required, visible decision. Omitting it is still supported for local development — every entry then falls back to the short unstored lifetime.

cacheLife profiles

ProfileDefaultWhen it applies
cached'days'The tag mapping persisted, so the webhook can reach the entry
unstored'minutes'The mapping could not be stored — the entry must expire on its own
draft'seconds'Draft mode is enabled (Next writes nothing to the cache regardless)

Override per client via profiles, or per query via cacheProfile. A per-query override is ignored when the mapping did not persist, so it can never extend the lifetime of an entry the webhook cannot reach.

Draft mode

draftMode().isEnabled is read inside the cached scope, which Next.js permits. While draft mode is on, cached functions re-execute per request and nothing is written to the cache, so includeDrafts no longer needs threading through your data layer.

Pass includeDrafts: true (or skipCache: true) explicitly only when you need a guaranteed fresh read — preview slug resolution, for example. That bypasses the cached scope entirely.

@smartive/utils/cache-tags

The query-ID ⇄ DatoCMS cache-tag mapping used by @smartive/utils/datocms/next. Zero dependencies; no Next.js import.

import{buildQueryId,createMemoryCacheTagStore,parseXCacheTagsResponseHeader}from'@smartive/utils/cache-tags';

createMemoryCacheTagStore() is for tests, local development, and single-instance deployments only — on a serverless platform each instance would see a different subset of the mapping. It also accepts { configured: false } and { failing: true } for exercising fail-soft paths.

@smartive/utils/cache-tags/neon

import{cacheTagStoreSchemaSql,createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';conststore=createNeonCacheTagStore({onError: (error)=>captureException(error)});

Create the table once per Neon branch:

psql "$CACHETAGS_POSTGRES_URL" -c "$(node -e "import('@smartive/utils/cache-tags/neon').then((m) => console.log(m.cacheTagStoreSchemaSql()))")"

The store never throws: every method fails soft to a documented fallback and arms a 30-second backoff, because a store outage must degrade cache lifetimes rather than fail a page render. Writes report a boolean, and lookups return null on failure versus [] for "nothing matched" — that distinction is what drives the cacheLife choice and the webhook's 503-vs-200 response.

Isolate the store per DatoCMS environment (a Neon branch per deployment environment). The query ID already includes the resolved environment, but a table shared between apps also needs a tagPrefix.

Env varPurpose
CACHETAGS_POSTGRES_URLNeon connection string for the mapping table

One synthetic tag per query, rather than one tag per DatoCMS record, is deliberate: Vercel caps a cache entry at 128 tags.

@smartive/utils/next

Next.js App Router helpers for draft mode, DatoCMS web previews, and cache revalidation.

npm install next
// app/api/draft/enable/route.tsimport{createDraftHandlers}from'@smartive/utils/next';exportconst{enable: GET}=createDraftHandlers();// app/api/draft/preview-links/route.tsimport{createWebPreviewsHandler}from'@smartive/utils/next';exportconst{OPTIONS,POST}=createWebPreviewsHandler({baseUrl: 'https://example.com/api/draft',resolvePreviewUrl: async({ item, itemType })=>{if(itemType.attributes.api_key==='page')return`/${item.attributes.slug}`;returnnull;},});// app/api/revalidate-path/route.tsimport{createRevalidateHandler}from'@smartive/utils/next';exportconstPOST=createRevalidateHandler({paths: ['/sitemap.xml']});

Draft enable/disable and web-preview links use the url query parameter for redirect targets (e.g. /api/draft/enable?url=/page&token=…).

createRevalidateHandler revalidates '/' with 'layout' — the whole app — on every call. For per-query invalidation, use createCacheTagInvalidationHandler with a cache-tag store instead. createCacheTagInvalidateAllHandler covers the manual full reset (a missed webhook, or a change to the query-ID format).

Env varPurpose
DRAFT_SECRET_TOKENAuthorizes draft enable/disable and web-previews
CACHE_INVALIDATION_SECRET_TOKENAuthorizes the revalidate and cache-tag webhooks (Webhook-Token)

Migrating from @smartive/datocms-utils

This package was previously published as @smartive/datocms-utils. With 4.0.0 it was renamed to @smartive/utils and the old cache-tag utilities were removed.

  • classNames and getTelLink are unchanged — only the import specifier needs to be updated.
  • DatoCMS / Next.js helpers live under @smartive/utils/http, @smartive/utils/datocms, and @smartive/utils/next.

Cache tags

The cache-tag utilities are back, rebuilt for Next.js Cache Components. You no longer need to stay on @smartive/datocms-utils@3.

Removed in 4.0.0Replacement
CacheTagsProvider (interface)CacheTagStore from @smartive/utils/cache-tags
NeonCacheTagsProvidercreateNeonCacheTagStore (/cache-tags/neon)
NoopCacheTagsProvidercreateMemoryCacheTagStore (/cache-tags)
RedisCacheTagsProviderNot ported — open an issue if you need it
generateQueryIdbuildQueryIddifferent output format, see below
parseXCacheTagsResponseHeaderUnchanged, now from @smartive/utils/cache-tags
CacheTag (branded type)Plain string
CacheTagsInvalidateWebhookUnchanged, plus an isCacheTagsInvalidateWebhook guard

Behavioural changes worth knowing:

  • Query IDs changed format.buildQueryId emits <operationName>-<hash16> rather than a bare sha1, so every stored mapping from v3 is unreachable. Run the invalidate-all handler once after upgrading to clear them.
  • Stores never throw. The throwOnError option is gone: a store outage must degrade cache lifetimes, not fail a render. onError remains, for telemetry.
  • Return types carry more signal.storeQueryCacheTags returns boolean and queriesReferencingCacheTags returns string[] | null, which is what lets the client pick a cacheLife profile and the webhook distinguish 503 from 200.
  • buildQueryId needs no graphql dependency. It hashes the document structurally, ignoring loc, so graphql-tag output is stable across unrelated edits in the same file.

License

MIT © smartive AG

About

A set of utilities and helpers to work with DatoCMS in a Next.js project.

Resources

Stars

0 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

smartive Utilities

A collection of general purpose utilities and helpers for web projects.

Installation

npm install @smartive/utils

The root export (@smartive/utils) stays dependency-free. Optional peer dependencies are only required when you import the corresponding subpath.

Utilities

classNames

Cleans and joins an array of class names (strings and numbers), filtering out undefined and boolean values.

import{classNames}from'@smartive/utils';constclassName=classNames('btn',isActive&&'btn-active',42,undefined,'btn-primary');// Result: "btn btn-active 42 btn-primary"

getTelLink

Converts a phone number into a tel: link by removing non-digit characters (except + for international numbers).

import{getTelLink}from'@smartive/utils';constlink=getTelLink('+1 (555) 123-4567');// Result: "tel:+15551234567"

@smartive/utils/http

Framework-agnostic HTTP helpers for token checks, open-redirect protection, and CORS headers.

import{isSafeRelativePath,isValidToken,withCORS}from'@smartive/utils/http';isValidToken(request.headers.get('Webhook-Token'),process.env.CACHE_INVALIDATION_SECRET_TOKEN);isSafeRelativePath('/relative/path');withCORS({status: 401});

No environment variables are required by this subpath; callers pass secrets explicitly.

@smartive/utils/datocms

Typed GraphQL client factory wrapping @datocms/cda-client.

npm install @datocms/cda-client
import{createDatoClient,queryDatoCMS}from'@smartive/utils/datocms';// Default client (reads env vars)constdata=awaitqueryDatoCMS({document: MyDocument,includeDrafts: true});// Or configure explicitlyconstquery=createDatoClient({apiToken: process.env.DATOCMS_API_TOKEN,revalidate: 60*60,});
Env varPurpose
DATOCMS_API_TOKENRead-only CDA token (draft-capable when needed)
DATOCMS_ENVIRONMENTOptional X-Environment header
NEXT_DATOCMS_BASE_EDITING_URLEnables Content Link headers when querying drafts

Config passed to createDatoClient always wins over environment variables.

@smartive/utils/datocms/next

The same client, backed by Next.js Cache Components instead of the fetch data cache. Queries are wrapped in use cache, tagged with a deterministic query ID, and given a cacheLife profile — so a DatoCMS publish can invalidate exactly the affected queries instead of purging the whole app.

Requires Next.js >= 16 with cacheComponents: true. Without that flag, use @smartive/utils/datocms.

npm install @datocms/cda-client @neondatabase/serverless
// lib/dato.tsimport{createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';import{createCachedDatoClient}from'@smartive/utils/datocms/next';exportconststore=createNeonCacheTagStore();exportconstqueryDatoCMS=createCachedDatoClient({ store });
// app/api/invalidate-cache-tags/route.tsimport{createCacheTagInvalidationHandler}from'@smartive/utils/next';import{store}from'@/lib/dato';exportconst{POST,GET,OPTIONS}=createCacheTagInvalidationHandler({ store });

Point a DatoCMS cache tags invalidation webhook at that POST route, with the secret in the Webhook-Token header.

There is deliberately no default client: one without a store looks like it works but can never be invalidated, so the store is a required, visible decision. Omitting it is still supported for local development — every entry then falls back to the short unstored lifetime.

cacheLife profiles

ProfileDefaultWhen it applies
cached'days'The tag mapping persisted, so the webhook can reach the entry
unstored'minutes'The mapping could not be stored — the entry must expire on its own
draft'seconds'Draft mode is enabled (Next writes nothing to the cache regardless)

Override per client via profiles, or per query via cacheProfile. A per-query override is ignored when the mapping did not persist, so it can never extend the lifetime of an entry the webhook cannot reach.

Draft mode

draftMode().isEnabled is read inside the cached scope, which Next.js permits. While draft mode is on, cached functions re-execute per request and nothing is written to the cache, so includeDrafts no longer needs threading through your data layer.

Pass includeDrafts: true (or skipCache: true) explicitly only when you need a guaranteed fresh read — preview slug resolution, for example. That bypasses the cached scope entirely.

@smartive/utils/cache-tags

The query-ID ⇄ DatoCMS cache-tag mapping used by @smartive/utils/datocms/next. Zero dependencies; no Next.js import.

import{buildQueryId,createMemoryCacheTagStore,parseXCacheTagsResponseHeader}from'@smartive/utils/cache-tags';

createMemoryCacheTagStore() is for tests, local development, and single-instance deployments only — on a serverless platform each instance would see a different subset of the mapping. It also accepts { configured: false } and { failing: true } for exercising fail-soft paths.

@smartive/utils/cache-tags/neon

import{cacheTagStoreSchemaSql,createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';conststore=createNeonCacheTagStore({onError: (error)=>captureException(error)});

Create the table once per Neon branch:

psql "$CACHETAGS_POSTGRES_URL" -c "$(node -e "import('@smartive/utils/cache-tags/neon').then((m) => console.log(m.cacheTagStoreSchemaSql()))")"

The store never throws: every method fails soft to a documented fallback and arms a 30-second backoff, because a store outage must degrade cache lifetimes rather than fail a page render. Writes report a boolean, and lookups return null on failure versus [] for "nothing matched" — that distinction is what drives the cacheLife choice and the webhook's 503-vs-200 response.

Isolate the store per DatoCMS environment (a Neon branch per deployment environment). The query ID already includes the resolved environment, but a table shared between apps also needs a tagPrefix.

Env varPurpose
CACHETAGS_POSTGRES_URLNeon connection string for the mapping table

One synthetic tag per query, rather than one tag per DatoCMS record, is deliberate: Vercel caps a cache entry at 128 tags.

@smartive/utils/next

Next.js App Router helpers for draft mode, DatoCMS web previews, and cache revalidation.

npm install next
// app/api/draft/enable/route.tsimport{createDraftHandlers}from'@smartive/utils/next';exportconst{enable: GET}=createDraftHandlers();// app/api/draft/preview-links/route.tsimport{createWebPreviewsHandler}from'@smartive/utils/next';exportconst{OPTIONS,POST}=createWebPreviewsHandler({baseUrl: 'https://example.com/api/draft',resolvePreviewUrl: async({ item, itemType })=>{if(itemType.attributes.api_key==='page')return`/${item.attributes.slug}`;returnnull;},});// app/api/revalidate-path/route.tsimport{createRevalidateHandler}from'@smartive/utils/next';exportconstPOST=createRevalidateHandler({paths: ['/sitemap.xml']});

Draft enable/disable and web-preview links use the url query parameter for redirect targets (e.g. /api/draft/enable?url=/page&token=…).

createRevalidateHandler revalidates '/' with 'layout' — the whole app — on every call. For per-query invalidation, use createCacheTagInvalidationHandler with a cache-tag store instead. createCacheTagInvalidateAllHandler covers the manual full reset (a missed webhook, or a change to the query-ID format).

Env varPurpose
DRAFT_SECRET_TOKENAuthorizes draft enable/disable and web-previews
CACHE_INVALIDATION_SECRET_TOKENAuthorizes the revalidate and cache-tag webhooks (Webhook-Token)

Migrating from @smartive/datocms-utils

This package was previously published as @smartive/datocms-utils. With 4.0.0 it was renamed to @smartive/utils and the old cache-tag utilities were removed.

  • classNames and getTelLink are unchanged — only the import specifier needs to be updated.
  • DatoCMS / Next.js helpers live under @smartive/utils/http, @smartive/utils/datocms, and @smartive/utils/next.

Cache tags

The cache-tag utilities are back, rebuilt for Next.js Cache Components. You no longer need to stay on @smartive/datocms-utils@3.

Removed in 4.0.0Replacement
CacheTagsProvider (interface)CacheTagStore from @smartive/utils/cache-tags
NeonCacheTagsProvidercreateNeonCacheTagStore (/cache-tags/neon)
NoopCacheTagsProvidercreateMemoryCacheTagStore (/cache-tags)
RedisCacheTagsProviderNot ported — open an issue if you need it
generateQueryIdbuildQueryIddifferent output format, see below
parseXCacheTagsResponseHeaderUnchanged, now from @smartive/utils/cache-tags
CacheTag (branded type)Plain string
CacheTagsInvalidateWebhookUnchanged, plus an isCacheTagsInvalidateWebhook guard

Behavioural changes worth knowing:

  • Query IDs changed format.buildQueryId emits <operationName>-<hash16> rather than a bare sha1, so every stored mapping from v3 is unreachable. Run the invalidate-all handler once after upgrading to clear them.
  • Stores never throw. The throwOnError option is gone: a store outage must degrade cache lifetimes, not fail a render. onError remains, for telemetry.
  • Return types carry more signal.storeQueryCacheTags returns boolean and queriesReferencingCacheTags returns string[] | null, which is what lets the client pick a cacheLife profile and the webhook distinguish 503 from 200.
  • buildQueryId needs no graphql dependency. It hashes the document structurally, ignoring loc, so graphql-tag output is stable across unrelated edits in the same file.

License

MIT © smartive AG

About

A set of utilities and helpers to work with DatoCMS in a Next.js project.

Resources

Stars

0 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

smartive Utilities

A collection of general purpose utilities and helpers for web projects.

Installation

npm install @smartive/utils

The root export (@smartive/utils) stays dependency-free. Optional peer dependencies are only required when you import the corresponding subpath.

Utilities

classNames

Cleans and joins an array of class names (strings and numbers), filtering out undefined and boolean values.

import{classNames}from'@smartive/utils';constclassName=classNames('btn',isActive&&'btn-active',42,undefined,'btn-primary');// Result: "btn btn-active 42 btn-primary"

getTelLink

Converts a phone number into a tel: link by removing non-digit characters (except + for international numbers).

import{getTelLink}from'@smartive/utils';constlink=getTelLink('+1 (555) 123-4567');// Result: "tel:+15551234567"

@smartive/utils/http

Framework-agnostic HTTP helpers for token checks, open-redirect protection, and CORS headers.

import{isSafeRelativePath,isValidToken,withCORS}from'@smartive/utils/http';isValidToken(request.headers.get('Webhook-Token'),process.env.CACHE_INVALIDATION_SECRET_TOKEN);isSafeRelativePath('/relative/path');withCORS({status: 401});

No environment variables are required by this subpath; callers pass secrets explicitly.

@smartive/utils/datocms

Typed GraphQL client factory wrapping @datocms/cda-client.

npm install @datocms/cda-client
import{createDatoClient,queryDatoCMS}from'@smartive/utils/datocms';// Default client (reads env vars)constdata=awaitqueryDatoCMS({document: MyDocument,includeDrafts: true});// Or configure explicitlyconstquery=createDatoClient({apiToken: process.env.DATOCMS_API_TOKEN,revalidate: 60*60,});
Env varPurpose
DATOCMS_API_TOKENRead-only CDA token (draft-capable when needed)
DATOCMS_ENVIRONMENTOptional X-Environment header
NEXT_DATOCMS_BASE_EDITING_URLEnables Content Link headers when querying drafts

Config passed to createDatoClient always wins over environment variables.

@smartive/utils/datocms/next

The same client, backed by Next.js Cache Components instead of the fetch data cache. Queries are wrapped in use cache, tagged with a deterministic query ID, and given a cacheLife profile — so a DatoCMS publish can invalidate exactly the affected queries instead of purging the whole app.

Requires Next.js >= 16 with cacheComponents: true. Without that flag, use @smartive/utils/datocms.

npm install @datocms/cda-client @neondatabase/serverless
// lib/dato.tsimport{createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';import{createCachedDatoClient}from'@smartive/utils/datocms/next';exportconststore=createNeonCacheTagStore();exportconstqueryDatoCMS=createCachedDatoClient({ store });
// app/api/invalidate-cache-tags/route.tsimport{createCacheTagInvalidationHandler}from'@smartive/utils/next';import{store}from'@/lib/dato';exportconst{POST,GET,OPTIONS}=createCacheTagInvalidationHandler({ store });

Point a DatoCMS cache tags invalidation webhook at that POST route, with the secret in the Webhook-Token header.

There is deliberately no default client: one without a store looks like it works but can never be invalidated, so the store is a required, visible decision. Omitting it is still supported for local development — every entry then falls back to the short unstored lifetime.

cacheLife profiles

ProfileDefaultWhen it applies
cached'days'The tag mapping persisted, so the webhook can reach the entry
unstored'minutes'The mapping could not be stored — the entry must expire on its own
draft'seconds'Draft mode is enabled (Next writes nothing to the cache regardless)

Override per client via profiles, or per query via cacheProfile. A per-query override is ignored when the mapping did not persist, so it can never extend the lifetime of an entry the webhook cannot reach.

Draft mode

draftMode().isEnabled is read inside the cached scope, which Next.js permits. While draft mode is on, cached functions re-execute per request and nothing is written to the cache, so includeDrafts no longer needs threading through your data layer.

Pass includeDrafts: true (or skipCache: true) explicitly only when you need a guaranteed fresh read — preview slug resolution, for example. That bypasses the cached scope entirely.

@smartive/utils/cache-tags

The query-ID ⇄ DatoCMS cache-tag mapping used by @smartive/utils/datocms/next. Zero dependencies; no Next.js import.

import{buildQueryId,createMemoryCacheTagStore,parseXCacheTagsResponseHeader}from'@smartive/utils/cache-tags';

createMemoryCacheTagStore() is for tests, local development, and single-instance deployments only — on a serverless platform each instance would see a different subset of the mapping. It also accepts { configured: false } and { failing: true } for exercising fail-soft paths.

@smartive/utils/cache-tags/neon

import{cacheTagStoreSchemaSql,createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';conststore=createNeonCacheTagStore({onError: (error)=>captureException(error)});

Create the table once per Neon branch:

psql "$CACHETAGS_POSTGRES_URL" -c "$(node -e "import('@smartive/utils/cache-tags/neon').then((m) => console.log(m.cacheTagStoreSchemaSql()))")"

The store never throws: every method fails soft to a documented fallback and arms a 30-second backoff, because a store outage must degrade cache lifetimes rather than fail a page render. Writes report a boolean, and lookups return null on failure versus [] for "nothing matched" — that distinction is what drives the cacheLife choice and the webhook's 503-vs-200 response.

Isolate the store per DatoCMS environment (a Neon branch per deployment environment). The query ID already includes the resolved environment, but a table shared between apps also needs a tagPrefix.

Env varPurpose
CACHETAGS_POSTGRES_URLNeon connection string for the mapping table

One synthetic tag per query, rather than one tag per DatoCMS record, is deliberate: Vercel caps a cache entry at 128 tags.

@smartive/utils/next

Next.js App Router helpers for draft mode, DatoCMS web previews, and cache revalidation.

npm install next
// app/api/draft/enable/route.tsimport{createDraftHandlers}from'@smartive/utils/next';exportconst{enable: GET}=createDraftHandlers();// app/api/draft/preview-links/route.tsimport{createWebPreviewsHandler}from'@smartive/utils/next';exportconst{OPTIONS,POST}=createWebPreviewsHandler({baseUrl: 'https://example.com/api/draft',resolvePreviewUrl: async({ item, itemType })=>{if(itemType.attributes.api_key==='page')return`/${item.attributes.slug}`;returnnull;},});// app/api/revalidate-path/route.tsimport{createRevalidateHandler}from'@smartive/utils/next';exportconstPOST=createRevalidateHandler({paths: ['/sitemap.xml']});

Draft enable/disable and web-preview links use the url query parameter for redirect targets (e.g. /api/draft/enable?url=/page&token=…).

createRevalidateHandler revalidates '/' with 'layout' — the whole app — on every call. For per-query invalidation, use createCacheTagInvalidationHandler with a cache-tag store instead. createCacheTagInvalidateAllHandler covers the manual full reset (a missed webhook, or a change to the query-ID format).

Env varPurpose
DRAFT_SECRET_TOKENAuthorizes draft enable/disable and web-previews
CACHE_INVALIDATION_SECRET_TOKENAuthorizes the revalidate and cache-tag webhooks (Webhook-Token)

Migrating from @smartive/datocms-utils

This package was previously published as @smartive/datocms-utils. With 4.0.0 it was renamed to @smartive/utils and the old cache-tag utilities were removed.

  • classNames and getTelLink are unchanged — only the import specifier needs to be updated.
  • DatoCMS / Next.js helpers live under @smartive/utils/http, @smartive/utils/datocms, and @smartive/utils/next.

Cache tags

The cache-tag utilities are back, rebuilt for Next.js Cache Components. You no longer need to stay on @smartive/datocms-utils@3.

Removed in 4.0.0Replacement
CacheTagsProvider (interface)CacheTagStore from @smartive/utils/cache-tags
NeonCacheTagsProvidercreateNeonCacheTagStore (/cache-tags/neon)
NoopCacheTagsProvidercreateMemoryCacheTagStore (/cache-tags)
RedisCacheTagsProviderNot ported — open an issue if you need it
generateQueryIdbuildQueryIddifferent output format, see below
parseXCacheTagsResponseHeaderUnchanged, now from @smartive/utils/cache-tags
CacheTag (branded type)Plain string
CacheTagsInvalidateWebhookUnchanged, plus an isCacheTagsInvalidateWebhook guard

Behavioural changes worth knowing:

  • Query IDs changed format.buildQueryId emits <operationName>-<hash16> rather than a bare sha1, so every stored mapping from v3 is unreachable. Run the invalidate-all handler once after upgrading to clear them.
  • Stores never throw. The throwOnError option is gone: a store outage must degrade cache lifetimes, not fail a render. onError remains, for telemetry.
  • Return types carry more signal.storeQueryCacheTags returns boolean and queriesReferencingCacheTags returns string[] | null, which is what lets the client pick a cacheLife profile and the webhook distinguish 503 from 200.
  • buildQueryId needs no graphql dependency. It hashes the document structurally, ignoring loc, so graphql-tag output is stable across unrelated edits in the same file.

License

MIT © smartive AG

About

A set of utilities and helpers to work with DatoCMS in a Next.js project.

Resources

Stars

0 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

smartive Utilities

A collection of general purpose utilities and helpers for web projects.

Installation

npm install @smartive/utils

The root export (@smartive/utils) stays dependency-free. Optional peer dependencies are only required when you import the corresponding subpath.

Utilities

classNames

Cleans and joins an array of class names (strings and numbers), filtering out undefined and boolean values.

import{classNames}from'@smartive/utils';constclassName=classNames('btn',isActive&&'btn-active',42,undefined,'btn-primary');// Result: "btn btn-active 42 btn-primary"

getTelLink

Converts a phone number into a tel: link by removing non-digit characters (except + for international numbers).

import{getTelLink}from'@smartive/utils';constlink=getTelLink('+1 (555) 123-4567');// Result: "tel:+15551234567"

@smartive/utils/http

Framework-agnostic HTTP helpers for token checks, open-redirect protection, and CORS headers.

import{isSafeRelativePath,isValidToken,withCORS}from'@smartive/utils/http';isValidToken(request.headers.get('Webhook-Token'),process.env.CACHE_INVALIDATION_SECRET_TOKEN);isSafeRelativePath('/relative/path');withCORS({status: 401});

No environment variables are required by this subpath; callers pass secrets explicitly.

@smartive/utils/datocms

Typed GraphQL client factory wrapping @datocms/cda-client.

npm install @datocms/cda-client
import{createDatoClient,queryDatoCMS}from'@smartive/utils/datocms';// Default client (reads env vars)constdata=awaitqueryDatoCMS({document: MyDocument,includeDrafts: true});// Or configure explicitlyconstquery=createDatoClient({apiToken: process.env.DATOCMS_API_TOKEN,revalidate: 60*60,});
Env varPurpose
DATOCMS_API_TOKENRead-only CDA token (draft-capable when needed)
DATOCMS_ENVIRONMENTOptional X-Environment header
NEXT_DATOCMS_BASE_EDITING_URLEnables Content Link headers when querying drafts

Config passed to createDatoClient always wins over environment variables.

@smartive/utils/datocms/next

The same client, backed by Next.js Cache Components instead of the fetch data cache. Queries are wrapped in use cache, tagged with a deterministic query ID, and given a cacheLife profile — so a DatoCMS publish can invalidate exactly the affected queries instead of purging the whole app.

Requires Next.js >= 16 with cacheComponents: true. Without that flag, use @smartive/utils/datocms.

npm install @datocms/cda-client @neondatabase/serverless
// lib/dato.tsimport{createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';import{createCachedDatoClient}from'@smartive/utils/datocms/next';exportconststore=createNeonCacheTagStore();exportconstqueryDatoCMS=createCachedDatoClient({ store });
// app/api/invalidate-cache-tags/route.tsimport{createCacheTagInvalidationHandler}from'@smartive/utils/next';import{store}from'@/lib/dato';exportconst{POST,GET,OPTIONS}=createCacheTagInvalidationHandler({ store });

Point a DatoCMS cache tags invalidation webhook at that POST route, with the secret in the Webhook-Token header.

There is deliberately no default client: one without a store looks like it works but can never be invalidated, so the store is a required, visible decision. Omitting it is still supported for local development — every entry then falls back to the short unstored lifetime.

cacheLife profiles

ProfileDefaultWhen it applies
cached'days'The tag mapping persisted, so the webhook can reach the entry
unstored'minutes'The mapping could not be stored — the entry must expire on its own
draft'seconds'Draft mode is enabled (Next writes nothing to the cache regardless)

Override per client via profiles, or per query via cacheProfile. A per-query override is ignored when the mapping did not persist, so it can never extend the lifetime of an entry the webhook cannot reach.

Draft mode

draftMode().isEnabled is read inside the cached scope, which Next.js permits. While draft mode is on, cached functions re-execute per request and nothing is written to the cache, so includeDrafts no longer needs threading through your data layer.

Pass includeDrafts: true (or skipCache: true) explicitly only when you need a guaranteed fresh read — preview slug resolution, for example. That bypasses the cached scope entirely.

@smartive/utils/cache-tags

The query-ID ⇄ DatoCMS cache-tag mapping used by @smartive/utils/datocms/next. Zero dependencies; no Next.js import.

import{buildQueryId,createMemoryCacheTagStore,parseXCacheTagsResponseHeader}from'@smartive/utils/cache-tags';

createMemoryCacheTagStore() is for tests, local development, and single-instance deployments only — on a serverless platform each instance would see a different subset of the mapping. It also accepts { configured: false } and { failing: true } for exercising fail-soft paths.

@smartive/utils/cache-tags/neon

import{cacheTagStoreSchemaSql,createNeonCacheTagStore}from'@smartive/utils/cache-tags/neon';conststore=createNeonCacheTagStore({onError: (error)=>captureException(error)});

Create the table once per Neon branch:

psql "$CACHETAGS_POSTGRES_URL" -c "$(node -e "import('@smartive/utils/cache-tags/neon').then((m) => console.log(m.cacheTagStoreSchemaSql()))")"

The store never throws: every method fails soft to a documented fallback and arms a 30-second backoff, because a store outage must degrade cache lifetimes rather than fail a page render. Writes report a boolean, and lookups return null on failure versus [] for "nothing matched" — that distinction is what drives the cacheLife choice and the webhook's 503-vs-200 response.

Isolate the store per DatoCMS environment (a Neon branch per deployment environment). The query ID already includes the resolved environment, but a table shared between apps also needs a tagPrefix.

Env varPurpose
CACHETAGS_POSTGRES_URLNeon connection string for the mapping table

One synthetic tag per query, rather than one tag per DatoCMS record, is deliberate: Vercel caps a cache entry at 128 tags.

@smartive/utils/next

Next.js App Router helpers for draft mode, DatoCMS web previews, and cache revalidation.

npm install next
// app/api/draft/enable/route.tsimport{createDraftHandlers}from'@smartive/utils/next';exportconst{enable: GET}=createDraftHandlers();// app/api/draft/preview-links/route.tsimport{createWebPreviewsHandler}from'@smartive/utils/next';exportconst{OPTIONS,POST}=createWebPreviewsHandler({baseUrl: 'https://example.com/api/draft',resolvePreviewUrl: async({ item, itemType })=>{if(itemType.attributes.api_key==='page')return`/${item.attributes.slug}`;returnnull;},});// app/api/revalidate-path/route.tsimport{createRevalidateHandler}from'@smartive/utils/next';exportconstPOST=createRevalidateHandler({paths: ['/sitemap.xml']});

Draft enable/disable and web-preview links use the url query parameter for redirect targets (e.g. /api/draft/enable?url=/page&token=…).

createRevalidateHandler revalidates '/' with 'layout' — the whole app — on every call. For per-query invalidation, use createCacheTagInvalidationHandler with a cache-tag store instead. createCacheTagInvalidateAllHandler covers the manual full reset (a missed webhook, or a change to the query-ID format).

Env varPurpose
DRAFT_SECRET_TOKENAuthorizes draft enable/disable and web-previews
CACHE_INVALIDATION_SECRET_TOKENAuthorizes the revalidate and cache-tag webhooks (Webhook-Token)

Migrating from @smartive/datocms-utils

This package was previously published as @smartive/datocms-utils. With 4.0.0 it was renamed to @smartive/utils and the old cache-tag utilities were removed.

  • classNames and getTelLink are unchanged — only the import specifier needs to be updated.
  • DatoCMS / Next.js helpers live under @smartive/utils/http, @smartive/utils/datocms, and @smartive/utils/next.

Cache tags

The cache-tag utilities are back, rebuilt for Next.js Cache Components. You no longer need to stay on @smartive/datocms-utils@3.

Removed in 4.0.0Replacement
CacheTagsProvider (interface)CacheTagStore from @smartive/utils/cache-tags
NeonCacheTagsProvidercreateNeonCacheTagStore (/cache-tags/neon)
NoopCacheTagsProvidercreateMemoryCacheTagStore (/cache-tags)
RedisCacheTagsProviderNot ported — open an issue if you need it
generateQueryIdbuildQueryIddifferent output format, see below
parseXCacheTagsResponseHeaderUnchanged, now from @smartive/utils/cache-tags
CacheTag (branded type)Plain string
CacheTagsInvalidateWebhookUnchanged, plus an isCacheTagsInvalidateWebhook guard

Behavioural changes worth knowing:

  • Query IDs changed format.buildQueryId emits <operationName>-<hash16> rather than a bare sha1, so every stored mapping from v3 is unreachable. Run the invalidate-all handler once after upgrading to clear them.
  • Stores never throw. The throwOnError option is gone: a store outage must degrade cache lifetimes, not fail a render. onError remains, for telemetry.
  • Return types carry more signal.storeQueryCacheTags returns boolean and queriesReferencingCacheTags returns string[] | null, which is what lets the client pick a cacheLife profile and the webhook distinguish 503 from 200.
  • buildQueryId needs no graphql dependency. It hashes the document structurally, ignoring loc, so graphql-tag output is stable across unrelated edits in the same file.

License

MIT © smartive AG

About

A set of utilities and helpers to work with DatoCMS in a Next.js project.

Resources

Stars

0 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages