Code Monkey home page Code Monkey logo

ghost-cloudflare-r2's Introduction

Ghost Cloudflare R2 Storage Adapter

Cloudflare R2 storage adapter for Ghost.

Features

  • Save images in Cloudflare R2
  • Supports images, media and files
  • Resize images to emulate Responsive Images
    • Implements saveRaw to force ghost to generate the srcset attribute for image cards
  • Save images and media using UUID as name
  • Compress resized images
  • Written in TypeScript for maintainability

Installation

The adapter can be installed using npm or Docker.

Clone repo

# Starting from the Ghost base directory
mkdir -p content/adapters/storage
cd content/adapters/storage
git clone https://github.com/egeldenhuys/ghost-cloudflare-r2
cd ghost-cloudflare-r2
git checkout v0.1.1

Install using npm

Requires npm to be installed.

npm install
cp -f ./build/src/index.js ./build/src/index.js.map ./build/src/index.d.ts .

Install using Docker

Requires Docker to be installed. This has the advantage of not requiring you to pollute your server with node and npm if you are already using Docker.

./build-in-docker.sh

Configuration

The storage adapter makes use of the following environment variables:

Environment Variable Description
GHOST_STORAGE_ADAPTER_R2_ENDPOINT Cloudflare R2 Endpoint. Example: https://<account_id>.r2.cloudflarestorage.com
GHOST_STORAGE_ADAPTER_R2_ACCESS_KEY_ID Access Key ID from Cloudflare R2 API Token
GHOST_STORAGE_ADAPTER_R2_SECRET_ACCESS_KEY Secret Access Key from Cloudflare R2 API Token
GHOST_STORAGE_ADAPTER_R2_BUCKET R2 Bucket to use for storage
GHOST_STORAGE_ADAPTER_R2_DOMAIN R2 Custom domain to use for serving content
GHOST_STORAGE_ADAPTER_R2_UUID_NAME Use UUID as name when storing images. May cause issues when used with Responsive Images. Default false. Allowed values true, false
GHOST_STORAGE_ADAPTER_R2_IMAGES_URL_PREFIX URL prefix to use for storing and serving images from R2. Default /content/images/
GHOST_STORAGE_ADAPTER_R2_MEDIA_URL_PREFIX URL prefix to use for storing and serving media (video) from R2. Default /content/media/
GHOST_STORAGE_ADAPTER_R2_FILES_URL_PREFIX URL prefix to use for storing and serving files from R2. Default /content/files/
GHOST_STORAGE_ADAPTER_R2_CONTENT_PREFIX Prefix to apply to all prefixes. Default empty. Must not contain a trailing slash. Example /blog_data
GHOST_STORAGE_ADAPTER_R2_GHOST_RESIZE This needs to be set to false if Image resizing is disabled for Ghost ( env imageOptimization__resize). Default true
GHOST_STORAGE_ADAPTER_R2_RESPONSIVE_IMAGES Generate an image for each width specified. Uses undocumented Ghost internal logic to get srcset generated. Default false. Allowed values true, false
GHOST_STORAGE_ADAPTER_R2_SAVE_ORIGINAL Save the original unoptimized image. Only applicable if (env imageOptimization__resize) is set. Default true. Allowed Values true, false
GHOST_STORAGE_ADAPTER_R2_RESIZE_WIDTHS Comma separated list of widths to resize the image when saving. This should match the srcset of your theme and any Ghost overrides. Default 300,600,1000,1600,400,750,960,1140,1200
GHOST_STORAGE_ADAPTER_R2_RESIZE_JPEG_QUALITY Quality to use when resizing JPEG images. Default: 80
GHOST_STORAGE_ADAPTER_R2_LOG_LEVEL Log level for the storage adapter. Default info. Allowed values debug, info, warn, error
GHOST_STORAGE_ADAPTER_R2_SAVE_ORIG_NAME_METADATA Save the original file name in the object Metadata under the key original_name. Useful for correlating original images to images with UUID names. Default false. Allowed values true, false

All environment variables can also be used as keys in the JSON config. The following Ghost configuration is required to activate the plugin for images, media, and files: Alternatively they can be specified as environment variables (See docker-compose example below).

