SSR Decorators
Rikta provides decorators to define SSR routes and configure page metadata in a declarative way.
@SsrController()
Marks a class as an SSR controller and optionally sets a route prefix.
Basic Usage
import { SsrController, Get, Ssr } from '@riktajs/ssr';
@SsrController()
export class PageController {
@Get('/')
@Ssr({ title: 'Home' })
home() {
return { page: 'home' };
}
}
With Route Prefix
@SsrController('/pages')
export class PageController {
@Get('/home') // Matches /pages/home
@Ssr({ title: 'Home' })
home() {
return { page: 'home' };
}
@Get('/about') // Matches /pages/about
@Ssr({ title: 'About' })
about() {
return { page: 'about' };
}
}
With Default Options
Use the defaults option to set common metadata for all routes in the controller. Individual @Ssr() decorators can override or extend these defaults.
@SsrController({
defaults: {
og: {
siteName: 'My App',
type: 'website',
},
twitter: {
site: '@mycompany',
},
head: [
Head.meta('author', 'Your Name'),
],
},
})
export class PageController {
@Get('/')
@Ssr({
title: 'Home',
og: { title: 'Welcome Home' }, // Merges with defaults: siteName and type are inherited
})
home() {
return { page: 'home' };
}
@Get('/about')
@Ssr({ title: 'About Us' }) // Inherits all og, twitter, and head from defaults
about() {
return { page: 'about' };
}
}
Merge behavior:
- Simple properties (
title,description,canonical,robots): route overrides defaults - Nested objects (
og,twitter,cache): properties are merged (route takes precedence) - Arrays (
head): concatenated (defaults first, then route-specific)
@Ssr()
Configures SSR metadata and options for a route handler.
Title and Description
@Get('/about')
@Ssr({
title: 'About Us',
description: 'Learn more about our company and mission',
})
about() {
return { page: 'about' };
}
Open Graph Tags
@Get('/blog/:slug')
@Ssr({
title: 'Blog Post',
og: {
title: 'Amazing Blog Post',
description: 'Read our latest insights',
image: 'https://example.com/og-image.png',
type: 'article',
url: 'https://example.com/blog/post',
}
})
getBlogPost(@Param('slug') slug: string) {
const post = this.findPost(slug);
return { post };
}
Twitter Card Tags
@Get('/product/:id')
@Ssr({
title: 'Product Details',
twitter: {
card: 'summary_large_image',
site: '@mycompany',
creator: '@author',
title: 'Amazing Product',
description: 'Check out this product',
image: 'https://example.com/product.png',
}
})
getProduct(@Param('id') id: string) {
const product = this.findProduct(id);
return { product };
}
Canonical URL
@Get('/page')
@Ssr({
title: 'My Page',
canonical: 'https://example.com/page',
})
page() {
return { page: 'content' };
}
Robots Directive
@Get('/private')
@Ssr({
title: 'Private Page',
robots: 'noindex, nofollow',
})
privatePage() {
return { page: 'private' };
}
Custom Head Tags
@Get('/custom')
@Ssr({
title: 'Custom Page',
head: [
{
tag: 'link',
attrs: {
rel: 'preload',
href: '/fonts/custom.woff2',
as: 'font',
type: 'font/woff2',
crossorigin: 'anonymous',
},
},
{
tag: 'script',
attrs: {
src: 'https://analytics.example.com/script.js',
async: true,
},
},
],
})
customPage() {
return { page: 'custom' };
}
Cache Control
Define cache settings (note: you need to implement the Cache-Control header manually in your entry-server or middleware):
@Get('/cached-page')
@Ssr({
title: 'Cached Page',
cache: {
maxAge: 60, // Cache for 60 seconds
staleWhileRevalidate: 120, // Serve stale for 120s while revalidating
}
})
cachedPage() {
return { page: 'cached' };
}
The cache options are available in the SSR metadata but are not automatically applied as HTTP headers.
Complete Example
import { SsrController, Ssr, Get, Param, Query } from '@riktajs/ssr';
import { Injectable } from '@riktajs/core';
interface BlogPost {
id: string;
slug: string;
title: string;
content: string;
author: string;
publishedAt: Date;
}
@Injectable()
@SsrController('/blog')
export class BlogController {
@Get('/')
@Ssr({
title: 'Blog - My Site',
description: 'Read our latest articles and insights',
og: {
title: 'My Blog',
image: 'https://example.com/blog-og.png',
type: 'website',
},
cache: {
maxAge: 300, // Cache for 5 minutes
}
})
listPosts(@Query('page') page: string = '1') {
const posts = this.getPosts(parseInt(page));
return {
posts,
pagination: {
page: parseInt(page),
total: this.getTotalPages(),
}
};
}
@Get('/:slug')
@Ssr({
title: 'Blog Post',
og: {
type: 'article',
},
twitter: {
card: 'summary_large_image',
site: '@mysite',
},
cache: {
maxAge: 600,
staleWhileRevalidate: 1200,
}
})
getPost(@Param('slug') slug: string) {
const post = this.findPostBySlug(slug);
if (!post) {
throw new Error('Post not found');
}
return {
post,
relatedPosts: this.getRelatedPosts(post.id),
};
}
private getPosts(page: number): BlogPost[] {
// Implementation
return [];
}
private getTotalPages(): number {
// Implementation
return 10;
}
private findPostBySlug(slug: string): BlogPost | null {
// Implementation
return null;
}
private getRelatedPosts(postId: string): BlogPost[] {
// Implementation
return [];
}
}
Dynamic Metadata
You can also generate metadata dynamically. The @Ssr decorator options take precedence, but you can return dynamic data:
@Get('/user/:id')
@Ssr({
title: 'User Profile', // This title is used from decorator
})
getUserProfile(@Param('id') id: string) {
const user = this.findUser(id);
// Return data - it will be available in context.__SSR_DATA__
return {
user,
};
}
For fully dynamic metadata, you can override in your entry-server.tsx:
export function render(url: string, context: Record<string, unknown> = {}) {
// Extract metadata from context (set by @Ssr decorator)
const { title: contextTitle, description: contextDescription, __SSR_DATA__, ...restContext } = context;
// You can override title/description based on the data
const userData = __SSR_DATA__ as { user?: { name: string; avatar: string } };
const dynamicTitle = userData?.user?.name
? `${userData.user.name} - User Profile`
: (contextTitle as string || 'User Profile');
const ssrData: SsrData = {
data: __SSR_DATA__ ?? restContext,
url,
title: dynamicTitle,
description: contextDescription as string,
};
// Render app
const html = renderToString(
<RiktaProvider ssrData={ssrData}>
<App />
</RiktaProvider>
);
// Build head with dynamic data
const head = new HeadBuilder();
head.title(dynamicTitle);
if (contextDescription) {
head.description(contextDescription as string);
}
// Add custom og tags based on user data
if (userData?.user) {
head.og({
title: userData.user.name,
image: userData.user.avatar,
type: 'profile',
});
}
head.withSsrData(ssrData as unknown as Record<string, unknown>);
return {
html,
title: head.getTitle(),
head: head.build(),
};
}
API Reference
@SsrController Options
@SsrController(options?: string | SsrControllerOptions)
When passing a string, it's used as the route prefix. When passing an object:
interface SsrControllerOptions {
/** Route prefix for all SSR routes in this controller */
prefix?: string;
/** Default SSR route options for all routes in this controller */
defaults?: SsrRouteOptions;
/** SSR configuration options (entryServer, template, etc.) */
// ...SsrOptions
}
Example with defaults:
@SsrController({
prefix: '/blog',
defaults: {
og: { siteName: 'My Blog', type: 'article' },
twitter: { site: '@myblog', card: 'summary_large_image' },
head: [Head.meta('author', 'Blog Author')],
},
})
@Ssr Options
interface SsrRouteOptions {
/** Page title */
title?: string;
/** Meta description */
description?: string;
/** Open Graph tags */
og?: {
title?: string;
description?: string;
image?: string;
type?: string;
url?: string;
siteName?: string;
};
/** Twitter Card tags */
twitter?: {
card?: 'summary' | 'summary_large_image' | 'app' | 'player';
site?: string;
creator?: string;
title?: string;
description?: string;
image?: string;
};
/** Canonical URL */
canonical?: string;
/** Robots meta tag */
robots?: string;
/** Custom head tags */
head?: Array<{
tag: string;
attrs?: Record<string, string>;
children?: string;
}>;
/** HTTP cache control */
cache?: {
maxAge?: number;
staleWhileRevalidate?: number;
};
}