AWS Lambda and S3

There is no ready-made AWS adapter, but a Storage is only three methods. Implement it over an S3 bucket, then host the editor on Lambda, Fargate, or any Node server.

You write a small Storage over S3, then wire it like any other.

1. Install

npm install @xtrable-ltd/nanoesis @aws-sdk/client-s3 express

2. Two buckets

A private bucket for editable files, and a second bucket for the published site (turn on S3 static website hosting, or front it with CloudFront).

3. A Storage over S3

The three methods, plus list and wipe so the editor can reconcile and clear old pages.

// s3-storage.js
import {
  S3Client, GetObjectCommand, PutObjectCommand,
  DeleteObjectCommand, ListObjectsV2Command,
} from '@aws-sdk/client-s3';

const s3 = new S3Client({});

async function listKeys(bucket) {
  const keys = []; let token;
  do {
    const r = await s3.send(new ListObjectsV2Command(
      { Bucket: bucket, ContinuationToken: token }));
    for (const o of r.Contents ?? []) keys.push(o.Key);
    token = r.NextContinuationToken;
  } while (token);
  return keys;
}

export function s3Storage(bucket) {
  return {
    async get(key) {
      try {
        const r = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
        return new Uint8Array(await r.Body.transformToByteArray());
      } catch (e) {
        if (e.name === 'NoSuchKey') return undefined;
        throw e;
      }
    },
    async put(key, bytes) {
      await s3.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: bytes }));
    },
    async delete(key) {
      await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
    },
    async wipe() {
      for (const k of await listKeys(bucket)) await this.delete(k);
    },
    list: () => listKeys(bucket),
  };
}

4. Wire the editor

// editor.js
import { createEditor, devNoAuth } from '@xtrable-ltd/nanoesis/editor-api';
import { s3Storage } from './s3-storage.js';

const files = s3Storage(process.env.WORKING_BUCKET);

export const editor = createEditor({
  editorFiles: files,
  website: s3Storage(process.env.SITE_BUCKET),
  login: devNoAuth(),            // swap for real login before going live
  enumerate: () => files.list(),
});

5. Serve it

A small Express app, the same two routes. It runs on Fargate or EC2 as-is, or on Lambda wrapped with a serverless adapter.

// server.js
import express from 'express';
import { serveEditorAsset } from '@xtrable-ltd/nanoesis/editor-api';
import { editorDist } from '@xtrable-ltd/nanoesis/editor';
import { editor } from './editor.js';

const toBody = (b) => (b == null ? '' : typeof b === 'string' ? b : Buffer.from(b));
const app = express();
app.use(express.raw({ type: '*/*', limit: '25mb' }));

app.all('/api/*', async (req, res) => {
  const r = await editor.handleApi({
    method: req.method,
    path: req.path,
    query: new URL(req.originalUrl, 'http://localhost').searchParams,
    getHeader: (n) => req.get(n),
    body: async () => new Uint8Array(req.body),
  });
  res.status(r.status).set(r.headers).send(toBody(r.body));
});

app.get('*', async (req, res) => {
  const a = await serveEditorAsset(editorDist, req.path);
  res.status(a.status).set(a.headers).send(toBody(a.body));
});

app.listen(8080);

6. Publish

Open the editor, create a page, and press Publish. The built site is written to your site bucket; serve it from the S3 website endpoint or CloudFront.