A serverless, lazy loading image gallery with rokka and Vue.js

  • Christian Stocker

Vue.js is very popular here at Liip and used more and more. So is our image delivery service rokka.io. We developed some vue components for rokka images, which makes using them together even easier.

Those components are vue-rokka-image, vue-rokka-image-lazy and vue-rokka-uploader. In this blogpost, I want to show how you can develop your own little dynamic gallery with rokka and some of those packages and just one single HTML file. This can then be uploaded to any file hosting. Nothing else needed, almost serverless for you (if you use AWS S3 or such, you don't even need your own server).

Need a quick gallery to show some images to your friends? Don’t want to setup a server for that, but still want to keep the looks and feel under control? Then this might be useful for you. Or you just want to include rokka images into your vue.js app? Certainly some hints in here for you.

Due to the dynamic nature of rokka and the possibilities of rokka.js you don't even need to deploy a new version of that HTML file when uploading new images to rokka. It just takes the latest pictures directly from rokka (or whatever filtering you added to it).

Basic setup

First, we need to create a rokka api key, which only can read sourceimages info. Since that key will be included in the HTML file, you don't want that other people can delete or change your images.

Create a read only key

Do this on the commandline (or use our online API docs)

curl -X POST https://api.rokka.io/organizations/$ROKKA_ORG/memberships \
-H "Api-Key: $ROKKA_API_KEY” 
-d '{"roles": [ "sourceimages:read" ]}'

in return, you get a new api key. Copy that to somewhere safe.

HTML

First in the HTML, we need to include all the needed packages, vue, vue-rokka-image-lazy and the rokka.js library. You should use https://cdn.jsdelivr.net/npm/vue for production, once you're sure, everything works fine.

<html>
<head>
    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/vue-rokka-image-lazy@0.4"></script>
    <script src="https://unpkg.com/rokka@2.0.1"></script>
</head>    
</html>

Then, we need some html body, the template for the page. We keep it simple for now

<body>
<div id="app">
    <div class="photos">
        <div v-for="image in images">
            <div>
                <rokka-img-lazy
                     :sourceimage="image"
                     loading="https://rokka.io/gallery/assets/loader.svg"
                     :operations="[{name: 'resize', options: {width: 180, height: 180}}]"
                     :options="[{autoformat: 1}, {autoformat: 1, dpr: 2}]"
                     :postfix="['1x', '2x']">
                </rokka-img-lazy>
            </div>
        </div>
    </div>
</div>
</body>

Javascript

That doesn't do anyting yet, we need some script to run. So add the following just before </body> end tag

<script>
  const rokkaKey = 'yourkey' //read only key
  const rokkaOrg = 'yourorg'
  const rokkaClient = rokka({
    apiKey: rokkaKey,
  })
  new Vue({
    el: '#app',
    components: {
      RokkaImgLazy: vueRokkaImageLazy.RokkaImgLazy,

    },
    data: {
      images: []
    },
    created() {
      rokkaClient.sourceimages.list(rokkaOrg, {limit: 100}).then(result => {
        this.images = result.body.items
      })
    }
  })
</script>

This initiates the Vue instance and when it's created, it runs a query to rokka to get the 100 latest images. As soon as we have them, we set this.images so that the template gets rerendered and all the images are output with the help of rokka-image-lazy. The nice thing about rokka-image-lazy is, that it actually loads the images only shortly before they become visible to the visitor. And not all 100 at once. Saves bandwidth for you and the visitor.

With the postfix and options attribute we also tell it to use srcset tags for retina and non-retina screens. And the autoformat options tells rokka to deliver the best format to different browsers (in this case, this is only about WebP vs. JPEG, since the original pictures are all just JPEG).

CSS

When you now open that page in a browser (you can even load it from the filesystem, doesn't need a server), the output isn't very nice. So let's add this to the <head> section:

<style>
    .photos {
        display: grid;
        grid-template-columns: repeat(auto-fill, 200px);
        grid-gap: 6px;
        text-align: center;
    }
    .photos div {
        /* needed for lazy loading to properly kick in */
        min-height: 180px;
    }    
</style>

and already much better. A lazy-loading, responsive, serverless, almost image gallery (check the rendered output or the source code). But there's just not much functionality in this yet.

Adding more functionality

A more advanced version could include a slideshow and some image metadata. To have some info about the image, we output some user metadata we added during upload.

 <div class="credits" v-if="image.user_metadata && image.user_metadata.unsplash_artist_id">
      Photo by 
      <a :href="`https://unsplash.com/@${image.user_metadata.unsplash_artist_id}`">
          {{image.static_metadata.exif.artist}}
      </a><br/>
      on <a :href="`https://unsplash.com/photos/${image.user_metadata.unsplash_photo_id}`">
        Unsplash
      </a>
</div>

And to include a slideshow/full-screen gallery, we include vue-gallery, a responsive and customizable image gallery. I save you the code here, but check out the full source code or the rendered output.

Still just one single, handcrafted HTML file needed for all this.

As a Vue.js app

Of course, you can also do this in a more “traditional” vue.js app. The source code of an example of such an App, which also shows the different image modes vue-rokka-image-lazy and vue-rokka-image provides (mainly, using img or picture) is available in our github repo. If you use npm run build with this setup, you can also just upload the dist/ folder to some file hosting and don't need a "traditional" server.

An almost full blown gallery app at rokka.io/gallery

We're also currently working on a more advanced rokka-gallery app, with even more features like search, adding labels, organizing by albums, video support and more. You can use it at rokka.io/gallery or check out the source code, if you want to host that by yourself. Again, no need for a traditional server, just npm run build and upload the files to S3 or such.

Uploading images

If you want to upload easily via the browser, we also have you covered. There's the vue-rokka-uploader component. It's a smallish wrapper around vue-upload-component. You can check out our barebones example, but since there's no valid key there, the actual uploading doesn't work... You can also use rokka.io/dashboard or the already mentioned rokka.io/gallery to quickly upload pictures. The later is using exactly that vue-rokka-uploader component.

A word about security

Since your rokka API key is disclosed in the final HTML (or javascript files, when using the vue-cli app approach), people getting hold of that can use that to access your rokka organization with the rokka API or one of the clients for it. You definitely should create a read only api key as mentioned above. But still, with such a key they can read all your images and their metadata (for example also, where those images were taken, since we extract that exif data). If that's a problem for you, you'd still need some "middleware" to filter that data.

To keep your setup still “serverless”, you could use something like AWS API Gateway and Lambda and do the filtering and storing of the API key there.

AWS API Gateway / Lambda setup

How to setup AWS API Gateway with a Lambda function is out of the scope of this blogpost, but there's good documentation about the basics.

The actual code for the lambda function, with querying rokka and filtering out some data out of the response, looks like this

const org = 'yourorg'
const key = 'yourkey'
const https = require('https') 
// only take 100 images, and only those with an unsplash_artist_id user metadata field
const url = 'https://api.rokka.io/sourceimages/' + org + '?limit=100&user:str:unsplash_artist_id=*'
exports.handler = async(event) => {
    const options = { headers: { 'Api-Key': key } }
    const promise = new Promise(function(resolve, reject) {
        https.get(url, options, (res) => {
            res.setEncoding("utf8");
            let body = ''
            res.on('data', (d) => {
                body += d
            })
            res.on('end', () => {
                body = JSON.parse(body);
                body.items = body.items.map(image => {
                    // filter out data you don't want to have published
                    if (image.static_metadata && image.static_metadata.location) {
                        delete image.static_metadata.location
                    }
                    return image
                })
                let response = {
                    statusCode: 200,
                    headers: {
                        "Access-Control-Allow-Origin": "*"
                    },
                    body: JSON.stringify(body)
                };
                resolve(response)
            });
        }).on('error', (e) => {
            reject(Error(e))
        })
    })
    return promise
};

It would be better to use rokka.js instead of the https module (retry functionality, less code and such), but to keep the initial setup easier, we decided not to do that here. Including npm packages in Lambda needs a different approach than just copy/pasting some code in the web interface of Lambda.

After you made sure your API Gateway/Lambda setup works, you can add the following snippet to script and use that instead of the rokkaClient.sourceimages.list call (and even remove the <script> call to include the rokka library)

fetch('https://9iv7ht2xcd.execute-api.eu-central-1.amazonaws.com/test/gallerydemo').then(resp => {
  resp.json().then(body => {
    this.images = body.items
  })
})

Disclaimer

As you can see from the version numbers of our vue packages, they're all in more or less alpha/beta state. They may not be perfect yet, but we're using them already in some places, so they're mostly production ready and we will try to avoid adding breaking changes and soon release more stable version numbers.

Header photo by Raychan on Unsplash


Tell us what you think