{"_id":"@alexchenh/quartz-cloudflare-media","name":"@alexchenh/quartz-cloudflare-media","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@alexchenh/quartz-cloudflare-media","version":"1.0.0","description":"Production-ready Cloudflare R2 and Images delivery for Quartz 5","type":"module","license":"MIT","author":{"name":"Alex Chen"},"homepage":"https://github.com/alexchenh/quartz-cloudflare-media#readme","repository":{"type":"git","url":"git+https://github.com/alexchenh/quartz-cloudflare-media.git"},"bugs":{"url":"https://github.com/alexchenh/quartz-cloudflare-media/issues"},"keywords":["quartz","quartz-plugin","cloudflare","r2","images","obsidian"],"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"bin":{"quartz-cloudflare-media":"dist/cli.js"},"main":"./dist/index.js","types":"./dist/index.d.ts","sideEffects":false,"scripts":{"build":"tsup","check":"npm run typecheck && npm run format:check && npm test && npm run worker:check","format":"prettier . --write","format:check":"prettier . --check","test":"node --import tsx --test test/*.test.ts","typecheck":"tsc --noEmit","worker:check":"npm --prefix worker run check","prepack":"npm run build"},"engines":{"node":">=22","npm":">=10.9.0"},"quartz":{"name":"cloudflare-media","displayName":"Cloudflare Media","description":"Serve published Quartz media from Cloudflare R2 and Images","category":"transformer","version":"1.0.0","quartzVersion":">=5.0.0","dependencies":["@quartz-community/crawl-links"],"defaultOrder":65,"defaultEnabled":true,"defaultOptions":{"backend":"worker","contentDirectory":"content","outputDirectory":"public","cacheDirectory":".quartz-cache","excludeDrafts":true,"imageWidths":[640,1280,1920],"defaultImageWidth":1280,"imageQuality":88},"configSchema":{"type":"object","required":["backend","publicOrigin"],"properties":{"backend":{"type":"string","enum":["worker","direct-r2"]},"publicOrigin":{"type":"string"},"imageTransformOrigin":{"type":"string"},"bucketName":{"type":"string"},"keyPrefix":{"type":"string"},"contentDirectory":{"type":"string"},"outputDirectory":{"type":"string"},"cacheDirectory":{"type":"string"},"manifestFilename":{"type":"string"},"ignorePatterns":{"type":"array","items":{"type":"string"}},"excludeDrafts":{"type":"boolean"},"imageWidths":{"type":"array","items":{"type":"number"}},"defaultImageWidth":{"type":"number"},"imageQuality":{"type":"number"},"imageSizes":{"type":"string"},"uploadConcurrency":{"type":"number"},"multipartConcurrency":{"type":"number"},"multipartPartSize":{"type":"number"},"workerUploadTokenEnvironment":{"type":"string"}}}},"publishConfig":{"access":"public","provenance":true},"overrides":{"esbuild":"^0.28.1"},"dependencies":{"@aws-sdk/client-s3":"^3.1115.0","@aws-sdk/lib-storage":"^3.1115.0","@quartz-community/types":"^0.3.0","@quartz-community/utils":"^0.1.0","globby":"^16.1.0","github-slugger":"^2.0.0","sharp":"^0.35.3","unist-util-visit":"^5.0.0","wrangler":"^4.118.0","yaml":"^2.8.2"},"devDependencies":{"@types/hast":"^3.0.4","@types/node":"^25.0.10","esbuild":"^0.28.1","prettier":"^3.8.1","tsup":"^8.5.0","tsx":"^4.21.0","typescript":"^5.9.3"},"_id":"@alexchenh/quartz-cloudflare-media@1.0.0","_integrity":"sha512-hOCcGfzV4vOx42fr9bB6CKMEa/Dahx5wBfsrtNE61afWU+jIpPHeQoMUkDTnhlKO707ygNTQlbC1Qw5jeveHcQ==","_resolved":"/private/tmp/qcm-pack/alexchenh-quartz-cloudflare-media-1.0.0.tgz","_from":"file:/private/tmp/qcm-pack/alexchenh-quartz-cloudflare-media-1.0.0.tgz","_nodeVersion":"22.22.3","_npmVersion":"10.9.8","dist":{"integrity":"sha512-hOCcGfzV4vOx42fr9bB6CKMEa/Dahx5wBfsrtNE61afWU+jIpPHeQoMUkDTnhlKO707ygNTQlbC1Qw5jeveHcQ==","shasum":"9e279e24839017833d39f4b665fce3a9d48487bc","tarball":"https://registry.npmjs.org/@alexchenh/quartz-cloudflare-media/-/quartz-cloudflare-media-1.0.0.tgz","fileCount":17,"unpackedSize":880120,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIAqJnwq5IMd5muEwSbTQRnMLFjLAnLKrpgy1gDM1TeqcAiEA05DThhWOKxkt+9NnMB2FUjl2dM0te50f81o/LgfK6VI="}]},"_npmUser":{"name":"alexchenh","email":"alex.chen.h@gmail.com"},"directories":{},"maintainers":[{"name":"alexchenh","email":"alex.chen.h@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/quartz-cloudflare-media_1.0.0_1788212107805_0.024489745170485522"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-31T21:35:07.620Z","1.0.0":"2026-08-31T21:35:07.973Z","modified":"2026-08-31T21:35:08.240Z"},"maintainers":[{"name":"alexchenh","email":"alex.chen.h@gmail.com"}],"description":"Production-ready Cloudflare R2 and Images delivery for Quartz 5","homepage":"https://github.com/alexchenh/quartz-cloudflare-media#readme","keywords":["quartz","quartz-plugin","cloudflare","r2","images","obsidian"],"repository":{"type":"git","url":"git+https://github.com/alexchenh/quartz-cloudflare-media.git"},"author":{"name":"Alex Chen"},"bugs":{"url":"https://github.com/alexchenh/quartz-cloudflare-media/issues"},"license":"MIT","readme":"# Quartz Cloudflare Media\n\nProduction-ready R2 and Cloudflare Images delivery for Quartz 5. Your Obsidian vault and Git repository stay the source of truth; production builds upload only referenced media, rewrite generated HTML to immutable Cloudflare URLs, and remove redundant copies from the site artifact.\n\n## Quick start\n\nFrom an existing Quartz 5 site:\n\n```bash\nnpx @alexchenh/quartz-cloudflare-media init\n```\n\nThe guided installer defaults to a private R2 bucket behind a small Worker. It verifies Wrangler authentication, provisions storage, deploys the Worker, installs the Quartz transformer, adds build scripts, copies Quartz publication exclusions, and runs local validation. Preview changes without writing anything:\n\n[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/alexchenh/quartz-cloudflare-media/tree/main/worker)\n\n```bash\nnpx @alexchenh/quartz-cloudflare-media init --dry-run --backend worker\n```\n\nFor unattended setup, provide non-secret values as flags:\n\n```bash\nnpx @alexchenh/quartz-cloudflare-media init \\\n  --backend worker \\\n  --worker-name my-garden-media \\\n  --bucket my-garden-media \\\n  --pages-project my-garden \\\n  --yes\n```\n\nThe upload secret is generated in memory and sent directly to Wrangler. It is never accepted as a CLI argument, printed, or written to the repository.\n\n## How it works\n\n```text\npublished Markdown + local media\n             |\n             | prepare + content hash\n             v\n       local manifest (private)\n             |\n             | upload missing immutable keys\n             v\n       Cloudflare R2 (private in Worker mode)\n          |                         |\n          | Images binding          | original/range response\n          v                         v\n responsive AVIF/WebP/JPEG         video or full-size image\n          \\_________________________/\n                       |\n                       v\n              generated Quartz HTML\n```\n\nThe scanner understands Markdown images, Obsidian embeds, and HTML `img`, `video`, and `source` elements. It honors `.gitignore`, configured Quartz ignore patterns, and `draft: true`. Missing or ambiguous references fail the build instead of publishing the wrong file.\n\nObject keys use `v1/<sha256>/<safe-filename>`. Changing bytes creates a new URL; unchanged objects are skipped. Automatic deletion is intentionally unavailable.\n\n## Backends\n\n### Worker (recommended)\n\nWorker mode gives the simplest and safest setup:\n\n- R2 stays private.\n- The Worker serves originals, range-aware video, and Images-binding transformations.\n- Upload, listing, and multipart routes require a generated bearer secret.\n- `workers.dev` works without DNS; a custom domain can be added later.\n- Large files are split below Cloudflare's request-body limit and uploaded through R2 multipart APIs.\n\nEvery media request invokes the Worker. Review current Workers, R2, and Images allowances before high-traffic use.\n\n### Direct R2\n\nDirect mode avoids Worker request usage and is useful for established R2 custom-domain deployments. It requires an R2 S3 token restricted to the target bucket:\n\n```bash\nnpx @alexchenh/quartz-cloudflare-media init \\\n  --backend direct-r2 \\\n  --bucket my-garden-media \\\n  --origin https://media.example.com \\\n  --image-transform-origin https://media.example.com\n```\n\nSet these only in the build environment:\n\n- `CLOUDFLARE_ACCOUNT_ID`\n- `R2_ACCESS_KEY_ID`\n- `R2_SECRET_ACCESS_KEY`\n\nConnect the bucket to a custom domain, disable the public `r2.dev` hostname, enable Images transformations for the zone, cache the media hostname including video extensions, and scope any bot-management exception to that hostname only.\n\n## Quartz configuration\n\nThe installer adds the plugin immediately after Crawl Links:\n\n```yaml\nplugins:\n  - source: \"@alexchenh/quartz-cloudflare-media\"\n    enabled: true\n    order: 65\n    options:\n      backend: worker\n      publicOrigin: https://my-garden-media.example.workers.dev\n      contentDirectory: content\n      outputDirectory: public\n      cacheDirectory: .quartz-cache\n      ignorePatterns:\n        - private\n        - templates\n        - .obsidian\n      excludeDrafts: true\n      imageWidths: [640, 1280, 1920]\n      defaultImageWidth: 1280\n      imageQuality: 88\n      imageSizes: \"(max-width: 800px) 100vw, 800px\"\n```\n\nLocal builds remain unchanged. A remote media build uses:\n\n```bash\nnpm run media:sync\nCLOUDFLARE_MEDIA_MODE=remote npx quartz build\nnpm run media:finalize\nnpm run media:check\n```\n\nThe installer adds this sequence as `npm run build:media`.\n\n## CLI\n\n| Command           | Purpose                                                    |\n| ----------------- | ---------------------------------------------------------- |\n| `init`            | Install and configure an existing Quartz 5 site            |\n| `doctor`          | Validate scripts, secrets, configuration, and media origin |\n| `prepare`         | Discover published media and write the local manifest      |\n| `sync`            | Upload only missing content-addressed objects              |\n| `finalize`        | Remove redundant media from Quartz output                  |\n| `check`           | Verify generated URLs and artifact removal                 |\n| `deploy-worker`   | Provision or update the companion Worker and R2 bucket     |\n| `configure-ci`    | Rotate and connect Worker and Pages upload secrets         |\n| `prune --dry-run` | List unused objects without deleting them                  |\n| `undo-init`       | Restore installer-owned files if they have not changed     |\n\nUse `--root` when invoking the CLI outside the Quartz directory. `init` also supports `--dry-run`, `--yes`, `--json`, `--backend`, `--bucket`, `--worker-name`, `--pages-project`, and `--origin`.\n\n## Operations and rollback\n\n- Retry failed syncs safely; immutable keys make uploads idempotent.\n- Run `doctor` after Quartz, Node, Cloudflare, or build-provider changes.\n- Validate video seeking with a byte-range request.\n- Review `prune --dry-run` manually. The package cannot delete stale production media.\n- `undo-init` restores only files whose hashes still match the installer receipt. It never removes Workers, buckets, secrets, or R2 objects.\n- To disable remote media immediately, remove `CLOUDFLARE_MEDIA_MODE=remote` from the build. Quartz will use local asset URLs again.\n\n## Privacy and non-goals\n\nWorker mode uploads object bytes, content type, and the content hash. Note paths and the manifest stay local. Anyone with a published media URL can retrieve that media; private content must remain under ignored paths.\n\nNon-goals: Cloudflare Stream, video transcoding, adaptive bitrate, generated posters, browser uploads, replacing Obsidian assets, or automatic deletion.\n\n## Development\n\n```bash\nnpm install\nnpm --prefix worker install\nnpm --prefix worker run types\nnpm run check\nnpm pack --dry-run\n```\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-2df9d56d0be86258e77be35804708933"}