@gradial/aci 0.1.52-rc.2 → 0.1.52-rc.21
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 +7 -0
- package/README.md +4 -4
- package/dist/astro/preview.js +2 -17
- package/dist/audience-data-models.d.ts +289 -16
- package/dist/audience-data-models.js +99 -2
- package/dist/cli/compile-registry.d.mts +5 -1
- package/dist/cli/compile-registry.mjs +25 -10
- package/dist/cli/validate-content.d.mts +31 -1
- package/dist/cli/validate-content.mjs +50 -12
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/api.d.ts +3 -7
- package/dist/collect/api.js +2 -8
- package/dist/collect/batcher.d.ts +6 -5
- package/dist/collect/batcher.js +6 -5
- package/dist/collect/index.js +1 -1
- package/dist/content/provider.d.ts +1 -0
- package/dist/content/resolve-slots.d.ts +9 -2
- package/dist/content/resolve-slots.js +5 -3
- package/dist/content/types.d.ts +2 -0
- package/dist/edge/expression-capabilities.v1.d.ts +106 -0
- package/dist/edge/expression-capabilities.v1.js +165 -0
- package/dist/internal/domain/edge/compose.d.ts +24 -0
- package/dist/internal/domain/edge/compose.js +89 -0
- package/dist/internal/domain/edge/identity.js +1 -1
- package/dist/internal/domain/edge/index.d.ts +2 -0
- package/dist/internal/domain/edge/index.js +2 -0
- package/dist/internal/domain/edge/mimic_token.d.ts +13 -0
- package/dist/internal/domain/edge/mimic_token.js +58 -0
- package/dist/internal/domain/edge/personalization-core.d.ts +69 -5
- package/dist/internal/domain/edge/personalization-core.js +693 -58
- package/dist/internal/domain/edge/record/codec.d.ts +5 -2
- package/dist/internal/domain/edge/record/codec.js +85 -12
- package/dist/internal/domain/edge/resolve.js +38 -18
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
- package/dist/internal/domain/edge/types.d.ts +52 -9
- package/dist/internal/domain/edge/types.js +3 -3
- package/dist/mimic/core.d.ts +2 -0
- package/dist/mimic/core.js +31 -0
- package/dist/next/error-seat-payload.js +1 -1
- package/dist/next/hosted-page-target.d.ts +7 -0
- package/dist/next/hosted-page-target.js +39 -0
- package/dist/next/index.d.ts +0 -1
- package/dist/next/index.js +0 -1
- package/dist/next/page.d.ts +29 -6
- package/dist/next/page.js +47 -25
- package/dist/next/server.js +28 -1
- package/dist/next/vercel-edge-projection-v1.d.ts +0 -9
- package/dist/next/vercel-edge-projection-v1.js +9 -539
- package/dist/preview/core.d.ts +1 -0
- package/dist/preview/core.js +27 -0
- package/dist/providers/s3.js +4 -3
- package/dist/react/VideoPlayer.d.ts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/types/block-ref.d.ts +2 -0
- package/dist/types/block-ref.js +1 -0
- package/package.json +16 -7
- package/src/cli/compile-registry.mjs +24 -10
- package/src/cli/validate-content.mjs +52 -13
- package/src/types/block-ref.test.ts +45 -0
- package/src/types/block-ref.ts +2 -0
- package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
- package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
- package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
- package/templates/astro/template/package.json.tmpl +1 -1
- package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
- package/templates/common/.agents/skills/aci/SKILL.md +6 -0
- package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
- package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
- package/templates/common/.agents/skills/dam-delivery/SKILL.md +228 -0
- package/templates/common/.agents/skills/dam-delivery/references/parameter-contract.md +107 -0
- package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +131 -0
- package/templates/common/.content/pages/{home/_index.json → _index.json} +10 -0
- package/templates/common/.content/schemas/site.schema.json +1 -1
- package/templates/common/.nvmrc +1 -1
- package/templates/common/starter-content/.content/.rules-baseline.json +8 -0
- package/templates/common/starter-content/.content/config/site.json +1 -1
- package/templates/common/starter-content/.content/content-rules/no-public-resource-paths.json +6 -0
- package/templates/common/starter-content/.content/fragments/navbar/_index.json +23 -20
- package/templates/common/starter-content/.content/fragments/site-footer/_index.json +59 -56
- package/templates/common/starter-content/.content/pages/404/_index.json +1 -0
- package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
- package/templates/common/starter-content/.content/pages/error/_index.json +1 -0
- package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
- package/templates/common/starter-content/.content/patterns/components/button.json +55 -0
- package/templates/common/starter-content/.content/patterns/components/container.json +62 -0
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +30 -0
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +57 -0
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +142 -0
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +42 -0
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +203 -0
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +115 -0
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +45 -0
- package/templates/common/starter-content/.content/patterns/layouts/default.json +14 -0
- package/templates/common/starter-content/.content/schemas/site.schema.json +1 -1
- package/templates/nextjs/template/.aci.yaml +10 -0
- package/templates/nextjs/template/package.json.tmpl +4 -3
- package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
- package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
- package/templates/nextjs/template/src/app/layout.tsx +2 -0
- package/dist/edge/personalization-core.d.ts +0 -11
- package/dist/edge/personalization-core.js +0 -269
- package/dist/next/collect-route.d.ts +0 -19
- package/dist/next/collect-route.js +0 -459
- package/dist/next/envelope-digest.d.ts +0 -21
- package/dist/next/envelope-digest.js +0 -58
- package/dist/next/middleware.d.ts +0 -40
- package/dist/next/middleware.js +0 -475
- package/dist/next/personalization-host.d.ts +0 -134
- package/dist/next/personalization-host.js +0 -270
- package/dist/next/root-layout.d.ts +0 -4
- package/dist/next/root-layout.js +0 -5
- package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
- package/dist/next/vercel-collect-sdk-route.js +0 -181
- package/dist/next/vercel-personalization.d.ts +0 -4
- package/dist/next/vercel-personalization.js +0 -4
- package/dist/next/vercel-s3-personalization.d.ts +0 -79
- package/dist/next/vercel-s3-personalization.js +0 -570
- /package/templates/common/.content/overlays/audience/preference-blue/pages/{home/_index.json → _index.json} +0 -0
- /package/templates/common/.content/overlays/audience/preference-red/pages/{home/_index.json → _index.json} +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dam-delivery
|
|
3
|
+
description: Mint and use signed DAM delivery URLs for on-demand image transforms, and manage public permanent URLs (permalinks) for CMS fields and email. Use when a page or server function needs a resized, cropped, or format-converted image at render time, when choosing between pre-baked derivatives and dynamic transforms, when an image URL must be pasted somewhere that cannot call a mint API (Contentful-class CMS fields, Marketo/SFMC email), or when a delivery URL returns 403/404 unexpectedly.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DAM Delivery — Signed Transform URLs
|
|
7
|
+
|
|
8
|
+
The DAM serves parameterized image transforms (resize, crop, quality,
|
|
9
|
+
webp/avif) through signed URLs under `/dam/i/{assetId}/…`. The API mints
|
|
10
|
+
them; clients can neither construct nor alter them — every URL is
|
|
11
|
+
HMAC-signed server-side over a closed, validated parameter contract.
|
|
12
|
+
|
|
13
|
+
## The One Rule: URLs Are Render-Time Artifacts
|
|
14
|
+
|
|
15
|
+
Delivery URLs **expire** (24h maximum by default) and are **banned from
|
|
16
|
+
published payloads**. The publish validator rejects any payload containing
|
|
17
|
+
the `/dam/i/` path marker, exactly as it rejects raw `dam/blobs` pointers.
|
|
18
|
+
|
|
19
|
+
- **DO** mint at render time: SSR handlers, server functions, API-backed
|
|
20
|
+
routes, previews.
|
|
21
|
+
- **DO NOT** bake minted URLs into content JSON, compiled payloads, or
|
|
22
|
+
anything that gets published — a frozen release would carry URLs that
|
|
23
|
+
permanently 404 within a day.
|
|
24
|
+
- For content baked into releases, keep using the pre-baked derivative
|
|
25
|
+
pipeline (`imageSlots` in contracts → `dam/derivatives/…` URLs via
|
|
26
|
+
`imageAttrs()`/`Picture`). Delivery URLs complement it; they do not
|
|
27
|
+
replace it.
|
|
28
|
+
|
|
29
|
+
## Minting
|
|
30
|
+
|
|
31
|
+
One endpoint mints single or batch; the request is always an array of
|
|
32
|
+
parameter sets and one signed URL comes back per set, in order.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
POST /v1/dam/assets/{assetId}/delivery-urls
|
|
36
|
+
{
|
|
37
|
+
"requests": [
|
|
38
|
+
{ "width": 800, "height": 600, "fit": "fill", "gravity": "noea",
|
|
39
|
+
"quality": 90, "dpr": 2, "format": "avif" },
|
|
40
|
+
{ "width": 400 }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Response — exactly one of `url`/`error` per item:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"results": [
|
|
50
|
+
{ "url": "/dam/i/ast_…/…", "expiresAt": "2026-08-16T12:00:00Z" },
|
|
51
|
+
{ "error": { "status": 400, "code": "dam.delivery.invalid_param",
|
|
52
|
+
"message": "width must be 1-4096", "details": { "param": "width" } } }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Asset-level failures (asset not found, unavailable, permission denied)
|
|
58
|
+
fail the whole request; parameter failures are per-item so one bad set
|
|
59
|
+
cannot sink its siblings. Batches cap at 100 parameter sets.
|
|
60
|
+
|
|
61
|
+
CLI equivalents:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Single mint from flags
|
|
65
|
+
aci dam delivery-url ast_2E9qL0001aBcDeF --width 800 --height 600 \
|
|
66
|
+
--fit fill --gravity noea --quality 90 --format avif --dpr 2
|
|
67
|
+
|
|
68
|
+
# Batch from a JSON array (prints one URL<TAB>expiresAt line per mint)
|
|
69
|
+
aci dam delivery-url ast_2E9qL0001aBcDeF \
|
|
70
|
+
--params '[{"width":400},{"width":800,"dpr":2}]'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## The Parameter Contract (Closed)
|
|
74
|
+
|
|
75
|
+
Everything outside this table is rejected. Full ranges, interactions, and
|
|
76
|
+
error codes: `references/parameter-contract.md`.
|
|
77
|
+
|
|
78
|
+
| Param | Values | Default | Notes |
|
|
79
|
+
| --- | --- | --- | --- |
|
|
80
|
+
| `width`, `height` | 1–4096 | none | `width·dpr` and `height·dpr` must each stay ≤ 4096 |
|
|
81
|
+
| `fit` | `fit` \| `fill` | `fit` | requires `width` or `height` |
|
|
82
|
+
| `gravity` | `ce`, `no`, `so`, `ea`, `we`, `noea`, `nowe`, `soea`, `sowe`, or `fp:{x}:{y}` (x,y ∈ [0,1]) | `ce` | non-default gravity requires `fit: "fill"` |
|
|
83
|
+
| `quality` | 1–100 | 82 | |
|
|
84
|
+
| `dpr` | 1–3 | 1 | multiplies output dimensions |
|
|
85
|
+
| `format` | `webp` \| `avif` \| `jpeg` \| `png` | `webp` | becomes the URL extension |
|
|
86
|
+
|
|
87
|
+
Special cases:
|
|
88
|
+
|
|
89
|
+
- **SVG** is passthrough-only: only the empty parameter set `{}` is
|
|
90
|
+
accepted; any transform param is rejected.
|
|
91
|
+
- **Non-image assets** are never served through `/dam/i/*`; use the
|
|
92
|
+
download endpoints instead.
|
|
93
|
+
- Oversized blobs (over the origin's 200 MB streaming envelope) refuse to
|
|
94
|
+
mint with `dam.delivery.source_too_large`.
|
|
95
|
+
|
|
96
|
+
## Expiry Semantics
|
|
97
|
+
|
|
98
|
+
- Every URL carries a signed expiry; `expiresAt` in the response is that
|
|
99
|
+
instant. After it, the URL 404s at the origin and cached copies die at
|
|
100
|
+
the CDN.
|
|
101
|
+
- The bound is quantized (default 1h): identical requests minted within
|
|
102
|
+
the same quantum return **byte-equal URLs**, so re-minting per render is
|
|
103
|
+
cheap and cache-friendly. Effective lifetime is between `maxTTL − 1h`
|
|
104
|
+
and `maxTTL` (default 24h).
|
|
105
|
+
- A version whose scheduled expiry revokes delivery gets URLs that expire
|
|
106
|
+
exactly at that boundary, which may be much sooner. Never assume a
|
|
107
|
+
fixed TTL — read `expiresAt`.
|
|
108
|
+
- Re-mint instead of caching URLs long-term. Caching a minted URL for the
|
|
109
|
+
duration of one render/request is fine.
|
|
110
|
+
|
|
111
|
+
## Availability: Why Mints Get Denied
|
|
112
|
+
|
|
113
|
+
Minting serves **only the latest approved, publishable version** and fails
|
|
114
|
+
closed. Expect typed errors (HTTP 422 unless noted):
|
|
115
|
+
|
|
116
|
+
| Code | Meaning |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `dam.asset.no_approved_version` | draft-only, embargoed, expired, or usage-restricted versions — nothing publishable |
|
|
119
|
+
| `dam.asset.not_active` | archived or deleted |
|
|
120
|
+
| `dam.asset.legal_hold` | under legal hold |
|
|
121
|
+
| `dam.asset.blocked` | asset-wide publication block (usage restriction) |
|
|
122
|
+
| `dam.permission_denied` | caller lacks download permission (403) |
|
|
123
|
+
| `dam.delivery.not_image` | non-image MIME |
|
|
124
|
+
|
|
125
|
+
There is no fallback to unapproved versions anywhere. Approval changes URL
|
|
126
|
+
identity (new blob → new URLs on next mint); already-minted URLs keep
|
|
127
|
+
serving what they named until they expire.
|
|
128
|
+
|
|
129
|
+
## Public Permanent URLs (Permalinks)
|
|
130
|
+
|
|
131
|
+
Assets flagged `lifecycle.publicDelivery` also serve through a **permanent,
|
|
132
|
+
public URL** — Sanity-style, deterministic from the assetId alone, no mint
|
|
133
|
+
API in the loop:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
https://{site}/dam/a/{assetId}?w=640&h=384&fit=crop&fm=jpg&q=80
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
It always resolves to the **current approved version** (approve a new
|
|
140
|
+
version and pasted URLs pick it up within ~30s, no re-paste), and it dies
|
|
141
|
+
within the revocation budget (≤ 30s for fresh resolution) on embargo,
|
|
142
|
+
expiry, archive, or unpublish. Transform parameters are
|
|
143
|
+
client-constructable but **validated and quantized** at the edge — `w`/`h`
|
|
144
|
+
snap down to a fixed ladder, `q` to steps of 5 — and unknown parameter
|
|
145
|
+
names (`utm_*`, `mkt_tok`, any ESP decoration) are **stripped, never
|
|
146
|
+
errors**, so tracking-decorated links keep working. Invalid *values* of
|
|
147
|
+
the contract params (`fm=bmp`, `w=abc`) are a 400: author error fails
|
|
148
|
+
loudly. Full grammar, quantization ladder, and Sanity-style examples:
|
|
149
|
+
`references/permalink-grammar.md`.
|
|
150
|
+
|
|
151
|
+
**When to use which lane:**
|
|
152
|
+
|
|
153
|
+
- **Signed delivery URLs** (`/dam/i/*`, above): everything that renders
|
|
154
|
+
server-side and can re-mint — SSR, server functions, previews.
|
|
155
|
+
- **Permalinks** (`/dam/a/*`): places where a URL is pasted once and must
|
|
156
|
+
live for years with no code in the loop — CMS fields, Marketo/SFMC
|
|
157
|
+
email. Requires the asset to be flagged public.
|
|
158
|
+
|
|
159
|
+
### Getting the URL
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
GET /v1/dam/assets/{assetId}/permalinks
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Returns the base permalink and per-recipe URLs (path + absolute URL), the
|
|
166
|
+
grammar constants (width ladder, quality step, defaults), the
|
|
167
|
+
`publicDelivery` flag state, and a `warning` field (below). CLI:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
aci dam permalink ast_2E9qL0001aBcDeF # base URL
|
|
171
|
+
aci dam permalink ast_2E9qL0001aBcDeF --recipe email-hero
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The permalink path is deterministic, so the endpoint answers even before
|
|
175
|
+
the flag is flipped — check `publicDelivery` to know whether it resolves.
|
|
176
|
+
|
|
177
|
+
### Flipping the Flag
|
|
178
|
+
|
|
179
|
+
`publicDelivery` is patched through the governance plane:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
PATCH /v1/dam/assets/{assetId}/governance
|
|
183
|
+
{ "expectedHead": "…", "lifecycle": { "publicDelivery": true } }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Flipping **on** is a "make public" act and passes the eligibility gate
|
|
187
|
+
against the version the permalink would serve. A refusal is a typed 400,
|
|
188
|
+
code `dam.delivery.public_flip_denied`, with `details.reason` carrying the
|
|
189
|
+
gate's own reason — e.g. `no_approved_version`, `not_image`, a
|
|
190
|
+
`block_new_usage` restriction, a publication-less license, or an expiry
|
|
191
|
+
the lane cannot enforce. Fix the governance fact; do not retry blind.
|
|
192
|
+
Flipping **off** is a plain patch and kills public serving within the
|
|
193
|
+
revocation budget.
|
|
194
|
+
|
|
195
|
+
### Doctrine for Email and CMS (the two rules)
|
|
196
|
+
|
|
197
|
+
1. **Paste the permalink; never let the ESP import the bytes.** Many email
|
|
198
|
+
tools offer to "import" or "copy" images to their own CDN — refuse. An
|
|
199
|
+
imported copy escapes governance and revocation forever; the permalink
|
|
200
|
+
is the governed pointer, and it is precisely the ≤ 30s revocation
|
|
201
|
+
budget that makes it safe to paste.
|
|
202
|
+
2. **Assets needing `block_new_usage` semantics do not get flagged
|
|
203
|
+
public.** The permalink lane cannot tell an old placement from a new
|
|
204
|
+
one, so `block_new_usage` cannot be enforced there — the flip gate
|
|
205
|
+
refuses such assets at authoring time. Use `unpublish` or
|
|
206
|
+
`block_all_usage` expiry actions (the lane enforces those boundaries
|
|
207
|
+
exactly), or keep the asset on the signed lane. If `block_new_usage`
|
|
208
|
+
governance lands on an *already-public* asset, the permalink keeps
|
|
209
|
+
serving and the permalinks API surfaces
|
|
210
|
+
`warning: "block_new_usage_in_force"` — treat that warning as "review
|
|
211
|
+
this asset's public status now".
|
|
212
|
+
|
|
213
|
+
Email format tip: Outlook desktop cannot render WebP — use `fm=jpg` or
|
|
214
|
+
`fm=png` in email permalinks.
|
|
215
|
+
|
|
216
|
+
## Draft Previews (Management Surfaces Only)
|
|
217
|
+
|
|
218
|
+
Drafts are reachable only by naming their version — authenticated, never
|
|
219
|
+
CDN-cached, never via `/dam/i/*`:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
GET /v1/dam/assets/{assetId}/versions/{versionId}/preview?width=512
|
|
223
|
+
GET /v1/dam/assets/{assetId}/versions/{versionId}/download
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The preview takes the same transform parameters as query strings and
|
|
227
|
+
streams the transformed image with `Cache-Control: private, no-store`.
|
|
228
|
+
Use these for editorial UIs; never for site rendering.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Delivery Parameter Contract Reference
|
|
2
|
+
|
|
3
|
+
The mint API accepts exactly this parameter set. Anything else — unknown
|
|
4
|
+
fields, out-of-range values, invalid combinations — is rejected with a
|
|
5
|
+
typed error. Parameters are canonicalized server-side (fixed order,
|
|
6
|
+
defaults elided) so numerically equal requests produce byte-equal URLs.
|
|
7
|
+
|
|
8
|
+
## Parameters
|
|
9
|
+
|
|
10
|
+
### width, height
|
|
11
|
+
|
|
12
|
+
- Integers, 1–4096. Omit (or 0) for "not constrained".
|
|
13
|
+
- The **effective** output dimension is capped, not the raw value:
|
|
14
|
+
`width·dpr ≤ 4096` and `height·dpr ≤ 4096`. `width: 4096, dpr: 2` is
|
|
15
|
+
rejected even though each value is individually legal.
|
|
16
|
+
- Supplying only one dimension scales the other to preserve aspect ratio.
|
|
17
|
+
|
|
18
|
+
### fit
|
|
19
|
+
|
|
20
|
+
- `"fit"` (default): scale to fit inside width×height, preserving aspect
|
|
21
|
+
ratio, never cropping.
|
|
22
|
+
- `"fill"`: scale and crop to exactly fill width×height.
|
|
23
|
+
- Requires at least one of `width`/`height` when set explicitly.
|
|
24
|
+
|
|
25
|
+
### gravity
|
|
26
|
+
|
|
27
|
+
- Controls which region survives a `fill` crop.
|
|
28
|
+
- Compass points: `ce` (center, default), `no`, `so`, `ea`, `we`, `noea`,
|
|
29
|
+
`nowe`, `soea`, `sowe`.
|
|
30
|
+
- Focal point: `fp:{x}:{y}` with x and y in `[0,1]` — `fp:0.5:0.25` keeps
|
|
31
|
+
the point half across, a quarter down. Numerically equal focal points
|
|
32
|
+
canonicalize identically (`fp:0.50:0.250` ≡ `fp:0.5:0.25`).
|
|
33
|
+
- **Non-default gravity requires `fit: "fill"`** — gravity only means
|
|
34
|
+
something when cropping. `{ "width": 100, "gravity": "noea" }` is
|
|
35
|
+
rejected; add `"fit": "fill"`.
|
|
36
|
+
|
|
37
|
+
### quality
|
|
38
|
+
|
|
39
|
+
- Integer 1–100, default 82. Applies to lossy output formats.
|
|
40
|
+
|
|
41
|
+
### dpr
|
|
42
|
+
|
|
43
|
+
- Integer 1–3, default 1. Multiplies the output dimensions for
|
|
44
|
+
high-density displays; see the effective-dimension cap above.
|
|
45
|
+
|
|
46
|
+
### format
|
|
47
|
+
|
|
48
|
+
- `"webp"` (default), `"avif"`, `"jpeg"`, `"png"`. Becomes the URL's file
|
|
49
|
+
extension.
|
|
50
|
+
|
|
51
|
+
## Special Cases
|
|
52
|
+
|
|
53
|
+
- **SVG assets**: passthrough only. The empty parameter set `{}` mints a
|
|
54
|
+
passthrough URL serving the original SVG; any transform parameter is
|
|
55
|
+
rejected with `dam.delivery.svg_transform`.
|
|
56
|
+
- **Non-image assets**: never served through delivery URLs
|
|
57
|
+
(`dam.delivery.not_image`). Use the download endpoints.
|
|
58
|
+
- **Oversized blobs**: blobs over the origin's 200 MB streaming envelope
|
|
59
|
+
refuse to mint (`dam.delivery.source_too_large`).
|
|
60
|
+
|
|
61
|
+
## Error Codes
|
|
62
|
+
|
|
63
|
+
Per-item (inside `results[i].error`, HTTP 200 on the envelope):
|
|
64
|
+
|
|
65
|
+
| Code | Cause |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `dam.delivery.invalid_param` | range or combination violation; `details.param` names the offender |
|
|
68
|
+
| `dam.delivery.svg_transform` | transform params on an SVG asset |
|
|
69
|
+
| `dam.delivery.not_image` | non-image MIME |
|
|
70
|
+
| `dam.delivery.source_too_large` | blob over the streaming envelope |
|
|
71
|
+
|
|
72
|
+
Request-level (problem+json):
|
|
73
|
+
|
|
74
|
+
| Code | HTTP | Cause |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `dam.asset.no_approved_version` | 422 | no publishable approved version (draft-only, embargoed, expired, or version-scoped restriction) |
|
|
77
|
+
| `dam.asset.not_active` | 422 | archived / deleted asset |
|
|
78
|
+
| `dam.asset.legal_hold` | 422 | legal hold active |
|
|
79
|
+
| `dam.asset.blocked` | 422 | asset-wide publication block (usage restriction) |
|
|
80
|
+
| `dam.permission_denied` | 403 | caller lacks download permission |
|
|
81
|
+
| `NOT_FOUND` | 404 | unknown asset |
|
|
82
|
+
|
|
83
|
+
## Worked Examples
|
|
84
|
+
|
|
85
|
+
```jsonc
|
|
86
|
+
// Hero image, art-directed crop on a focal point, retina
|
|
87
|
+
{ "width": 1440, "height": 600, "fit": "fill", "gravity": "fp:0.62:0.3", "dpr": 2 }
|
|
88
|
+
|
|
89
|
+
// Thumbnail grid cell, default gravity center-crop
|
|
90
|
+
{ "width": 320, "height": 320, "fit": "fill" }
|
|
91
|
+
|
|
92
|
+
// Constrain width only, keep aspect ratio, high quality JPEG
|
|
93
|
+
{ "width": 1200, "quality": 95, "format": "jpeg" }
|
|
94
|
+
|
|
95
|
+
// SVG passthrough (the ONLY valid SVG request)
|
|
96
|
+
{ }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Rejected examples and why:
|
|
100
|
+
|
|
101
|
+
```jsonc
|
|
102
|
+
{ "width": 4096, "dpr": 3 } // effective width 12288 > 4096
|
|
103
|
+
{ "width": 100, "gravity": "noea" } // gravity requires fit "fill"
|
|
104
|
+
{ "fit": "fill" } // fit requires width or height
|
|
105
|
+
{ "gravity": "fp:1.5:0" } // focal coords must be in [0,1]
|
|
106
|
+
{ "format": "gif" } // outside the closed format set
|
|
107
|
+
```
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Public Permalink Grammar Reference (`/dam/a/*`)
|
|
2
|
+
|
|
3
|
+
The public permanent URL for an asset:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
/dam/a/{assetId}[/{slug-or-recipe}[.{ext}]]?w=400&h=300&fit=crop&fm=jpg&q=80
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
(The query above shows the *shape* only — `w=400`/`h=300` are off-ladder
|
|
10
|
+
and would serve 384x256 after snapping. The recommended examples below
|
|
11
|
+
sit exactly on rungs; pick rung values when you control the URL.)
|
|
12
|
+
|
|
13
|
+
Permanent, deterministic from the assetId alone, resolving to the current
|
|
14
|
+
approved version while the asset is flagged `publicDelivery` and available
|
|
15
|
+
for delivery. Everything below is edge-enforced; the API is never in the
|
|
16
|
+
per-view path. The grammar constants (ladder, steps, defaults) are also
|
|
17
|
+
served machine-readably by `GET /v1/dam/assets/{assetId}/permalinks`.
|
|
18
|
+
|
|
19
|
+
## Parameters (open but quantized)
|
|
20
|
+
|
|
21
|
+
Names are Sanity-compatible. Unknown parameter *names* are stripped —
|
|
22
|
+
never errors — so ESP tracking decorations (`utm_*`, `mkt_tok`) cannot
|
|
23
|
+
break an image. Invalid *values* of these contract params are a 400.
|
|
24
|
+
|
|
25
|
+
| Param | Values | Default | Quantization |
|
|
26
|
+
| --- | --- | --- | --- |
|
|
27
|
+
| `w`, `h` | positive integers | none | snapped DOWN to the ladder `16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840`; below 16 snaps up to 16, above 3840 down to 3840 |
|
|
28
|
+
| `fit` | `crop` \| `max` | `max` | `crop` requires **both** `w` and `h` |
|
|
29
|
+
| `fm` | `webp` \| `avif` \| `jpg` \| `png` | `webp` | — |
|
|
30
|
+
| `q` | 1–100 | absent → renders at 82 | snapped DOWN to steps of 5; below 5 snaps up to 5; an explicit `q=80` serves 80, never the default — explicit quality is honored after snapping, not silently inflated |
|
|
31
|
+
|
|
32
|
+
The ladder is the Next.js image `srcset` ladder verbatim, so breakpoints
|
|
33
|
+
authored against any modern framework land on rungs instead of snapping
|
|
34
|
+
visibly. Quantization is deliberate abuse control: it bounds the
|
|
35
|
+
transform cache keyspace per asset.
|
|
36
|
+
|
|
37
|
+
Deliberately **not** in the grammar:
|
|
38
|
+
|
|
39
|
+
- No `dpr` — pick the right `w` rung instead (the ladder already covers
|
|
40
|
+
device densities the way `srcset` consumes them).
|
|
41
|
+
- No `Accept`-header format negotiation — format is always the explicit
|
|
42
|
+
`fm`. For email use `fm=jpg` or `fm=png` (Outlook desktop has no WebP).
|
|
43
|
+
- `crop=focal` is reserved, not shipped.
|
|
44
|
+
|
|
45
|
+
## Canonical Form, Slugs, and Recipes
|
|
46
|
+
|
|
47
|
+
The canonical URL is **extension-less**: `/dam/a/{assetId}?…`. The
|
|
48
|
+
optional second path segment (extension stripped) is either:
|
|
49
|
+
|
|
50
|
+
- **A registered public recipe** (`/dam/a/{id}/email-hero.jpg`): expands
|
|
51
|
+
server-side into a fixed parameter set from the public recipe registry.
|
|
52
|
+
Recipe URLs are a second canonical form — no redirect hop. Adding
|
|
53
|
+
transform query params alongside a recipe is a 400 (ambiguity is author
|
|
54
|
+
error); ESP decorations are stripped first, as everywhere. The
|
|
55
|
+
extension is readability only — the recipe's format wins. The registry
|
|
56
|
+
ships empty; `GET …/permalinks` lists what is registered.
|
|
57
|
+
- **Anything else — a readable slug** (`/dam/a/{id}/spring-sale.jpg`):
|
|
58
|
+
answers a 301 to the canonical no-slug URL with the same (canonicalized)
|
|
59
|
+
query. Use the slug form when a tool insists on an extension-bearing
|
|
60
|
+
image URL; it costs one cached redirect and is stable forever.
|
|
61
|
+
|
|
62
|
+
## Response Semantics
|
|
63
|
+
|
|
64
|
+
| Response | Meaning |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| 302 → `/dam/i/…` | serving: a short-lived signed inner URL (≤ 2h), re-minted per redirect |
|
|
67
|
+
| 301 | slug normalization to the canonical URL |
|
|
68
|
+
| 400 `no-store` | invalid value of a contract param, transform params on an SVG or alongside a recipe, malformed assetId |
|
|
69
|
+
| 404 `no-store` | asset is not publicly served: flag off, not yet approved, embargoed, expired, archived, legal hold |
|
|
70
|
+
| 503 `no-store` | resolution state unreadable — fail-closed, never a guess |
|
|
71
|
+
|
|
72
|
+
The 302 carries a short cache TTL (default 30s) — the revocation dial.
|
|
73
|
+
On unpublish/archive/embargo/revoking-expiry, fresh resolution dies
|
|
74
|
+
within ≤ 30s; a flip **to** serving may additionally ride a ~10s
|
|
75
|
+
error-caching echo. Approving a new version re-points the permalink
|
|
76
|
+
within the same budget; already-fetched inner URLs keep serving the bytes
|
|
77
|
+
they named until they expire (≤ 2h).
|
|
78
|
+
|
|
79
|
+
## SVG
|
|
80
|
+
|
|
81
|
+
SVG permalinks are passthrough-only: the bare permalink 302s to the SVG
|
|
82
|
+
passthrough; any transform parameter (or recipe) is a 400.
|
|
83
|
+
|
|
84
|
+
## Sanity-Style Examples
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<!-- Fixed-size email hero, JPEG for Outlook, explicit quality.
|
|
88
|
+
640 and 384 are ladder rungs; keep the HTML width/height on the same
|
|
89
|
+
rungs so the served crop and the layout box agree exactly -->
|
|
90
|
+
<img src="https://www.example.com/dam/a/ast_2E9qL0001aBcDeF?w=640&h=384&fit=crop&fm=jpg&q=80"
|
|
91
|
+
width="640" height="384" alt="Spring sale hero">
|
|
92
|
+
|
|
93
|
+
<!-- Responsive srcset on ladder rungs (no dpr param — use w) -->
|
|
94
|
+
<img src="https://www.example.com/dam/a/ast_2E9qL0001aBcDeF?w=828"
|
|
95
|
+
srcset="https://www.example.com/dam/a/ast_2E9qL0001aBcDeF?w=640 640w,
|
|
96
|
+
https://www.example.com/dam/a/ast_2E9qL0001aBcDeF?w=828 828w,
|
|
97
|
+
https://www.example.com/dam/a/ast_2E9qL0001aBcDeF?w=1200 1200w"
|
|
98
|
+
sizes="(max-width: 640px) 100vw, 640px" alt="…">
|
|
99
|
+
|
|
100
|
+
<!-- Extension-bearing slug form for ESP editors that validate by extension:
|
|
101
|
+
one cached 301 to the canonical URL, stable forever -->
|
|
102
|
+
https://www.example.com/dam/a/ast_2E9qL0001aBcDeF/spring-sale.jpg?w=640&fm=jpg
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Quantization in practice: `?w=333&q=77` and `?w=256&q=75` serve the
|
|
106
|
+
identical rendition (333 snaps to 256, 77 snaps to 75) — do not expect
|
|
107
|
+
pixel-exact widths off the ladder.
|
|
108
|
+
|
|
109
|
+
## The Flag, the Gate, and the Warning
|
|
110
|
+
|
|
111
|
+
- `lifecycle.publicDelivery` (asset-level, governance PATCH) is the one
|
|
112
|
+
admission act. Flip-on passes the eligibility gate against the version
|
|
113
|
+
the permalink would serve; refusal is a typed 400
|
|
114
|
+
`dam.delivery.public_flip_denied` with `details.reason` (e.g.
|
|
115
|
+
`no_approved_version`, `not_image`, `source_too_large`, a
|
|
116
|
+
`block_new_usage` restriction, a publication-less license, an
|
|
117
|
+
unenforceable expiry action).
|
|
118
|
+
- Version selection is automatic and fail-closed: the latest approved,
|
|
119
|
+
deliverable version whose license (when present) grants `publication`
|
|
120
|
+
and whose `expiresAt` (when set) the lane can actually enforce
|
|
121
|
+
(`unpublish`/`block_all_usage`). Versions failing those rows are
|
|
122
|
+
skipped in favor of the previous passing one.
|
|
123
|
+
- `GET …/permalinks` returns `warning: "block_new_usage_in_force"` when
|
|
124
|
+
`block_new_usage` governance landed on an already-public asset (the
|
|
125
|
+
flip gate refuses it up front, so this only happens for governance
|
|
126
|
+
added after the flip). The permalink keeps serving — the lane cannot
|
|
127
|
+
distinguish existing placements from new ones — so treat the warning as
|
|
128
|
+
a prompt to review the asset's public status.
|
|
129
|
+
- Legal hold: flipping a legal-held asset on succeeds but the permalink
|
|
130
|
+
404s for the hold's duration and starts serving the moment the hold
|
|
131
|
+
lifts — no re-flip needed.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"regions": {
|
|
12
12
|
"main": [
|
|
13
13
|
{
|
|
14
|
+
"$type": "component",
|
|
14
15
|
"id": "hero",
|
|
15
16
|
"component": "home_hero",
|
|
16
17
|
"props": {
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
}
|
|
28
29
|
},
|
|
29
30
|
{
|
|
31
|
+
"$type": "component",
|
|
30
32
|
"id": "why-meridian",
|
|
31
33
|
"component": "feature_grid",
|
|
32
34
|
"props": {
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
"subtitle": "Engineered for the edges of the world.",
|
|
35
37
|
"features": [
|
|
36
38
|
{
|
|
39
|
+
"$type": "component",
|
|
37
40
|
"id": "global-coverage",
|
|
38
41
|
"component": "feature_card",
|
|
39
42
|
"props": {
|
|
@@ -43,6 +46,7 @@
|
|
|
43
46
|
}
|
|
44
47
|
},
|
|
45
48
|
{
|
|
49
|
+
"$type": "component",
|
|
46
50
|
"id": "low-latency",
|
|
47
51
|
"component": "feature_card",
|
|
48
52
|
"props": {
|
|
@@ -52,6 +56,7 @@
|
|
|
52
56
|
}
|
|
53
57
|
},
|
|
54
58
|
{
|
|
59
|
+
"$type": "component",
|
|
55
60
|
"id": "easy-setup",
|
|
56
61
|
"component": "feature_card",
|
|
57
62
|
"props": {
|
|
@@ -64,6 +69,7 @@
|
|
|
64
69
|
}
|
|
65
70
|
},
|
|
66
71
|
{
|
|
72
|
+
"$type": "component",
|
|
67
73
|
"id": "personalization-choice",
|
|
68
74
|
"component": "personalization_choice",
|
|
69
75
|
"props": {
|
|
@@ -75,6 +81,7 @@
|
|
|
75
81
|
}
|
|
76
82
|
},
|
|
77
83
|
{
|
|
84
|
+
"$type": "component",
|
|
78
85
|
"id": "stats",
|
|
79
86
|
"component": "stats_bar",
|
|
80
87
|
"props": {
|
|
@@ -87,6 +94,7 @@
|
|
|
87
94
|
}
|
|
88
95
|
},
|
|
89
96
|
{
|
|
97
|
+
"$type": "component",
|
|
90
98
|
"id": "cta",
|
|
91
99
|
"component": "container",
|
|
92
100
|
"props": {
|
|
@@ -94,6 +102,7 @@
|
|
|
94
102
|
"className": "items-center justify-center text-center",
|
|
95
103
|
"content": [
|
|
96
104
|
{
|
|
105
|
+
"$type": "component",
|
|
97
106
|
"id": "cta-primary",
|
|
98
107
|
"component": "button",
|
|
99
108
|
"props": {
|
|
@@ -105,6 +114,7 @@
|
|
|
105
114
|
}
|
|
106
115
|
},
|
|
107
116
|
{
|
|
117
|
+
"$type": "component",
|
|
108
118
|
"id": "cta-secondary",
|
|
109
119
|
"component": "button",
|
|
110
120
|
"props": {
|
package/templates/common/.nvmrc
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
24
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$type": "fragment",
|
|
3
3
|
"fragmentId": "navbar",
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
4
|
+
"block": {
|
|
5
|
+
"$type": "component",
|
|
6
|
+
"component": "site-navigation",
|
|
7
|
+
"props": {
|
|
8
|
+
"brandHref": "/",
|
|
9
|
+
"brandLabel": "Meridian",
|
|
10
|
+
"brandLogo": {
|
|
11
|
+
"src": "/assets/meridian-logo.svg",
|
|
12
|
+
"alt": "Meridian"
|
|
13
|
+
},
|
|
14
|
+
"navigation": [
|
|
15
|
+
{ "id": "home", "label": "Home", "href": "/" },
|
|
16
|
+
{ "id": "plans", "label": "Plans", "href": "/plans/" },
|
|
17
|
+
{ "id": "devices", "label": "Devices", "href": "/devices/" },
|
|
18
|
+
{ "id": "business", "label": "Business", "href": "/business/" },
|
|
19
|
+
{ "id": "blog", "label": "Blog", "href": "/blog/" },
|
|
20
|
+
{ "id": "about", "label": "About", "href": "/about/" }
|
|
21
|
+
],
|
|
22
|
+
"utilities": [
|
|
23
|
+
{ "id": "sign-in", "label": "Sign in", "href": "/sign-in/", "variant": "ghost" },
|
|
24
|
+
{ "id": "contact-sales", "label": "Contact sales", "href": "/contact-sales/", "variant": "primary" }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
24
27
|
}
|
|
25
28
|
}
|