Stellen Sie sich vor, Sie betreten ein Restaurant. In einem traditionellen Restaurant (Client-Side Rendering) erhalten Sie eine leere Speisekarte und müssen warten, während der Kellner einzeln jeden Gang erklärt und beschreibt. Bei Server-Side Rendering hingegen bekommen Sie eine vollständig ausgefüllte Speisekarte mit allen Details bereits beim Hinsetzen. Diese Analogie verdeutlicht den fundamentalen Unterschied zwischen verschiedenen Rendering-Strategien in modernen Webanwendungen.
Server-Side Rendering (SSR) und Static Site Generation (SSG) haben in den letzten Jahren eine Renaissance erlebt, da Entwickler erkannt haben, dass reine Client-Side-Anwendungen zwar interaktiv sind, aber oft Nachteile bei der Performance, Suchmaschinenoptimierung und der initialen Ladezeit haben. Für NestJS-Entwickler eröffnet die Kombination mit Next.js eine faszinierende Möglichkeit: Sie können die robuste Backend-Architektur von NestJS mit den modernen Frontend-Rendering-Strategien von Next.js verbinden.
Die Magie liegt darin, dass Sie nicht zwischen Backend und Frontend wählen müssen, sondern beide Welten elegant miteinander verschmelzen können. Ihre NestJS-Anwendung wird zum kraftvollen Datenlieferanten, während Next.js die Präsentationsschicht mit optimierten Rendering-Strategien übernimmt. Diese Symbiose ermöglicht es, sowohl die Vorteile einer starken API-Architektur als auch moderne Frontend-Performance zu nutzen.
Die Integration von NestJS mit Next.js ist wie das Zusammenführen zweier Orchester zu einer harmonischen Symphonie. Jedes Framework bringt seine eigenen Stärken mit, und wenn sie richtig kombiniert werden, entsteht etwas Größeres als die Summe ihrer Teile.
Next.js ist von Natur aus darauf ausgelegt, verschiedene Rendering-Strategien zu unterstützen. Es kann statische Seiten zur Build-Zeit generieren, Seiten bei jeder Anfrage server-seitig rendern oder eine Kombination aus beiden Ansätzen verwenden. NestJS hingegen brilliert als strukturiertes, skalierbares Backend-Framework mit seiner modularen Architektur und seinem robusten Dependency-Injection-System.
Die grundlegende Architektur einer integrierten NestJS-Next.js-Anwendung folgt einem klaren Muster. Stellen Sie sich vor, Sie hätten ein zweistöckiges Haus: Im Erdgeschoss (NestJS) befindet sich die gesamte Geschäftslogik, Datenbankanbindung und API-Endpoints. Im ersten Stock (Next.js) wird diese Logik in benutzerfreundliche Oberflächen umgewandelt, die optimal geladen und dargestellt werden.
Ein praktisches Setup könnte folgendermaßen strukturiert sein:
// apps/api/src/main.ts - NestJS Backend
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// CORS für Next.js Frontend konfigurieren
app.enableCors({
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
credentials: true,
});
// API-Präfix für klare Trennung
app.setGlobalPrefix('api');
await app.listen(process.env.API_PORT || 3001);
console.log(`API läuft auf: ${await app.getUrl()}`);
}
bootstrap();Die Trennung in verschiedene Ports ist zunächst hilfreich für die Entwicklung, aber in der Produktion können beide Anwendungen elegant hinter einem Reverse Proxy wie Nginx zusammengeführt werden. Dies schafft eine nahtlose Benutzererfahrung, bei der API-Aufrufe und Seitenaufrufe über dieselbe Domain laufen.
// apps/frontend/next.config.js - Next.js Konfiguration
/** @type {import('next').NextConfig} */
const nextConfig = {
// API-Proxy für Entwicklungsumgebung
async rewrites() {
return [
{
source: '/api/:path*',
destination: `${process.env.API_URL || 'http://localhost:3001'}/api/:path*`,
},
];
},
// Optimierungen für Produktionsumgebung
experimental: {
outputFileTracingExcludes: {
'*': [
'node_modules/@swc/core-linux-x64-gnu',
'node_modules/@swc/core-linux-x64-musl',
],
},
},
// Bilder-Optimierung konfigurieren
images: {
domains: ['localhost', 'your-api-domain.com'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
},
};
module.exports = nextConfig;Ein entscheidender Aspekt der Integration ist die gemeinsame Typisierung. TypeScript ermöglicht es, Interfaces und Typen zwischen Backend und Frontend zu teilen, was die Entwicklererfahrung erheblich verbessert und Laufzeitfehler reduziert:
// shared/types/user.interface.ts - Gemeinsame Typen
export interface User {
id: string;
email: string;
firstName: string;
lastName: string;
createdAt: Date;
updatedAt: Date;
}
export interface CreateUserDto {
email: string;
firstName: string;
lastName: string;
password: string;
}
export interface UpdateUserDto {
firstName?: string;
lastName?: string;
email?: string;
}
export interface UserListResponse {
users: User[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}Diese geteilten Typen können dann sowohl in NestJS-Controllern als auch in Next.js-Komponenten verwendet werden, was eine durchgängige Typsicherheit gewährleistet:
// apps/api/src/users/users.controller.ts - NestJS Controller
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
import { User, CreateUserDto, UserListResponse } from '../../../shared/types/user.interface';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll(
@Query('page') page: number = 1,
@Query('limit') limit: number = 10,
): Promise<UserListResponse> {
return this.usersService.findAll(page, limit);
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findOne(id);
}
@Post()
async create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto);
}
}Next.js API Routes bieten eine interessante Alternative zu separaten NestJS-Endpoints für bestimmte Anwendungsfälle. Denken Sie an API Routes wie an Brücken zwischen Frontend und Backend - sie leben physisch im Frontend-Code, können aber backend-ähnliche Funktionen ausführen.
Diese Funktionalität ist besonders wertvoll für Anwendungsfälle, die eng mit der Frontend-Logik verknüpft sind, wie Authentifizierung, Session-Management oder spezielle Datenverarbeitungsschritte, die nur für das Frontend relevant sind. Jedoch sollten sie nicht als Ersatz für eine robuste NestJS-API betrachtet werden, sondern als Ergänzung.
// apps/frontend/pages/api/auth/login.ts - Next.js API Route
import { NextApiRequest, NextApiResponse } from 'next';
import { serialize } from 'cookie';
import jwt from 'jsonwebtoken';
interface LoginRequest {
email: string;
password: string;
}
interface LoginResponse {
success: boolean;
user?: {
id: string;
email: string;
firstName: string;
lastName: string;
};
error?: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<LoginResponse>
) {
if (req.method !== 'POST') {
return res.status(405).json({
success: false,
error: 'Method not allowed'
});
}
try {
const { email, password }: LoginRequest = req.body;
// Authentifizierung über NestJS API
const authResponse = await fetch(`${process.env.API_URL}/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (!authResponse.ok) {
return res.status(401).json({
success: false,
error: 'Invalid credentials',
});
}
const authData = await authResponse.json();
// JWT-Token in httpOnly Cookie speichern (sicherer als localStorage)
const token = jwt.sign(
{ userId: authData.user.id },
process.env.JWT_SECRET!,
{ expiresIn: '24h' }
);
const cookie = serialize('auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24, // 24 Stunden
path: '/',
});
res.setHeader('Set-Cookie', cookie);
return res.status(200).json({
success: true,
user: authData.user,
});
} catch (error) {
console.error('Login error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error',
});
}
}Ein weiterer leistungsstarker Anwendungsfall für Next.js API Routes ist die Implementierung von Middleware-Funktionen, die Anfragen verarbeiten, bevor sie an das NestJS-Backend weitergeleitet werden:
// apps/frontend/pages/api/proxy/[...path].ts - API Proxy mit Middleware
import { NextApiRequest, NextApiResponse } from 'next';
import jwt from 'jsonwebtoken';
export default async function apiProxy(
req: NextApiRequest,
res: NextApiResponse
) {
const { path } = req.query;
const apiPath = Array.isArray(path) ? path.join('/') : path;
try {
// Token aus Cookie extrahieren
const token = req.cookies['auth-token'];
let userId = null;
if (token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
userId = decoded.userId;
} catch (error) {
// Token ungültig - trotzdem weiterleiten, Backend entscheidet
console.warn('Invalid token:', error.message);
}
}
// Anfrage an NestJS weiterleiten
const backendUrl = `${process.env.API_URL}/api/${apiPath}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
// User-ID als Header hinzufügen, wenn verfügbar
if (userId) {
headers['X-User-ID'] = userId;
}
// Original Headers weiterleiten (außer Cookie für Sicherheit)
Object.keys(req.headers).forEach(key => {
if (key !== 'cookie' && key !== 'host') {
headers[key] = req.headers[key] as string;
}
});
const response = await fetch(backendUrl, {
method: req.method,
headers,
body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined,
});
const data = await response.json();
return res.status(response.status).json(data);
} catch (error) {
console.error('API Proxy error:', error);
return res.status(500).json({
error: 'Internal server error'
});
}
}Diese Proxy-Implementierung zeigt, wie Next.js API Routes als intelligente Vermittler zwischen Frontend und Backend fungieren können. Sie handhaben Authentifizierung, fügen Kontext hinzu und können sogar Caching oder Rate Limiting implementieren, bevor Anfragen an das NestJS-Backend weitergeleitet werden.
Server-Side Data Fetching ist das Herzstück moderner SSR-Anwendungen. Stellen Sie sich vor, Sie wären ein Koch, der bereits alle Zutaten vorbereitet hat, bevor der Gast das Restaurant betritt. Genau das macht SSR - alle notwendigen Daten werden server-seitig geladen und die Seite wird vollständig gerendert, bevor sie an den Browser gesendet wird.
Next.js bietet verschiedene Strategien für das Data Fetching, die jeweils für unterschiedliche Anwendungsfälle optimiert sind. Die Wahl der richtigen Strategie hängt davon ab, wie oft sich Ihre Daten ändern und wie kritisch die Performance für Ihre Anwendung ist.
// apps/frontend/pages/users/index.tsx - Server-Side Rendering mit getServerSideProps
import { GetServerSideProps } from 'next';
import { User, UserListResponse } from '../../../shared/types/user.interface';
interface UsersPageProps {
users: User[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
initialLoadTime: string;
}
export default function UsersPage({ users, pagination, initialLoadTime }: UsersPageProps) {
return (
<div className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Benutzer</h1>
<span className="text-sm text-gray-500">
Geladen um: {initialLoadTime}
</span>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{users.map((user) => (
<div key={user.id} className="bg-white p-6 rounded-lg shadow-md">
<h3 className="text-lg font-semibold text-gray-800">
{user.firstName} {user.lastName}
</h3>
<p className="text-gray-600">{user.email}</p>
<p className="text-sm text-gray-500 mt-2">
Erstellt: {new Date(user.createdAt).toLocaleDateString('de-DE')}
</p>
</div>
))}
</div>
{/* Pagination-Komponente */}
<div className="mt-8 flex justify-center">
<nav className="flex space-x-2">
{Array.from({ length: pagination.totalPages }, (_, i) => i + 1).map((page) => (
<a
key={page}
href={`/users?page=${page}`}
className={`px-3 py-2 rounded ${
page === pagination.page
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
{page}
</a>
))}
</nav>
</div>
</div>
);
}
// Diese Funktion läuft bei jeder Anfrage auf dem Server
export const getServerSideProps: GetServerSideProps<UsersPageProps> = async (context) => {
const { page = '1', limit = '12' } = context.query;
try {
// Direkte API-Anfrage an NestJS Backend
const response = await fetch(
`${process.env.API_URL}/api/users?page=${page}&limit=${limit}`,
{
headers: {
'Content-Type': 'application/json',
// Interne API-Anfragen können einen speziellen Header verwenden
'X-Internal-Request': 'true',
},
}
);
if (!response.ok) {
throw new Error(`API responded with status: ${response.status}`);
}
const data: UserListResponse = await response.json();
return {
props: {
users: data.users,
pagination: data.pagination,
initialLoadTime: new Date().toLocaleTimeString('de-DE'),
},
};
} catch (error) {
console.error('Error fetching users:', error);
// Fallback: Leere Liste mit Fehlermeldung
return {
props: {
users: [],
pagination: {
page: parseInt(page as string, 10),
limit: parseInt(limit as string, 10),
total: 0,
totalPages: 0,
},
initialLoadTime: new Date().toLocaleTimeString('de-DE'),
},
};
}
};Für Anwendungsfälle, bei denen Daten nicht bei jeder Anfrage aktuell sein müssen, bietet sich Incremental Static Regeneration (ISR) an. Diese Technik kombiniert die Performance-Vorteile statischer Seiten mit der Flexibilität dynamischer Inhalte:
// apps/frontend/pages/blog/[slug].tsx - ISR mit getStaticProps und getStaticPaths
import { GetStaticProps, GetStaticPaths } from 'next';
interface BlogPost {
id: string;
slug: string;
title: string;
content: string;
author: {
name: string;
email: string;
};
publishedAt: Date;
updatedAt: Date;
}
interface BlogPostPageProps {
post: BlogPost;
relatedPosts: BlogPost[];
}
export default function BlogPostPage({ post, relatedPosts }: BlogPostPageProps) {
return (
<article className="max-w-4xl mx-auto px-4 py-8">
<header className="mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
{post.title}
</h1>
<div className="text-gray-600">
<span>Von {post.author.name}</span>
<span className="mx-2">•</span>
<time dateTime={post.publishedAt.toString()}>
{new Date(post.publishedAt).toLocaleDateString('de-DE')}
</time>
</div>
</header>
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
<aside className="mt-12">
<h2 className="text-2xl font-bold mb-6">Ähnliche Artikel</h2>
<div className="grid gap-4 md:grid-cols-2">
{relatedPosts.map((relatedPost) => (
<a
key={relatedPost.id}
href={`/blog/${relatedPost.slug}`}
className="block p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
>
<h3 className="font-semibold text-gray-900 mb-2">
{relatedPost.title}
</h3>
<p className="text-sm text-gray-600">
{new Date(relatedPost.publishedAt).toLocaleDateString('de-DE')}
</p>
</a>
))}
</div>
</aside>
</article>
);
}
// Definiert, welche Pfade zur Build-Zeit statisch generiert werden sollen
export const getStaticPaths: GetStaticPaths = async () => {
try {
// Lade die beliebtesten oder neuesten Blog-Posts für initiale Generierung
const response = await fetch(`${process.env.API_URL}/api/blog/popular?limit=50`);
const posts: BlogPost[] = await response.json();
const paths = posts.map((post) => ({
params: { slug: post.slug },
}));
return {
paths,
// fallback: 'blocking' bedeutet, dass nicht vorgenerierte Seiten
// bei der ersten Anfrage server-seitig generiert werden
fallback: 'blocking',
};
} catch (error) {
console.error('Error generating static paths:', error);
return {
paths: [],
fallback: 'blocking',
};
}
};
// Lädt Daten für eine spezifische Seite
export const getStaticProps: GetStaticProps<BlogPostPageProps> = async (context) => {
const { slug } = context.params!;
try {
// Blog-Post und verwandte Artikel parallel laden
const [postResponse, relatedResponse] = await Promise.all([
fetch(`${process.env.API_URL}/api/blog/${slug}`),
fetch(`${process.env.API_URL}/api/blog/related/${slug}?limit=4`),
]);
if (!postResponse.ok) {
return {
notFound: true,
};
}
const post: BlogPost = await postResponse.json();
const relatedPosts: BlogPost[] = relatedResponse.ok
? await relatedResponse.json()
: [];
return {
props: {
post,
relatedPosts,
},
// Seite wird alle 60 Sekunden im Hintergrund neu generiert
revalidate: 60,
};
} catch (error) {
console.error('Error fetching blog post:', error);
return {
notFound: true,
};
}
};Static Site Generation (SSG) ist wie das Vorbereiten einer Bibliothek mit allen Büchern bereits aufgeschlagen an der richtigen Stelle. Besucher können sofort lesen, ohne warten zu müssen, bis die Seite geladen ist. Diese Strategie ist besonders wertvoll für Content-lastige Websites wie Blogs, Dokumentationsseiten oder Produktkataloge.
Die Kombination von NestJS als Content-API mit Next.js für die statische Generierung schafft ein mächtiges System für inhaltsgetriebene Websites. Ihre NestJS-Anwendung fungiert als Content Management System, während Next.js optimierte, statische HTML-Seiten generiert.
// apps/api/src/content/content.controller.ts - NestJS Content API
import { Controller, Get, Param, Query } from '@nestjs/common';
interface SiteMapEntry {
url: string;
lastModified: Date;
changeFrequency: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
priority: number;
}
@Controller('content')
export class ContentController {
constructor(private readonly contentService: ContentService) {}
// Endpoint für Sitemap-Generierung
@Get('sitemap')
async getSitemap(): Promise<SiteMapEntry[]> {
const [pages, blogPosts, products] = await Promise.all([
this.contentService.getAllPages(),
this.contentService.getAllBlogPosts(),
this.contentService.getAllProducts(),
]);
const sitemap: SiteMapEntry[] = [
// Statische Seiten
{
url: '/',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1.0,
},
{
url: '/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
// Dynamische Seiten aus Content
...pages.map(page => ({
url: `/${page.slug}`,
lastModified: page.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.7,
})),
...blogPosts.map(post => ({
url: `/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'monthly' as const,
priority: 0.6,
})),
...products.map(product => ({
url: `/products/${product.slug}`,
lastModified: product.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.9,
})),
];
return sitemap;
}
// Endpoint für Build-Zeit-Datenexport
@Get('export')
async getFullContentExport() {
const [pages, blogPosts, products, categories] = await Promise.all([
this.contentService.getAllPages(),
this.contentService.getAllBlogPosts(),
this.contentService.getAllProducts(),
this.contentService.getAllCategories(),
]);
return {
pages,
blogPosts,
products,
categories,
metadata: {
exportedAt: new Date(),
totalItems: pages.length + blogPosts.length + products.length,
},
};
}
}Die statische Generierung kann dann vollständige Websites zur Build-Zeit erstellen:
// apps/frontend/scripts/generate-static-site.ts - Build-Zeit Script
import fs from 'fs/promises';
import path from 'path';
interface ContentExport {
pages: Page[];
blogPosts: BlogPost[];
products: Product[];
categories: Category[];
metadata: {
exportedAt: Date;
totalItems: number;
};
}
async function generateStaticSite() {
console.log('🏗️ Starte statische Website-Generierung...');
try {
// Alle Inhalte von NestJS API laden
const response = await fetch(`${process.env.API_URL}/api/content/export`);
const contentExport: ContentExport = await response.json();
console.log(`📊 Gefundene Inhalte: ${contentExport.metadata.totalItems} Elemente`);
// Dynamische Routen für Next.js generieren
await generateDynamicRoutes(contentExport);
// Sitemap generieren
await generateSitemap(contentExport);
// RSS-Feed generieren
await generateRSSFeed(contentExport.blogPosts);
// Such-Index generieren
await generateSearchIndex(contentExport);
console.log('✅ Statische Website-Generierung erfolgreich abgeschlossen');
} catch (error) {
console.error('❌ Fehler bei der statischen Generierung:', error);
process.exit(1);
}
}
async function generateDynamicRoutes(content: ContentExport) {
const routesConfig = {
// Blog-Post-Routen
'/blog/[slug]': content.blogPosts.map(post => ({
slug: post.slug,
lastModified: post.updatedAt,
})),
// Produkt-Routen
'/products/[slug]': content.products.map(product => ({
slug: product.slug,
lastModified: product.updatedAt,
})),
// Kategorie-Routen
'/categories/[slug]': content.categories.map(category => ({
slug: category.slug,
lastModified: category.updatedAt,
})),
// Dynamische Seiten
'/[slug]': content.pages.map(page => ({
slug: page.slug,
lastModified: page.updatedAt,
})),
};
// Routes-Konfiguration für Next.js speichern
const routesPath = path.join(process.cwd(), 'generated', 'routes.json');
await fs.mkdir(path.dirname(routesPath), { recursive: true });
await fs.writeFile(routesPath, JSON.stringify(routesConfig, null, 2));
console.log(`📝 ${Object.keys(routesConfig).length} dynamische Routen-Typen generiert`);
}
async function generateSitemap(content: ContentExport) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
const sitemapEntries = [
// Statische Seiten
` <url>
<loc>${baseUrl}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>`,
// Blog-Posts
...content.blogPosts.map(post => ` <url>
<loc>${baseUrl}/blog/${post.slug}</loc>
<lastmod>${new Date(post.updatedAt).toISOString()}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>`),
// Produkte
...content.products.map(product => ` <url>
<loc>${baseUrl}/products/${product.slug}</loc>
<lastmod>${new Date(product.updatedAt).toISOString()}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>`),
];
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${sitemapEntries.join('\n')}
</urlset>`;
const sitemapPath = path.join(process.cwd(), 'public', 'sitemap.xml');
await fs.writeFile(sitemapPath, sitemap);
console.log(`🗺️ Sitemap mit ${sitemapEntries.length} URLs generiert`);
}
async function generateRSSFeed(blogPosts: BlogPost[]) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
const feedItems = blogPosts
.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime())
.slice(0, 20) // Nur die neuesten 20 Posts
.map(post => ` <item>
<title><![CDATA[${post.title}]]></title>
<link>${baseUrl}/blog/${post.slug}</link>
<description><![CDATA[${post.excerpt || ''}]]></description>
<pubDate>${new Date(post.publishedAt).toUTCString()}</pubDate>
<guid>${baseUrl}/blog/${post.slug}</guid>
</item>`);
const rssFeed = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Ihr Blog Titel</title>
<link>${baseUrl}</link>
<description>Beschreibung Ihres Blogs</description>
<language>de-DE</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<atom:link href="${baseUrl}/feed.xml" rel="self" type="application/rss+xml"/>
${feedItems.join('\n')}
</channel>
</rss>`;
const feedPath = path.join(process.cwd(), 'public', 'feed.xml');
await fs.writeFile(feedPath, rssFeed);
console.log(`📡 RSS-Feed mit ${feedItems.length} Einträgen generiert`);
}
async function generateSearchIndex(content: ContentExport) {
// Vereinfachter Such-Index für Client-Side-Suche
const searchIndex = [
...content.blogPosts.map(post => ({
type: 'blog',
title: post.title,
slug: post.slug,
content: post.content.replace(/<[^>]*>/g, ''), // HTML-Tags entfernen
url: `/blog/${post.slug}`,
publishedAt: post.publishedAt,
})),
...content.products.map(product => ({
type: 'product',
title: product.name,
slug: product.slug,
content: product.description,
url: `/products/${product.slug}`,
publishedAt: product.createdAt,
})),
...content.pages.map(page => ({
type: 'page',
title: page.title,
slug: page.slug,
content: page.content.replace(/<[^>]*>/g, ''),
url: `/${page.slug}`,
publishedAt: page.updatedAt,
})),
];
const indexPath = path.join(process.cwd(), 'public', 'search-index.json');
await fs.writeFile(indexPath, JSON.stringify(searchIndex));
console.log(`🔍 Such-Index mit ${searchIndex.length} Einträgen generiert`);
}
// Script ausführen
if (require.main === module) {
generateStaticSite();
}Diese umfassende Herangehensweise zeigt, wie NestJS und Next.js zusammenarbeiten können, um sowohl performance-optimierte statische Websites als auch dynamische, datengetriebene Anwendungen zu erstellen. Die Kombination aus server-seitigem Rendering, statischer Generierung und intelligenter API-Integration ermöglicht es, das Beste aus beiden Welten zu nutzen - die Robustheit und Skalierbarkeit von NestJS mit der modernen Frontend-Performance von Next.js.
Der Schlüssel liegt darin, die richtige Rendering-Strategie für jeden Anwendungsfall zu wählen: SSG für inhaltsbasierte Seiten, SSR für personalisierte Inhalte und Client-Side Rendering für hochinteraktive Bereiche. Diese hybride Herangehensweise ermöglicht es, optimale Benutzererfahrungen zu schaffen, die sowohl schnell laden als auch reich an Funktionen sind.