diff --git a/docs/.gitignore b/docs/.gitignore index 8a11fd9b..10c920f4 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -10,6 +10,7 @@ /out/ /build *.tsbuildinfo +/.wrangler/ # misc .DS_Store @@ -24,3 +25,12 @@ yarn-error.log* .env*.local .vercel next-env.d.ts + +# doc images -- served from R2 (see worker/index.ts), not committed here. +# Fine to drop a file here temporarily for a `next dev` preview; it just +# won't be tracked, and scripts/nest-static-export.mjs strips it from any +# build output before deploy either way. +/public/img/ + +# holds a real Cloudflare API token -- see scripts/README.md +/scripts/.env.publish-image diff --git a/docs/README.md b/docs/README.md index b4897219..03626549 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,7 +19,8 @@ This site follows the same Cloudflare Workers static-assets pattern used by the - **Next.js static export** - `next build` outputs static files to `out/` - **Next.js `basePath`** - links and assets are generated under `/docs/atom` -- **Post-build nesting** - `scripts/nest-static-export.mjs` moves the export under `out/docs/atom/` so Cloudflare static assets can serve it from the route prefix without custom Worker code +- **Post-build nesting** - `scripts/nest-static-export.mjs` moves the export under `out/docs/atom/` so Cloudflare static assets can serve it from the route prefix +- **Doc images via R2** - `worker/index.ts` is a small Worker in front of the static assets that serves `/docs/atom/img/...` from a shared Cloudflare R2 bucket instead of the repo, so images can be updated and cache-purged without a rebuild. It only runs as a fallback for requests that don't match a static asset (`run_worker_first: false`, the default) - every actual page is still served directly from `out/` with no Worker involved. See [`scripts/README.md`](./scripts/README.md) for the full picture and the publishing workflow. ### Cloudflare Build Settings @@ -57,9 +58,14 @@ flowchart LR end subgraph Runtime_Request_Flow - U[Browser request] --> H[Cloudflare static asset route] - H --> J[Static asset lookup] + U[Browser request] --> H{Matches a static asset?} + H -->|yes| J[Serve from out/] J --> U + H -->|no, e.g. /docs/atom/img/...| K[worker/index.ts] + K -->|img path| L[R2: websites-images/atom-docs/...] + K -->|anything else| M[404 via assets binding] + L --> U + M --> U end ``` @@ -73,10 +79,13 @@ NEXT_PUBLIC_BASE_URL=https://www.absmach.eu/docs/atom ## Project Structure -| Path | Description | -| -------------------------------- | --------------------------------------- | -| `app/[[...slug]]/page.tsx` | Docs page renderer | -| `content/docs` | MDX source files | -| `lib/source.ts` | Fumadocs source adapter | -| `scripts/nest-static-export.mjs` | Moves static export under `/docs/atom` | -| `wrangler.jsonc` | Cloudflare Workers static-assets config | +| Path | Description | +| ---------------------------------- | -------------------------------------------------------------------------------- | +| `app/[[...slug]]/page.tsx` | Docs page renderer | +| `content/docs` | MDX source files | +| `lib/source.ts` | Fumadocs source adapter | +| `components/doc-image.tsx` | Renders doc images as a plain, zoomable `` (no `next/image`, no manifest) | +| `worker/index.ts` | Serves `/docs/atom/img/...` from R2, falls back to static assets otherwise | +| `scripts/nest-static-export.mjs` | Moves static export under `/docs/atom`, strips any local `img/` from it | +| `scripts/publish-image.mjs` | Maintainer-only: uploads a doc image to R2 and purges its cache | +| `wrangler.jsonc` | Cloudflare Workers static-assets + Worker + R2 binding config | diff --git a/docs/app/[[...slug]]/page.tsx b/docs/app/[[...slug]]/page.tsx index f5e2b027..f63bc429 100644 --- a/docs/app/[[...slug]]/page.tsx +++ b/docs/app/[[...slug]]/page.tsx @@ -13,6 +13,7 @@ import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import type { Metadata } from 'next'; import { Mermaid } from '@/components/mermaid'; +import { DocImage } from '@/components/doc-image'; const mdxComponents = { ...defaultMdxComponents, @@ -24,6 +25,10 @@ const mdxComponents = { Mermaid, Tab, Tabs, + // Overrides defaultMdxComponents' next/image-backed img -- doc images are + // served from R2 via a same-origin proxy, not bundled by next/image. See + // components/doc-image.tsx. + img: DocImage, }; export default async function Page({ diff --git a/docs/components/doc-image.tsx b/docs/components/doc-image.tsx new file mode 100644 index 00000000..360090b7 --- /dev/null +++ b/docs/components/doc-image.tsx @@ -0,0 +1,39 @@ +import { ImageZoom } from "fumadocs-ui/components/image-zoom"; +import type { ImgHTMLAttributes } from "react"; + +// Doc content images (content/docs/**/*.mdx) are no longer bundled by +// Next.js's image pipeline (see source.config.ts: remarkImageOptions is +// disabled). They're stored in the shared Cloudflare R2 bucket and served +// same-origin at their usual "/img/..." path -- see docs/wrangler.jsonc and +// worker/index.ts. Rendered as a plain, zoomable -- no next/image, no +// width/height needed, so there's nothing to keep in sync when images +// change. +const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + +export function DocImage({ + src, + alt, + className, + ...props +}: ImgHTMLAttributes) { + if (typeof src !== "string") return null; + + const resolvedSrc = src.startsWith("/") ? `${BASE_PATH}${src}` : src; + + return ( + // src/alt passed here too, not just to the inner : ImageZoom's + // zoomed-in view reads its image from these props directly, not from + // `children` -- omitting them renders a blank zoomed-in image even + // though the inline thumbnail (via children) looks correct. + + {/* biome-ignore lint/performance/noImgElement: doc content images are served from R2, not Next's image pipeline */} + {alt + + ); +} diff --git a/docs/mdx-components.tsx b/docs/mdx-components.tsx index 7536859d..7c09e829 100644 --- a/docs/mdx-components.tsx +++ b/docs/mdx-components.tsx @@ -5,6 +5,7 @@ import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import type { MDXComponents } from 'mdx/types'; import { Mermaid } from './components/mermaid'; +import { DocImage } from './components/doc-image'; export function useMDXComponents(components: MDXComponents): MDXComponents { return { @@ -17,6 +18,9 @@ export function useMDXComponents(components: MDXComponents): MDXComponents { Mermaid, Tab, Tabs, + // Overrides defaultComponents' next/image-backed img -- see + // components/doc-image.tsx for why. + img: DocImage, ...components, }; } diff --git a/docs/package.json b/docs/package.json index c98a5f01..ba22a669 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,8 +7,11 @@ "dev": "next dev --turbopack", "build": "next build && node scripts/nest-static-export.mjs", "start": "serve out", - "deploy": "pnpm run build && wrangler deploy", - "upload": "pnpm run build && wrangler versions upload" + "typecheck:worker": "tsc --noEmit -p worker/tsconfig.json", + "deploy": "pnpm run build && pnpm run typecheck:worker && wrangler deploy", + "upload": "pnpm run build && pnpm run typecheck:worker && wrangler versions upload", + "preview": "pnpm run build && wrangler dev", + "publish-image": "node scripts/publish-image.mjs" }, "dependencies": { "@orama/orama": "^3.1.18", @@ -21,6 +24,7 @@ "react-dom": "^19" }, "devDependencies": { + "@cloudflare/workers-types": "^4", "@types/mdx": "^2.0.13", "@types/node": "^22", "@types/react": "^19", @@ -32,5 +36,12 @@ "typescript": "^5", "wrangler": "^4.95.0" }, - "packageManager": "pnpm@10.33.0" + "packageManager": "pnpm@10.33.0", + "pnpm": { + "overrides": { + "js-yaml": "^4.3.1", + "sharp": "^0.35.3", + "postcss": "^8.5.18" + } + } } diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 0a18b189..600f2ea3 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -5,10 +5,9 @@ settings: excludeLinksFromLockfile: false overrides: - dompurify: 3.4.12 - js-yaml: 4.3.0 - postcss: 8.5.18 - sharp: 0.35.3 + js-yaml: ^4.3.1 + sharp: ^0.35.3 + postcss: ^8.5.18 importers: @@ -39,6 +38,9 @@ importers: specifier: ^19 version: 19.2.5(react@19.2.5) devDependencies: + '@cloudflare/workers-types': + specifier: ^4 + version: 4.20260702.1 '@types/mdx': specifier: ^2.0.13 version: 2.0.13 @@ -55,7 +57,7 @@ importers: specifier: ^10 version: 10.5.0(postcss@8.5.18) postcss: - specifier: 8.5.18 + specifier: ^8.5.18 version: 8.5.18 serve: specifier: ^14.2.5 @@ -68,7 +70,7 @@ importers: version: 5.9.3 wrangler: specifier: ^4.95.0 - version: 4.98.0(@types/node@22.19.17) + version: 4.98.0(@cloudflare/workers-types@4.20260702.1)(@types/node@22.19.17) packages: @@ -128,6 +130,9 @@ packages: cpu: [x64] os: [win32] + '@cloudflare/workers-types@4.20260702.1': + resolution: {integrity: sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1351,7 +1356,7 @@ packages: engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: - postcss: 8.5.18 + postcss: ^8.5.18 bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -2048,8 +2053,8 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true json-schema-traverse@1.0.0: @@ -2439,20 +2444,20 @@ packages: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} peerDependencies: - postcss: 8.5.18 + postcss: ^8.5.18 postcss-js@4.1.0: resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: - postcss: 8.5.18 + postcss: ^8.5.18 postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} peerDependencies: jiti: '>=1.21.0' - postcss: 8.5.18 + postcss: ^8.5.18 tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: @@ -2469,7 +2474,7 @@ packages: resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} peerDependencies: - postcss: 8.5.18 + postcss: ^8.5.18 postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} @@ -2977,6 +2982,8 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260603.1': optional: true + '@cloudflare/workers-types@4.20260702.1': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -4609,7 +4616,7 @@ snapshots: esbuild: 0.25.12 estree-util-value-to-estree: 3.5.0 fumadocs-core: 14.7.7(@types/react@19.2.14)(next@15.5.22(@types/node@22.19.17)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - js-yaml: 4.3.0 + js-yaml: 4.3.1 lru-cache: 11.3.5 picocolors: 1.1.1 remark-mdx: 3.1.1 @@ -4809,7 +4816,7 @@ snapshots: jiti@1.21.7: {} - js-yaml@4.3.0: + js-yaml@4.3.1: dependencies: argparse: 2.0.1 @@ -6014,7 +6021,7 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260603.1 '@cloudflare/workerd-windows-64': 1.20260603.1 - wrangler@4.98.0(@types/node@22.19.17): + wrangler@4.98.0(@cloudflare/workers-types@4.20260702.1)(@types/node@22.19.17): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260603.1) @@ -6025,6 +6032,7 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20260603.1 optionalDependencies: + '@cloudflare/workers-types': 4.20260702.1 fsevents: 2.3.3 transitivePeerDependencies: - '@types/node' diff --git a/docs/public/img/user-guide/account/access-token-create-highlight.png b/docs/public/img/user-guide/account/access-token-create-highlight.png deleted file mode 100644 index 4e5068f6..00000000 Binary files a/docs/public/img/user-guide/account/access-token-create-highlight.png and /dev/null differ diff --git a/docs/public/img/user-guide/account/access-token-form.png b/docs/public/img/user-guide/account/access-token-form.png deleted file mode 100644 index cc309128..00000000 Binary files a/docs/public/img/user-guide/account/access-token-form.png and /dev/null differ diff --git a/docs/public/img/user-guide/account/access-token-list.png b/docs/public/img/user-guide/account/access-token-list.png deleted file mode 100644 index 819b4f00..00000000 Binary files a/docs/public/img/user-guide/account/access-token-list.png and /dev/null differ diff --git a/docs/public/img/user-guide/account/account-menu.png b/docs/public/img/user-guide/account/account-menu.png deleted file mode 100644 index 1f19cc2d..00000000 Binary files a/docs/public/img/user-guide/account/account-menu.png and /dev/null differ diff --git a/docs/public/img/user-guide/account/profile-page.png b/docs/public/img/user-guide/account/profile-page.png deleted file mode 100644 index 92301f2d..00000000 Binary files a/docs/public/img/user-guide/account/profile-page.png and /dev/null differ diff --git a/docs/public/img/user-guide/account/theme-menu.png b/docs/public/img/user-guide/account/theme-menu.png deleted file mode 100644 index 794f0449..00000000 Binary files a/docs/public/img/user-guide/account/theme-menu.png and /dev/null differ diff --git a/docs/public/img/user-guide/actions/action-applicability-create-dialog.png b/docs/public/img/user-guide/actions/action-applicability-create-dialog.png deleted file mode 100644 index 8d4b73ff..00000000 Binary files a/docs/public/img/user-guide/actions/action-applicability-create-dialog.png and /dev/null differ diff --git a/docs/public/img/user-guide/actions/action-applicability-create-filled.png b/docs/public/img/user-guide/actions/action-applicability-create-filled.png deleted file mode 100644 index a4b9aa21..00000000 Binary files a/docs/public/img/user-guide/actions/action-applicability-create-filled.png and /dev/null differ diff --git a/docs/public/img/user-guide/actions/action-applicability-list.png b/docs/public/img/user-guide/actions/action-applicability-list.png deleted file mode 100644 index 4c230e50..00000000 Binary files a/docs/public/img/user-guide/actions/action-applicability-list.png and /dev/null differ diff --git a/docs/public/img/user-guide/actions/action-inspect.png b/docs/public/img/user-guide/actions/action-inspect.png deleted file mode 100644 index ae9dcb92..00000000 Binary files a/docs/public/img/user-guide/actions/action-inspect.png and /dev/null differ diff --git a/docs/public/img/user-guide/actions/actions-create-dialog.png b/docs/public/img/user-guide/actions/actions-create-dialog.png deleted file mode 100644 index 723d2d11..00000000 Binary files a/docs/public/img/user-guide/actions/actions-create-dialog.png and /dev/null differ diff --git a/docs/public/img/user-guide/actions/actions-list.png b/docs/public/img/user-guide/actions/actions-list.png deleted file mode 100644 index 7e766eea..00000000 Binary files a/docs/public/img/user-guide/actions/actions-list.png and /dev/null differ diff --git a/docs/public/img/user-guide/actions/assignment-guardrails-create-dialog.png b/docs/public/img/user-guide/actions/assignment-guardrails-create-dialog.png deleted file mode 100644 index d72d3dc7..00000000 Binary files a/docs/public/img/user-guide/actions/assignment-guardrails-create-dialog.png and /dev/null differ diff --git a/docs/public/img/user-guide/actions/assignment-guardrails-list.png b/docs/public/img/user-guide/actions/assignment-guardrails-list.png deleted file mode 100644 index 74590975..00000000 Binary files a/docs/public/img/user-guide/actions/assignment-guardrails-list.png and /dev/null differ diff --git a/docs/public/img/user-guide/audit/audit-export-csv-dialog.png b/docs/public/img/user-guide/audit/audit-export-csv-dialog.png deleted file mode 100644 index 777f1c8e..00000000 Binary files a/docs/public/img/user-guide/audit/audit-export-csv-dialog.png and /dev/null differ diff --git a/docs/public/img/user-guide/audit/audit-export-highlight.png b/docs/public/img/user-guide/audit/audit-export-highlight.png deleted file mode 100644 index 29cf50b0..00000000 Binary files a/docs/public/img/user-guide/audit/audit-export-highlight.png and /dev/null differ diff --git a/docs/public/img/user-guide/audit/audit-inspect.png b/docs/public/img/user-guide/audit/audit-inspect.png deleted file mode 100644 index f82f60f8..00000000 Binary files a/docs/public/img/user-guide/audit/audit-inspect.png and /dev/null differ diff --git a/docs/public/img/user-guide/audit/audit-list.png b/docs/public/img/user-guide/audit/audit-list.png deleted file mode 100644 index 85744d79..00000000 Binary files a/docs/public/img/user-guide/audit/audit-list.png and /dev/null differ diff --git a/docs/public/img/user-guide/authorization/authz-decision-allowed.png b/docs/public/img/user-guide/authorization/authz-decision-allowed.png deleted file mode 100644 index c4650379..00000000 Binary files a/docs/public/img/user-guide/authorization/authz-decision-allowed.png and /dev/null differ diff --git a/docs/public/img/user-guide/authorization/authz-decision-denied.png b/docs/public/img/user-guide/authorization/authz-decision-denied.png deleted file mode 100644 index 344accfb..00000000 Binary files a/docs/public/img/user-guide/authorization/authz-decision-denied.png and /dev/null differ diff --git a/docs/public/img/user-guide/authorization/authz-explain-highlight.png b/docs/public/img/user-guide/authorization/authz-explain-highlight.png deleted file mode 100644 index e14a3cea..00000000 Binary files a/docs/public/img/user-guide/authorization/authz-explain-highlight.png and /dev/null differ diff --git a/docs/public/img/user-guide/authorization/authz-request-filled.png b/docs/public/img/user-guide/authorization/authz-request-filled.png deleted file mode 100644 index 67918098..00000000 Binary files a/docs/public/img/user-guide/authorization/authz-request-filled.png and /dev/null differ diff --git a/docs/public/img/user-guide/dashboard/context-switcher.png b/docs/public/img/user-guide/dashboard/context-switcher.png deleted file mode 100644 index abea211d..00000000 Binary files a/docs/public/img/user-guide/dashboard/context-switcher.png and /dev/null differ diff --git a/docs/public/img/user-guide/dashboard/dashboard-overview.png b/docs/public/img/user-guide/dashboard/dashboard-overview.png deleted file mode 100644 index 7afe8707..00000000 Binary files a/docs/public/img/user-guide/dashboard/dashboard-overview.png and /dev/null differ diff --git a/docs/public/img/user-guide/developer/endpoint-inspect.png b/docs/public/img/user-guide/developer/endpoint-inspect.png deleted file mode 100644 index ea45b4af..00000000 Binary files a/docs/public/img/user-guide/developer/endpoint-inspect.png and /dev/null differ diff --git a/docs/public/img/user-guide/developer/endpoints-create-filled.png b/docs/public/img/user-guide/developer/endpoints-create-filled.png deleted file mode 100644 index f1218de2..00000000 Binary files a/docs/public/img/user-guide/developer/endpoints-create-filled.png and /dev/null differ diff --git a/docs/public/img/user-guide/developer/endpoints-list-empty.png b/docs/public/img/user-guide/developer/endpoints-list-empty.png deleted file mode 100644 index 756ccd04..00000000 Binary files a/docs/public/img/user-guide/developer/endpoints-list-empty.png and /dev/null differ diff --git a/docs/public/img/user-guide/developer/endpoints-templates.png b/docs/public/img/user-guide/developer/endpoints-templates.png deleted file mode 100644 index 6505765d..00000000 Binary files a/docs/public/img/user-guide/developer/endpoints-templates.png and /dev/null differ diff --git a/docs/public/img/user-guide/developer/playground-curl-tab.png b/docs/public/img/user-guide/developer/playground-curl-tab.png deleted file mode 100644 index 01e18392..00000000 Binary files a/docs/public/img/user-guide/developer/playground-curl-tab.png and /dev/null differ diff --git a/docs/public/img/user-guide/developer/playground-run-response.png b/docs/public/img/user-guide/developer/playground-run-response.png deleted file mode 100644 index dbb41651..00000000 Binary files a/docs/public/img/user-guide/developer/playground-run-response.png and /dev/null differ diff --git a/docs/public/img/user-guide/developer/playground.png b/docs/public/img/user-guide/developer/playground.png deleted file mode 100644 index cc1218e8..00000000 Binary files a/docs/public/img/user-guide/developer/playground.png and /dev/null differ diff --git a/docs/public/img/user-guide/direct-policies/direct-policies-list-create-highlight.png b/docs/public/img/user-guide/direct-policies/direct-policies-list-create-highlight.png deleted file mode 100644 index 69e5e5c3..00000000 Binary files a/docs/public/img/user-guide/direct-policies/direct-policies-list-create-highlight.png and /dev/null differ diff --git a/docs/public/img/user-guide/direct-policies/direct-policies-list-empty.png b/docs/public/img/user-guide/direct-policies/direct-policies-list-empty.png deleted file mode 100644 index 9173ea39..00000000 Binary files a/docs/public/img/user-guide/direct-policies/direct-policies-list-empty.png and /dev/null differ diff --git a/docs/public/img/user-guide/direct-policies/direct-policies-list-populated.png b/docs/public/img/user-guide/direct-policies/direct-policies-list-populated.png deleted file mode 100644 index ba3cb837..00000000 Binary files a/docs/public/img/user-guide/direct-policies/direct-policies-list-populated.png and /dev/null differ diff --git a/docs/public/img/user-guide/direct-policies/direct-policy-inspect.png b/docs/public/img/user-guide/direct-policies/direct-policy-inspect.png deleted file mode 100644 index 8d313ee0..00000000 Binary files a/docs/public/img/user-guide/direct-policies/direct-policy-inspect.png and /dev/null differ diff --git a/docs/public/img/user-guide/direct-policies/dp-wizard-step1-tenant.png b/docs/public/img/user-guide/direct-policies/dp-wizard-step1-tenant.png deleted file mode 100644 index c02d16c3..00000000 Binary files a/docs/public/img/user-guide/direct-policies/dp-wizard-step1-tenant.png and /dev/null differ diff --git a/docs/public/img/user-guide/direct-policies/dp-wizard-step2-subject.png b/docs/public/img/user-guide/direct-policies/dp-wizard-step2-subject.png deleted file mode 100644 index e4432454..00000000 Binary files a/docs/public/img/user-guide/direct-policies/dp-wizard-step2-subject.png and /dev/null differ diff --git a/docs/public/img/user-guide/direct-policies/dp-wizard-step3-permblock.png b/docs/public/img/user-guide/direct-policies/dp-wizard-step3-permblock.png deleted file mode 100644 index f07d3f66..00000000 Binary files a/docs/public/img/user-guide/direct-policies/dp-wizard-step3-permblock.png and /dev/null differ diff --git a/docs/public/img/user-guide/direct-policies/dp-wizard-step4-review.png b/docs/public/img/user-guide/direct-policies/dp-wizard-step4-review.png deleted file mode 100644 index a9c94bb5..00000000 Binary files a/docs/public/img/user-guide/direct-policies/dp-wizard-step4-review.png and /dev/null differ diff --git a/docs/public/img/user-guide/entities/entities-create-dialog.png b/docs/public/img/user-guide/entities/entities-create-dialog.png deleted file mode 100644 index 4a5ca97b..00000000 Binary files a/docs/public/img/user-guide/entities/entities-create-dialog.png and /dev/null differ diff --git a/docs/public/img/user-guide/entities/entities-kind-options.png b/docs/public/img/user-guide/entities/entities-kind-options.png deleted file mode 100644 index 78feed32..00000000 Binary files a/docs/public/img/user-guide/entities/entities-kind-options.png and /dev/null differ diff --git a/docs/public/img/user-guide/entities/entities-list-create-highlight.png b/docs/public/img/user-guide/entities/entities-list-create-highlight.png deleted file mode 100644 index 2952b61e..00000000 Binary files a/docs/public/img/user-guide/entities/entities-list-create-highlight.png and /dev/null differ diff --git a/docs/public/img/user-guide/entities/entities-list-populated.png b/docs/public/img/user-guide/entities/entities-list-populated.png deleted file mode 100644 index 13ac50ee..00000000 Binary files a/docs/public/img/user-guide/entities/entities-list-populated.png and /dev/null differ diff --git a/docs/public/img/user-guide/entities/entity-add-api-key.png b/docs/public/img/user-guide/entities/entity-add-api-key.png deleted file mode 100644 index 975d856f..00000000 Binary files a/docs/public/img/user-guide/entities/entity-add-api-key.png and /dev/null differ diff --git a/docs/public/img/user-guide/entities/entity-add-password.png b/docs/public/img/user-guide/entities/entity-add-password.png deleted file mode 100644 index 567566f6..00000000 Binary files a/docs/public/img/user-guide/entities/entity-add-password.png and /dev/null differ diff --git a/docs/public/img/user-guide/entities/entity-audit-logs-tab.png b/docs/public/img/user-guide/entities/entity-audit-logs-tab.png deleted file mode 100644 index a5dee6ac..00000000 Binary files a/docs/public/img/user-guide/entities/entity-audit-logs-tab.png and /dev/null differ diff --git a/docs/public/img/user-guide/entities/entity-check-authorization-highlight.png b/docs/public/img/user-guide/entities/entity-check-authorization-highlight.png deleted file mode 100644 index 2cf454e4..00000000 Binary files a/docs/public/img/user-guide/entities/entity-check-authorization-highlight.png and /dev/null differ diff --git a/docs/public/img/user-guide/entities/entity-credentials-list.png b/docs/public/img/user-guide/entities/entity-credentials-list.png deleted file mode 100644 index b87943f8..00000000 Binary files a/docs/public/img/user-guide/entities/entity-credentials-list.png and /dev/null differ diff --git a/docs/public/img/user-guide/entities/entity-inspect-details.png b/docs/public/img/user-guide/entities/entity-inspect-details.png deleted file mode 100644 index a88dc346..00000000 Binary files a/docs/public/img/user-guide/entities/entity-inspect-details.png and /dev/null differ diff --git a/docs/public/img/user-guide/entities/entity-issue-certificate.png b/docs/public/img/user-guide/entities/entity-issue-certificate.png deleted file mode 100644 index 690e4d80..00000000 Binary files a/docs/public/img/user-guide/entities/entity-issue-certificate.png and /dev/null differ diff --git a/docs/public/img/user-guide/groups/group-inspect-empty-members.png b/docs/public/img/user-guide/groups/group-inspect-empty-members.png deleted file mode 100644 index e6625d55..00000000 Binary files a/docs/public/img/user-guide/groups/group-inspect-empty-members.png and /dev/null differ diff --git a/docs/public/img/user-guide/groups/group-inspect-members.png b/docs/public/img/user-guide/groups/group-inspect-members.png deleted file mode 100644 index adf4de94..00000000 Binary files a/docs/public/img/user-guide/groups/group-inspect-members.png and /dev/null differ diff --git a/docs/public/img/user-guide/groups/groups-create-dialog.png b/docs/public/img/user-guide/groups/groups-create-dialog.png deleted file mode 100644 index 9b32089a..00000000 Binary files a/docs/public/img/user-guide/groups/groups-create-dialog.png and /dev/null differ diff --git a/docs/public/img/user-guide/groups/groups-list-create-highlight.png b/docs/public/img/user-guide/groups/groups-list-create-highlight.png deleted file mode 100644 index 13d5ef39..00000000 Binary files a/docs/public/img/user-guide/groups/groups-list-create-highlight.png and /dev/null differ diff --git a/docs/public/img/user-guide/groups/groups-list-populated.png b/docs/public/img/user-guide/groups/groups-list-populated.png deleted file mode 100644 index e4e3698c..00000000 Binary files a/docs/public/img/user-guide/groups/groups-list-populated.png and /dev/null differ diff --git a/docs/public/img/user-guide/operations/signing-keys-list.png b/docs/public/img/user-guide/operations/signing-keys-list.png deleted file mode 100644 index 096d1e48..00000000 Binary files a/docs/public/img/user-guide/operations/signing-keys-list.png and /dev/null differ diff --git a/docs/public/img/user-guide/operations/signing-keys-rotate-dialog.png b/docs/public/img/user-guide/operations/signing-keys-rotate-dialog.png deleted file mode 100644 index e23e7690..00000000 Binary files a/docs/public/img/user-guide/operations/signing-keys-rotate-dialog.png and /dev/null differ diff --git a/docs/public/img/user-guide/operations/system-health.png b/docs/public/img/user-guide/operations/system-health.png deleted file mode 100644 index 75be199b..00000000 Binary files a/docs/public/img/user-guide/operations/system-health.png and /dev/null differ diff --git a/docs/public/img/user-guide/permission-blocks/pb-wizard-step1-boundary.png b/docs/public/img/user-guide/permission-blocks/pb-wizard-step1-boundary.png deleted file mode 100644 index 5cb7173a..00000000 Binary files a/docs/public/img/user-guide/permission-blocks/pb-wizard-step1-boundary.png and /dev/null differ diff --git a/docs/public/img/user-guide/permission-blocks/pb-wizard-step2-scope-filled.png b/docs/public/img/user-guide/permission-blocks/pb-wizard-step2-scope-filled.png deleted file mode 100644 index a6985ccc..00000000 Binary files a/docs/public/img/user-guide/permission-blocks/pb-wizard-step2-scope-filled.png and /dev/null differ diff --git a/docs/public/img/user-guide/permission-blocks/pb-wizard-step2-scope-options.png b/docs/public/img/user-guide/permission-blocks/pb-wizard-step2-scope-options.png deleted file mode 100644 index 733444f5..00000000 Binary files a/docs/public/img/user-guide/permission-blocks/pb-wizard-step2-scope-options.png and /dev/null differ diff --git a/docs/public/img/user-guide/permission-blocks/pb-wizard-step3-actions.png b/docs/public/img/user-guide/permission-blocks/pb-wizard-step3-actions.png deleted file mode 100644 index cd706088..00000000 Binary files a/docs/public/img/user-guide/permission-blocks/pb-wizard-step3-actions.png and /dev/null differ diff --git a/docs/public/img/user-guide/permission-blocks/pb-wizard-step4-conditions.png b/docs/public/img/user-guide/permission-blocks/pb-wizard-step4-conditions.png deleted file mode 100644 index ec0afaa8..00000000 Binary files a/docs/public/img/user-guide/permission-blocks/pb-wizard-step4-conditions.png and /dev/null differ diff --git a/docs/public/img/user-guide/permission-blocks/pb-wizard-step5-review.png b/docs/public/img/user-guide/permission-blocks/pb-wizard-step5-review.png deleted file mode 100644 index 0e03b315..00000000 Binary files a/docs/public/img/user-guide/permission-blocks/pb-wizard-step5-review.png and /dev/null differ diff --git a/docs/public/img/user-guide/permission-blocks/permission-block-inspect.png b/docs/public/img/user-guide/permission-blocks/permission-block-inspect.png deleted file mode 100644 index cf659430..00000000 Binary files a/docs/public/img/user-guide/permission-blocks/permission-block-inspect.png and /dev/null differ diff --git a/docs/public/img/user-guide/permission-blocks/permission-blocks-list-create-highlight.png b/docs/public/img/user-guide/permission-blocks/permission-blocks-list-create-highlight.png deleted file mode 100644 index cce93f9c..00000000 Binary files a/docs/public/img/user-guide/permission-blocks/permission-blocks-list-create-highlight.png and /dev/null differ diff --git a/docs/public/img/user-guide/permission-blocks/permission-blocks-list-populated.png b/docs/public/img/user-guide/permission-blocks/permission-blocks-list-populated.png deleted file mode 100644 index da3e752a..00000000 Binary files a/docs/public/img/user-guide/permission-blocks/permission-blocks-list-populated.png and /dev/null differ diff --git a/docs/public/img/user-guide/profiles/profile-inspect.png b/docs/public/img/user-guide/profiles/profile-inspect.png deleted file mode 100644 index 8d41ef11..00000000 Binary files a/docs/public/img/user-guide/profiles/profile-inspect.png and /dev/null differ diff --git a/docs/public/img/user-guide/profiles/profiles-create-dialog.png b/docs/public/img/user-guide/profiles/profiles-create-dialog.png deleted file mode 100644 index d4cd8fa7..00000000 Binary files a/docs/public/img/user-guide/profiles/profiles-create-dialog.png and /dev/null differ diff --git a/docs/public/img/user-guide/profiles/profiles-create-version-step.png b/docs/public/img/user-guide/profiles/profiles-create-version-step.png deleted file mode 100644 index 276a41aa..00000000 Binary files a/docs/public/img/user-guide/profiles/profiles-create-version-step.png and /dev/null differ diff --git a/docs/public/img/user-guide/profiles/profiles-list.png b/docs/public/img/user-guide/profiles/profiles-list.png deleted file mode 100644 index a7f90dff..00000000 Binary files a/docs/public/img/user-guide/profiles/profiles-list.png and /dev/null differ diff --git a/docs/public/img/user-guide/resources/resource-inspect.png b/docs/public/img/user-guide/resources/resource-inspect.png deleted file mode 100644 index d45e3d80..00000000 Binary files a/docs/public/img/user-guide/resources/resource-inspect.png and /dev/null differ diff --git a/docs/public/img/user-guide/resources/resources-create-dialog.png b/docs/public/img/user-guide/resources/resources-create-dialog.png deleted file mode 100644 index eddb521c..00000000 Binary files a/docs/public/img/user-guide/resources/resources-create-dialog.png and /dev/null differ diff --git a/docs/public/img/user-guide/resources/resources-list-create-highlight.png b/docs/public/img/user-guide/resources/resources-list-create-highlight.png deleted file mode 100644 index 18481747..00000000 Binary files a/docs/public/img/user-guide/resources/resources-list-create-highlight.png and /dev/null differ diff --git a/docs/public/img/user-guide/resources/resources-list-empty.png b/docs/public/img/user-guide/resources/resources-list-empty.png deleted file mode 100644 index efdc6454..00000000 Binary files a/docs/public/img/user-guide/resources/resources-list-empty.png and /dev/null differ diff --git a/docs/public/img/user-guide/roles/role-inspect.png b/docs/public/img/user-guide/roles/role-inspect.png deleted file mode 100644 index 8e063dc1..00000000 Binary files a/docs/public/img/user-guide/roles/role-inspect.png and /dev/null differ diff --git a/docs/public/img/user-guide/roles/role-wizard-step1-basics.png b/docs/public/img/user-guide/roles/role-wizard-step1-basics.png deleted file mode 100644 index 927ff04a..00000000 Binary files a/docs/public/img/user-guide/roles/role-wizard-step1-basics.png and /dev/null differ diff --git a/docs/public/img/user-guide/roles/role-wizard-step2-blocks.png b/docs/public/img/user-guide/roles/role-wizard-step2-blocks.png deleted file mode 100644 index 5ca719d8..00000000 Binary files a/docs/public/img/user-guide/roles/role-wizard-step2-blocks.png and /dev/null differ diff --git a/docs/public/img/user-guide/roles/role-wizard-step2-selected.png b/docs/public/img/user-guide/roles/role-wizard-step2-selected.png deleted file mode 100644 index 273ea852..00000000 Binary files a/docs/public/img/user-guide/roles/role-wizard-step2-selected.png and /dev/null differ diff --git a/docs/public/img/user-guide/roles/role-wizard-step3-review.png b/docs/public/img/user-guide/roles/role-wizard-step3-review.png deleted file mode 100644 index 430698bc..00000000 Binary files a/docs/public/img/user-guide/roles/role-wizard-step3-review.png and /dev/null differ diff --git a/docs/public/img/user-guide/roles/roles-list-create-highlight.png b/docs/public/img/user-guide/roles/roles-list-create-highlight.png deleted file mode 100644 index 3fd77347..00000000 Binary files a/docs/public/img/user-guide/roles/roles-list-create-highlight.png and /dev/null differ diff --git a/docs/public/img/user-guide/roles/roles-list-populated.png b/docs/public/img/user-guide/roles/roles-list-populated.png deleted file mode 100644 index bfe4e7b2..00000000 Binary files a/docs/public/img/user-guide/roles/roles-list-populated.png and /dev/null differ diff --git a/docs/public/img/user-guide/tenants/tenant-edit-dialog.png b/docs/public/img/user-guide/tenants/tenant-edit-dialog.png deleted file mode 100644 index 0d666d28..00000000 Binary files a/docs/public/img/user-guide/tenants/tenant-edit-dialog.png and /dev/null differ diff --git a/docs/public/img/user-guide/tenants/tenant-inspect-details.png b/docs/public/img/user-guide/tenants/tenant-inspect-details.png deleted file mode 100644 index 2dd43060..00000000 Binary files a/docs/public/img/user-guide/tenants/tenant-inspect-details.png and /dev/null differ diff --git a/docs/public/img/user-guide/tenants/tenant-inspect-members.png b/docs/public/img/user-guide/tenants/tenant-inspect-members.png deleted file mode 100644 index 1c4eebcb..00000000 Binary files a/docs/public/img/user-guide/tenants/tenant-inspect-members.png and /dev/null differ diff --git a/docs/public/img/user-guide/tenants/tenants-create-dialog.png b/docs/public/img/user-guide/tenants/tenants-create-dialog.png deleted file mode 100644 index 3c1de2f3..00000000 Binary files a/docs/public/img/user-guide/tenants/tenants-create-dialog.png and /dev/null differ diff --git a/docs/public/img/user-guide/tenants/tenants-list-create-highlight.png b/docs/public/img/user-guide/tenants/tenants-list-create-highlight.png deleted file mode 100644 index 9b256f01..00000000 Binary files a/docs/public/img/user-guide/tenants/tenants-list-create-highlight.png and /dev/null differ diff --git a/docs/public/img/user-guide/tenants/tenants-list-populated.png b/docs/public/img/user-guide/tenants/tenants-list-populated.png deleted file mode 100644 index b83d0bd5..00000000 Binary files a/docs/public/img/user-guide/tenants/tenants-list-populated.png and /dev/null differ diff --git a/docs/scripts/.env.publish-image.example b/docs/scripts/.env.publish-image.example new file mode 100644 index 00000000..fa4df6f3 --- /dev/null +++ b/docs/scripts/.env.publish-image.example @@ -0,0 +1,10 @@ +# Copy to scripts/.env.publish-image (gitignored) and fill in the token. +# Create the token under My Profile -> API Tokens -> Create Token -> Custom Token, with: +# - Workers R2 Storage: Edit +# - Zone -> Cache Purge -> Purge (scoped to the absmach.eu zone) +CLOUDFLARE_API_TOKEN= + +# Not secret - the absmach.eu zone ID, visible on the domain's Overview page. +# Same zone as the main absmach-website repo (this docs site is served at +# https://www.absmach.eu/docs/atom, same domain). +CLOUDFLARE_ZONE_ID=9cb2232dc0e21fbfabf9ce52b1834f56 diff --git a/docs/scripts/README.md b/docs/scripts/README.md new file mode 100644 index 00000000..bc28f018 --- /dev/null +++ b/docs/scripts/README.md @@ -0,0 +1,160 @@ +# Publishing doc images (maintainers only) + +Doc images are no longer committed to this repo. They're stored in a shared Cloudflare R2 +bucket (`websites-images`, under the `atom-docs/` key prefix so they don't collide with other +properties in the same bucket) and served at their usual `/docs/atom/img/...` URLs by +[`worker/index.ts`](../worker/index.ts), a small Worker that sits in front of this site's +static assets. + +## Why a Worker exists here at all + +This site is a Next.js **static export** (`output: 'export'` in `next.config.mjs`) deployed +as plain Cloudflare Workers static assets -- there's no Next.js server runtime in production, +so nothing like `@cloudflare/next-on-pages` or `@opennextjs/cloudflare` applies, and no +per-request Next.js code path exists to hang an R2 lookup off of. `worker/index.ts` is a +minimal, hand-written Worker (not part of Next.js) that Cloudflare only invokes as a fallback +when a request doesn't match a static asset (`run_worker_first: false`, the default -- see +`wrangler.jsonc`). Doc images aren't part of the static export output, so every request under +`/docs/atom/img/...` falls through to it automatically; everything else (every actual page, +`_next/static`, etc.) is served directly from the assets directory without ever touching this +Worker. + +## Why doc images also needed an MDX change + +Fumadocs' default MDX pipeline (`remark-image`, `useImport: true`) turns +`![alt](/img/foo.png)` into a static `import` of the file from `public/`, which Next bundles +into a content-hashed `_next/static/media/.png` URL at build time. That requires the +source file on local disk at build time, and the URL changes every time the image's content +changes -- neither works once the file only lives in R2. `source.config.ts` disables that +plugin (`remarkImageOptions: false`), so `/img/...` paths in MDX stay literal, and +[`components/doc-image.tsx`](../components/doc-image.tsx) renders them as a plain, zoomable +`` (`fumadocs-ui`'s `ImageZoom` wrapping a plain element, not `next/image`) -- the `src` +is still basePath-prefixed manually, same pattern as `components/search.tsx`, but there's no +width/height requirement and nothing to keep in sync when an image changes. + +**Authoring is unchanged** -- MDX content already referenced doc images by their final +`/img/...` path from the start (there was never a relative-path convention to preserve here), +so nothing about how you write `![alt](/img/foo.png)` needs to change. + +## One-time setup + +1. Create `scripts/.env.publish-image` from the template: + + ```bash + cp scripts/.env.publish-image.example scripts/.env.publish-image + ``` + +2. Create a Cloudflare API token: dashboard -> **My Profile -> API Tokens -> Create Token -> + Custom Token**, with both permissions on the same token: + - `Workers R2 Storage: Edit` + - `Zone -> Cache Purge -> Purge`, **Zone Resources** scoped to the `absmach.eu` zone + + (If you already hold the token used for the main `absmach-website` repo's + `publish-image` script, it covers the same bucket and zone -- you can reuse it here + instead of creating a new one.) + +3. Paste the token into `CLOUDFLARE_API_TOKEN` in `scripts/.env.publish-image`. The zone ID is + already filled in (it's not secret, safe to share/commit -- it can't authenticate anything + by itself). + +4. Sanity-check the token before first use: + + ```bash + curl -s https://api.cloudflare.com/client/v4/user/tokens/verify \ + -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" + ``` + + Should return `"status":"active"`. If it doesn't, the token value itself is wrong (bad + copy/paste, expired, revoked) -- fix that before troubleshooting anything else. + +`scripts/.env.publish-image` is gitignored. Never commit it, never paste the token value into +a PR, issue, or chat. + +## Publishing an image + +```bash +pnpm run publish-image +``` + +`` must start with `img/` and match the path already written (or about to be +written) into the MDX content, e.g.: + +```bash +pnpm run publish-image ./roles-list.png img/user-guide/roles/roles-list-populated.png +# -> https://www.absmach.eu/docs/atom/img/user-guide/roles/roles-list-populated.png +# -> reference in MDX as: ![Roles list](/img/user-guide/roles/roles-list-populated.png) +``` + +The script does two things, in order: + +1. `wrangler r2 object put ... --remote` -- uploads to the **real** bucket. `--remote` is + required; without it, `wrangler` silently writes to a local simulated bucket and prints a + normal-looking "Upload complete" with no error, and the object is never actually live. +2. Purges that exact URL from Cloudflare's edge cache (`POST /zones/{id}/purge_cache`), so the + update is visible within seconds instead of waiting out the cache TTL. + +If you re-run the same command for an existing path, it overwrites the object in place and +purges again -- that's the intended way to update an image without changing its URL. + +## Local development + +`public/img/` is gitignored. For a `next dev` preview with working images, drop the file +there locally under the same path used in MDX (e.g. `public/img/user-guide/roles/foo.png` for +`/img/user-guide/roles/foo.png`) -- Next's dev server serves `public/` under the site's +basePath automatically, so it resolves at the exact same URL production does. It just won't be +committed, and `pnpm run build`'s `nest-static-export.mjs` step strips `public/img` from the +deployed output either way, so a leftover local copy can never accidentally ship instead of +the R2-backed version. + +To test the actual production path -- Worker + static assets + R2 binding together, the way +Cloudflare will actually serve it -- run: + +```bash +pnpm run preview # builds, then `wrangler dev` +``` + +By default this talks to a local simulated R2 bucket (empty unless you've seeded it with +`wrangler r2 object put ... --local`). Add `"remote": true` to the `r2_buckets` binding in +`wrangler.jsonc` temporarily if you want `wrangler dev` to read the real bucket instead. + +## Migrating the existing images (one-time, already done) + +The 88 images removed from `public/img/` have already been uploaded to the real R2 bucket and +spot-checked byte-for-byte against the originals. Nothing further to do here unless an image +needs updating -- use `publish-image` for that, same as any other image. + +## Why maintainer-only + +This repo is public. The risk isn't the script being visible -- it's inert without a +credential. The risk is _credential distribution_: whoever holds `CLOUDFLARE_API_TOKEN` can +write to the shared bucket. So nobody, internal or external, gets a personal R2 token. Only a +maintainer, holding this one scoped token, runs `publish-image`. + +Practical flow for a PR that adds a doc image: the contributor attaches the image to the PR +description or a comment the normal GitHub way. A maintainer reviewing the PR runs +`pnpm run publish-image` locally before merging, then approves. + +## Troubleshooting + +- **`Local file not found: --`** -- you ran `pnpm run publish-image -- `. pnpm + forwards a leading `--` to the script literally instead of stripping it like npm does. The + script strips it defensively now, but plain `pnpm run publish-image ` (no `--`) + is the form to use. +- **`Destination must start with "img/"`** -- the second argument is the path as it appears + after `/docs/atom/` in the final URL (and after the leading `/` in MDX `src`), e.g. + `img/user-guide/roles/foo.png`, not `user-guide/roles/foo.png` or a full URL. +- **`Resource location: local` in the upload output** -- means `--remote` didn't get applied + for some reason (e.g. running the underlying `wrangler` command by hand without copying the + full flag list from the script). The object was never written to the real bucket even though + the CLI reports success. Always use `pnpm run publish-image`, or add `--remote` yourself if + invoking wrangler directly. +- **`Cache purge failed` / `Authentication error` (code 10000)** -- Cloudflare reuses this code + for both "bad token" and "token valid but missing this permission." Run the token verify curl + command above first to rule out a bad token. If that succeeds, the token is missing + `Zone -> Cache Purge -> Purge` for the `absmach.eu` zone, or that permission's Zone Resources + selector doesn't include it -- edit the token in the dashboard and add it. +- To confirm an object actually made it into the bucket after a `--remote` upload: + + ```bash + wrangler r2 object get websites-images/atom-docs/ --remote --file=/tmp/check + ``` diff --git a/docs/scripts/nest-static-export.mjs b/docs/scripts/nest-static-export.mjs index 7121b9e8..7cbd2c88 100644 --- a/docs/scripts/nest-static-export.mjs +++ b/docs/scripts/nest-static-export.mjs @@ -43,4 +43,18 @@ for (const entry of await fs.readdir(tempDir)) { await fs.rmdir(tempDir); +// Doc images are served from R2 via worker/index.ts, not from the static +// export -- public/img is gitignored and normally won't exist at build +// time, but strip it here too in case a locally-gitignored copy (kept +// around for `next dev` previews) is still sitting in public/img when +// someone runs a build. If it survived into out/, Cloudflare would serve +// it directly as a static asset instead of falling through to the R2 +// proxy Worker, silently defeating the whole point of the migration +// (stale content, no purge-on-update). +const imgDir = path.join(nestedDir, "img"); +if (await pathExists(imgDir)) { + await fs.rm(imgDir, { recursive: true, force: true }); + console.log(`Stripped ${path.join(basePath, "img")} from static export`); +} + console.log(`Nested static export under out/${basePath}`); diff --git a/docs/scripts/publish-image.mjs b/docs/scripts/publish-image.mjs new file mode 100644 index 00000000..e15851f9 --- /dev/null +++ b/docs/scripts/publish-image.mjs @@ -0,0 +1,128 @@ +#!/usr/bin/env node +// Maintainer-only. Uploads a doc image to the shared R2 bucket and purges +// it from Cloudflare's edge cache, so it's live right after this finishes. +// Requires CLOUDFLARE_API_TOKEN (scoped: R2 Edit on websites-images + Zone +// Cache Purge on absmach.eu) and CLOUDFLARE_ZONE_ID. +// +// Usage: +// pnpm run publish-image +// +// is the path used in MDX content, starting with "img/" to +// match the route worker/index.ts serves it back on: +// pnpm run publish-image ./roles-list.png img/user-guide/roles/roles-list-populated.png +// -> referenced in MDX as /img/user-guide/roles/roles-list-populated.png +// -> live at https://www.absmach.eu/docs/atom/img/user-guide/roles/roles-list-populated.png + +import { execFileSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { extname } from "node:path"; +import process from "node:process"; + +const BUCKET_NAME = "websites-images"; +// This docs site is served under https://www.absmach.eu/docs/atom (see +// README.md, app/sitemap.ts) -- same zone as the main absmach-website repo, +// which is why CLOUDFLARE_ZONE_ID below matches that repo's. +const SITE_ORIGIN = "https://www.absmach.eu"; +const BASE_PATH = "docs/atom"; + +// Shared bucket ("websites-images") holds assets for multiple properties; +// this prefix keeps atom-docs' objects from colliding with the others. +// Keep in sync with worker/index.ts. +const R2_KEY_PREFIX = "atom-docs"; + +const MIME_TYPES = { + ".webp": "image/webp", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".svg": "image/svg+xml", + ".gif": "image/gif", + ".avif": "image/avif", +}; + +try { + process.loadEnvFile(new URL("./.env.publish-image", import.meta.url)); +} catch { + // No local env file -- assume CLOUDFLARE_API_TOKEN / CLOUDFLARE_ZONE_ID + // are already exported (e.g. in CI). +} + +// pnpm forwards a leading "--" to the underlying command instead of +// stripping it (unlike npm), so tolerate it either way. +const cliArgs = process.argv.slice(2).filter((arg) => arg !== "--"); +const [localFile, publicPath] = cliArgs; + +if (!localFile || !publicPath) { + console.error( + "Usage: pnpm run publish-image \n" + + "Example: pnpm run publish-image ./roles-list.png img/user-guide/roles/roles-list-populated.png", + ); + process.exit(1); +} + +if (!existsSync(localFile)) { + console.error(`Local file not found: ${localFile}`); + process.exit(1); +} + +const destKey = publicPath.replace(/^\/+/, ""); +if (!destKey.startsWith("img/") || destKey === "img/") { + console.error(`Destination must start with "img/" and include a path, got: ${destKey}`); + process.exit(1); +} +const restPath = destKey.slice("img/".length); + +const contentType = MIME_TYPES[extname(restPath).toLowerCase()]; +if (!contentType) { + console.error(`Unrecognized file extension for: ${destKey}`); + process.exit(1); +} + +const { CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID } = process.env; +if (!CLOUDFLARE_API_TOKEN || !CLOUDFLARE_ZONE_ID) { + console.error( + "Missing CLOUDFLARE_API_TOKEN and/or CLOUDFLARE_ZONE_ID.\n" + + "Copy scripts/.env.publish-image.example to scripts/.env.publish-image and fill in the token.", + ); + process.exit(1); +} + +const objectPath = `${BUCKET_NAME}/${R2_KEY_PREFIX}/${restPath}`; + +console.log(`Uploading ${localFile} -> r2://${objectPath}`); +execFileSync( + "wrangler", + [ + "r2", + "object", + "put", + objectPath, + `--file=${localFile}`, + `--content-type=${contentType}`, + "--remote", + ], + { stdio: "inherit", env: process.env }, +); + +const publicUrl = `${SITE_ORIGIN}/${BASE_PATH}/${destKey}`; + +console.log(`Purging edge cache for ${publicUrl}`); +const purgeResponse = await fetch( + `https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache`, + { + method: "POST", + headers: { + Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ files: [publicUrl] }), + }, +); + +const purgeResult = await purgeResponse.json(); +if (!purgeResponse.ok || !purgeResult.success) { + console.error("Cache purge failed:", JSON.stringify(purgeResult, null, 2)); + process.exit(1); +} + +console.log(`Done. Live at ${publicUrl}`); diff --git a/docs/source.config.ts b/docs/source.config.ts index 8dc21074..c875500f 100644 --- a/docs/source.config.ts +++ b/docs/source.config.ts @@ -4,4 +4,15 @@ export const { docs, meta } = defineDocs({ dir: 'content/docs', }); -export default defineConfig(); +export default defineConfig({ + mdxOptions: { + // Doc images are served from R2 via a same-origin proxy route (see + // docs/worker/index.ts) rather than committed to public/img and bundled + // by next/image. The default remark-image plugin needs the source file + // on local disk (either to import it as a static asset, or just to + // probe its dimensions) -- disabling it keeps "/img/..." src strings in + // MDX content literal instead. Width/height come from + // lib/image-dimensions.json instead (see components/doc-image.tsx). + remarkImageOptions: false, + }, +}); diff --git a/docs/tsconfig.json b/docs/tsconfig.json index e66db53f..950ba9ef 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -20,5 +20,7 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + // worker/ runs in the Workers runtime (Cloudflare global types), not the + // Next.js/DOM one this tsconfig targets -- see worker/tsconfig.json. + "exclude": ["node_modules", "worker"] } diff --git a/docs/worker/index.ts b/docs/worker/index.ts new file mode 100644 index 00000000..1496f77d --- /dev/null +++ b/docs/worker/index.ts @@ -0,0 +1,87 @@ +// This site is a Next.js static export (see next.config.mjs: output: +// "export") deployed as Cloudflare Workers static assets -- there is no +// Next.js server runtime here, so no @cloudflare/next-on-pages or +// @opennextjs/cloudflare adapter, and no per-request Next.js code path to +// hang an R2 lookup off of. This tiny Worker sits in front of the static +// assets (see wrangler.jsonc: "main" + "assets.binding") purely to serve +// doc images from the shared R2 bucket instead of the git repo. +// +// Cloudflare's default asset routing (run_worker_first: false, the +// default -- see wrangler.jsonc) serves a request directly from the +// assets directory whenever a matching file exists, and only invokes this +// Worker when nothing matches. Doc images are no longer part of the +// static export output (scripts/nest-static-export.mjs strips +// out/docs/atom/img/ as a defensive measure even though public/img/ is +// gitignored and normally won't be present at build time), so every +// request under the docs img path automatically falls through to here. +// Everything else that reaches this Worker is a genuine 404, deferred to +// the assets binding's own not-found handling. +interface Env { + ASSETS: Fetcher; + IMAGES_BUCKET: R2Bucket; +} + +// Matches the Next.js basePath ("/docs/atom", see next.config.mjs) plus +// the literal "/img/..." convention doc content already uses in +// content/docs/**/*.mdx (e.g. "![](/img/user-guide/roles/roles-list.png)"). +// Keep in sync with scripts/publish-image.mjs. +const IMG_PREFIX = "/docs/atom/img/"; + +// Shared bucket ("websites-images") holds assets for multiple properties; +// this prefix keeps atom-docs' objects from colliding with the others. +const R2_KEY_PREFIX = "atom-docs"; + +function notFound(): Response { + return new Response("Not found", { + status: 404, + headers: { "content-type": "text/plain", "cache-control": "no-store" }, + }); +} + +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext, + ): Promise { + const url = new URL(request.url); + + if (!url.pathname.startsWith(IMG_PREFIX)) { + return env.ASSETS.fetch(request); + } + + // env.IMAGES_BUCKET.get() is an R2 binding call, not an HTTP + // subrequest -- it never touches Cloudflare's HTTP cache. Without + // explicitly writing the response into the Cache API, every request + // (from every visitor, at every edge location) would re-read from R2, + // no matter what Cache-Control header gets set on the returned + // Response. Using the request's own URL (unmodified) as the cache key + // keeps this purgeable by the existing purge-by-URL call in + // scripts/publish-image.mjs. + const cache = caches.default; + const cacheKey = new Request(request.url, request); + + const cached = await cache.match(cacheKey); + if (cached) return cached; + + const key = `${R2_KEY_PREFIX}/${url.pathname.slice(IMG_PREFIX.length)}`; + const object = await env.IMAGES_BUCKET.get(key); + if (!object) return notFound(); + + const headers = new Headers(); + object.writeHttpMetadata(headers); + headers.set("etag", object.httpEtag); + headers.set("content-length", String(object.size)); + // Browser TTL long enough to skip most repeat-visit requests, short + // enough to self-heal within the hour if a purge is ever missed. Edge + // TTL is effectively unbounded -- scripts/publish-image.mjs purges it + // explicitly and immediately on every upload, so there's no benefit to + // a shorter one, and every edge location that has ever served an image + // now actually caches it (see the Cache API use above). + headers.set("cache-control", "public, max-age=3600, s-maxage=31536000"); + + const response = new Response(object.body, { headers }); + ctx.waitUntil(cache.put(cacheKey, response.clone())); + return response; + }, +}; diff --git a/docs/worker/tsconfig.json b/docs/worker/tsconfig.json new file mode 100644 index 00000000..d0c60a11 --- /dev/null +++ b/docs/worker/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2022", + "lib": ["es2022"], + "module": "es2022", + "moduleResolution": "bundler", + "types": ["@cloudflare/workers-types"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true + }, + "include": ["./**/*.ts"] +} diff --git a/docs/wrangler.jsonc b/docs/wrangler.jsonc index 2dcf3fd1..7b2d9b70 100644 --- a/docs/wrangler.jsonc +++ b/docs/wrangler.jsonc @@ -1,12 +1,24 @@ { "$schema": "node_modules/wrangler/config-schema.json", "name": "atom-docs", + "main": "./worker/index.ts", "compatibility_date": "2026-06-08", "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], "observability": { "enabled": true }, "assets": { - "directory": "./out" - } + "directory": "./out", + "binding": "ASSETS", + // Default (false): serve a matching static file directly and only + // invoke worker/index.ts for requests that don't match one -- e.g. + // "/docs/atom/img/...", which the static export no longer contains. + "run_worker_first": false + }, + "r2_buckets": [ + { + "binding": "IMAGES_BUCKET", + "bucket_name": "websites-images" + } + ] }