Edge Templates for AdonisJS
This skill covers the Edge.js server-rendered view layer in AdonisJS Hypermedia apps.
Use adonisjs alongside it for controllers, routes, middleware, auth, validation, services, and framework architecture.
When to Use
Use this skill for:
- Rendering
.edge templates from controllers or route handlers
- Working in
resources/views/**
- Applying Edge syntax rules for curly braces,
@ tags, comments, and newline swallowing
- Writing interpolation with
{{ }}, {{{ }}}, html.safe(), or escaped @{{ }}
- Writing conditional blocks with
@if, @elseif, @else, @unless, or short ternaries
- Looping over arrays and objects with
@each, indexes, keys, and empty-state fallbacks
- Building layouts, components, slots, partials, and reusable template fragments
- Building components with props,
$props, slots, layout components, and provide/inject
- Including partials and pushing template-specific content into stacks
- Passing template state to
view.render()
- Using AdonisJS-provided Edge helpers and tags from official packages
- Using Hypermedia starter kit form and UI components
- Debugging template variables, helpers, and rendering problems
Do not use this skill for Inertia page components. Use inertia-vue or inertia-react for those frontend layers.
References
Load the specific reference that matches the template concern:
| Reference |
Use it for |
references/template-state.md |
Data available to templates: view.render() state, framework-provided values, @let, @assign, globals, and avoiding Edge standalone renderer APIs in AdonisJS |
references/syntax-specification.md |
Low-level Edge syntax: curly braces, @ tags, auto-closing tags, comments, and newline swallowing |
references/interpolation.md |
{{ }}, {{{ }}}, html.safe(), multiline expressions, stringified output, escaped frontend braces with @{{ }} |
references/conditionals.md |
Conditional rendering with @if, @elseif, @else, @unless, and short ternaries |
references/loops.md |
@each over arrays/objects, indexes, object key/value loops, and @each ... @else empty states |
references/partials-and-stacks.md |
@include, @includeIf, @stack, @pushTo, push ordering, and push-once behavior |
references/components.md |
Custom Edge components: tag names, props, $props, slots, layout components, @inject, and $context |
references/hypermedia-components.md |
Hypermedia starter kit components: layout, form, fields, input/select/textarea, checkbox/radio, alert, button, link, avatar |
references/adonisjs-helpers-and-tags.md |
Helpers/tags contributed by AdonisJS packages: request, route, signedRoute, session, flashMessages, old, auth, asset, @vite, @can, @flashMessage, @error, @inputError |
Rendering Templates
Render templates from controllers with view.render(template, state).
The template path is relative to resources/views and omits the .edge extension.
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async index({ view }: HttpContext) {
return view.render('pages/posts/index', {
posts: await Post.all(),
})
}
}
For static pages, routes can render a template directly.
import router from '@adonisjs/core/services/router'
router.on('/').render('pages/home')
Template State
Read references/template-state.md when working with data passed to templates, framework-provided values, @let, @assign, globals, or Edge standalone state APIs.
Every property passed as the second argument to view.render() becomes a template variable.
Prefer explicit state names over reaching into broad service objects from the template.
@layout()
@each(post in posts)
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.summary }}</p>
</article>
@end
@end
AdonisJS also shares request/auth context and Edge helpers with templates. When a value is unclear, inspect the state during development with @dump(state) or a specific variable with @dump(posts).
Syntax Defaults
Read references/interpolation.md when working with interpolation, multiline JavaScript expressions, stringified output, HTML escaping, or @{{ }} escaping for frontend frameworks.
Read references/syntax-specification.md when working with low-level Edge syntax rules, tag placement, auto-closing tags, comments, or ~ newline swallowing.
Read references/conditionals.md when working with @if, @elseif, @else, @unless, or ternary expressions in templates.
Read references/loops.md when working with @each, indexes, object key/value loops, or @each ... @else fallback content.
Read references/components.md when working with component files, component tag names, props, $props, slots, layout components, @inject, or $context.
Read references/partials-and-stacks.md when working with @include, @includeIf, @stack, @pushTo, @pushToTop, or push-once stack behavior.
Read references/adonisjs-helpers-and-tags.md when working with AdonisJS-provided Edge helpers/tags such as request, route, signedRoute, session, flashMessages, old, auth, asset, @vite, @can, @flashMessage, @error, or @inputError.
Use escaped output by default.
{{ post.title }}
Use unescaped output only for trusted HTML that has already been sanitized or generated by trusted code.
{{{ post.html }}}
Common control flow:
@if(auth.user)
<p>Welcome back, {{ auth.user.fullName }}</p>
@else
<p>Please log in</p>
@end
@each(post in posts)
<h2>{{ post.title }}</h2>
@end
Layouts and Components
The Hypermedia starter kit provides a @layout() component at resources/views/components/layout.edge.
It owns the HTML document shell and renders page content through the default slot.
Read references/components.md when creating or changing custom components.
@layout()
<main>
Page content
</main>
@end
Create reusable components under resources/views/components.
The file name becomes the component tag name.
{{-- resources/views/components/card.edge --}}
<div class="card">
{{{ await $slots.main() }}}
</div>
@card()
<h2>Card title</h2>
@end
Use the self-closing @!component() form only for components that do not need slots.
Hypermedia Starter Kit Components
Read references/hypermedia-components.md when working with the starter kit's pre-built components.
Important defaults:
- Each component renders at most one HTML element.
- Unknown props are passed through as HTML attributes.
@form() injects CSRF protection and supports method spoofing for PUT, PATCH, and DELETE.
- Field controls are composed from
@field.root(), @field.label(), control components, and @field.error().
@link() can generate URLs from named routes with route, routeParams, and routeOptions.
Form Pattern
Prefer route names over hard-coded URLs when the route is named.
@form({ route: 'posts.store', method: 'POST' })
@field.root({ name: 'title' })
@!field.label({ text: 'Title' })
@!input.control({ type: 'text', maxlength: '120' })
@!field.error()
@end
@!button({ text: 'Create post', type: 'submit' })
@end
For update/delete forms, use method spoofing through the component.
@form({ route: 'posts.update', method: 'PUT', routeParams: [post.id] })
{{-- fields --}}
@end
Review Checklist
- Template files live under
resources/views and use .edge.
- Controller state is explicit and named for template usage.
- Request/page-specific data is passed through
view.render() rather than Edge standalone renderer APIs.
- Escaped output
{{ }} is the default; unescaped output {{{ }}} is justified.
- Multiline interpolation keeps each double-curly delimiter on the same line.
- Edge tags are placed on their own line; use
@!tag() with no space for auto-closed components.
- Conditional blocks use
@unless only for simple negated conditions; complex branches use clearer state or @if.
- Empty collection states use
@each ... @else when a fallback is tied directly to the loop.
- Partials are used for markup fragments; components are preferred when props or slots are needed.
- Components forward unknown HTML attributes with
$props.toAttrs() or $props.merge(...).toAttrs() when wrapping native elements.
- Provide/inject is limited to tightly coupled component families, not page-level state.
- Stacks are reserved for content that must render in a layout-owned placeholder, such as page-specific scripts.
- Forms use the starter kit
@form() component for CSRF/method spoofing.
- Field inputs stay inside
@field.root() so labels and errors remain accessible.
- Route-aware components use route names instead of duplicated URLs when possible.
- AdonisJS helpers/tags are used only when their contributing package and rendering context are available.
- Debug-only
@dump() calls are removed before completion.
1---2name: edge3description: Use this skill alongside the adonisjs skill when the project uses the AdonisJS Hypermedia starter kit or Edge.js server-side templates. Trigger for .edge files, view.render(), router.on().render(), resources/views, Edge syntax, AdonisJS Edge helpers, request/auth/session/flashMessages/old/route/signedRoute/config/app/asset/i18n, @vite/@viteReactRefresh/@can/@cannot/@flashMessage/@error/@inputError, template state, interpolation, conditionals, @if/@elseif/@else/@unless, loops, @each, layouts, Edge components, component props, $props, slots, provide/inject, $context, partials, @include/@includeIf, stacks, @stack/@pushTo, form components, CSRF form helpers, field/input/select/textarea components, alert/button/link/avatar components, or debugging templates with @dump.4---56# Edge Templates for AdonisJS78This skill covers the Edge.js server-rendered view layer in AdonisJS Hypermedia apps.9Use `adonisjs` alongside it for controllers, routes, middleware, auth, validation, services, and framework architecture.1011## When to Use1213Use this skill for:1415- Rendering `.edge` templates from controllers or route handlers16- Working in `resources/views/**`17- Applying Edge syntax rules for curly braces, `@` tags, comments, and newline swallowing18- Writing interpolation with `{{ }}`, `{{{ }}}`, `html.safe()`, or escaped `@{{ }}`19- Writing conditional blocks with `@if`, `@elseif`, `@else`, `@unless`, or short ternaries20- Looping over arrays and objects with `@each`, indexes, keys, and empty-state fallbacks21- Building layouts, components, slots, partials, and reusable template fragments22- Building components with props, `$props`, slots, layout components, and provide/inject23- Including partials and pushing template-specific content into stacks24- Passing template state to `view.render()`25- Using AdonisJS-provided Edge helpers and tags from official packages26- Using Hypermedia starter kit form and UI components27- Debugging template variables, helpers, and rendering problems2829Do not use this skill for Inertia page components. Use `inertia-vue` or `inertia-react` for those frontend layers.3031## References3233Load the specific reference that matches the template concern:3435| Reference | Use it for |36| --- | --- |37| `references/template-state.md` | Data available to templates: `view.render()` state, framework-provided values, `@let`, `@assign`, globals, and avoiding Edge standalone renderer APIs in AdonisJS |38| `references/syntax-specification.md` | Low-level Edge syntax: curly braces, `@` tags, auto-closing tags, comments, and newline swallowing |39| `references/interpolation.md` | `{{ }}`, `{{{ }}}`, `html.safe()`, multiline expressions, stringified output, escaped frontend braces with `@{{ }}` |40| `references/conditionals.md` | Conditional rendering with `@if`, `@elseif`, `@else`, `@unless`, and short ternaries |41| `references/loops.md` | `@each` over arrays/objects, indexes, object key/value loops, and `@each ... @else` empty states |42| `references/partials-and-stacks.md` | `@include`, `@includeIf`, `@stack`, `@pushTo`, push ordering, and push-once behavior |43| `references/components.md` | Custom Edge components: tag names, props, `$props`, slots, layout components, `@inject`, and `$context` |44| `references/hypermedia-components.md` | Hypermedia starter kit components: layout, form, fields, input/select/textarea, checkbox/radio, alert, button, link, avatar |45| `references/adonisjs-helpers-and-tags.md` | Helpers/tags contributed by AdonisJS packages: `request`, `route`, `signedRoute`, `session`, `flashMessages`, `old`, `auth`, `asset`, `@vite`, `@can`, `@flashMessage`, `@error`, `@inputError` |4647## Rendering Templates4849Render templates from controllers with `view.render(template, state)`.50The template path is relative to `resources/views` and omits the `.edge` extension.5152```ts53import Post from '#models/post'54import type { HttpContext } from '@adonisjs/core/http'5556export default class PostsController {57 async index({ view }: HttpContext) {58 return view.render('pages/posts/index', {59 posts: await Post.all(),60 })61 }62}63```6465For static pages, routes can render a template directly.6667```ts68import router from '@adonisjs/core/services/router'6970router.on('/').render('pages/home')71```7273## Template State7475Read `references/template-state.md` when working with data passed to templates, framework-provided values, `@let`, `@assign`, globals, or Edge standalone state APIs.7677Every property passed as the second argument to `view.render()` becomes a template variable.78Prefer explicit state names over reaching into broad service objects from the template.7980```edge81@layout()82 @each(post in posts)83 <article>84 <h2>{{ post.title }}</h2>85 <p>{{ post.summary }}</p>86 </article>87 @end88@end89```9091AdonisJS also shares request/auth context and Edge helpers with templates. When a value is unclear, inspect the state during development with `@dump(state)` or a specific variable with `@dump(posts)`.9293## Syntax Defaults9495Read `references/interpolation.md` when working with interpolation, multiline JavaScript expressions, stringified output, HTML escaping, or `@{{ }}` escaping for frontend frameworks.96Read `references/syntax-specification.md` when working with low-level Edge syntax rules, tag placement, auto-closing tags, comments, or `~` newline swallowing.97Read `references/conditionals.md` when working with `@if`, `@elseif`, `@else`, `@unless`, or ternary expressions in templates.98Read `references/loops.md` when working with `@each`, indexes, object key/value loops, or `@each ... @else` fallback content.99Read `references/components.md` when working with component files, component tag names, props, `$props`, slots, layout components, `@inject`, or `$context`.100Read `references/partials-and-stacks.md` when working with `@include`, `@includeIf`, `@stack`, `@pushTo`, `@pushToTop`, or push-once stack behavior.101Read `references/adonisjs-helpers-and-tags.md` when working with AdonisJS-provided Edge helpers/tags such as `request`, `route`, `signedRoute`, `session`, `flashMessages`, `old`, `auth`, `asset`, `@vite`, `@can`, `@flashMessage`, `@error`, or `@inputError`.102103Use escaped output by default.104105```edge106{{ post.title }}107```108109Use unescaped output only for trusted HTML that has already been sanitized or generated by trusted code.110111```edge112{{{ post.html }}}113```114115Common control flow:116117```edge118@if(auth.user)119 <p>Welcome back, {{ auth.user.fullName }}</p>120@else121 <p>Please log in</p>122@end123124@each(post in posts)125 <h2>{{ post.title }}</h2>126@end127```128129## Layouts and Components130131The Hypermedia starter kit provides a `@layout()` component at `resources/views/components/layout.edge`.132It owns the HTML document shell and renders page content through the default slot.133Read `references/components.md` when creating or changing custom components.134135```edge136@layout()137 <main>138 Page content139 </main>140@end141```142143Create reusable components under `resources/views/components`.144The file name becomes the component tag name.145146```edge147{{-- resources/views/components/card.edge --}}148<div class="card">149 {{{ await $slots.main() }}}150</div>151```152153```edge154@card()155 <h2>Card title</h2>156@end157```158159Use the self-closing `@!component()` form only for components that do not need slots.160161## Hypermedia Starter Kit Components162163Read `references/hypermedia-components.md` when working with the starter kit's pre-built components.164165Important defaults:166167- Each component renders at most one HTML element.168- Unknown props are passed through as HTML attributes.169- `@form()` injects CSRF protection and supports method spoofing for `PUT`, `PATCH`, and `DELETE`.170- Field controls are composed from `@field.root()`, `@field.label()`, control components, and `@field.error()`.171- `@link()` can generate URLs from named routes with `route`, `routeParams`, and `routeOptions`.172173## Form Pattern174175Prefer route names over hard-coded URLs when the route is named.176177```edge178@form({ route: 'posts.store', method: 'POST' })179 @field.root({ name: 'title' })180 @!field.label({ text: 'Title' })181 @!input.control({ type: 'text', maxlength: '120' })182 @!field.error()183 @end184185 @!button({ text: 'Create post', type: 'submit' })186@end187```188189For update/delete forms, use method spoofing through the component.190191```edge192@form({ route: 'posts.update', method: 'PUT', routeParams: [post.id] })193 {{-- fields --}}194@end195```196197## Review Checklist198199- Template files live under `resources/views` and use `.edge`.200- Controller state is explicit and named for template usage.201- Request/page-specific data is passed through `view.render()` rather than Edge standalone renderer APIs.202- Escaped output `{{ }}` is the default; unescaped output `{{{ }}}` is justified.203- Multiline interpolation keeps each double-curly delimiter on the same line.204- Edge tags are placed on their own line; use `@!tag()` with no space for auto-closed components.205- Conditional blocks use `@unless` only for simple negated conditions; complex branches use clearer state or `@if`.206- Empty collection states use `@each ... @else` when a fallback is tied directly to the loop.207- Partials are used for markup fragments; components are preferred when props or slots are needed.208- Components forward unknown HTML attributes with `$props.toAttrs()` or `$props.merge(...).toAttrs()` when wrapping native elements.209- Provide/inject is limited to tightly coupled component families, not page-level state.210- Stacks are reserved for content that must render in a layout-owned placeholder, such as page-specific scripts.211- Forms use the starter kit `@form()` component for CSRF/method spoofing.212- Field inputs stay inside `@field.root()` so labels and errors remain accessible.213- Route-aware components use route names instead of duplicated URLs when possible.214- AdonisJS helpers/tags are used only when their contributing package and rendering context are available.215- Debug-only `@dump()` calls are removed before completion.