@fourtwelvelabs/fetch-contentful 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,90 @@
1
1
  # @fourtwelvelabs/fetch-contentful
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 73b5ec2: Separate `deliveryToken` and `previewToken`, and stop reading the preview
8
+ token from a `NEXT_PUBLIC_` variable.
9
+
10
+ ### The preview token can no longer leak into a client bundle
11
+
12
+ `readEnvSettings` used to fall back to
13
+ `NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN`. Next.js inlines
14
+ `NEXT_PUBLIC_` variables into client bundles by string-replacing the literal
15
+ `process.env.NEXT_PUBLIC_…` accesses this package ships, so a project that
16
+ set that variable had its preview token baked into public JavaScript —
17
+ whether or not any client code ever requested preview content. Importing the
18
+ library was enough.
19
+
20
+ The preview token is now resolved **only** from the unprefixed
21
+ `CONTENTFUL_PREVIEW_ACCESS_TOKEN`. In Next.js that name is still readable on
22
+ the server, so server-side preview keeps working; the migration is to drop
23
+ the `NEXT_PUBLIC_` prefix from that one variable. The space, environment and
24
+ delivery token keep their client-safe names — a delivery token is read-only
25
+ and commonly public.
26
+
27
+ If your app genuinely needs preview in the browser, pass `previewToken`
28
+ explicitly.
29
+
30
+ ### `deliveryToken` and `previewToken`
31
+
32
+ A single `token` could not serve both modes: it applied to whichever mode was
33
+ active, so a factory configured with one sent the wrong token as soon as a
34
+ call passed `preview: true` — and there was no way to configure both
35
+ explicitly at all.
36
+
37
+ ```ts
38
+ export const fetchContentful = createFetchContentful({
39
+ deliveryToken: process.env.CONTENTFUL_ACCESS_TOKEN,
40
+ previewToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN,
41
+ });
42
+
43
+ await fetchContentful(QUERY); // → deliveryToken
44
+ await fetchContentful(QUERY, { preview: true }); // → previewToken
45
+ ```
46
+
47
+ `token` still works and still applies to the active mode, so existing code is
48
+ unaffected, but it is deprecated in favour of the two specific options.
49
+ `readEnvSettings()` gains `deliveryToken`; its `token` field remains as a
50
+ deprecated alias of the same value.
51
+
52
+ The GraphQL endpoint is unchanged — unlike Contentful's REST APIs, preview
53
+ uses the same `graphql.contentful.com` host and is selected by the token plus
54
+ the injected `preview: true` argument. (The REST locale lookup already
55
+ switched between `cdn.` and `preview.` hosts correctly.)
56
+
57
+ ## 0.3.0
58
+
59
+ ### Minor Changes
60
+
61
+ - 6013f57: `locale` now defaults to `"en-US"`.
62
+
63
+ Previously, omitting `locale` meant no `locale` argument was sent at all, and
64
+ Contentful served whichever locale the space itself defaults to. Now `en-US`
65
+ is requested explicitly unless you say otherwise.
66
+
67
+ **This changes which content comes back for a space whose default locale is
68
+ not `en-US`.** If that describes your space, either set the locale once for
69
+ the project:
70
+
71
+ ```ts
72
+ export const fetchContentful = createFetchContentful({ locale: "de-DE" });
73
+ ```
74
+
75
+ or restore the previous behavior — no `locale` argument, space decides — with
76
+ the new `null` opt-out:
77
+
78
+ ```ts
79
+ await fetchContentful(QUERY, { locale: null });
80
+ ```
81
+
82
+ `locale: null` is accepted anywhere `locale` is, including as a
83
+ `createFetchContentful` default and as a per-call override of one. A space
84
+ that has no `en-US` locale configured will now reject with a `LOCALE` error
85
+ under `validateLocale: true`, rather than silently returning the space
86
+ default.
87
+
3
88
  ## 0.2.1
4
89
 
5
90
  ### Patch Changes
@@ -64,7 +149,7 @@
64
149
  - b6e2577: Initial release. A type-safe GraphQL fetching utility for Contentful:
65
150
 
66
151
  - Automatic query splitting for nested reference collections (and `@split`-annotated one-to-one references), fetched in id batches and stitched back into the original response shape.
67
- - Automatic retries with exponential backoff and jitter, honouring `Retry-After`.
152
+ - Automatic retries with exponential backoff and jitter, honoring `Retry-After`.
68
153
  - Response shaping (`fooCollection.items` → `foo`) and single-root unwrapping, at runtime and in the return type.
