Skip to main content

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​

src/controllers/page.controller.ts
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;
};
}