@samveys/template

Versi
1.0.35 Nov 2026
Pasang
pnpm add @samveys/template

Dari packages/template/README.md di repository framework.

Halaman ini belum diterjemahkan. Kamu sedang membaca versi bahasa Inggris.

Server-side templates (.tpl.html) for @samveys/view. No runtime dependencies.

Each template is compiled to an ES module (a JavaScript string written to storage/views/<hash>.mjs) and loaded with import(). The framework never uses eval or new Function, and a template can only read the data it was rendered with.

Set up

ts
import { http } from '@samveys/http'
import { template } from '@samveys/template'
import { view } from '@samveys/view'

export const plugins = [http(), view(), template({ viewsDir: 'resources/views', cacheDir: 'storage/views' })]

Names map to files: users.index → resources/views/users/index.tpl.html. With app.env === 'local' a changed source is recompiled on the next render; otherwise compiled modules are kept.

Syntax

SyntaxResult
{{ expr }}Value, HTML-escaped (& < > " ' \``). nullandundefined` print nothing
{!! expr !!}Value, raw
{{-- comment --}}Nothing
@@ifA literal @if
@if(a) @elseif(b) @else @endifConditions
@each(item in items) … @empty … @endeachLoop; $loop has index, iteration, first, last, count, remaining, parent
@layout('layouts.base')Wrap the template in a layout
@section('name') … @endsectionContent for a layout
@yield('name', default)Print a section (or the escaped default)
@include('partial', { a: 1 })Another template with the current data plus { a: 1 }
<x-card title="Hi" :count="n + 1">…</x-card>Component components/card.tpl.html
@csrf<input type="hidden" name="_csrf" value="…"> (needs @samveys/security and its csrf middleware on the route)
@vite('resources/app.ts')Asset tags (needs @samveys/vite)
@inertia, @inertiaHeadInertia root element and SSR head (used by @samveys/inertia)
html
{{-- resources/views/layouts/base.tpl.html --}}
<title>@yield('title', 'Samveys')</title>
<main>@yield('content')</main>
html
{{-- resources/views/users/index.tpl.html --}}
@layout('layouts.base')
@section('content')
  <h1>{{ title }}</h1>
  @each(user in users)
    <x-user-card :user="user" />
  @empty
    <p>No users yet.</p>
  @endeach
@endsection

A component receives its attributes as variables (title, count), the default slot as $slot and named slots (<x-slot:footer>) as $slots.footer. It does not see the data of the page that uses it.

Expressions

Expressions are a safe subset that is parsed, not evaluated: identifiers, a.b, a[b], a?.b, calls (name.toUpperCase(), fmt(price)), literals (strings, numbers, true, false, null, arrays, objects), + - * / %, comparisons, && || ??, !, and a ? b : c. Assignments, new, functions, template literals and statements are errors.

Member names constructor, prototype and __* are rejected when the template is compiled, and again at run time for computed keys:

ts
import { compileTemplate } from '@samveys/template'

// Throws SVS_E109: "views/evil.tpl.html:2: Access to "constructor" is not allowed in templates"
compileTemplate('line one\n{{ constructor.constructor("x")() }}', 'views/evil.tpl.html')

Errors are SVS_E109 and name the file and line.

Render

ts
import type { Router } from '@samveys/http'

export function routes(router: Router): void {
  router.get('/', (ctx) => ctx.view.render('home', { title: 'Home' }))
}

Use createTemplateEngine({ viewsDir, cacheDir, watch }) to render without the plugin (scripts, mail bodies).

ts
import { createTemplateEngine } from '@samveys/template'

const engine = createTemplateEngine({ viewsDir: 'resources/views', cacheDir: 'storage/views' })

export const html = (): Promise<string> => engine.render('home', { title: '<Home>' })