Skip to content

Install Frontend

This guide covers setting up the client-facing website that consumes content from your Stelo CMS backend.

Before starting, ensure you have:

  • Stelo CMS Backend running (see Backend Installation)
  • Node.js 18+ installed
  • pnpm or npm package manager
  • Git for version control
  • Access to CMS API endpoints
Terminal window
# Clone the frontend template
git clone https://github.com/your-username/template-stelo-frontend.git client-project-frontend
cd client-project-frontend
# Remove the original git history
rm -rf .git
git init
git add .
git commit -m "Initial commit from Stelo frontend template"
Terminal window
# Using pnpm (recommended)
pnpm install
# Or using npm
npm install

Copy the environment template and configure your variables:

Terminal window
cp .env.example .env.local

Edit .env.local with your configuration:

Terminal window
# CMS API Configuration
NEXT_PUBLIC_CMS_URL="http://localhost:3000" # Your CMS backend URL
CMS_API_TOKEN="your-api-token" # For server-side requests
# Site Configuration
NEXT_PUBLIC_SITE_URL="http://localhost:3001"
NEXT_PUBLIC_SITE_NAME="Client Site Name"
# Analytics (Optional)
NEXT_PUBLIC_GA_ID="G-XXXXXXXXXX"
NEXT_PUBLIC_GTM_ID="GTM-XXXXXXX"
# Email Contact Form (Optional)
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_USER="contact@client-domain.com"
SMTP_PASSWORD="your-app-password"
# SEO & Social
NEXT_PUBLIC_DEFAULT_OG_IMAGE="/images/og-default.jpg"
NEXT_PUBLIC_TWITTER_HANDLE="@clienthandle"

Update the tRPC client configuration in src/lib/trpc.ts:

import { createTRPCNext } from '@trpc/next';
import { httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../../../stelo-cms/src/app/api/trpc/route';
function getBaseUrl() {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3001}`;
}
export const trpc = createTRPCNext<AppRouter>({
config() {
return {
links: [
httpBatchLink({
url: `${process.env.NEXT_PUBLIC_CMS_URL}/api/trpc`,
headers() {
return {
Authorization: `Bearer ${process.env.CMS_API_TOKEN}`,
};
},
}),
],
};
},
ssr: false,
});

Configure supported locales in src/middleware.ts:

import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// A list of all locales that are supported
locales: ['en', 'fr', 'es'],
// Used when no locale matches
defaultLocale: 'en'
});
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(fr|es)/:path*']
};

Create locale message files:

Terminal window
# Create message directories
mkdir -p messages
# Create message files for each locale
touch messages/en.json messages/fr.json messages/es.json

Example messages/en.json:

{
"navigation": {
"home": "Home",
"about": "About",
"services": "Services",
"contact": "Contact"
},
"common": {
"readMore": "Read More",
"contactUs": "Contact Us",
"loading": "Loading..."
},
"pages": {
"home": {
"title": "Welcome to Our Website",
"subtitle": "We provide excellent services"
}
}
}
Terminal window
# Start the development server
pnpm dev
# The frontend will be available at:
# http://localhost:3001

Create a test page to verify CMS connection:

src/app/[locale]/test/page.tsx
import { trpc } from '@/lib/trpc';
export default async function TestPage() {
const pages = await trpc.pages.getAll.query();
return (
<div>
<h1>CMS Connection Test</h1>
<p>Found {pages.length} pages in CMS</p>
<ul>
{pages.map((page) => (
<li key={page.id}>{page.title.en}</li>
))}
</ul>
</div>
);
}
Terminal window
# Install Vercel CLI
npm i -g vercel
# Deploy to Vercel
vercel
# Follow the prompts to configure your project
Terminal window
# In your Coolify dashboard:
# 1. Create new application
# 2. Connect Git repository
# 3. Set build pack to Node.js
# 4. Configure environment variables
Terminal window
# CMS API Configuration
NEXT_PUBLIC_CMS_URL="https://cms.client-domain.com"
CMS_API_TOKEN="production-api-token"
# Site Configuration
NEXT_PUBLIC_SITE_URL="https://client-domain.com"
NEXT_PUBLIC_SITE_NAME="Client Name"
# Analytics
NEXT_PUBLIC_GA_ID="G-REAL-ID"
NEXT_PUBLIC_GTM_ID="GTM-REAL-ID"
# Email
SMTP_HOST="smtp.client-domain.com"
SMTP_PORT="587"
SMTP_USER="noreply@client-domain.com"
SMTP_PASSWORD="production-password"

Optimize next.config.js for production:

/** @type {import('next').NextConfig} */
const withNextIntl = require('next-intl/plugin')('./src/i18n.ts');
const nextConfig = {
// Image optimization
images: {
domains: ['your-cms-domain.com', 'fra1.digitaloceanspaces.com'],
formats: ['image/webp', 'image/avif'],
},
// Compression
compress: true,
// Static export for better performance
output: 'export',
trailingSlash: true,
// Security headers
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin',
},
],
},
];
},
};
module.exports = withNextIntl(nextConfig);
Type: A
Name: @
Value: your-vercel-ip (or VPS IP)
TTL: 300
Type: A
Name: www
Value: your-vercel-ip (or VPS IP)
TTL: 300
  • Vercel: Automatic SSL via Let’s Encrypt
  • Coolify: Automatic SSL configuration

Configure Next.js Image component:

src/components/OptimizedImage.tsx
import Image from 'next/image';
import { useState } from 'react';
interface OptimizedImageProps {
src: string;
alt: string;
width: number;
height: number;
className?: string;
}
export default function OptimizedImage({
src,
alt,
width,
height,
className,
}: OptimizedImageProps) {
const [isLoading, setLoading] = useState(true);
return (
<Image
src={src}
alt={alt}
width={width}
height={height}
className={`${className} ${
isLoading ? 'blur-sm' : 'blur-0'
} transition-all duration-300`}
onLoadingComplete={() => setLoading(false)}
priority={false}
/>
);
}

Configure static page generation:

src/app/[locale]/[slug]/page.tsx
import { trpc } from '@/lib/trpc';
// Generate static paths for all pages
export async function generateStaticParams() {
const pages = await trpc.pages.getPublished.query();
return pages.map((page) => ({
slug: page.slug,
}));
}
// Static generation with revalidation
export const revalidate = 3600; // Revalidate every hour

Create SEO component:

src/components/SEO.tsx
import Head from 'next/head';
import { useTranslations } from 'next-intl';
interface SEOProps {
title?: string;
description?: string;
image?: string;
url?: string;
}
export default function SEO({ title, description, image, url }: SEOProps) {
const t = useTranslations('seo');
const siteTitle = process.env.NEXT_PUBLIC_SITE_NAME;
const fullTitle = title ? `${title} | ${siteTitle}` : siteTitle;
const metaDescription = description || t('defaultDescription');
const ogImage = image || process.env.NEXT_PUBLIC_DEFAULT_OG_IMAGE;
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
return (
<Head>
<title>{fullTitle}</title>
<meta name="description" content={metaDescription} />
{/* Open Graph */}
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={metaDescription} />
<meta property="og:image" content={ogImage} />
<meta property="og:url" content={url || siteUrl} />
<meta property="og:type" content="website" />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={metaDescription} />
<meta name="twitter:image" content={ogImage} />
{/* Additional Meta */}
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="canonical" href={url || siteUrl} />
</Head>
);
}
Terminal window
# Install testing dependencies
pnpm add -D @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom
# Create test configuration
touch jest.config.js

Jest configuration:

jest.config.js
const nextJest = require('next/jest');
const createJestConfig = nextJest({
dir: './',
});
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
},
testEnvironment: 'jest-environment-jsdom',
};
module.exports = createJestConfig(customJestConfig);

Optimize for Core Web Vitals:

src/app/layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={inter.variable}>
<body className="font-sans antialiased">
{children}
</body>
</html>
);
}
src/lib/analytics.ts
import { GoogleAnalytics } from '@next/third-parties/google';
export function Analytics() {
const gaId = process.env.NEXT_PUBLIC_GA_ID;
if (!gaId) return null;
return <GoogleAnalytics gaId={gaId} />;
}
Terminal window
# Check CMS availability
curl https://cms.client-domain.com/api/health
# Verify API token
echo $CMS_API_TOKEN
Terminal window
# Clear Next.js cache
rm -rf .next
pnpm build
Terminal window
# Verify locale configuration
cat src/middleware.ts
cat messages/en.json
  • Weekly: Check site performance and analytics
  • Monthly: Update dependencies and security patches
  • Quarterly: Review and optimize Core Web Vitals
  • Annually: Audit and update SEO strategy
  • Uptime: Set up monitoring with services like UptimeRobot
  • Performance: Use Lighthouse CI for continuous monitoring
  • Analytics: Regular review of Google Analytics data
  • Errors: Set up error tracking with Sentry

After successful frontend installation:

  1. Configure Content - Set up your content structure in CMS
  2. Implement i18n - Complete internationalization setup
  3. Optimize SEO - Implement advanced SEO strategies