69
154
  - Automatic `preview` / `locale` argument injection, with optional locale validation against the space.
70
155
  - Typed `FetchContentfulError` carrying an error code, HTTP status, and GraphQL errors.
package/README.md CHANGED
@@ -3,10 +3,10 @@
3
3
  A foolproof, type-safe GraphQL fetching utility for Contentful. One function in, shaped data out:
4
4
 
5
5
  - **Automatic query splitting** — nested reference collections (and `@split`-annotated one-to-one references) are broken into separate subqueries, fetched in id batches, and stitched back into the original response shape. Fully recursive, so deeply nested queries never hit Contentful's query complexity limit.
6
- - **Automatic retries** — transient failures (network errors, 408/429/5xx) retry with exponential backoff + jitter, honouring `Retry-After`. Configurable, default 5.
6
+ - **Automatic retries** — transient failures (network errors, 408/429/5xx) retry with exponential backoff + jitter, honoring `Retry-After`. Configurable, default 5.
7
7
  - **Response shaping (built in)** — before the promise resolves, every `fooCollection.items` in the response becomes `foo` (a plain array), at runtime *and* in the return type. No separate call needed.
8
8
  - **Single-root unwrapping (built in)** — when a query has one root field, the promise resolves with that field's contents directly (`data` *is* the array/entry, not `{ siteSettings: ... }`). Multi-root queries stay wrapped.
9
- - **No preview/locale boilerplate** — `preview` and `locale` are injected as arguments onto your query's root fields automatically (Contentful cascades them to every nested field), so queries never need to declare or thread them.
9
+ - **No preview/locale boilerplate** — `preview` and `locale` are injected as arguments onto your query's root fields automatically (Contentful cascades them to every nested field), so queries never need to declare or thread them. The locale defaults to `en-US`.
10
10
  - **All-or-nothing promise** — resolves only when every request in the tree succeeded; rejects with a typed `FetchContentfulError` if anything fails.
11
11
  - **Locale awareness** — locales are fetched from Contentful once and cached in-module, so validation costs nothing per fetch.
