🎸Framework Integration

Integrating MeshBase with Remix

Build performant, progressively-enhanced apps with Remix and MeshBase. Loaders, actions, nested routes—all the Remix patterns you need.

Why MeshBase + Remix?

🌐Web Standards First

Remix uses native fetch, FormData, and Web APIs. MeshBase integrates naturally with these standards.

Server-Side by Default

Loaders run server-side. Fetch MeshBase content server-side for optimal performance and SEO.

🔄Progressive Enhancement

Works without JavaScript. MeshBase content renders server-side, then enhances with client-side interactions.

🎯Nested Routes

Parallel data loading with nested routes. Fetch different MeshBase content types in parallel efficiently.

What You'll Need

  • Remix project (v2+)
  • A MeshBase account with content types defined
  • Your MeshBase API key

Quick Start

Fetch MeshBase content in Remix loaders.

1. Configure environment

Create .env:

MESHBASE_API_URL=https://api.meshbase.io/v1
MESHBASE_API_KEY=your-api-key-here

2. Create API client

Create app/lib/meshbase.server.ts:

// .server.ts ensures this only runs server-side
const API_URL = process.env.MESHBASE_API_URL;
const API_KEY = process.env.MESHBASE_API_KEY;

export interface BlogPost {
  id: string;
  title: string;
  excerpt: string;
  content: string;
  coverImage?: string;
  slug: string;
}

export async function fetchFromMeshBase<T>(endpoint: string): Promise<T> {
  const response = await fetch(`${API_URL}${endpoint}`, {
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
  });

  if (!response.ok) {
    throw new Response('Failed to fetch from MeshBase', {
      status: response.status
    });
  }

  const json = await response.json();
  return json.data;
}

export async function getBlogPosts(): Promise<BlogPost[]> {
  return fetchFromMeshBase('/blog-posts');
}

export async function getBlogPost(id: string): Promise<BlogPost> {
  return fetchFromMeshBase(`/blog-posts/${id}`);
}

3. Use in route loader

Create app/routes/blog._index.tsx:

import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData, Link } from '@remix-run/react';
import { getBlogPosts } from '~/lib/meshbase.server';

export async function loader({ request }: LoaderFunctionArgs) {
  const posts = await getBlogPosts();
  return json({ posts });
}

export default function BlogIndex() {
  const { posts } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>Blog Posts</h1>
      
      <div className="blog-list">
        {posts.map(post => (
          <article key={post.id}>
            <h2>
              <Link to={`/blog/${post.slug || post.id}`}>
                {post.title}
              </Link>
            </h2>
            <p>{post.excerpt}</p>
          </article>
        ))}
      </div>
    </div>
  );
}

You're Done!

Your Remix app now fetches MeshBase content server-side. Perfect SEO, fast initial load, progressively enhanced!

Dynamic Routes

Create dynamic blog post pages.

Create app/routes/blog.$slug.tsx:

import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { getBlogPost } from '~/lib/meshbase.server';

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getBlogPost(params.slug!);
  return json({ post });
}

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    { title: data?.post.title },
    { name: 'description', content: data?.post.excerpt }
  ];
};

export default function BlogPost() {
  const { post } = useLoaderData<typeof loader>();

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

Nested Routes & Parallel Loading

Load different MeshBase content types in parallel with nested routes.

Parent route

app/routes/blog.tsx (layout):

import { Outlet } from '@remix-run/react';

export default function BlogLayout() {
  return (
    <div>
      <header>
        <h1>My Blog</h1>
        {/* Sidebar, nav, etc. */}
      </header>
      
      <main>
        <Outlet /> {/* Child routes render here */}
      </main>
    </div>
  );
}

Child routes load in parallel

Both loaders run simultaneously:

  • blog.tsx - layout data
  • blog._index.tsx - blog post list

Waterfall Prevention

Remix loads nested routes in parallel. No request waterfalls—all MeshBase data fetches happen simultaneously!

Caching with Headers

Add cache control headers for better performance.

export async function loader({ request }: LoaderFunctionArgs) {
  const posts = await getBlogPosts();
  
  return json(
    { posts },
    {
      headers: {
        'Cache-Control': 'public, max-age=60, s-maxage=3600'
      }
    }
  );
}

Cache-Control explained:

  • max-age=60 - Browser cache for 60s
  • s-maxage=3600 - CDN cache for 1 hour

Actions (Creating Content)

Use Remix actions to create/update MeshBase content (admin key required).

import type { ActionFunctionArgs } from '@remix-run/node';
import { redirect } from '@remix-run/node';

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  
  const title = formData.get('title');
  const content = formData.get('content');

  // Create post via MeshBase API (use admin key!)
  await fetch(`${API_URL}/blog-posts`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${ADMIN_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ title, content })
  });

  return redirect('/blog');
}

export default function NewPost() {
  return (
    <form method="post">
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create Post</button>
    </form>
  );
}
🔒

Admin Key Required

Creating/updating content requires your MeshBase admin API key. Never expose this in client-side code—keep it server-side only!

Next Steps