Azure Functions and Blob Storage

The stack this site runs on: Azure Functions for the editor and Blob Storage for files, with the built site served straight from a $web container. Ready-made adapters mean almost no code of your own.

Azure has ready-made adapters, so you implement no storage yourself.

1. Install

npm install @xtrable-ltd/nanoesis @azure/storage-blob

2. Create two containers

In your storage account, add a private working container for editable files, and turn on Static website (which creates the $web container) for the published site.

3. Wire the editor

// editor.js
import { createEditor, devNoAuth } from '@xtrable-ltd/nanoesis/editor-api';
import { AzureBlobContainer, azureStorage } from '@xtrable-ltd/nanoesis/adapter-azure-blob';
import { createSharpEncoder } from '@xtrable-ltd/nanoesis/adapter-sharp';

const working = AzureBlobContainer.fromConnectionString(process.env.DATA_CONN, 'working');
const web = AzureBlobContainer.fromConnectionString(process.env.SITE_CONN, '$web');

export const editor = createEditor({
  editorFiles: azureStorage(working),
  website: azureStorage(web),
  login: devNoAuth(),                 // swap for real login, see step 6
  images: createSharpEncoder(),       // responsive images
  enumerate: () => working.list(''),  // lets the editor self-heal its index
});

4. Serve it from a Function

One function for the API, one for the editor UI.

// functions.js
import { app } from '@azure/functions';
import { serveEditorAsset } from '@xtrable-ltd/nanoesis/editor-api';
import { editorDist } from '@xtrable-ltd/nanoesis/editor';
import { editor } from './editor.js';

app.http('api', {
  route: 'api/{*rest}',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  authLevel: 'anonymous',
  handler: async (req) => {
    const url = new url(req.url);
    const r = await editor.handleApi({
      method: req.method,
      path: url.pathname,
      query: url.searchParams,
      getHeader: (n) => req.headers.get(n) ?? undefined,
      body: async () => new Uint8Array(await req.arrayBuffer()),
    });
    return { status: r.status, headers: r.headers, body: r.body };
  },
});

app.http('spa', {
  route: '{*path}',
  methods: ['GET'],
  authLevel: 'anonymous',
  handler: async (req) => {
    const a = await serveEditorAsset(editorDist, new url(req.url).pathname);
    return { status: a.status, headers: a.headers, body: a.body };
  },
});

5. Publish

Deploy the Function, open it, and press Publish. The built site is written to $web and served at your account's static-website endpoint. Put a CDN in front for a custom domain.

6. Add real login

Replace devNoAuth() with the local-jwt provider, passing your user store and a secret:

import { createLocalJwtAuth } from '@xtrable-ltd/nanoesis/adapter-local-jwt';

const { identity, endpoints, userAdmin } = createLocalJwtAuth({
  users,                       // your user store
  signingSecret: process.env.JWT_SECRET,
});
// in createEditor: login: identity, authEndpoints: endpoints, userAdmin

That is the exact setup this site runs on.