Raw Responses
Setting contentType switches a spec from the HTML pipeline to a raw response mode. The view returns the response body directly — no doctype, no hydration script. Security headers and compression still apply.
The basics
Set contentType to any valid MIME type. When present, the normal HTML wrapper (doctype, head, body, hydration script) is skipped. The render function receives (ctx, serverState) and returns a string:
export default {
route: '/feed.xml',
contentType: 'application/rss+xml',
state: {},
server: {
data: async () => ({
posts: await db.posts.latest(20),
}),
},
view: (ctx, server) => `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>My Blog</title>
<link>https://example.com</link>
<description>Recent posts</description>
${server.posts.map(post => `
<item>
<title>${esc(post.title)}</title>
<link>https://example.com/blog/${post.slug}</link>
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
<description>${esc(post.excerpt)}</description>
</item>
`).join('')}
</channel>
</rss>`,
}
JSON API endpoints
export default {
route: '/api/products',
contentType: 'application/json',
state: {},
server: {
data: async (ctx) => ({
products: await db.products.list({
page: parseInt(ctx.query.page ?? '1', 10),
category: ctx.query.category,
}),
}),
},
view: (ctx, server) => JSON.stringify({
products: server.products,
page: parseInt(ctx.query.page ?? '1', 10),
}),
}
XML sitemap
export default {
route: '/sitemap.xml',
contentType: 'application/xml',
serverTtl: 3600,
state: {},
server: {
data: async () => ({
pages: await db.pages.allPublished(),
}),
},
view: (ctx, server) => `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${server.pages.map(p => `
<url>
<loc>https://example.com${p.path}</loc>
<lastmod>${p.updatedAt.toISOString().slice(0, 10)}</lastmod>
</url>
`).join('')}
</urlset>`,
}
View function signature
For raw responses, the view function signature is (ctx, serverState), not (state, serverState). The ctx argument is the request context object with params, query, headers, and cookies.
state) for raw response specs — they are purely server-side. The state: {} field is still required for spec validation, but is not used.Escaping in XML and HTML
Pulse does not auto-escape raw response bodies. When returning XML or HTML, escaping all user-supplied and dynamic content is required — unescaped output is an injection vulnerability:
// Simple escape helper for XML/HTML contexts
function esc(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
Caching raw responses
Raw response specs support the same serverTtl and cache options as HTML specs. For feeds and sitemaps that update infrequently, a generous TTL dramatically reduces server load:
export default {
route: '/feed.xml',
contentType: 'application/rss+xml',
serverTtl: 300, // cache data for 5 minutes
cache: {
public: true,
maxAge: 300,
},
// ...
}