Codementor Events

Using EJS with Vite

Published Jun 15, 2023

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}`;
        }
      },
    },
  ],
});

How to make .ejs imports work with TypeScript?

  1. Add **/*.ejs to the list of included files in your tsconfig.json file.
  2. 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!

References

Discover and read more posts from Konstantin Tarkus
get started
post commentsBe the first to share your opinion
Show more replies