AvalonAvalon
GitHub

Data Loading

Fetching data in layouts and pages during server-side rendering.

Avalon pages and layouts are async server components. You can fetch data directly in the component function — no special loader API needed.

Fetching in pages

Since pages render on the server, you can use fetch, database queries, or any async operation directly:

export default async function BlogIndex() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>
          <a href={`/blog/${post.slug}`}>{post.title}</a>
        </li>
      ))}
    </ul>
  );
}

This runs entirely on the server. The client receives pure HTML with zero JavaScript.

Fetching in layouts

Layouts are also async. Use this for data that's shared across multiple pages, like navigation items or user session info:

import type { LayoutProps } from '@useavalon/avalon';

export default async function DocsLayout({ children, frontmatter }: LayoutProps) {
  const nav = await fetch('https://api.example.com/docs-nav').then(r => r.json());

  return (
    <div class="docs-layout">
      <aside>
        {nav.map((item: any) => (
          <a href={item.href}>{item.title}</a>
        ))}
      </aside>
      <main>{children}</main>
    </div>
  );
}

Using route parameters

Access dynamic route parameters via the event prop:

// pages/blog/[slug].tsx
export default async function BlogPost({ event }: { event: any }) {
  const slug = event.context.params?.slug;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Passing data to islands

Islands hydrate on the client, so they can't call server-side APIs directly. Pass fetched data as serializable props:

export default async function Dashboard() {
  const stats = await fetch('https://api.example.com/stats').then(r => r.json());

  return (
    <div>
      <h1>Dashboard</h1>
      <StatsChart island={{ condition: 'on:visible' }} data={stats} />
    </div>
  );
}

The data prop is serialized to JSON and sent to the client for hydration. Keep props small — avoid passing entire database records when you only need a few fields.

Error handling

Wrap data fetching in try/catch to handle failures gracefully:

export default async function BlogPost({ event }: { event: any }) {
  const slug = event.context.params?.slug;

  try {
    const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => {
      if (!r.ok) throw new Error(`Post not found: ${slug}`);
      return r.json();
    });

    return <article><h1>{post.title}</h1></article>;
  } catch {
    return <div><h1>Post not found</h1><a href="/blog">Back to blog</a></div>;
  }
}

Streaming

When your page fetches data, Avalon streams the layout shell (head, CSS, navigation) to the browser before the data resolves. The user sees the page layout instantly while the server waits on your API call. This happens automatically — no configuration or special APIs needed. See Streaming SSR for details.

Caching

For data that doesn't change often, use Nitro's route rules to cache the entire response:

// vite.config.ts
nitro: {
  routeRules: {
    '/blog/**': {
      cache: { maxAge: 300 }, // 5 minutes
    },
  },
}

Static data

For data known at build time (like a sidebar config or feature flags), import it directly — no fetch needed:

import { SIDEBAR } from '../utils/sidebar';

export default function DocsLayout({ children }: LayoutProps) {
  return (
    <div>
      <nav>{SIDEBAR.map(item => <a href={item.href}>{item.title}</a>)}</nav>
      <main>{children}</main>
    </div>
  );
}