"storage": {
  "active": "ghost-cloudflare-r2",
  "ghost-cloudflare-r2": {
    "GHOST_STORAGE_ADAPTER_R2_ENDPOINT": "https://<account_id>.r2.cloudflarestorage.com"
    ...
  },
  "media": {
    "adapter": "ghost-cloudflare-r2",
    "storage_type_media": true
  },
  "files": {
    "adapter": "ghost-cloudflare-r2",
    "storage_type_files": true
  }
}

The section for media and files and be removed if the adapter should not handle those types. Note: this is an undocumented syntax and might change in future Ghost releases (tested on 5.30.1). See Configuring Storage Adapters for more details.

Redirects for backwards compatibility

If your blog is already live, and you have sent out newsletters with images, then you no longer have control over the image URLs in the emails. The URLs will be pointing to example.com/content/images/* but you want to serve them from the CDN cdn.example.com/content/images/*.

One solution is to use Ghost Redirects (assuming your content has been copied to the CDN):

# Temporary redirect if you might be changing the CDN in the future
302:
  ^\/content\/images\/(.*)$: https://cdn.example.com/content/images/$1
  ^\/content\/media\/(.*)$: https://cdn.example.com/content/media/$1
  ^\/content\/files\/(.*)$: https://cdn.example.com/content/files/$1

If you want the flexibility to later change to a different blog domain or CDN you can set GHOST_STORAGE_ADAPTER_R2_DOMAIN to blog.example.com and use Ghost to redirect old and new image requests to the CDN.

Example Docker Compose environment variables

environment:
  storage__active: ghost-cloudflare-r2
  storage__media__adapter: ghost-cloudflare-r2
  storage__media__storage_type_media: true
  storage__files__adapter: ghost-cloudflare-r2
  storage__files__storage_type_files: true
  GHOST_STORAGE_ADAPTER_R2_ENDPOINT: https://<account_id>.r2.cloudflarestorage.com
  GHOST_STORAGE_ADAPTER_R2_ACCESS_KEY_ID: xxxxxx
  GHOST_STORAGE_ADAPTER_R2_SECRET_ACCESS_KEY: xxxxxx
  GHOST_STORAGE_ADAPTER_R2_BUCKET: my-ghost-bucket
  GHOST_STORAGE_ADAPTER_R2_DOMAIN: https://cdn.example.com
  GHOST_STORAGE_ADAPTER_R2_UUID_NAME: false  # optional. Default: false
  GHOST_STORAGE_ADAPTER_R2_IMAGES_URL_PREFIX: /content/images/  # optional. Default: /content/images/
  GHOST_STORAGE_ADAPTER_R2_MEDIA_URL_PREFIX: /content/media/  # optional. Default: /content/media/
  GHOST_STORAGE_ADAPTER_R2_FILES_URL_PREFIX: /content/files/  # optional. Default: /content/files/
  GHOST_STORAGE_ADAPTER_R2_CONTENT_PREFIX: ''  # optional. Default: ''
  GHOST_STORAGE_ADAPTER_R2_GHOST_RESIZE: true  # optional. Default: true
  GHOST_STORAGE_ADAPTER_R2_RESPONSIVE_IMAGES: false  # optional. Default: false
  GHOST_STORAGE_ADAPTER_R2_SAVE_ORIGINAL: true  # optional. Default: true
  # Example widths to get Dawn theme working correctly:
  GHOST_STORAGE_ADAPTER_R2_RESIZE_WIDTHS: 300,600,1000,1600,400,750,960,1140,1200 # optional. Default: 300,600,1000,1600,400,750,960,1140,1200
  GHOST_STORAGE_ADAPTER_R2_RESIZE_JPEG_QUALITY: 80  # optional. Default: 80
  GHOST_STORAGE_ADAPTER_R2_LOG_LEVEL: info  # optional. Default: info
  GHOST_STORAGE_ADAPTER_R2_SAVE_ORIG_NAME_METADATA: false  # optional. Default: false

Testing

The tests require a S3 compatible endpoint. A docker-compose file for MinIO has been included to run a local instance for testing.

Also note that the tests will generate random images in /tmp.

The tests can be refactored and improved.

ghost-cloudflare-r2's People

Contributors

egeldenhuys avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

ghost-cloudflare-r2's Issues

Does this integrate with Cloudflare Images as well?

I'm considering Ghost CMS instead of another Wordpress site, but I have a WP plugin that saves images to Cloudflare images and thus WP doesn't save any size variants, and optionally the original image, and when a visitor comes to the site the image is shown with the exact dimensions required as detected by Cloudflare. In the end, it results in really clean images and no need to worry about image sizes whatsoever.

Sharp installation error after upgrading Ghost

SHARP_INSTALLATION error when uploading an image after upgrading from Ghost 5.59.2-alpine to 5.74.0-alpine
ghost-cloudflare-r2 version: 0.1.0

Stack Trace:

[2023-11-24 06:42:07] ERROR "POST /ghost/api/admin/images/upload/" 500 515ms
Sharp wasn't installed
Error ID:
    9613ffb0-8a94-11ee-b1da-87d7936028e4
Error Code:
    SHARP_INSTALLATION
----------------------------------------
Error:
    at Object.resizeFromPath (/var/lib/ghost/versions/5.74.0/node_modules/@tryghost/image-transform/lib/transform.js:138:31)
Something went wrong installing the "sharp" module
Error relocating /var/lib/ghost/versions/5.74.0/node_modules/sharp/build/Release/sharp-linuxmusl-x64.node: vips_text_wrap_get_type: symbol not found
Possible solutions:
- Install with verbose logging and look for errors: "npm install --ignore-scripts=false --foreground-scripts --verbose sharp"
- Install for the current linuxmusl-x64 runtime: "npm install --platform=linuxmusl --arch=x64 sharp"
- Consult the installation documentation: https://sharp.pixelplumbing.com/install
- Ensure the version of sharp aligns with the sharp package: "npm ls sharp"
    at Object.<anonymous> (/var/lib/ghost/versions/5.74.0/node_modules/sharp/lib/sharp.js:37:9)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at Module.require (node:internal/modules/cjs/loader:1143:19)
    at require (node:internal/modules/cjs/helpers:119:18)
    at Object.<anonymous> (/var/lib/ghost/versions/5.74.0/node_modules/sharp/lib/constructor.js:11:1)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at Module.require (node:internal/modules/cjs/loader:1143:19)
    at require (node:internal/modules/cjs/helpers:119:18)
    at Object.<anonymous> (/var/lib/ghost/versions/5.74.0/node_modules/sharp/lib/index.js:6:15)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)

Storage adapter config:

GHOST_STORAGE_ADAPTER_R2_UUID_NAME: true 
GHOST_STORAGE_ADAPTER_R2_IMAGES_URL_PREFIX: /content/images/ 
GHOST_STORAGE_ADAPTER_R2_MEDIA_URL_PREFIX: /content/media/ 
GHOST_STORAGE_ADAPTER_R2_FILES_URL_PREFIX: /content/files/ 
GHOST_STORAGE_ADAPTER_R2_CONTENT_PREFIX: '' 
GHOST_STORAGE_ADAPTER_R2_GHOST_RESIZE: true 
GHOST_STORAGE_ADAPTER_R2_RESPONSIVE_IMAGES: true 
GHOST_STORAGE_ADAPTER_R2_SAVE_ORIGINAL: false 
# Example widths to get Dawn theme working correctly:
GHOST_STORAGE_ADAPTER_R2_RESIZE_WIDTHS: 300,600,1000,1600,400,750,960,1140,1200 
GHOST_STORAGE_ADAPTER_R2_RESIZE_JPEG_QUALITY: 80 
GHOST_STORAGE_ADAPTER_R2_LOG_LEVEL: debug 
GHOST_STORAGE_ADAPTER_R2_SAVE_ORIG_NAME_METADATA: true 

Issue with Image Sizing on Cloudflare R2 Adapter

Hello,

Great adapter! I'm really impressed of the features it has, but at the same time I've got stuck on one of them.

Here's one aspect that I was unable to get functioning or comprehend.

When employing local storage, images set to lazy load typically come from this path example.com/content/images/size/.

However, if you switch to using the cloudflare-r2 storage provider, and enable the RESPONSIVE_IMAGES feature, the images are not delivered from cdn.example.com/content/images/size/ as expected.

Despite this, the URLs for these images still point to cdn.example.com/content/images, which serves up the full-sized images, instead of directing to the specific size path. Without a specific URL prefix for resized images in the configuration, it's not clear how the system differentiates URLs for resized images from the original ones.

On the other hand, at least the images get sized in the R2 bucket

CleanShot 2023-11-07 at 21 24 00@2x

This is the configuration I'm using

"storage": {
        "active": "ghost-cloudflare-r2",
        "ghost-cloudflare-r2": {
          "GHOST_STORAGE_ADAPTER_R2_ENDPOINT":"https://r2.cloudflarestorage.com",
          "GHOST_STORAGE_ADAPTER_R2_ACCESS_KEY_ID": "secret",
          "GHOST_STORAGE_ADAPTER_R2_SECRET_ACCESS_KEY":"secret",
          "GHOST_STORAGE_ADAPTER_R2_BUCKET": "my-ghost-assets",
          "GHOST_STORAGE_ADAPTER_R2_DOMAIN": "https://cdn.example.com",
          "GHOST_STORAGE_ADAPTER_R2_IMAGES_URL_PREFIX": "/content/images/",
          "GHOST_STORAGE_ADAPTER_R2_RESIZE_WIDTHS": "300,600,1000,1600,2000",
          "GHOST_STORAGE_ADAPTER_R2_RESPONSIVE_IMAGES": "true"
        },
        "media": {
            "adapter": "ghost-cloudflare-r2",
            "storage_type_media": true
          },
        "files": {
            "adapter": "ghost-cloudflare-r2",
            "storage_type_files": true
        }

Here are two instances showcasing the same image: one using the adapter and the other using local storage.

###Images Loaded Using Cloudflare R2 Adapter 

<img class="post-hero__img lazyautosizes lazyloaded" data-srcset="https://cdn.example.com/content/images/2023/11/bla.jpg 300w,
                    https://cdn.example.com/content/images/2023/11/bla.jpg 600w,
                    https://cdn.example.com/content/images/2023/11/bla.jpg 1000w,
                    https://cdn.example.com/content/images/2023/11/bla.jpg 2000w" srcset="https://cdn.example.com/content/images/2023/11/bla.jpg 300w,
                    https://cdn.example.com/content/images/2023/11/bla.jpg 600w,
                    https://cdn.example.com/content/images/2023/11/bla.jpg 1000w,
                    https://cdn.example.com/content/images/2023/11/bla.jpg 2000w" data-sizes="auto" data-src="https://cdn.example.com/content/images/2023/11/bla-1.jpg" src="https://cdn.example.com/content/images/2023/11/bla-1.jpg" alt="testing cdn2" sizes="908px">


###Images Loaded Using Ghost local Storage 
<img class="post-hero__img lazyautosizes lazyloaded" data-srcset="/content/images/size/w300/2023/11/bla.jpg 300w,
                    /content/images/size/w600/2023/11/bla.jpg 600w,
                    /content/images/size/w1000/2023/11/bla.jpg 1000w,
                    /content/images/size/w2000/2023/11/bla.jpg 2000w" srcset="/content/images/size/w300/2023/11/bla.jpg 300w,
                    /content/images/size/w600/2023/11/bla.jpg 600w,
                    /content/images/size/w1000/2023/11/bla.jpg 1000w,
                    /content/images/size/w2000/2023/11/bla.jpg 2000w" data-sizes="auto" data-src="/content/images/size/w300/2023/11/bla.jpg" src="/content/images/size/w300/2023/11/bla.jpg" alt="local storage" sizes="908px">

Perhaps there's something I'm overlooking!
I'd appreciate your feedback!
Thank you. ๐Ÿ™

Ghost fails with error when r2 adapter is integrated

I tried using the adapter but it failed with the below error. I never worked on TS hence was not able to debug and fix it.
My deployment environment is as follows.
OS: ubuntu 22.04
Ghost : Latest Ghost with docker
Cloudflare already configured

[2023-04-19 00:36:33] INFO Ghost is running in production...
[2023-04-19 00:36:33] INFO Your site is now available on https://sitename.tld/
[2023-04-19 00:36:33] INFO Ctrl+C to shut down
[2023-04-19 00:36:33] INFO Ghost server started in 1.285s
[2023-04-19 00:36:33] INFO Database is in a ready state.
[2023-04-19 00:36:34] INFO Ghost database ready in 1.677s
[2023-04-19 00:36:37] ERROR We detected a misuse. Please read the stack trace.

We detected a misuse. Please read the stack trace.

Error ID:
3e7c67c0-de4a-11ed-9324-3587dc794858


Error:
at AdapterManager.getAdapter (/var/lib/ghost/versions/5.43.0/node_modules/@tryghost/adapter-manager/lib/AdapterManager.js:114:27)
Something went wrong installing the "sharp" module

Cannot find module '../build/Release/sharp-linux-x64.node'
Require stack:

  • /var/lib/ghost/content/adapters/storage/ghost-cloudflare-r2/node_modules/sharp/lib/sharp.js
  • /var/lib/ghost/content/adapters/storage/ghost-cloudflare-r2/node_modules/sharp/lib/constructor.js
  • /var/lib/ghost/content/adapters/storage/ghost-cloudflare-r2/node_modules/sharp/lib/index.js
  • /var/lib/ghost/content/adapters/storage/ghost-cloudflare-r2/index.js
  • /var/lib/ghost/versions/5.43.0/core/server/services/adapter-manager/index.js
  • /var/lib/ghost/versions/5.43.0/core/server/adapters/storage/index.js
  • /var/lib/ghost/versions/5.43.0/core/server/lib/image/index.js
  • /var/lib/ghost/versions/5.43.0/core/server/models/member.js
  • /var/lib/ghost/versions/5.43.0/core/server/models/index.js
  • /var/lib/ghost/versions/5.43.0/core/server/services/url/Resources.js
  • /var/lib/ghost/versions/5.43.0/core/server/services/url/UrlService.js
  • /var/lib/ghost/versions/5.43.0/core/server/services/url/index.js
  • /var/lib/ghost/versions/5.43.0/core/app.js
  • /var/lib/ghost/versions/5.43.0/core/boot.js
  • /var/lib/ghost/versions/5.43.0/ghost.js
  • /var/lib/ghost/versions/5.43.0/index.js

Possible solutions:

  • Install with verbose logging and look for errors: "npm install --ignore-scripts=false --foreground-scripts --verbose sharp"
  • Install for the current linux-x64 runtime: "npm install --platform=linux --arch=x64 sharp"
  • Consult the installation documentation: https://sharp.pixelplumbing.com/install
    at Object. (/var/lib/ghost/content/adapters/storage/ghost-cloudflare-r2/node_modules/sharp/lib/sharp.js:34:9)
    at Module._compile (node:internal/modules/cjs/loader:1196:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1250:10)
    at Module.load (node:internal/modules/cjs/loader:1074:32)
    at Function.Module._load (node:internal/modules/cjs/loader:909:12)
    at Module.require (node:internal/modules/cjs/loader:1098:19)
    at require (node:internal/modules/cjs/helpers:108:18)
    at Object. (/var/lib/ghost/content/adapters/storage/ghost-cloudflare-r2/node_modules/sharp/lib/constructor.js:8:1)
    at Module._compile (node:internal/modules/cjs/loader:1196:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1250:10)
    at Module.load (node:internal/modules/cjs/loader:1074:32)
    at Function.Module._load (node:internal/modules/cjs/loader:909:12)
    at Module.require (node:internal/modules/cjs/loader:1098:19)
    at require (node:internal/modules/cjs/helpers:108:18)
    at Object. (/var/lib/ghost/content/adapters/storage/ghost-cloudflare-r2/node_modules/sharp/lib/index.js:3:15)
    at Module._compile (node:internal/modules/cjs/loader:1196:14)

Thanks in advance

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.