Using EJS with Vite
Why Use EJS with Vite?
Let's consider an example scenario: you are building a web app that will run at CDN edge locations using Cloudflare Workers. In this scenario, you may have the following requirements:
- You need to configure the reverse proxy for certain third-party websites, such as Framer, Intercom Helpdesk, etc.
- You should be able to inject custom HTML/JS snippets into the pages of these websites.
- The code snippets should function correctly in different environments, such as production and test/QA.
- To optimize the application bundle, it is necessary to pre-compile these templates instead of including a template library.
In such cases, using EJS in combination with Vite can be a beneficial choice.
How does it look like?
The HTML snippet is conveniently placed into a separate file with HTML/JS syntax highlightning and code completion (views/analytics.ejs
):
<script async src="https://www.googletagmanager.com/gtag/js?id=<%- env.GA_MEASUREMENT_ID %>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "<%- env.GA_MEASUREMENT_ID %>");
</script>
While the Cloudflare Worker script injects it into an (HTML) landing page loaded from Framer:
import { Hono } from "hono";
import analytics from "../views/analytics.ejs";
export const app = new Hono<Env>();
// Serve landing pages, inject Google Analytics
app.use("*", async ({ req, env }, next) => {
const url = new URL(req.url);
// Skip non-landing pages
if (!["/", "/about", "/home"].includes(url.pathname)) {
return next();
}
const res = await fetch("https://example.framer.app/", req.raw);
return new HTMLRewriter()
.on("body", {
element(el) {
el.onEndTag((tag) => {
try {
tag.before(analytics(env), { html: true });
} catch (err) {
console.error(err);
}
});
},
})
.transform(res.clone());
});
How to pre-compile EJS templates with Vite?
Install ejs
and @types/ejs
NPM modules as development dependencies (yarn add ejs @types/ejs -D
).
Add the following plugin to your vite.config.ts
file:
import { compile } from "ejs";
import { readFile } from "node:fs/promises";
import { relative, resolve } from "node:path";
import { defineConfig } from "vite";
export default defineProject({
...
plugins: [
{
name: "ejs",
async transform(_, id) {
if (id.endsWith(".ejs")) {
const src = await readFile(id, "utf-8");
const code = compile(src, {
client: true,
strict: true,
localsName: "env",
views: [resolve(__dirname, "views")],
filename: relative(__dirname, id),
}).toString();
return `export default ${code}`;
}
},
},
],
});
.ejs
imports work with TypeScript?
How to make - Add
**/*.ejs
to the list of included files in yourtsconfig.json
file. - Add the following type declaration to you
global.d.ts
file:
declare module "*.ejs" {
/**
* Generates HTML markup from an EJS template.
*
* @param locals an object of data to be passed into the template.
* @param escape callback used to escape variables
* @param include callback used to include files at runtime with `include()`
* @param rethrow callback used to handle and rethrow errors
*
* @return Return type depends on `Options.async`.
*/
const fn: (
locals?: Data,
escape?: EscapeCallback,
include?: IncludeCallback,
rethrow?: RethrowCallback,
) => string;
export default fn;
}
The kriasoft/relay-starter-kit
is a comprehensive full-stack web application project template that comes pre-configured with all the mentioned features (located in the /edge
folder).
If you require any assistance with web infrastructure and DevOps, feel free to reach out to me on Codementor or Discord. I'm here to help! Happy coding!