Skip to content

Core Concepts

This guide introduces the key concepts and patterns used in our frontend architecture.

Our frontend is built with:

  • Astro: Zero-JS-by-default page rendering
  • SolidJS: Interactive component framework with SSR support
  • TypeScript: Type safety throughout
  • Tailwind: Utility-first styling
  • GraphQL: Data fetching via graphql-request

Choose between .astro and .tsx based on these criteria:

  • Component is purely static
  • Component needs server-side data fetching
  • Component is primarily markup with minimal logic
  • Component uses complex template composition
  • Component doesn’t need client-side state

Example of a good .astro component:

---
// Good .astro use case: Static layout with data composition
interface Props {
title: string;
items: Array<{ id: string; name: string }>;
}
const { title, items } = Astro.props;
const groupedItems = groupByCategory(items);
---
<section class="py-8">
<h2 class="text-24 font-bold">{title}</h2>
{
Object.entries(groupedItems).map(([category, items]) => (
<div class="mt-4">
<h3 class="text-20">{category}</h3>
<ul class="grid grid-cols-3 gap-4">
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
))
}
</section>
  • Component needs client-side interactivity
  • Component maintains client-side state
  • Component handles user events
  • Component needs lifecycle hooks
  • Component uses SolidJS features

Example of a good .tsx component:

// Good .tsx use case: Interactive feature with state
interface Props {
initialItems: string[];
}
export const FilterableList: Component<Props> = (props) => {
const [searchTerm, setSearchTerm] = createSignal("");
const [loading, setLoading] = createSignal(false);
const filteredItems = createMemo(() =>
props.initialItems.filter(item =>
item.toLowerCase().includes(searchTerm().toLowerCase())
)
);
return (
<div>
<input
type="search"
value={searchTerm()}
onInput={(e) => setSearchTerm(e.currentTarget.value)}
/>
<Show
when={!loading()}
fallback={<LoadingSpinner />}
>
<ul>
{filteredItems().map(item => (
<li>{item}</li>
))}
</ul>
</Show>
</div>
);
};

We use a mix of static and interactive components to build features:

---
// Example of component composition
import { PageLayout } from "#components/layout";
import { StaticHeader } from "#components/header";
import { InteractiveFeature } from "#components/feature";
import { StaticFooter } from "#components/footer";
---
<PageLayout>
<StaticHeader />
<!-- Critical interactive feature -->
<InteractiveFeature client:load />
<!-- Below-fold interactive features -->
<InteractiveFeature client:visible />
<!-- Non-critical features -->
<InteractiveFeature client:idle />
<StaticFooter />
</PageLayout>

Key composition patterns:

  • Use static components for layout and structure
  • Add interactive components strategically
  • Choose appropriate client directives
  • Keep components focused and composable

Components are organized by feature and type:

src/
├── components/ # Shared components
│ └── feature/
│ ├── index.astro # Main component
│ ├── Component.tsx # Interactive parts
│ └── utils.ts # Helper functions
├── pages/ # Routes and pages
│ └── route/
│ ├── _components/ # Route-specific (`_` prefixed to hide automatic routing)
│ └── index.astro # Page template

We follow these patterns for state:

  1. Component State: Use SolidJS signals

    const [count, setCount] = createSignal(0);
  2. Route State: Use URL parameters

    const params = new URLSearchParams(location.search);
  3. Shared State: Use nanostores

    stores/cartStore.ts
    export const cartStore = map({
    items: [] as CartItem[],
    });

We use structured error handling with Airbrake:

try {
await operation();
} catch (err) {
const { airbrakeClient } = await import("#utils/airbrake/browser");
airbrakeClient.notify({
error: err,
context: { component: "ComponentName" },
});
}

We use Imgix for image optimization:

<ImgixImage
src={src}
width={800}
height={600}
loading="lazy"
imgixParams={{
auto: "format,compress",
q: 75
}}
/>
  1. Component Development

    • Start with static Astro component
    • Add interactivity only when needed
    • Use appropriate client directive
    • Implement error handling
    • Add loading states
  2. State Management

    • Keep state close to where it’s used
    • Use URL for route-level state
    • Use stores for truly shared state
  3. Performance

    • Default to static components
    • Optimize images with Imgix
    • Use appropriate loading strategies
    • Monitor with Grafana Faro
  4. Error Handling

    • Use structured error reporting
    • Implement fallback UI
    • Log errors with context

Don’t: Add client-side JavaScript by default ✅ Do: Start with static components, add interactivity only when needed

Don’t: Use global state for component-level concerns ✅ Do: Keep state as close as possible to where it’s used

Don’t: Skip error handling or use console.log ✅ Do: Use structured error reporting with Airbrake

Don’t: Use direct DOM manipulation ✅ Do: Use framework-provided state management

  1. Review the Component Patterns guide
  2. Understand our State Management approach
  3. Learn about Error Handling