#Custom Pages
Rspress allows you to create custom page layouts that go beyond the default documentation format. This chapter explores how to build unique, custom pages using React components.
#What are Custom Pages?
#Overview
Custom pages allow you to create pages that don't follow the standard documentation layout. They're perfect for:
- Landing pages
- Product showcases
- Dashboards
- Interactive demos
- Marketing pages
- Custom tools
#Standard Page vs Custom Page
Standard Page:
# Documentation Title
Regular Markdown content...Custom Page:
export default function CustomPage() {
return (
<div className="custom-layout">
<Hero />
<Features />
<CTA />
</div>
);
}#Creating Custom Pages
#Basic Custom Page
Create a page with custom layout:
// docs/custom-page.tsx
import React from 'react';
export default function CustomPage() {
return (
<div className="custom-page">
<header className="hero">
<h1>Welcome to My Project</h1>
<p>Build amazing documentation with Rspress</p>
<button className="cta-button">Get Started</button>
</header>
<section className="features">
<div className="feature">
<h3>Fast</h3>
<p>Lightning-fast builds</p>
</div>
<div className="feature">
<h3>Flexible</h3>
<p>Fully customizable</p>
</div>
<div className="feature">
<h3>Modern</h3>
<p>Modern developer experience</p>
</div>
</section>
</div>
);
}#Custom Page with Styling
Add styling with CSS modules:
// docs/landing.tsx
import React from 'react';
import styles from './landing.module.css';
export default function LandingPage() {
return (
<div className={styles.container}>
<div className={styles.hero}>
<h1 className={styles.title}>Next Generation Documentation</h1>
<p className={styles.subtitle}>
Fast, modern, and powerful
</p>
<div className={styles.actions}>
<a href="/guide/quick-start" className={styles.primary}>
Quick Start
</a>
<a href="/guide/introduction" className={styles.secondary}>
Learn More
</a>
</div>
</div>
</div>
);
}/* docs/landing.module.css */
.container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.hero {
text-align: center;
max-width: 900px;
padding: 0 20px;
}
.title {
font-size: 4rem;
font-weight: bold;
margin-bottom: 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 1.5rem;
color: #666;
margin-bottom: 2rem;
}
.actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.primary, .secondary {
padding: 12px 32px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
transition: all 0.3s;
}
.primary {
background: #667eea;
color: white;
}
.primary:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.secondary {
background: white;
color: #667eea;
border: 2px solid #667eea;
}
.secondary:hover {
background: #f0f0f0;
}#Page Layout Patterns
#Full-Width Layout
Create full-width layouts without container constraints:
// docs/showcase.tsx
import React from 'react';
export default function Showcase() {
return (
<div style={{ width: '100vw', margin: 0, padding: 0 }}>
<section style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: '100px 20px',
color: 'white',
textAlign: 'center'
}}>
<h1>Showcase</h1>
<p>Amazing projects built with our technology</p>
</section>
<section style={{ padding: '60px 20px' }}>
<div style={{
maxWidth: '1200px',
margin: '0 auto',
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '20px'
}}>
<ProjectCard
title="Project 1"
description="Amazing documentation site"
image="/project1.png"
/>
<ProjectCard
title="Project 2"
description="Interactive guide"
image="/project2.png"
/>
<ProjectCard
title="Project 3"
description="API documentation"
image="/project3.png"
/>
</div>
</section>
</div>
);
}
function ProjectCard({ title, description, image }) {
return (
<div style={{
border: '1px solid #eee',
borderRadius: '8px',
overflow: 'hidden',
transition: 'transform 0.3s'
}}>
<img src={image} alt={title} style={{ width: '100%', height: '200px', objectFit: 'cover' }} />
<div style={{ padding: '20px' }}>
<h3>{title}</h3>
<p>{description}</p>
</div>
</div>
);
}#Multi-Column Layout
Create multi-column layouts:
// docs/comparison.tsx
import React from 'react';
export default function Comparison() {
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '40px 20px' }}>
<h1 style={{ textAlign: 'center', marginBottom: '60px' }}>
Choose the Right Plan
</h1>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '30px'
}}>
<PricingCard
title="Free"
price="$0"
features={[
'Up to 5 projects',
'Basic support',
'Community access'
]}
/>
<PricingCard
title="Pro"
price="$29"
features={[
'Unlimited projects',
'Priority support',
'Advanced features',
'Custom domain'
]}
highlighted
/>
<PricingCard
title="Enterprise"
price="Contact us"
features={[
'Everything in Pro',
'Dedicated support',
'SLA guarantee',
'Custom development'
]}
/>
</div>
</div>
);
}
function PricingCard({ title, price, features, highlighted = false }) {
return (
<div style={{
border: highlighted ? '3px solid #667eea' : '1px solid #eee',
borderRadius: '12px',
padding: '30px',
textAlign: 'center',
position: 'relative',
transform: highlighted ? 'scale(1.05)' : 'scale(1)',
boxShadow: highlighted ? '0 10px 40px rgba(0,0,0,0.1)' : 'none'
}}>
{highlighted && (
<div style={{
position: 'absolute',
top: '-15px',
left: '50%',
transform: 'translateX(-50%)',
background: '#667eea',
color: 'white',
padding: '5px 20px',
borderRadius: '20px',
fontSize: '12px',
fontWeight: 'bold'
}}>
MOST POPULAR
</div>
)}
<h3 style={{ fontSize: '24px', marginBottom: '10px' }}>{title}</h3>
<div style={{ fontSize: '36px', fontWeight: 'bold', marginBottom: '20px' }}>
{price}
</div>
<ul style={{ listStyle: 'none', padding: 0, marginBottom: '30px' }}>
{features.map((feature, i) => (
<li key={i} style={{ padding: '10px 0', borderBottom: '1px solid #f0f0f0' }}>
✓ {feature}
</li>
))}
</ul>
<button style={{
width: '100%',
padding: '12px',
background: highlighted ? '#667eea' : '#f0f0f0',
color: highlighted ? 'white' : '#333',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
fontWeight: '600',
cursor: 'pointer'
}}>
Get Started
</button>
</div>
);
}#Interactive Pages
#Interactive Page with State
Create pages with interactivity:
// docs/demo.tsx
import React, { useState } from 'react';
export default function DemoPage() {
const [activeTab, setActiveTab] = useState('overview');
const [code, setCode] = useState('console.log("Hello, World!");');
const [output, setOutput] = useState('');
const runCode = () => {
try {
// Warning: eval is unsafe in production
const result = eval(code);
setOutput(String(result));
} catch (error) {
setOutput(`Error: ${error.message}`);
}
};
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '40px 20px' }}>
<h1>Interactive Demo</h1>
{/* Tab Navigation */}
<div style={{
display: 'flex',
gap: '10px',
borderBottom: '2px solid #eee',
marginBottom: '30px'
}}>
{['Overview', 'Code', 'Output'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab.toLowerCase())}
style={{
padding: '10px 20px',
background: 'none',
border: 'none',
borderBottom: activeTab === tab.toLowerCase() ? '3px solid #667eea' : 'none',
color: activeTab === tab.toLowerCase() ? '#667eea' : '#666',
fontWeight: activeTab === tab.toLowerCase() ? 'bold' : 'normal',
cursor: 'pointer'
}}
>
{tab}
</button>
))}
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div>
<h2>Overview</h2>
<p>This is an interactive code playground. Try writing some JavaScript and running it!</p>
</div>
)}
{activeTab === 'code' && (
<div>
<h2>Code Editor</h2>
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
style={{
width: '100%',
height: '300px',
fontFamily: 'monospace',
fontSize: '14px',
padding: '15px',
border: '1px solid #ddd',
borderRadius: '8px'
}}
/>
<button
onClick={runCode}
style={{
marginTop: '10px',
padding: '10px 20px',
background: '#667eea',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer'
}}
>
Run Code
</button>
</div>
)}
{activeTab === 'output' && (
<div>
<h2>Output</h2>
<pre style={{
background: '#f5f5f5',
padding: '20px',
borderRadius: '8px',
minHeight: '100px'
}}>
{output || 'No output yet'}
</pre>
</div>
)}
</div>
);
}#Form Page
Create forms with validation:
// docs/contact.tsx
import React, { useState } from 'react';
export default function ContactPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [errors, setErrors] = useState({});
const [submitted, setSubmitted] = useState(false);
const validate = () => {
const newErrors = {};
if (!formData.name) newErrors.name = 'Name is required';
if (!formData.email) newErrors.email = 'Email is required';
else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.message) newErrors.message = 'Message is required';
return newErrors;
};
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length === 0) {
// Submit form
console.log('Form submitted:', formData);
setSubmitted(true);
} else {
setErrors(newErrors);
}
};
if (submitted) {
return (
<div style={{
maxWidth: '600px',
margin: '100px auto',
textAlign: 'center',
padding: '40px',
background: '#f0f8ff',
borderRadius: '12px'
}}>
<h2>Thank You!</h2>
<p>Your message has been sent successfully. We'll get back to you soon.</p>
</div>
);
}
return (
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '40px 20px' }}>
<h1>Contact Us</h1>
<p>Have questions? We'd love to hear from you.</p>
<form onSubmit={handleSubmit} style={{ marginTop: '30px' }}>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Name
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
style={{
width: '100%',
padding: '10px',
border: errors.name ? '2px solid red' : '1px solid #ddd',
borderRadius: '6px'
}}
/>
{errors.name && <span style={{ color: 'red', fontSize: '14px' }}>{errors.name}</span>}
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Email
</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
style={{
width: '100%',
padding: '10px',
border: errors.email ? '2px solid red' : '1px solid #ddd',
borderRadius: '6px'
}}
/>
{errors.email && <span style={{ color: 'red', fontSize: '14px' }}>{errors.email}</span>}
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Message
</label>
<textarea
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
rows={6}
style={{
width: '100%',
padding: '10px',
border: errors.message ? '2px solid red' : '1px solid #ddd',
borderRadius: '6px',
fontFamily: 'inherit'
}}
/>
{errors.message && <span style={{ color: 'red', fontSize: '14px' }}>{errors.message}</span>}
</div>
<button
type="submit"
style={{
padding: '12px 40px',
background: '#667eea',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
fontWeight: '600',
cursor: 'pointer'
}}
>
Send Message
</button>
</form>
</div>
);
}#Using Rspress Features
#Accessing Theme Context
Use theme features in custom pages:
// docs/themed-page.tsx
import React from 'react';
import { usePageData } from 'rspress/runtime';
export default function ThemedPage() {
const pageData = usePageData();
return (
<div style={{ padding: '40px 20px' }}>
<h1>Page Information</h1>
<p>Current page: {pageData.page.title}</p>
<p>Route path: {pageData.page.routePath}</p>
<h2>Site Configuration</h2>
<p>Site title: {pageData.siteData.title}</p>
<p>Site description: {pageData.siteData.description}</p>
</div>
);
}#Including Navigation
Add navigation to custom pages:
// docs/custom-with-nav.tsx
import React from 'react';
import { Link } from 'rspress/runtime';
export default function CustomWithNav() {
return (
<div>
{/* Custom Navigation */}
<nav style={{
background: '#667eea',
padding: '20px',
color: 'white'
}}>
<div style={{
maxWidth: '1200px',
margin: '0 auto',
display: 'flex',
gap: '30px',
alignItems: 'center'
}}>
<Link to="/" style={{ color: 'white', textDecoration: 'none', fontWeight: 'bold' }}>
Home
</Link>
<Link to="/guide" style={{ color: 'white', textDecoration: 'none' }}>
Guide
</Link>
<Link to="/api" style={{ color: 'white', textDecoration: 'none' }}>
API
</Link>
</div>
</nav>
{/* Page Content */}
<div style={{ maxWidth: '1200px', margin: '40px auto', padding: '0 20px' }}>
<h1>Custom Content</h1>
<p>This page has custom navigation but still uses Rspress routing.</p>
</div>
</div>
);
}#Page Templates
#Creating Reusable Templates
Build reusable page templates:
// components/PageTemplate.tsx
import React from 'react';
interface PageTemplateProps {
title: string;
subtitle?: string;
children: React.ReactNode;
fullWidth?: boolean;
}
export function PageTemplate({
title,
subtitle,
children,
fullWidth = false
}: PageTemplateProps) {
return (
<div>
{/* Header */}
<header style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: '60px 20px',
color: 'white',
textAlign: 'center'
}}>
<h1 style={{ fontSize: '3rem', marginBottom: '10px' }}>{title}</h1>
{subtitle && <p style={{ fontSize: '1.25rem', opacity: 0.9 }}>{subtitle}</p>}
</header>
{/* Content */}
<main style={{
maxWidth: fullWidth ? '100%' : '1200px',
margin: '0 auto',
padding: '40px 20px'
}}>
{children}
</main>
{/* Footer */}
<footer style={{
background: '#f5f5f5',
padding: '40px 20px',
textAlign: 'center',
marginTop: '60px'
}}>
<p>© 2024 My Project. All rights reserved.</p>
</footer>
</div>
);
}Using the template:
// docs/features.tsx
import React from 'react';
import { PageTemplate } from '../components/PageTemplate';
export default function FeaturesPage() {
return (
<PageTemplate
title="Features"
subtitle="Explore the features that make our product powerful"
>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '30px',
marginTop: '40px'
}}>
<FeatureCard
icon="⚡"
title="Lightning Fast"
description="Built with Vite for incredibly fast dev and build times"
/>
<FeatureCard
icon="🎨"
title="Customizable"
description="Full control over theme and styling"
/>
<FeatureCard
icon="📱"
title="Responsive"
description="Works perfectly on all devices"
/>
</div>
</PageTemplate>
);
}
function FeatureCard({ icon, title, description }) {
return (
<div style={{
padding: '30px',
border: '1px solid #eee',
borderRadius: '12px',
textAlign: 'center',
transition: 'transform 0.3s, box-shadow 0.3s'
}}>
<div style={{ fontSize: '3rem', marginBottom: '20px' }}>{icon}</div>
<h3 style={{ marginBottom: '10px' }}>{title}</h3>
<p style={{ color: '#666' }}>{description}</p>
</div>
);
}#Best Practices
#Performance Optimization
- Code Splitting
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}- Image Optimization
import { Image } from 'rspress/runtime';
export default function Page() {
return (
<Image
src="/large-image.jpg"
alt="Description"
loading="lazy"
width={800}
height={600}
/>
);
}#Responsive Design
Use media queries for responsive pages:
export default function ResponsivePage() {
return (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '20px',
padding: '20px'
}}>
{/* Content automatically adjusts to screen size */}
</div>
);
}#SEO Optimization
Add metadata for custom pages:
// docs/landing.tsx
import { Head } from 'rspress/runtime';
export default function LandingPage() {
return (
<>
<Head>
<title>My Landing Page - Project Name</title>
<meta name="description" content="This is an amazing landing page" />
<meta property="og:title" content="My Landing Page" />
<meta property="og:description" content="This is an amazing landing page" />
</Head>
<div>{/* Page content */}</div>
</>
);
}#Troubleshooting
#Common Issues
- Style Conflicts
// Use CSS modules to avoid conflicts
import styles from './page.module.css';
export default function Page() {
return <div className={styles.container}>Content</div>;
}- Routing Issues
// Use Rspress Link component for proper routing
import { Link } from 'rspress/runtime';
<Link to="/guide">Go to Guide</Link>- State Management
// Use React Context for complex state
import { createContext, useContext } from 'react';
const AppContext = createContext();
export function useAppContext() {
return useContext(AppContext);
}#Next Steps
- Learn about Custom Theme
- Explore Built-in Components
- Read about Build Extensions
::: tip Best Practices
- Keep custom pages performant
- Use TypeScript for custom pages
- Implement proper error handling
- Test responsiveness on all devices
- Add proper metadata for SEO :::
::: warning Considerations
- Avoid using eval() in custom pages
- Sanitize all user inputs
- Handle sensitive data carefully
- Test cross-browser compatibility :::
::: info Resources