Views hold no logic: all data assembly lives in src/server
A page here exports a route, imports two components and passes props. Everything else — fetching, paths, dates — happens in src/server. Here is what that buys, and the type constraint it forces.
This site has over forty pages, and every one of their frontmatter blocks is three to five lines long. Not because the pages are simple — because they are not allowed to be clever.
The rule
Page and component frontmatter must not fetch data, build paths, or transform values. All assembly lives in src/server/*. Each page exports a function that returns a complete view model, getStaticPaths is generated centrally in src/server/paths.ts, and the model arrives through props.
A standard page looks like this:
---
import '@/styles/pages/_blog.scss';
import { blogListPaths } from '@/server/paths';
export const getStaticPaths = blogListPaths;
const { page } = Astro.props;
const { default: BaseLayout } = await import('@/layouts/BaseLayout.astro');
const { default: PostList } = await import('@/components/PostList.astro');
---
<BaseLayout locale={page.locale} meta={page.meta}>
<PostList cards={page.cards} empty={page.empty} />
</BaseLayout>
No fetching, no map, no new Date(), no string-concatenated URLs. What remains is a declaration: this page uses that layout and that component.
Why components may not fetch
Three concrete losses, not taste.
First, the same derived value gets computed twice or three times. Date labels, reading time, card links, canonical paths — leave any of them in the view and you get one version on the list page and another on the post page.
Second, once fetching and markup are mixed, nothing can answer “why is this page showing this data” on its own. Build-time data bugs can only be reverse-engineered from rendered output.
Third, i18n multiplies the problem. Scatter translation calls into templates and the same keys repeat across every page.
What a view model looks like
For the blog list, BlogListPage returns title, subtitle, empty, cards, pagination and meta in one shot. The pagination object even decides whether prev and next are null, so the view only tests truthiness and never does arithmetic.
Deciding whether a page has pagination at all is logic, not presentation, so the server layer answers it.
The constraint that keeps it honest
The PagePath<T> returned by getStaticPaths must keep its type parameter:
export interface PagePath<T> {
params: Record<string, string>;
props: { page: T };
}
Degrade it to Record<string, unknown> and Astro.props becomes unknown everywhere; page.cards fails immediately. The type checker closes the shortcut on purpose.
The price
A new page touches three files: server/x.ts for the model, paths.ts for the route, pages/x.astro to consume it. A page cannot “just” adjust the data — that means going back to the server layer.
For a page of static copy this split is pure overhead. The moment a list, pagination or a second language shows up, the view layer’s freedom becomes the source of inconsistency.

Comments
…