> ## Documentation Index
> Fetch the complete documentation index at: https://www.lettercamp.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom layouts

> Arrange Lettercamp's default components in your own layouts, replace individual site regions, and understand how settings and fallback behavior work.

Layouts arrange components. Components provide their default appearance and
behavior. Start with [design settings](https://www.lettercamp.com/docs/design/homepage.md), then copy a starter
layout when you need to change the arrangement or add your own UI.

Your articles stay in Markdown or MDX. Lettercamp handles routing, SEO, search
indexing, and publishing, including when you replace the presentation.

## Layouts and elements

| Type | Where it lives | How it is used |
| - | - | - |
| Layout override | `components/layouts/` | Lettercamp detects the five filenames below automatically. |
| Lettercamp UI element | Import from `lettercamp/components` | Render a default header, card, TOC, or other standard control. |
| Your React component | Anywhere else under `components/`, such as `components/elements/` | Import it into a layout or MDX article. |
| Content block | Inside `pages/*.md` or `pages/*.mdx` | Write images, tables, code, FAQs, and other article content. |

```text theme={null}
components/
├── layouts/
│   ├── Header.jsx
│   └── PostLayout.jsx
└── elements/
    ├── NewsletterSignup.jsx
    └── NewsletterSignup.module.scss
```

`components/elements/` is a convention for organizing your own components, not
an automatically detected directory. A component there only appears when imported
and rendered. Keep styles beside the component they belong to.

## Override files and fallback

Each override is optional and independent:

| File | Replaces | When absent |
| - | - | - |
| `components/layouts/Header.jsx` | Site header on every page | Default header, configured by `site.header` |
| `components/layouts/Footer.jsx` | Site footer on every page | Default footer, configured by `site.footer` |
| `components/layouts/PostList.jsx` | Homepage, category, and paginated listings | Default listing, configured by `site.home` |
| `components/layouts/PostCard.jsx` | Cards in default listings and related posts | Default post card |
| `components/layouts/PostLayout.jsx` | Article layout between the site header and footer | Default article layout, configured by `site.post` |

Use `.jsx` or `.tsx` with a default export. Do not create both extensions for the
same override. Older flat paths such as `components/Header.jsx` still work;
`components/layouts/Header.jsx` takes precedence when both exist. Remove both
versions to restore the built-in header. Other regions keep their own defaults.

A broken override reports an error; it does not silently fall back. Returning
`null` intentionally hides that region. Omitting a TOC or pagination from a custom
layout also leaves it out; Lettercamp does not insert missing controls.

## Try the starter examples

The [bundled starter](https://github.com/lettercamp/lettercamp/tree/main/core/starter)
includes `examples/layouts/`. These examples use the same elements and default
styles as the built-in site. They are inactive until copied into
`components/layouts/`.

Copy only the layout you want to edit:

```sh theme={null}
mkdir -p components/layouts
cp examples/layouts/PostLayout.jsx components/layouts/
npx lettercamp dev
```

Or copy all five examples:

```sh theme={null}
mkdir -p components/layouts
cp examples/layouts/*.jsx components/layouts/
```

You own their arrangement. Updating Lettercamp updates the imported components;
it does not rewrite your layout files or rearrange your custom page.

## Keep a default element

The public `Header`, `Footer`, and `PostCard` always render Lettercamp's default
element. Import one with an alias to distinguish it from your override:

```jsx theme={null}
// components/layouts/Header.jsx
import { Header as DefaultHeader } from 'lettercamp/components';

export default function Header(props) {
  return <DefaultHeader {...props} />;
}
```

This renders the same header as having no override. You can add markup around it,
pass a CSS Module class, or replace it entirely. It does not call your override
again. See [UI components](https://www.lettercamp.com/docs/ui-components.md) for props and styling hooks.

## Post list

A custom list owns its heading, navigation, cards, empty state, and pagination.
Use the supplied `PostCard` to honor your card override and its default fallback:

```jsx theme={null}
import { SearchButton, CategoryNavigation, Pagination } from 'lettercamp/components';

export default function PostList({ site, posts, categories, category, pagination, PostCard }) {
  const home = site.home ?? {};
  const width = Number.isFinite(home.contentWidthPx) && home.contentWidthPx > 0 ? home.contentWidthPx : 1280;
  const readingWidth = Number.isFinite(site.post?.contentWidthEm) && site.post.contentWidthEm > 0 ? site.post.contentWidthEm : 40;
  const count = pagination.count;
  return <div className="blog-home" data-blog-home="" data-home-layout={home.feedLayout ?? 'grid'}
    data-image-content-width={readingWidth * (Number(site.design?.typography?.bodyBaseSizePx) || 16)}
    style={{ '--home-content-width': `${width}px`, '--post-content-width': `calc(var(--text-base) * ${readingWidth})` }}>
    <header className="blog-heading">
      <h1>{category || (home.title ?? `${site.name} Blog`)}</h1>
      {site.description && <p className="description" style={{ marginTop: 12 }}>{site.description}</p>}
    </header>
    <div className="blog-toolbar">
      {categories.length ? <CategoryNavigation categories={categories} category={category} /> : <p className="post-count">{count} {count === 1 ? 'post' : 'posts'}</p>}
      <SearchButton />
    </div>
    <p className="sr-only">{count} articles — Page {pagination.current} of {pagination.total}</p>
    <section aria-label="Recent articles">
      <h2 className="sr-only">Recent articles</h2>
      <div className="post-list">
        {posts.map((post, index) => <PostCard key={post.href} post={post} priority={index === 0} />)}
      </div>
      {!posts.length && <p className="empty-state">No articles here yet.</p>}
    </section>
    <Pagination {...pagination} />
  </div>;
}
```

| Prop | Value |
| - | - |
| `posts` | Posts on the current page |
| `categories` | Category objects with `name` and resolved `href` |
| `category` | Current category name, or an empty string |
| `pagination` | `current`, `total`, `base`, `count`, `previous`, and `next` |
| `PostCard` | Resolved custom or default card; pass `post` and optionally `priority` |

An imported `PostCard` from `lettercamp/components` always uses the standard card.
The `PostCard` prop uses your override when present. Choose the prop when you want
your card design to apply throughout the publication.

## Post card

Start with the standard card:

```jsx theme={null}
import { PostCard as DefaultPostCard } from 'lettercamp/components';

// Keep the default element, or replace this markup with your own composition.
export default function PostCard(props) {
  return <DefaultPostCard {...props} />;
}
```

Or render your own markup from `post`. The card receives `site`, `pathname`,
`basePath`, and `priority` in addition to `post`. Set eager image loading when
`priority` is true. Search results use their own presentation.

## Post layout

The starter composes the standard article header, TOC, article body, and related
posts. Add your own component directly where you want it, for example immediately
after `{children}`:

```jsx theme={null}
import { PostHeader, TableOfContents, RelatedPosts } from 'lettercamp/components';

export default function PostLayout({ site, post, headings, relatedPosts, pathname, children }) {
  const layout = site.post ?? {};
  const width = Number.isFinite(layout.contentWidthEm) && layout.contentWidthEm > 0 ? layout.contentWidthEm : 40;
  return <article className="publication publication-post" data-post-hero={layout.heroLayout ?? 'stacked'}
    data-reading-minutes={post.readingMinutes}
    data-image-content-width={width * (Number(site.design?.typography?.bodyBaseSizePx) || 16)}
    data-post-toc={layout.showTableOfContents === false ? 'hidden' : 'visible'}
    style={{ '--post-content-width': `calc(var(--text-base) * ${width})` }}>
    <PostHeader site={site} post={post} />
    <div className={`article-body ${headings.length ? 'article-body-with-toc' : ''}`}>
      <TableOfContents site={site} headings={headings} variant={layout.heroLayout === 'split' ? 'sidebar' : 'floating'} />
      {children}
    </div>
    <RelatedPosts site={site} posts={relatedPosts} pathname={pathname} />
  </article>;
}
```

`PostHeader` includes the title, description, category, cover, byline, and sharing.
`TableOfContents` defaults to the floating control on the left. The starter
explicitly selects `sidebar` for the horizontal article layout.

**Render `children` once.** Lettercamp supplies the rendered body and its built-in
block styles. Keep article content in `pages/`; do not parse Markdown in the layout
or copy article text into React code.

| Prop | Value |
| - | - |
| `post` | Normalized article metadata |
| `children` | Managed article body; render once without modifying its internal markup |
| `headings` | Level 2 and 3 headings with `depth`, `slug`, and `text` |
| `relatedPosts` | Up to three related posts, excluding the current article |
| `PostCard` | Resolved custom or default card, also used internally by `RelatedPosts` |

## Shared props and post data

All five overrides receive:

| Prop | Value |
| - | - |
| `site` | `settings.json` with default branding applied |
| `pathname` | The current page URL path, including a publication subfolder |
| `basePath` | Publication subfolder, such as `/blog`, or an empty string |

A `post` includes the article's frontmatter plus these normalized fields:

| Field | Value |
| - | - |
| `title` | Article title |
| `href` | Post URL path, including the publication subfolder |
| `description`, `category` | Article metadata, when present |
| `image` | Resolved `image` or `ogImage`, when present |
| `imageAlt` | Authored image alternative text, when present |
| `authors` | Array of `{ name, image? }`, with resolved image paths |
| `date` | `{ iso, label, timestamp }`; an absent date has empty `iso` and `label` |
| `readingMinutes` | Lettercamp's calculated reading time |

Use supplied URLs directly. Do not prepend `basePath` again to post links,
images, category links, or pagination URLs. Use `basePath` when creating your own
publication-relative navigation. Absolute URLs can link to the parent website.

Components render during the build and hydrate in the browser. Use browser APIs
such as `window` inside effects or event handlers, not during rendering. Keep
these components compatible with browser execution for online previews.

## Settings and defaults

Pass the layout's `site` to settings-aware components so they use live settings
in the editor preview. `PostHeader` honors hero layout, cover visibility, reading
width, and reading-time visibility. `TableOfContents` and `RelatedPosts` honor
their visibility settings. The default header and footer honor their settings.

Your own markup only follows settings you use in code. The starter layouts show
how to retain reading width, listing layout, and the standard components. You can
intentionally replace those choices with your own design.

The online design customizer identifies custom layouts and replaces the built-in
layout picker for a custom list or article. Other controls apply where your
components use them. Removing an override restores the saved default settings.

Colors and typography remain available through CSS variables. Site identity,
favicon, language, time zone, SEO, and analytics remain managed by Lettercamp.

## Write a component

Create ordinary components under `components/elements/` and import them from your
layout using a relative path, such as `../elements/NewsletterSignup.jsx`. See
[React components](https://www.lettercamp.com/docs/react-components.md) for authoring and
[supported imports](https://www.lettercamp.com/docs/react-components.md#supported-imports) for Cloud dependencies.

## Add styles with CSS Modules

Create `components/layouts/Header.module.css` beside your component:

```css theme={null}
.header {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px;
  padding: 24px;
  background: #18251d;
  color: white;
}
```

Import the stylesheet and use `className={styles.header}`. Lettercamp bundles the
CSS with unique class names. Site typography and global element styles still apply.
Use variables such as `var(--font-family)` and `var(--site-background)` to match
your publication. Local previews and standalone builds support assets in
`public/`; see [Cloud asset preparation](https://www.lettercamp.com/docs/publishing/deploy.md#local-images-and-cloud-assets)
when publishing to Lettercamp.

## Style your post components

Keep each stylesheet beside its component and import it:

```scss theme={null}
/* PostLayout.module.scss */
.article {
  max-width: 1100px;
  margin: 64px auto;
  padding-inline: 24px;
}
.header {
  margin-bottom: 48px;
  h1 { font-size: clamp(2.5rem, 5vw, 4rem); }
}
.content {
  :global(.prose > h2) { margin-top: 2.5em; }
  :global(.prose > p) { margin-block: 1.3em; }
  [data-lettercamp-faq] { border-radius: 16px; }
}
```

Your module classes control your own markup. For the article body, scope rules
beneath your content wrapper and use these hooks:

* Standard elements: `h2`, `p`, `a`, `ul`, `blockquote`, and other Markdown output.
* Body root: `[data-lettercamp-body]`.
* Rich blocks: `[data-lettercamp-table]`, `[data-lettercamp-code]`,
  `[data-lettercamp-faq]`, `[data-lettercamp-key-takeaways]`, and
  `[data-lettercamp-subscribe]`.
* Width choices: `data-block-width`, `data-image-width`, `data-table-width`, and
  `data-code-width`, with values such as `regular`, `wide`, and `full`.
* Shared variables: `--font-family`, `--heading-font`, `--site-background`,
  `--site-foreground`, `--site-border`, and `--post-content-width`.

Retain the built-in sizing and interaction markup. Wide and full-width blocks
expand within the managed body wrapper; give that wrapper enough room and avoid
clipping it with `overflow: hidden`. Avoid depending on generated CSS Module
class names. A custom stylesheet can still hide or distort content, so check
representative tables, media, code, and FAQs at desktop and mobile widths.

### Keep article typography separate from block UI

CSS Modules make class names local; they do not isolate descendant elements.
A rule such as `.content h2` also matches an email subscribe title inside that
wrapper. Use `.content :global(.prose > h2)` for top-level article headings,
and give your own post header a separate module class. Use explicit selectors
for nested article content, such as `.content :global(.prose blockquote > p)`.

Lettercamp's subscribe title, description, and status message define their own
spacing inside `.prose`, so ordinary article typography does not add heading
dividers or paragraph margins to them. They still inherit your site's fonts and
colors. This is CSS scoping, not a Shadow DOM boundary: highly specific selectors
and `!important` can still override native styles.

To intentionally customize a subscribe block, target its parts instead of all
headings or paragraphs:

```css theme={null}
/* PostLayout.module.css, with className={styles.content} around children */
.content :global(.prose [data-subscribe-card] [data-subscribe-title]) {
  font-size: 1.25em;
  color: var(--site-accent);
}
```

The block exposes `[data-subscribe-card]`, `[data-subscribe-title]`,
`[data-subscribe-description]`, `[data-subscribe-form]`,
`[data-subscribe-button]`, and `[data-subscribe-message]`. Preserve form controls,
focus styles, and status messages. Check the rendered **Preview** as well as the
rich text editor, which uses its own editing styles.

## Use the web editor

Article content stays editable through either the Markdown/MDX files or the
online rich editor. **Write** uses Lettercamp's editing surface; **Preview**
shows the custom post components from the last successful build with current
draft content and settings. New and unpublished articles can use that layout too.

The site build bundles the post components and their CSS once. Editing text,
a table, metadata, or settings updates preview data without compiling the theme
again. Publish component or stylesheet changes to update the online theme bundle;
local `lettercamp dev` updates while you work on those files. No per-editor
server is required. Keep generated assets with the site when self-hosting.

Built-in rich blocks render in the draft preview. Arbitrary React imports and
JavaScript expressions inside an MDX article remain placeholders in the online
editor and draft preview; inspect those with `lettercamp dev` or a published page.
Custom headers and footers retain their existing online preview placeholders.

## Preview and publish

Run `npx lettercamp dev` for live updates as you edit code and CSS or add and remove
layout components.

Run [`npx lettercamp check`](https://www.lettercamp.com/docs/cli/check) before publishing. Agents should use
`npx lettercamp check --json` and fix the reported problems until the full build
passes. This catches dependencies and aliases that can work in a local preview
but fail on Cloud. Use local icon components with inline SVG instead of installing
an icon library.

Publish your component files and stylesheets with `npx lettercamp deploy`, or
commit them to the repository connected to GitHub publishing.
For self-hosting, run `npx lettercamp build --site https://example.com` and upload the
entire output directory, including its generated CSS and JavaScript assets.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.