ejs-fw
v0.2.0
Published
EJS live framework — SSR first paint, WebSocket soft-nav, automatic form POST, room HTML broadcast
Maintainers
Readme
ejs-fw
EJS live module — server-rendered first paint, then WebSocket-only navigation and form POSTs. No separate JSON API. Rooms broadcast HTML patches to every connected peer (Discord-style chat).
HTTP is used only for the initial full-page SSR. After that, the client never reloads the document unless the user refreshes manually.
Install
npm install ejs-fw
# local
npm install file:./ejs-fw --install-linksPeer: express >= 4. Dependency: ws.
Host app (module consumer)
You build the app; ejs-fw supplies runtime + WS hub. Register pages/posts — no per-page client WebSocket code.
const http = require('http');
const express = require('express');
const ejsfw = require('ejs-fw');
const app = express();
app.set('view engine', 'ejs');
const server = http.createServer(app);
const fw = ejsfw.install(app, {
server,
// optional: authenticate: async (req) => ({ id: req.session?.userId }),
});
fw.page('/chat/:room', async (ctx) => ({
view: 'chat',
locals: {
title: 'Chat',
messages: await loadMessages(ctx.params.room),
},
room: `chat:${ctx.params.room}`,
}));
// <form method="post" action="/chat/general/messages"> → here over WebSocket
fw.post('/chat/:room/messages', async (ctx) => {
const msg = await saveMessage(ctx.params.room, ctx.data.text, ctx.user);
const html = await ctx.render('partials/message', { msg });
return {
broadcast: {
target: '[data-seyfo-stream="messages"]',
html,
mode: 'append',
},
ack: { clearForm: true },
};
});
app.get('/chat/:room', (req, res) => fw.renderDefinedPage(req, res));
server.listen(3000);fw.post is not app.post — it only runs over the WebSocket. The browser never issues an HTTP POST for intercepted forms.
Layout contract
<root id="seyfo">
<%- body %>
</root>
<script type="application/json" id="seyfo-data"><%- JSON.stringify({
title: title,
path: currentPath,
room: typeof room !== 'undefined' ? room : currentPath,
wsPath: '/ejs-fw/ws'
}).replace(/</g, '\\u003c') %></script>
<script type="module" src="/ejs-fw/runtime.js"></script><a href="/chat/general" data-seyfo-link>General</a>Automatic form POST
Any form with method="post" (or data-seyfo-post / data-seyfo-action) is intercepted by the runtime:
<ul data-seyfo-stream="messages">
<% messages.forEach(function (msg) { %>
<%- include('partials/message', { msg: msg }) %>
<% }) %>
</ul>
<form method="post" action="/chat/general/messages">
<input name="text" autocomplete="off" />
<button type="submit">Send</button>
</form>preventDefault— no full-page submit- Form fields → WebSocket
{ type: 'post', path, data } - Opt out:
data-seyfo-nativeordata-seyfo-ignore - Named action override:
data-seyfo-action="chat.send"→fw.action('chat.send', …)
API
| Export / return | Role |
|-----------------|------|
| install(app, { server, wsPath?, authenticate? }) | Middleware, static client, WS hub |
| fw.page(pattern, handler) | SSR + WS navigation resolver |
| fw.post(pattern, handler) | WS form POST handler |
| fw.action(name, handler) | Optional named WS action |
| fw.renderPage(req, res, view, locals) | Full SSR (or legacy HTTP partial) |
| fw.renderDefinedPage(req, res) | Run matched fw.page for HTTP GET |
| fw.renderToHtml(view, locals) | EJS → HTML string |
| fw.broadcast(room, message) | Raw room broadcast |
| fw.broadcastPatch(room, patch) | { target, html, mode } to room |
| scriptTag() | <script type="module" src="/ejs-fw/runtime.js"> |
Handler results
return {
broadcast: { target: '#list', html, mode: 'append' | 'prepend' | 'replace' },
ack: { clearForm: true },
patch: { target, html, mode }, // sender only
page: { view, locals, path, room }, // full #seyfo swap
redirect: '/other',
};ctx.render(view, locals), ctx.user, ctx.data, ctx.params, ctx.room, ctx.broadcast(patch) are available inside handlers.
Protocol (WebSocket JSON)
Client → server: join | nav | prefetch | post | action
Server → client: page | prefetch | patch | ack | error | redirect
Default WS path: /ejs-fw/ws.
License
MIT
