Layouts and pages
Templates are written in EdgeJS and live in src/views/. Pages go in pages/, and the pieces they share go in layouts/ and components/. Template names are relative to src/views, so layouts/main is src/views/layouts/main.edge.
Layouts
A layout is the HTML shell around a page: the <head>, the header and footer. It is an ordinary component that renders its slots:
{{-- src/views/layouts/main.edge --}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{{ await $slots.title?.() ?? 'My site' }}}</title>
{{{ await vite.asset('src/client/main.ts') }}}
</head>
<body>
{{{ await $slots.main() }}}
</body>
</html>$slots.title?.()renders thetitleslot if the page provides one. The?.and the??fallback make it optional.$slots.main()renders themainslot.- Slot output is already HTML, so print it with three braces.
vite.asset()adds your CSS and JavaScript. See Tailwind and Alpine.js.
Pages
A page uses a layout with @component and fills its slots with @slot:
{{-- src/views/pages/about.edge --}}
@component('layouts/main')
@slot('title')
About
@end
@slot('main')
<h1>About us</h1>
@end
@endVariables you define with @let before the @component are available inside every slot, which is handy for loading a record once and using it in both the title and the body:
@let(result = await post.read({ filters: { slug: params.slug }, limit: 1 }))
@let(item = result.data[0])
@if(!item)
{{ abort(404) }}
@end
@component('layouts/main')
@slot('title')
{{ item.title }}
@end
@slot('main')
<h1>{{ item.title }}</h1>
@end
@endComponents and slots
A component is a template that you call with props and slots. Props arrive as variables:
{{-- src/views/components/card.edge --}}
<div class="card">
<h3>{{ title }}</h3>
{{{ await $slots.main() }}}
</div>Call it with @component when it has slot content, or with @!component when it does not:
@component('components/card', { title: 'Hello' })
<p>Body of the card</p>
@end
@!component('components/card', { title: 'Empty card' })Named slots work the same way as in layouts. Check for an optional one before calling it:
{{{ $slots.footer ? await $slots.footer() : '' }}}Including a partial
@include pastes another template in place. It takes only the template name, with no props:
@include('partials/analytics')Use a component when you need to pass data.
Comments
{{-- This is not sent to the browser --}}Custom error pages
To style an error page, add src/views/errors/<status>.edge, for example 404.edge or 500.edge. The template receives statusCode and message:
{{-- src/views/errors/404.edge --}}
<h1>{{ statusCode }}</h1>
<p>{{ message }}</p>This template is rendered on its own, so use @component inside it if you want your layout around it.
Errors reach this template in two ways: a page calls abort(status, message?), or no page matches the URL. When there is no template for a status, njin shows a plain built-in page.
If a page throws an unexpected error, njin dev shows a debug page with the stack trace. In production the visitor gets errors/500.edge, or the built-in page, and the error is logged.
Default response headers
Every page is sent with these headers: X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, X-Frame-Options: SAMEORIGIN, and a Content-Security-Policy limited to frame-ancestors 'self'; base-uri 'self'; object-src 'none'; form-action 'self'. In production over HTTPS, Strict-Transport-Security is added too.