Layouts

Layouts let you wrap templates with consistent headers, footers, and styling. A child template specifies a layout in its meta, and the layout receives the child content as a children prop.

Basic Usage

Layout Template

Create a layout template that receives children:

// .loopwind/base-layout/template.tsx
export const meta = {
  name: 'base-layout',
  type: 'image',
  size: { width: 1200, height: 630 },
  props: {},
};

export default function BaseLayout({ tw, children }) {
  return (
    <div style={tw('flex flex-col w-full h-full bg-background')}>
      {/* Header */}
      <div style={tw('flex items-center px-8 py-4 border-b border-border')}>
        <span style={tw('text-2xl font-bold text-primary')}>loopwind</span>
      </div>

      {/* Content slot */}
      <div style={tw('flex flex-1')}>
        {children}
      </div>

      {/* Footer */}
      <div style={tw('flex items-center justify-between px-8 py-4 border-t border-border')}>
        <span style={tw('text-muted-foreground')}>loopwind.dev</span>
      </div>
    </div>
  );
}

Usage in Templates

Reference the layout using a relative path:

// .loopwind/blog-post/template.tsx
export const meta = {
  name: 'blog-post',
  type: 'image',
  layout: '../base-layout', // Layout controls size
  props: {
    title: 'string',
    excerpt: 'string',
  },
};

export default function BlogPost({ tw, title, excerpt }) {
  return (
    <div style={tw('flex flex-col justify-center p-12')}>
      <h1 style={tw('text-5xl font-bold text-foreground mb-4 text-balance')}>
        {title}
      </h1>
      <p style={tw('text-xl text-muted-foreground leading-relaxed')}>
        {excerpt}
      </p>
    </div>
  );
}

Render

loopwind render blog-post '{"title":"Hello World","excerpt":"My first post"}'

The output uses the layout’s size (1200x630) with the child content inside.


Key Concepts

Size

When using a layout, the layout’s size controls the final output dimensions. The child template doesn’t need a size property.

Path Resolution

Use relative paths to reference layouts:

layout: '../base-layout'      // Sibling directory
layout: './shared/layout'     // Subdirectory
layout: '../../layouts/main'  // Parent's sibling

Props Flow

The layout receives:

  • All standard helpers (tw, image, qr, template, etc.)
  • children prop containing the rendered child content
  • Animation context (frame, progress) for video layouts
export default function Layout({ tw, children, frame, progress }) {
  // tw, image, qr, template, path, textPath all available
  return (
    <div style={tw('flex w-full h-full')}>
      {children}
    </div>
  );
}

Video Layouts

Layouts work with video templates. Both the layout and child can use animations:

// .loopwind/video-layout/template.tsx
export const meta = {
  name: 'video-layout',
  type: 'video',
  size: { width: 1920, height: 1080 },
  video: { fps: 60, duration: 4 },
  props: {},
};

export default function VideoLayout({ tw, children }) {
  return (
    <div style={tw('flex flex-col w-full h-full bg-background')}>
      {/* Animated header */}
      <div style={tw('flex items-center px-12 py-6 ease-out enter-slide-down/0/500')}>
        <span style={tw('text-3xl font-bold text-primary')}>loopwind</span>
      </div>

      {/* Content */}
      <div style={tw('flex flex-1')}>
        {children}
      </div>

      {/* Animated footer */}
      <div style={tw('flex px-12 py-6 ease-out enter-fade-in/500/400')}>
        <span style={tw('text-muted-foreground')}>loopwind.dev</span>
      </div>
    </div>
  );
}

Example: Consistent OG Images

Create a layout for all your OG images:

// .loopwind/og-layout/template.tsx
export const meta = {
  name: 'og-layout',
  type: 'image',
  size: { width: 1200, height: 630 },
  props: {},
};

export default function OGLayout({ tw, image, children }) {
  return (
    <div style={tw('flex w-full h-full bg-background')}>
      {/* Content area */}
      <div style={tw('flex flex-col flex-1 p-12')}>
        {/* Logo */}
        <div style={tw('flex items-center gap-3 mb-auto')}>
          <img src={image('logo.svg')} style={tw('h-10 w-auto')} />
          <span style={tw('text-2xl font-bold')}>MyBrand</span>
        </div>

        {/* Slot for page-specific content */}
        <div style={tw('flex flex-1 items-center')}>
          {children}
        </div>

        {/* Domain */}
        <span style={tw('text-muted-foreground mt-auto')}>mybrand.com</span>
      </div>
    </div>
  );
}

Then create page-specific templates:

// .loopwind/og-blog/template.tsx
export const meta = {
  name: 'og-blog',
  type: 'image',
  layout: '../og-layout',
  props: {
    title: 'string',
    author: 'string',
  },
};

export default function OGBlog({ tw, title, author }) {
  return (
    <div style={tw('flex flex-col')}>
      <span style={tw('text-sm text-muted-foreground uppercase tracking-wider mb-2')}>
        Blog Post
      </span>
      <h1 style={tw('text-4xl font-bold text-foreground mb-4 text-balance')}>
        {title}
      </h1>
      <span style={tw('text-muted-foreground')}>By {author}</span>
    </div>
  );
}

Next Steps

  • Templates - Template structure and metadata
  • Animation - Animation classes for video layouts
  • Helpers - Using image(), qr(), and template()