API Reference: Routing

Complete guide to MarkoPress routing system and custom routes


# API Reference: Routing

Complete guide to the MarkoPress routing system, including file-based routing, dynamic routes, and custom API routes.

# Overview

MarkoPress uses file-based routing powered by @marko/run. Routes are automatically generated from your content structure, and you can create custom routes for API endpoints and special functionality.

# How Routing Works

# Content Routes

Content files automatically become routes:

content/
├── pages/
│   ├── index.md          → /
│   ├── about.md          → /about
│   └── contact.md        → /contact
├── docs/
│   ├── intro.md          → /guides/intro
│   └── guide.md          → /guides/guide
└── blog/
    └── 2024-01-15-post.md → /blog/2024-01-15-post

# Route Metadata

Each route has metadata:

interface RouteData {
  path: string;              // URL path
  component?: string;        // Component path
  layout?: string;           // Layout to use
  meta?: {
    title?: string;
    description?: string;
    [key: string]: any;
  };
}

# File-Based Routing

# Pages

Create pages in content/pages/:

---
title: "About Us"
description: "Learn about our company"
---

# About Us

We are awesome!

Result: /about route

# Index Pages

Create index.md for directory roots:

---
title: "Documentation"
---

# Documentation

Welcome to the docs!

Result: /docs route (when placed in content/guides/index.md)

# Dynamic Routes

Create dynamic routes using [param] syntax:

content/
└── pages/
    └── posts/
        └── [slug].md    → /posts/:slug

Access the parameter in your component:

class {
  onInput(input) {
    const slug = input.params.slug;
    // Fetch post by slug
  }
}

# Custom Routes

Create custom routes in src/routes/.

# Route Handlers

Create src/routes/api/hello/+handler.ts:

export const GET = async function() {
  return new Response(JSON.stringify({ hello: 'world' }), {
    headers: { 'Content-Type': 'application/json' },
  });
};

Result: /api/hello endpoint

# Multiple Methods

export const GET = async function() {
  return new Response('GET request');
};

export const POST = async function(request: Request) {
  const body = await request.json();
  return new Response(JSON.stringify({ received: body }));
};

# Route Parameters

// src/routes/api/posts/[id]/+handler.ts
export const GET = async function({ params }) {
  const postId = params.id;
  const post = await getPost(postId);

  return new Response(JSON.stringify(post), {
    headers: { 'Content-Type': 'application/json' },
  });
};

Result: /api/posts/123 returns post with ID 123

# Query Parameters

export const GET = async function({ request }) {
  const url = new URL(request.url);
  const search = url.searchParams.get('q');
  const page = url.searchParams.get('page') || '1';

  // Search and paginate
  const results = await searchPosts(search, page);

  return new Response(JSON.stringify(results));
};

# Route Configuration

# Route Options

Configure route behavior in markopress.config.ts:

export default defineConfig({
  routes: {
    // Custom base path
    base: '/docs',

    // Trailing slash behavior
    trailingSlash: false,

    // Case sensitivity
    caseSensitive: false,
  },
});

# Route Aliases

Create aliases for routes:

export default defineConfig({
  routes: {
    aliases: {
      '/old-url': '/new-url',
      '/legacy/:path': '/:path',
    },
  },
});

# Route Redirects

Create redirects:

export default defineConfig({
  routes: {
    redirects: {
      '/old-post': '/new-post',
      '/legacy/:path*': '/new/:path*',
    },
  },
});

# Advanced Routing

# Middleware

Add middleware to routes:

// src/routes/middleware.ts
export const authMiddleware = async ({ request }, next) => {
  const token = request.headers.get('Authorization');

  if (!token) {
    return new Response('Unauthorized', { status: 401 });
  }

  return next();
};

// Use in route
export const GET = [
  authMiddleware,
  async function() {
    return new Response('Protected content');
  },
];

# Route Guards

Protect routes:

// src/routes/admin/+handler.ts
export const GET = async function({ request }) {
  const isAuthenticated = await checkAuth(request);

  if (!isAuthenticated) {
    return Response.redirect('/login');
  }

  return new Response('Admin dashboard');
};

# Error Handling

Handle errors gracefully:

export const GET = async function() {
  try {
    const data = await fetchData();
    return new Response(JSON.stringify(data));
  } catch (error) {
    return new Response(JSON.stringify({
      error: 'Failed to fetch data',
      message: error.message,
    }), { status: 500 });
  }
};

# SPA Mode

Enable Single Page Application mode:

export default defineConfig({
  spa: true, // Enable SPA mode

  spaFallback: '/app', // Fallback route
});

In SPA mode, all routes are handled client-side.

# Server-Side Rendering

MarkoPress automatically SSRs your pages for optimal performance and SEO.

# Disable SSR

Disable for specific routes:

// src/routes/+handler.ts
export const config = {
  ssr: false, // Client-side only
};

