Skip to content

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>&copy; 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

  1. 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>
  );
}
  1. 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

  1. 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>;
}
  1. Routing Issues
tsx
// Use Rspress Link component for proper routing
import { Link } from 'rspress/runtime';

<Link to="/guide">Go to Guide</Link>
  1. 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


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

Content is for learning and research only.