This tutorial is just a temporary alternative, while Supabase team is working hard to ship more and more features, where Storage CDN and Transformation is in their pipeline.
source: https://supabase.io/storage
⭐🎉🎊
On that note, congratulations Supabase team on raising $30M as an open source backend-as-a-service startup!!
Get started!
Take note ⚠:
- We will be using Vercel Serverless function to make this magic happens, the code might be different but the logic is the same.
- We will be serving and transforming
Public
bucket only. If you wish to see how to implement these magic with Supabase Auth for RLS, remember to follow me for more tutorial.
With that said, we will go through just a few simple steps to implement this magic on
our Supabase Storage's images.
1. Getting Image bucket & name
We will be using bucket_name
and file_name
variable to call the serverless function, instead of the full public url. If not, your image link would be super-duper long, and unneccessary.
Here are some of the way you could prepare the bucket_name
and/or file_name
.
1.If you are allowing your users to upload static content to Public
bucket, then take note of the bucket_name
and file_name
users keyed-in.
const bucket_name = 'static' // your bucket name
const file_name = 'avatar.png' // name for the file
const avatarFile = event.target.files[0]
const { data, error } = await supabase
.storage
.from('avatars')
.upload(`${ bucket_name }/${ file_name }`, avatarFile, {
cacheControl: '3600',
upsert: false
})
2.You can get use from.list() to retrieve the images you want in a bucket
.
In this case, I will simply just list everything in my bucket_name
bucket.
const { data, error } = await supabase.storage.from(bucket_name).list()
const file_names = data.map(item => item.names)
3.If you already have the public URL fetch together in another query, with link such as https://asdasaeipbvsvnr.supabase.co/storage/v1/object/public/static/avatar.png
, then you can quickly get the bucket_name
and file_name
using
let link = 'https://asdasaeipbvsvnr.supabase.co/storage/v1/object/public/static/avatar.png'
let [ bucket_name, file_name ] = link.split('public/')[1].split('/')
Alright, now we have our appropriate variable, we can start construct our new link to slot into <img>
tag! 🙌
2. Construct new link
Because we are using Vercel serverless function, we need to wrap our img url around the api
route.
If you are using Vercel for your current project, you can simply use the following code to generate new link for your <img>
const params = new URLSearchParams({
f: file_name,
b: bucket_name,
// params we haven't mentioned...
})
const new_link = window.location.origin + "/api/resize?" + params.toString()
If you are not using, Vercel as deployment, you can easily forked this repo that I created for this tutorial. You just have to follow the steps and setup your .env
on Vercel. If you wanted to learn more on how this function works, continue follow along!
Serverless function
This part is where the magic happens, let's create a new file in your project root, named api/resize.ts
(be default Vercel will convert all files in api folder into serverless function).
Then, you have to install a few packages
I'm using yarn and typescript, you can use npm, and plain Javascript if you like
yarn add sharp axios
yarn add -D @vercel/node @types/sharp
Next, create a basic function as such:
import { VercelRequest, VercelResponse } from "@vercel/node"
import sharp from "sharp"
import axios from "axios"
export default async (req: VercelRequest, res: VercelResponse) => {
res.end("Hi")
}
To quickly test out the api
, run vercel dev
to spin up Vercel Development Server.
Then visit http://localhost:3000/api/resize
, it should response with 'Hi'.
After that , replace the function with this:
export default async (req: VercelRequest, res: VercelResponse) => {
const {
query: { w, h, f, b, q },
} = req
// this tricks to deconstruct all the nested query into it's own variable.
// parameters
// w: width (pixel)
// h: height (pixel)
// f: file_name
// b: bucket_name
// q: quality (0 to 100)
res.end("Hi")
}
Remember we have created a new link for the image just now?? Now we have to construct it back to original url, then convert it to Buffer. Thankfully, axios make this job so easy.
export default async (req: VercelRequest, res: VercelResponse) => {
...
// check if `bucket_name` and `file_name` are available, else return error
if (f && b) {
const url = `${ process.env.SUPABASE_URL }/storage/v1/object/public/${ b }/${ f }`
const buffer = (await axios({ url, responseType: "arraybuffer" })).data as Buffer
res.statusCode = 200
res.setHeader("Content-Type", "image/png")
res.end(buffer)
} else {
res.statusCode = 500
res.setHeader("Content-Type", "text/html")
res.end("<h1>Internal Error</h1><p>Sorry, there was a problem</p>")
}
}
You can now test this api endpoint as such http://localhost:3000/api/resize?f=avatar.png&b=static
(Of course you need to have the image in your bucket) to see if your image is generated. If it works, let continue on the longest script in this tutorial, where we use sharp to transfrom our image to the desire width, height or quality.
export default async (req: VercelRequest, res: VercelResponse) => {
...
if (f && b) {
...
// here we create a new_params object to convert string to number, and also set default value
const new_params = {
w: +w || 800, // set default 800px
h: +h || null, // set to null if not provided, so that Sharp automatically keep the aspect ratio
q: +q || 80 // set default 80% quality
}
// here's where the Transformation happens
sharp(buffer)
.resize(new_params.w, new_params.h)
.jpeg({quality: new_params.q}) // change to .webp() if you want to serve as webp
.toBuffer()
.then((data) => {
// here's where set the cache
// I set to cache the media for 1 week, 60seconds * 60minutes * 24hours * 7days
// remove setHeader('Cache-Control') if you wish not to cache it
res.statusCode = 200
res.setHeader("Cache-Control", `public, immutable, no-transform, s-maxage=604800, max-age=604800`)
res.setHeader("Content-Type", "image/jpeg")
res.end(data)
})
} else {
res.statusCode = 500
res.setHeader("Content-Type", "text/html")
res.end("<h1>Internal Error</h1><p>Sorry, there was a problem</p>")
}
}
That's it! Just a few line of codes and you have your own CDN and Transformation for Supabase Storage ready to go!!!! But! Don't forget the new_link
we created at our frontend.
Lastly!
This is the last step for this tutorial, we generated new_link
previously, but now it is ready to add more parameter.
// Set a few width so that cache is more efficient, and need not to create so many cache when different browser visit your website.
let windowWidth = 0
if(window.innerWidth >= 1200) {
windowWidth = 1000
} else if (window.innerWidth >= 800) {
windowWidth = 800
} else {
windowWidth = 600
}
const params = new URLSearchParams({
f: file_name,
b: bucket_name,
w: windowWidth,
h: null, // set to null to keep image's aspect ratio
q: 0.8
})
const new_link = window.location.origin + "/api/resize?" + params.toString()
// set the src to new link
document.getElementById("myImg").src = new_link;
And we are DONE!!!
All source code for this tutorial can be found here!
Showcase
Check out Made With Supabase, and inspect the <img>
, you will see the similar code there, with slight minor change.
What is Made With Supabase? It is a collection of projects that made with Supabase! Feel free to submit your Supabase project, share the awesome-ness of Supabase with the world!
Before you go
If you find this tutorial helpful, and wish to lean more, then follow me here, and follow my Twitter!