
Security News
pnpm 11.5 Adds Support for Recognizing npm Staged Publishes
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.
fastify-fragtml
Advanced tools
Fastify rendering decorators for fragtml.
It provides Fastify rendering ergonomics for function-based fragtml templates:
reply.render() renders HTML and returns a promise for the rendered string.reply.locals and defaultContext are merged into template data.reply.view, reply.viewAsync, or fastify.view, so it can coexist with @fastify/view.npm install fastify-fragtml
import Fastify from 'fastify'
import html from 'fragtml'
import fastifyFragtml from 'fastify-fragtml'
const fastify = Fastify()
await fastify.register(fastifyFragtml, {
defaultContext: {
siteName: 'Example'
}
})
fastify.get('/', async (request, reply) => {
const body = await reply.render(context => html`
<h1>${context.title}</h1>
<p>${context.siteName}</p>
`, {
title: 'Home'
})
return reply.send(body)
})
reply.render(template, data, options)Renders the template and returns the HTML string without sending it. It sets Content-Type to text/html; charset=utf-8 unless already set. Rendering errors reject the promise, so return await reply.render(...) or return reply.render(...) stays in Fastify's normal error handling flow.
const body = await reply.render(context => html`
<p>${context.message}</p>
`, {
message: 'Hello'
})
reply.send(body)
Template context is merged in this order:
defaultContextreply.localsdataLater values override earlier values.
fastify.addHook('preHandler', async (request, reply) => {
reply.locals = {
requestId: request.id
}
})
Layouts are callbacks or objects with a render callback. The render callback receives the already-rendered body value, merged context, and render options. This keeps layouts fragtml-native and lets them own fragment boundaries.
import { frag } from 'fragtml'
await fastify.register(fastifyFragtml, {
layout: (body, context, options) => {
const html = frag(options.fragmentId)
return html`
<!DOCTYPE html>
<html>
<head><title>${context.title}</title></head>
<body>
<main id="main">
${html.fragment.start('main')}
${body}
${html.fragment.end}
</main>
</body>
</html>
`
}
})
Render only the layout's main fragment:
reply.render(pageTemplate, data, { fragmentId: 'main' })
Disable a global layout for one render:
reply.render(pageTemplate, data, { layout: false })
Register named layouts when routes need to choose from a shared set:
import html from 'fragtml'
await fastify.register(fastifyFragtml, {
layout: 'main',
layouts: {
main: {
contentType: 'text/html; charset=utf-8',
render: (body, context) => html`
<!DOCTYPE html>
<html>
<head><title>${context.title}</title></head>
<body>${body}</body>
</html>
`
},
admin: body => html`
<main data-layout="admin">${body}</main>
`
}
})
reply.render(pageTemplate, data, { layout: 'admin' })
layout can be a callback, a layout object, a registered layout name, false in render options to disable the default, or null when registering to skip a default layout.
Content type is set only when the reply does not already have a Content-Type header. The precedence is:
Content-TypecontentTypecontentTypecontentTypetext/html; charset=utf-8const body = await reply.render(pageTemplate, data, {
contentType: 'text/vnd.turbo-stream.html; charset=utf-8'
})
reply.send(body)
That makes XML and RSS layouts straightforward:
import html from 'fragtml'
const feedTemplate = context => html`
<channel>
<title>${context.title}</title>
<link>${context.siteUrl}</link>
${context.posts.map(post => html`
<item>
<title>${post.title}</title>
<link>${context.siteUrl}${post.href}</link>
<guid>${context.siteUrl}${post.href}</guid>
</item>
`)}
</channel>
`
await fastify.register(fastifyFragtml, {
layouts: {
rss: {
contentType: 'application/rss+xml; charset=utf-8',
render: body => html`<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
${body}
</rss>
`
}
}
})
fastify.get('/feed.xml', async (request, reply) => {
const body = await reply.render(feedTemplate, {
title: 'Example Feed',
siteUrl: 'https://example.com',
posts: [
{
title: 'Hello & welcome',
href: '/posts/hello'
}
]
}, {
layout: 'rss'
})
return reply.send(body)
})
For stricter TypeScript checks, use the helper functions to infer layout names from the layout map:
import html from 'fragtml'
import fastifyFragtml, {
defineFastifyFragtmlOptions,
defineFragtmlLayouts
} from 'fastify-fragtml'
import type { FragtmlLayoutName, FragtmlTemplate } from 'fastify-fragtml'
interface PageContext {
title: string
}
type PageFragment = 'main'
const layouts = defineFragtmlLayouts<PageContext, PageFragment>()({
main: (body, context) => html`<body><h1>${context.title}</h1>${body}</body>`,
admin: body => html`<main data-layout="admin">${body}</main>`
})
type PageLayout = FragtmlLayoutName<typeof layouts>
const pageTemplate: FragtmlTemplate<PageContext, PageLayout, PageFragment> = (
context,
options
) => {
const h = html<PageFragment>(options.fragmentId)
return h/* html */`
${h.fragment.start('main')}
<p>${context.title}</p>
${h.fragment.end}
`
}
await fastify.register(fastifyFragtml, defineFastifyFragtmlOptions<
PageContext,
typeof layouts,
PageFragment
>({
layout: 'main',
layouts
}))
reply.render(pageTemplate, data, { layout: 'admin' })
// @ts-expect-error layout names are inferred from `layouts`.
reply.render(pageTemplate, data, { layout: 'missing' })
// @ts-expect-error fragment IDs use the `PageFragment` union.
reply.render(pageTemplate, data, { fragmentId: 'missing' })
interface FastifyFragtmlOptions {
charset?: string
contentType?: string | false
defaultContext?: object
fragtml?: FragtmlRuntime
layout?: FragtmlLayout | string | null
layouts?: Record<string, FragtmlLayout>
minify?: (html: string, options?: unknown) => string | Promise<string>
minifyOptions?: unknown
options?: {
useHtmlMinifier?: { minify: Function } | Function
htmlMinifierOptions?: unknown
pathsToExcludeHtmlMinifier?: string[]
}
pathsToExcludeMinify?: string[]
propertyName?: string
}
interface FragtmlRuntime {
render: (value: unknown) => string | Promise<string>
raw?: (value: unknown) => RawHtml
html?: HtmlTag
frag?: HtmlTag
default?: HtmlTag
}
interface FragtmlLayoutObject {
contentType?: string | false
render: FragtmlLayout
}
propertyName defaults to render. fastify-fragtml deliberately avoids the view, viewAsync, and fastify.view decorator names used by @fastify/view.
By default, rendered values are finalized with fragtml.render(). Pass fragtml when your app uses a custom fragtml instance or a wrapped renderer:
import html, { raw, render } from 'fragtml'
await fastify.register(fastifyFragtml, {
fragtml: {
html,
raw,
render: value => render(value)
}
})
Only render(value) is required by the plugin. The optional html, frag, default, and raw fields make module-like custom instances type cleanly.
Fastify rejects duplicate decorators in the same encapsulation scope. If @fastify/view is registered in the same scope, use custom names:
await fastify.register(fastifyFragtml, {
propertyName: 'renderHtml'
})
fastify.get('/', async (request, reply) => {
const body = await reply.renderHtml(template, data)
return reply.send(body)
})
The package augments Fastify's default types when imported:
import Fastify from 'fastify'
import html from 'fragtml'
import fastifyFragtml from 'fastify-fragtml'
import type { FragtmlTemplate } from 'fastify-fragtml'
interface PageContext {
title: string
}
const page: FragtmlTemplate<PageContext> = context => html`
<h1>${context.title}</h1>
`
const fastify = Fastify()
await fastify.register(fastifyFragtml)
fastify.get('/', (request, reply) => {
return reply.render(page, { title: 'Home' })
})
For custom decorator names, use the exported helper types:
import type { FastifyReply } from 'fastify'
import type { FragtmlReplyDecorators } from 'fastify-fragtml'
type FragtmlReply = FastifyReply & FragtmlReplyDecorators<'renderHtml'>
fastify-fragtml re-exports the public fragtml/types.js helpers, including FragmentTemplateTypes, FragmentArgs, FragmentIdOf, FragmentTemplateArgs, HtmlRenderable, HtmlTag, HtmlResult, RawHtml, and RenderOptions.
You can use the same FragmentTemplateTypes pattern from fragtml with reply.render():
import { frag } from 'fragtml'
import fastifyFragtml from 'fastify-fragtml'
import type {
FragmentTemplateTypes,
FragtmlArgsTemplate
} from 'fastify-fragtml'
type InnerPageContext = {
text: string
}
type OuterPageContext = InnerPageContext & {
title: string
}
type FullPageContext = OuterPageContext & {
foo: string
}
type PageTemplate = FragmentTemplateTypes<{
fragments: {
inner: InnerPageContext
outer: OuterPageContext
}
full: FullPageContext
}>
type PageArgs = PageTemplate['args']
type PageTemplateArgs = PageTemplate['templateArgs']
type PageFragment = PageTemplate['fragmentId']
const pageTemplate: FragtmlArgsTemplate<PageArgs> = ({
context,
fragmentId
}: PageTemplateArgs) => {
const html = frag<PageFragment>(fragmentId)
return html`
<div>${context.foo}</div>
${html.fragment.start('outer')}
<section>
<h2>${context.title}</h2>
${html.fragment.start('inner')}
<button>Inner update target</button>
<div>${context.text}</div>
${html.fragment.end}
</section>
${html.fragment.end}
`
}
await fastify.register(fastifyFragtml)
fastify.get('/inner', (request, reply) => {
return reply.render(pageTemplate, {
fragmentId: 'inner',
context: {
text: 'Updated body text'
}
})
})
fastify.get('/full', (request, reply) => {
return reply.render(pageTemplate, {
context: {
foo: 'Full page field',
title: 'Outer fragment title',
text: 'Updated body text'
}
})
})
FragtmlTemplate and FragtmlRenderOptions accept a fragment ID union as their third generic parameter. That lets TypeScript catch typos in opts.fragmentId:
import { frag } from 'fragtml'
import type { FragtmlRenderOptions, FragtmlTemplate } from 'fastify-fragtml'
type PageContext = { title: string }
type PageFragment = 'main'
const page: FragtmlTemplate<PageContext, string, PageFragment> = (context, options) => {
const html = frag<PageFragment>(options.fragmentId)
return html`
${html.fragment.start('main')}
<h1>${context.title}</h1>
${html.fragment.end}
`
}
const options: FragtmlRenderOptions<PageContext, string, PageFragment> = {
fragmentId: 'main'
}
reply.render(page, { title: 'Home' }, options)
// @ts-expect-error "missing" is not a known page fragment.
reply.render(page, { title: 'Home' }, { fragmentId: 'missing' })
MIT
FAQs
Fastify rendering decorators for fragtml
We found that fastify-fragtml demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.

Security News
Federal audit finds NIST lacked a plan to clear the NVD backlog, wasted funds on duplicate work, and delayed use of CISA data.

Research
/Security News
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.