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:
markdown
# Documentation Title
Regular Markdown content...Custom Page:
tsx
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:
tsx
// 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:
tsx
// 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>
);
}css
/* 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:
tsx
// 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:
tsx
// 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:
tsx
// 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:
tsx
// 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:
tsx
// 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:
tsx
// 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:
tsx
// 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:
tsx
// 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
tsx
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}- Image Optimization
tsx
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:
tsx
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:
tsx
// 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
tsx
// 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
tsx
// Use Rspress Link component for proper routing
import { Link } from 'rspress/runtime';
<Link to="/guide">Go to Guide</Link>- State Management
tsx
// 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
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
Considerations
- Avoid using eval() in custom pages
- Sanitize all user inputs
- Handle sensitive data carefully
- Test cross-browser compatibility