12
12
  - **Inferred types, optionally** — pass a [gql.tada](https://gql-tada.0no.co) or graphql-codegen document and the result and variable types come from the query itself. One command (`tada-init`) sets gql.tada up against your space. See [Typed queries with gql.tada](#typed-queries-with-gqltada).
@@ -22,7 +22,7 @@ yarn add @fourtwelvelabs/fetch-contentful graphql
22
22
 
23
23
  Settings resolve in this order — first hit wins, per setting:
24
24
 
25
- 1. **Per-call options** — `fetchContentful(query, { space, token, ... })`
25
+ 1. **Per-call options** — `fetchContentful(query, { space, deliveryToken, ... })`
26
26
  2. **Factory defaults** — set once with `createFetchContentful` (below)
27
27
  3. **Environment variables** — framework-neutral names first, then `NEXT_PUBLIC_`-prefixed equivalents
28
28
 
@@ -34,12 +34,22 @@ If `space` or the appropriate token can't be resolved, the promise rejects with
34
34
  | --- | --- | --- |
35
35
  | `space` | `CONTENTFUL_SPACE_ID` | `NEXT_PUBLIC_CONTENTFUL_SPACE_ID` |
36
36
  | `environment` | `CONTENTFUL_ENVIRONMENT` | `NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT` |
37
- | Delivery token | `CONTENTFUL_ACCESS_TOKEN` | `NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN` |
38
- | Preview token | `CONTENTFUL_PREVIEW_ACCESS_TOKEN` | `NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN` |
37
+ | `deliveryToken` | `CONTENTFUL_ACCESS_TOKEN` | `NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN` |
38
+ | `previewToken` | `CONTENTFUL_PREVIEW_ACCESS_TOKEN` | **none, by design** |
39
+
40
+ There is no environment variable for the locale: it defaults to **`en-US`**, and a project that wants a different one sets it once with `createFetchContentful` (below).
39
41
 
40
42
  The library reads the already-populated `process.env`; loading `.env` / `.env.local` files from disk is your platform's job (Next.js, Vite, dotenv all do this), so precedence between those files always matches your framework's rules.
41
43
 
42
- **In a Next.js project you don't need both names.** Set only the `NEXT_PUBLIC_` variant and it works everywhere: the server reads it like any other env var (the prefix only controls *client* exposure), and client bundles get it because the library reads these variables with literal `process.env.NEXT_PUBLIC_...` accesses — the exact pattern Next's build-time inliner string-replaces. Server-only projects (or any other framework) should use the neutral names. Think twice before exposing tokens to the browser at all: Contentful delivery tokens are read-only and commonly made public, but preview tokens should stay server-side.
44
+ **In a Next.js project you don't need both names.** Set only the `NEXT_PUBLIC_` variant and it works everywhere: the server reads it like any other env var (the prefix only controls *client* exposure), and client bundles get it because the library reads these variables with literal `process.env.NEXT_PUBLIC_...` accesses — the exact pattern Next's build-time inliner string-replaces. Server-only projects (or any other framework) should use the neutral names. This applies to the space, the environment and the delivery token; the preview token has no client-safe name, for the reason below.
45
+
46
+ ### The preview token is never read from a `NEXT_PUBLIC_` variable
47
+
48
+ Next.js exposes `NEXT_PUBLIC_` variables to the browser by string-replacing literal `process.env.NEXT_PUBLIC_…` accesses at build time. This library contains such accesses, so if it read the preview token from a prefixed name, setting that variable would bake the token into your public JavaScript — whether or not any client code ever asked for preview content. Importing the library would be enough.
49
+
50
+ A delivery token is read-only and commonly public. A **preview token reads unpublished content**, so it is resolved only from the unprefixed `CONTENTFUL_PREVIEW_ACCESS_TOKEN`. In Next.js that name is still readable on the server — the prefix only controls *client* exposure — so server-side preview (Draft Mode, route handlers, server components) works unchanged.
51
+
52
+ If you have decided that your app genuinely needs preview in the browser, pass `previewToken` explicitly. Shipping it is then your deliberate choice rather than something the library did on your behalf.
43
53
 
44
54
  ### Project-level defaults with `createFetchContentful`
45
55
 
@@ -51,9 +61,13 @@ import { createFetchContentful } from '@fourtwelvelabs/fetch-contentful';
51
61
 
52
62
  export const fetchContentful = createFetchContentful({
53
63
  space: 'abc123', // or leave unset to use env vars
54
- locale: 'en-US',
64
+ locale: 'de-DE', // override the 'en-US' default for the whole project
55
65
  retries: 3,
56
66
  });
67
+
68
+ // Configure both tokens once, and every call picks the right one:
69
+ // fetchContentful(QUERY) → deliveryToken
70
+ // fetchContentful(QUERY, { preview: true }) → previewToken
57
71
  ```
58
72
 
59
73
  ```ts
@@ -102,6 +116,8 @@ const data = await fetchContentful<PagesQuery>(
102
116
 
103
117
  Notice there is no `$preview` or `$locale` anywhere in the query: the options are injected as `preview: true` / `locale: "en-US"` arguments on the root field before the query is sent, and Contentful cascades both to every nested field. Arguments you write explicitly are never overridden, so per-field overrides like `title(locale: "de-DE")` keep working. Set `autoInjectArgs: false` to opt out. (The older style still works too — `$preview: Boolean` / `$locale: String` variables are auto-filled whenever a query declares them.)
104
118
 
119
+ `locale` is optional here — it defaults to `en-US`, so the same argument is injected whether or not you pass it. Pass a different code to override it, or `locale: null` to send no `locale` argument at all and let the space serve its own default locale.
120
+
105
121
  ## Typed queries with gql.tada
106
122
 
107
123
  Everything above types the response by hand (`fetchContentful<PagesQuery>`). You can instead have the types come from the query itself. Install the author-time peers, then run the setup command:
@@ -111,11 +127,31 @@ yarn add -D gql.tada @0no-co/graphqlsp
111
127
  npx @fourtwelvelabs/fetch-contentful tada-init
112
128
  ```
113
129
 
114
- That downloads your space's schema as SDL, registers `@0no-co/graphqlsp` in your `tsconfig.json` and writes `src/graphql.ts`. It reads the same environment variables the library does, is safe to re-run, and takes `--dry-run`. Restart the TypeScript server afterwards, and queries type themselves:
130
+ It reads the same environment variables the library does, is safe to re-run, and takes `--dry-run`. Three things happen: your space's schema is downloaded as SDL, `@0no-co/graphqlsp` is registered in your `tsconfig.json`, and **`src/graphql.ts`** is written a small file of your own that binds gql.tada to your schema:
131
+
132
+ ```ts
133
+ // src/graphql.ts — generated for you. Commit it; edit it freely.
134
+ import { initGraphQLTada } from 'gql.tada';
135
+ import type { ContentfulScalars } from '@fourtwelvelabs/fetch-contentful/tada';
136
+ import type { introspection } from './contentful-env.d.ts';
137
+
138
+ export const graphql = initGraphQLTada<{
139
+ introspection: introspection; // your space's content model, as types
140
+ scalars: ContentfulScalars; // DateTime → string, JSON → JsonValue, …
141
+ }>();
142
+
143
+ export { readFragment } from 'gql.tada';
144
+ export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada';
145
+ ```
146
+
147
+ `graphql` is a tagged template function doing two jobs at once: at runtime it parses your query into a GraphQL document, and at compile time it infers that query's result and variable types from the schema. It can't be an export of this package — it has to be bound to *your* space — which is why the command generates it.
148
+
149
+ Restart the TypeScript server so `@0no-co/graphqlsp` loads (it's what gives you autocomplete and field validation inside the backticks). Then import `graphql` from wherever `tada-init` put it, and queries type themselves:
115
150
 
116
151
  ```ts
152
+ // src/lib/pages.ts
117
153
  import { fetchContentful } from '@fourtwelvelabs/fetch-contentful';
118
- import { graphql } from './graphql';
154
+ import { graphql } from '../graphql';
119
155
 
120
156
  const PageQuery = graphql(`
121
157
  query Page($slug: String!) {
@@ -183,11 +219,12 @@ fetchContentful(query, {
183
219
  space: 'abc123', // default: NEXT_PUBLIC_CONTENTFUL_SPACE
184
220
  environment: 'master', // default: NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT ?? 'master'
185
221
  preview: false, // default: false
186
- locale: 'de-DE', // injected as a root-field argument (and $locale)
222
+ locale: 'en-US', // default: 'en-US'. null sends no locale at all
187
223
  validateLocale: true, // reject with LOCALE if locale isn't configured
188
- token: '...', // override the env-derived token
224
+ deliveryToken: '...', // CDA token, used when preview is false
225
+ previewToken: '...', // CPA token, used when preview is true
189
226
 
190
- // Request behaviour
227
+ // Request behavior
191
228
  variables: { slug: 'home' },
192
229
  retries: 5, // retry count after the initial attempt
193
230
  retryDelayMs: 250, // base backoff delay
@@ -12,10 +12,14 @@ function readEnvSettings() {
12
12
  return {
13
13
  space: void 0,
14
14
  environment: void 0,
15
+ deliveryToken: void 0,
15
16
  token: void 0,
16
17
  previewToken: void 0
17
18
  };
18
19
  }
20
+ const deliveryToken = orUndefined(
21
+ process.env.CONTENTFUL_ACCESS_TOKEN || process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN
22
+ );
19
23
  return {
20
24
  space: orUndefined(
21
25
  process.env.CONTENTFUL_SPACE_ID || process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID
@@ -23,12 +27,10 @@ function readEnvSettings() {
23
27
  environment: orUndefined(
24
28
  process.env.CONTENTFUL_ENVIRONMENT || process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT
25
29
  ),
26
- token: orUndefined(
27
- process.env.CONTENTFUL_ACCESS_TOKEN || process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN
28
- ),
29
- previewToken: orUndefined(
30
- process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN || process.env.NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN
31
- )
30
+ deliveryToken,
31
+ token: deliveryToken,
32
+ // No `NEXT_PUBLIC_` fallback: see the note at the top of this file.
33
+ previewToken: orUndefined(process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN)
32
34
  };
33
35
  }
34
36
 
@@ -673,7 +675,7 @@ the library reads them at runtime.`;
673
675
  function resolveConfig(args) {
674
676
  const env = readEnvSettings();
675
677
  const space = args.values.space ?? env.space;
676
- const token = args.values.token ?? env.token;
678
+ const token = args.values.token ?? env.deliveryToken;
677
679
  const environment = args.values.environment ?? env.environment ?? "master";
678
680
  const missing = [];
679
681
  if (!space) {
@@ -916,7 +918,10 @@ async function dispatch(argv, io) {
916
918
  }
917
919
  function knownTokens(argv) {
918
920
  const env = readEnvSettings();
919
- const tokens = [env.token, env.previewToken];
921
+ const tokens = [
922
+ env.deliveryToken,
923
+ env.previewToken
924
+ ];
920
925
  for (const [index, argument] of argv.entries()) {
921
926
  if (argument.startsWith("--token=")) tokens.push(argument.slice(8));
922
927
  else if (argument === "--token") tokens.push(argv[index + 1]);