On this page
· 2 min read
Layouts & Rendering
EJS runs only inside src/entry-server.tsx at SSG (and during dev SSR). The browser receives plain HTML plus optional client JS.
The SSR entry contract
The SSG plugin loads the entry module and calls:
export async function getRoutes(config: SsgRenderConfig): Promise<string[]>;export async function render(url: string, config: SsgRenderConfig): Promise<string>;SsgRenderConfig includes base, root, cssPath, jsPath, searchEnabled, and isDev. This example forwards searchEnabled (and isDev) into layout.ejs so Pagefind assets match the environment.
getRoutes() returns pathnames (with leading /, no index.html). This example builds the list from loaded collections plus / and /about when that page exists:
export async function getRoutes(config: SsgRenderConfig): Promise<string[]> { const { sortedGuide, renderedPages } = await loadContent(config.root); const routes = ["/"]; for (const item of sortedGuide) routes.push(`/guide/${item.entry.slug}`); if (renderedPages.find((p) => p.entry.slug === "about")) routes.push("/about"); return routes;}render(url, config) strips config.base from the request URL, loads markdown through createContentLayer, renders a fragment template, then wraps it with layout.ejs.
Template loading
function loadTemplate(root: string, name: string) { return readFileSync(join(root, "templates", `${name}.ejs`), "utf-8");}function renderWithLayout(root: string, body: string, vars: Record<string, any>) { const layout = loadTemplate(root, "layout"); return ejs.render(layout, { ...vars, body });}- Render
article.ejs/index.ejs/about.ejsto an HTML string (body). - Call
renderWithLayout()solayout.ejsreceives that string asbody(escaped vs unescaped is handled in the templates).
Layout responsibilities
layout.ejs is the document shell: <head> links (cssPath for the Vite client bundle, basePath for public assets), header, sidebar (via inline renderSidebarContent() used for desktop and the mobile dialog), optional TOC aside, footer, and gated Pagefind markup.
Sidebar navigation is driven by data prepared in entry-server.tsx (sidebar.guideGroups, first guide slug), not by a separate meta.json5 file.
EJS tag cheat sheet
<%= expr %>— escaped text.<%- expr %>— raw HTML (body, markdowncontent).<% code %>— logic only.
Output paths
For route /guide/installation, the plugin writes guide/installation/index.html under the configured build.outDir (this repo: gh-pages/examples/vanilla-ejs/).