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
.edgetemplates 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 forPUT,PATCH, andDELETE.- Field controls are composed from
@field.root(),@field.label(), control components, and@field.error(). @link()can generate URLs from named routes withroute,routeParams, androuteOptions.
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/viewsand 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
@unlessonly for simple negated conditions; complex branches use clearer state or@if. - Empty collection states use
@each ... @elsewhen 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.