chore: replace hand-rolled layout() with @fastify/view + EJS #19

Closed
opened 2026-05-14 23:59:56 +00:00 by brendan · 1 comment
Owner

Problem

HTML is generated by TypeScript template-literal functions in src/views/. A layout() function in src/views/layout.ts builds the full <!DOCTYPE html> shell as a string, and each page module (e.g. upload.ts, file-list.ts) builds its body as a string and passes it to layout(). Routes call reply.send(layout('Title', body, opts)).

This works but is non-standard — no syntax highlighting in templates, no partial reuse, manual escHtml() calls required everywhere, and the pattern differs from every other project in the fleet which is moving to @fastify/view.

Proposed fix

Introduce @fastify/view + EJS and convert the view modules to .ejs template files.

1. Install

npm i @fastify/view ejs

Register in the server with root: 'views/'.

2. Layout template

Create views/_layout.ejs from the current layout() function. Template variables mirror the current LayoutOptions:

<!-- views/_layout.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="color-scheme" content="light dark">
  <title><%= title %> — Nanodrop</title>
  <link rel="stylesheet" href="/public/style.css">
</head>
<body>
<% if (!hideHeader) { %>
  <header>
    <a href="/" class="logo">Nanodrop</a>
    <% if (authed) { %>
    <nav>
      <a href="/upload">Upload</a>
      <a href="/files">My Files</a>
      <form method="POST" action="/logout" style="display:inline">
        <button type="submit">Logout</button>
      </form>
    </nav>
    <% } %>
  </header>
<% } %>
  <main>
    <%- body %>
  </main>
</body>
</html>

3. Convert page modules to templates

Each src/views/foo.ts that returns an HTML string becomes views/foo.ejs. EJS auto-escapes <%= %> output, so explicit escHtml() calls on user data are no longer needed.

4. Update routes

Replace reply.send(viewFn(data)) with reply.view('foo.ejs', { title, authed, ...data }). The src/views/*.ts modules and layout.ts can be deleted.

View inventory

Current module New template Key vars
layout.ts views/_layout.ejs title, authed, hideHeader
login.ts views/login.ejs
upload.ts views/upload.ejs
file-list.ts views/file-list.ejs files
file-view.ts views/file-view.ejs file
not-found.ts views/not-found.ejs

Files to touch

  • package.json — add @fastify/view, ejs
  • src/server.ts (or wherever Fastify is configured) — register plugin
  • Route handlers — swap reply.send(view(...))reply.view('template.ejs', vars)
  • src/views/ — delete all .ts modules once templates are in place
  • New views/ directory with .ejs templates
## Problem HTML is generated by TypeScript template-literal functions in `src/views/`. A `layout()` function in `src/views/layout.ts` builds the full `<!DOCTYPE html>` shell as a string, and each page module (e.g. `upload.ts`, `file-list.ts`) builds its body as a string and passes it to `layout()`. Routes call `reply.send(layout('Title', body, opts))`. This works but is non-standard — no syntax highlighting in templates, no partial reuse, manual `escHtml()` calls required everywhere, and the pattern differs from every other project in the fleet which is moving to `@fastify/view`. ## Proposed fix Introduce `@fastify/view` + EJS and convert the view modules to `.ejs` template files. ### 1. Install ``` npm i @fastify/view ejs ``` Register in the server with `root: 'views/'`. ### 2. Layout template Create `views/_layout.ejs` from the current `layout()` function. Template variables mirror the current `LayoutOptions`: ```ejs <!-- views/_layout.ejs --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="color-scheme" content="light dark"> <title><%= title %> — Nanodrop</title> <link rel="stylesheet" href="/public/style.css"> </head> <body> <% if (!hideHeader) { %> <header> <a href="/" class="logo">Nanodrop</a> <% if (authed) { %> <nav> <a href="/upload">Upload</a> <a href="/files">My Files</a> <form method="POST" action="/logout" style="display:inline"> <button type="submit">Logout</button> </form> </nav> <% } %> </header> <% } %> <main> <%- body %> </main> </body> </html> ``` ### 3. Convert page modules to templates Each `src/views/foo.ts` that returns an HTML string becomes `views/foo.ejs`. EJS auto-escapes `<%= %>` output, so explicit `escHtml()` calls on user data are no longer needed. ### 4. Update routes Replace `reply.send(viewFn(data))` with `reply.view('foo.ejs', { title, authed, ...data })`. The `src/views/*.ts` modules and `layout.ts` can be deleted. ## View inventory | Current module | New template | Key vars | |---|---|---| | `layout.ts` | `views/_layout.ejs` | `title`, `authed`, `hideHeader` | | `login.ts` | `views/login.ejs` | — | | `upload.ts` | `views/upload.ejs` | — | | `file-list.ts` | `views/file-list.ejs` | `files` | | `file-view.ts` | `views/file-view.ejs` | `file` | | `not-found.ts` | `views/not-found.ejs` | — | ## Files to touch - `package.json` — add `@fastify/view`, `ejs` - `src/server.ts` (or wherever Fastify is configured) — register plugin - Route handlers — swap `reply.send(view(...))` → `reply.view('template.ejs', vars)` - `src/views/` — delete all `.ts` modules once templates are in place - New `views/` directory with `.ejs` templates
brendan added the feature label 2026-05-14 23:59:56 +00:00
Author
Owner

Resolved: PR #20 (merge_commit 829037f89c)

Resolved: PR https://gitea.bchen.dev/brendan/nanodrop/pulls/20 (merge_commit 829037f89cb9b15ae6ac3ed2422e537b089472fc)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: brendan/nanodrop#19