A café site — chrome, three bands, one template — built from the theme's vocabulary in fifty-odd lines of EJS. If you just want to paste and go, the complete file is at the bottom.
Juniper Coffee: a navbar with two anchor links, a hero, a features band for the menu, a call-to-action band for visiting, and a footer. One file, one route.
Every piece is a partial with declared fields, and the props you pass by hand are the same fields a CMS editor would fill in. A page written this way is already CMS-shaped — that is the point of the whole system, and this tutorial stays on that path.
From the theme root:
npm install
npm run dev # → http://localhost:4180
Files under pages/ are the routes: pages/about.ejs
serves /about, and pages/my-site/index.ejs serves
/my-site. Save any file and the browser reloads itself.
Create pages/my-site/index.ejs:
<%
const hero = require('/site/partials/wraps/container.ejs')({
title: 'Coffee, roasted on Fig Street.',
lead: 'Small-batch beans, an eight-seat espresso bar, and pastry that sells out by ten.',
})
%>
<%- require('/templates/default.ejs')({
meta_title: 'Juniper Coffee',
content: hero,
}) %>
A page is two moves. The <% %> prologue is plain
JavaScript that composes HTML strings — requiring an .ejs
partial gives you a function from props to markup. The single
<%- %> line writes one string out: the page handed to
a template. Paths starting with /site/partials/ or
/templates/ resolve inside the theme package.
Open localhost:4180/my-site — an accent-colored hero band with your headline. The banner surface is the hero's default; you stated only what differs from the defaults, which is how every band in this tutorial will work.
Chrome is the furniture that frames a site — here, the navbar and footer. Add these to the prologue:
const navbar = require('/site/partials/chrome/navbar.ejs')
const themeToggle = require('/site/partials/components/theme-toggle.ejs')
const footer = require('/site/partials/chrome/footer.ejs')
const nav = navbar({
brand: 'Juniper Coffee',
brand_href: '#',
actions: themeToggle(),
links: [
{ label: 'Menu', href: '#menu' },
{ label: 'Visit', href: '#visit' },
],
cta_label: 'Order beans',
cta_href: '#visit',
})
const foot = footer({
brand: 'Juniper Coffee',
tagline: '412 Fig Street · Open daily 7–3',
copyright: '© 2026 Juniper Coffee.',
})
The navbar's links point at #menu and
#visit — anchors the bands will declare in the next step.
actions is a slot rather than a field: it takes rendered
HTML, and here it gets the theme toggle so your site picks up light and
dark mode for free.
Patterns are full-width bands, and a page is a stack of them. Replace the hero from step one with the fuller version, then add the menu and the closing call to action:
const hero = require('/site/partials/wraps/container.ejs')({
eyebrow: 'Fig Street · Est. 2019',
title: 'Coffee, roasted here.',
lead: 'Small-batch beans, an eight-seat espresso bar, and pastry that sells out by ten.',
cta_label: 'See the menu',
cta_href: '#menu',
})
const menu = require('/site/partials/patterns/features.ejs')({
id: 'menu',
title: 'The short menu',
intro: 'Three things, done properly.',
items: [
{ icon: 'bi-cup-hot', title: 'Espresso', body: 'Two roasts on the grinders — one comfort, one experiment.' },
{ icon: 'bi-cookie', title: 'Pastry', body: 'From the oven at seven. When the case is empty, that is that.' },
{ icon: 'bi-box-seam', title: 'Beans to go', body: 'Whole-bean bags, roasted every Tuesday.' },
],
})
const visit = require('/site/partials/patterns/cta-band.ejs')({
fill: 'primary',
id: 'visit',
title: 'Come by the shop.',
body: '412 Fig Street, open daily 7 to 3. Beans ship anywhere.',
cta_label: 'Order beans',
cta_href: 'mailto:hello@juniper.example',
})
Icons are Bootstrap Icons
names, and an id on a band is the anchor the navbar links
to. Notice how much is defaulted: the hero picks the banner surface, the
features band lays out three columns, the CTA band centers itself. Fields
you do not set fall back to what the partial declares.
The template owns the document — doctype, head, stylesheets, scripts. The page owns the order of the bands. Concatenate and hand it over:
<%- require('/templates/default.ejs')({
meta_title: 'Juniper Coffee — Fig Street roastery',
meta_description: 'Small-batch coffee, an eight-seat espresso bar, and beans that ship anywhere.',
content: nav + '<main>' + hero + menu + visit + '</main>' + foot,
}) %>
Everything above in one place — pages/my-site/index.ejs,
complete:
<%
// Juniper Coffee — a one-page site.
const navbar = require('/site/partials/chrome/navbar.ejs')
const themeToggle = require('/site/partials/components/theme-toggle.ejs')
const footer = require('/site/partials/chrome/footer.ejs')
const nav = navbar({
brand: 'Juniper Coffee',
brand_href: '#',
actions: themeToggle(),
links: [
{ label: 'Menu', href: '#menu' },
{ label: 'Visit', href: '#visit' },
],
cta_label: 'Order beans',
cta_href: '#visit',
})
const hero = require('/site/partials/wraps/container.ejs')({
eyebrow: 'Fig Street · Est. 2019',
title: 'Coffee, roasted here.',
lead: 'Small-batch beans, an eight-seat espresso bar, and pastry that sells out by ten.',
cta_label: 'See the menu',
cta_href: '#menu',
})
const menu = require('/site/partials/patterns/features.ejs')({
id: 'menu',
title: 'The short menu',
intro: 'Three things, done properly.',
items: [
{ icon: 'bi-cup-hot', title: 'Espresso', body: 'Two roasts on the grinders — one comfort, one experiment.' },
{ icon: 'bi-cookie', title: 'Pastry', body: 'From the oven at seven. When the case is empty, that is that.' },
{ icon: 'bi-box-seam', title: 'Beans to go', body: 'Whole-bean bags, roasted every Tuesday.' },
],
})
const visit = require('/site/partials/patterns/cta-band.ejs')({
fill: 'primary',
id: 'visit',
title: 'Come by the shop.',
body: '412 Fig Street, open daily 7 to 3. Beans ship anywhere.',
cta_label: 'Order beans',
cta_href: 'mailto:hello@juniper.example',
})
const foot = footer({
brand: 'Juniper Coffee',
tagline: '412 Fig Street · Open daily 7–3',
copyright: '© 2026 Juniper Coffee.',
})
%>
<%- require('/templates/default.ejs')({
meta_title: 'Juniper Coffee — Fig Street roastery',
meta_description: 'Small-batch coffee, an eight-seat espresso bar, and beans that ship anywhere.',
content: nav + '<main>' + hero + menu + visit + '</main>' + foot,
}) %>
Reload /my-site: a finished single-page site, light and dark,
responsive, with working anchor navigation.
pages/pages/example-sites/small-business.ejs) is this
page with more bands and hand-composed content; the
portfolio example
shows the multi-page pattern — shared nav and footer props in a
site.js.
content slot — bring your own headings,
text, and arrangement wraps. The example sites are written that way.
scss/_variables.scss and run
npm run build:css.