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.

Fetched on the server

  • sunt aut facere repellat provident occaecati excepturi optio reprehenderit

    quia et suscipit suscipit recusandae consequuntur expedita et cum reprehenderit molestiae ut ut quas totam

  • qui est esse

    est rerum tempore vitae sequi sint nihil reprehenderit dolor beatae ea dolores neque fugiat blanditiis voluptate

  • ea molestias quasi exercitationem repellat qui ipsa sit aut

    et iusto sed quo iure voluptatem occaecati omnis eligendi aut ad voluptatem doloribus vel accusantium quis pariatur

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