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.