# Streaming

Enable streaming for slow pages:

export const config = {
  stream: true, // Stream response
};

# Route Groups

Organize routes with groups:

src/routes/
├── (auth)/
│   ├── login/
│   └── register/
├── (dashboard)/
│   ├── overview/
│   └── settings/
└── api/

Parentheses (name) create route groups without adding to the URL path.

# Layout Routes

Create layouts that wrap routes:

src/routes/
├── +layout.ts           # Root layout
├── (dashboard)/
│   ├── +layout.ts       # Dashboard layout
│   ├── overview/
│   └── settings/
└── (auth)/
    ├── +layout.ts       # Auth layout
    ├── login/
    └── register/

+layout.ts files automatically wrap child routes.

# API Routes Examples

# REST API

// src/routes/api/posts/+handler.ts

// GET /api/posts - List all posts
export const GET = async function({ request }) {
  const url = new URL(request.url);
  const page = parseInt(url.searchParams.get('page') || '1');
  const limit = parseInt(url.searchParams.get('limit') || '10');

  const posts = await getPosts({ page, limit });

  return new Response(JSON.stringify({
    data: posts,
    meta: { page, limit, total: posts.length },
  }));
};

// POST /api/posts - Create post
export const POST = async function({ request }) {
  const body = await request.json();
  const post = await createPost(body);

  return new Response(JSON.stringify(post), {
    status: 201,
    headers: { 'Content-Type': 'application/json' },
  });
};

# Search API

// src/routes/api/search/+handler.ts

export const GET = async function({ request }) {
  const url = new URL(request.url);
  const query = url.searchParams.get('q');

  if (!query) {
    return new Response(JSON.stringify({ error: 'Query required' }), {
      status: 400,
    });
  }

  const results = await searchContent(query);

  return new Response(JSON.stringify({
    query,
    results,
    count: results.length,
  }));
};

# RSS Feed

// src/routes/api/rss/+handler.ts

export const GET = async function() {
  const posts = await getPosts();

  const rss = generateRSS(posts);

  return new Response(rss, {
    headers: { 'Content-Type': 'application/xml' },
  });
};

# Sitemap

// src/routes/sitemap.xml/+handler.ts

export const GET = async function() {
  const routes = await getAllRoutes();

  const sitemap = generateSitemap(routes);

  return new Response(sitemap, {
    headers: { 'Content-Type': 'application/xml' },
  });
};

# Route Hooks

MarkoPress provides hooks for route lifecycle.

# Before Route

export const beforeRoute = async function({ request }) {
  // Run before route handler
  console.log('Incoming request:', request.url);
};

# After Route

export const afterRoute = async function({ response }) {
  // Run after route handler
  console.log('Response status:', response.status);
};

# Performance

# Route Caching

// Cache route responses
export const config = {
  cache: {
    maxAge: 3600, // 1 hour
    staleWhileRevalidate: 86400, // 1 day
  },
};

# Route Prefetching

<a href=/about prefetch=true>About</a>

# Code Splitting

Routes are automatically code-split. Lazy load components:

import LazyComponent from './LazyComponent.marko';

<if(input.shouldLoad)>
  <LazyComponent/>
</if>

# Examples

# JSON API

export const GET = async function() {
  return new Response(JSON.stringify({
    version: '1.0.0',
    name: 'MarkoPress API',
  }));
};

# File Upload

export const POST = async function({ request }) {
  const formData = await request.formData();
  const file = formData.get('file');

  await saveFile(file);

  return new Response(JSON.stringify({ success: true }));
};

# WebSocket

export const GET = async function({ request }) {
  const upgradeHeader = request.headers.get('Upgrade');

  if (upgradeHeader !== 'websocket') {
    return new Response('Expected WebSocket', { status: 426 });
  }

  // Upgrade to WebSocket
  return upgradeWebSocket(request);
};

# Best Practices

# 1. Use RESTful Conventions

GET    /api/posts      # List posts
GET    /api/posts/123  # Get post 123
POST   /api/posts      # Create post
PUT    /api/posts/123  # Update post 123
DELETE /api/posts/123  # Delete post 123

# 2. Handle Errors

export const GET = async function() {
  try {
    const data = await fetchData();
    return new Response(JSON.stringify(data));
  } catch (error) {
    return new Response(JSON.stringify({
      error: error.message,
    }), { status: 500 });
  }
};

# 3. Validate Input

export const POST = async function({ request }) {
  const body = await request.json();

  if (!body.title) {
    return new Response(JSON.stringify({
      error: 'Title is required',
    }), { status: 400 });
  }

  // Process...
};

# 4. Use Status Codes

return new Response('Not found', { status: 404 });
return new Response('Unauthorized', { status: 401 });
return new Response('Forbidden', { status: 403 });
return new Response('Server error', { status: 500 });

# Next Steps