Skip to main content

Overview

This website strategically combines three different frameworks to optimize for performance, developer experience, and specific use cases:
  • Astro: Server-side rendering and zero-JS static pages
  • React: Image optimization component
  • Solid.js: Interactive UI components
This approach leverages the strengths of each framework while minimizing JavaScript bundle size.

Framework Configuration

The multi-framework setup is configured in astro.config.mjs:
astro.config.mjs
import { defineConfig } from "astro/config";
import solid from "@astrojs/solid-js";
import react from "@astrojs/react";
import sitemap from "@astrojs/sitemap";
import mdx from "@astrojs/mdx";

export default defineConfig({
  site: "https://www.andrewgao.org/",

  integrations: [
    solid(),
    react({
      include: ["**/satori.tsx"],  // Only Satori component uses React
    }),
    sitemap({
      filter: (page) => !page.includes(".json"),
    }),
    mdx(),
  ],

  // ... other config
});
React is intentionally limited to specific files using the include pattern, preventing it from processing all components.

Astro: The Foundation

When to Use Astro

Static Pages

Home, about, blog posts, project pages - any content-focused page

Layouts

Page templates, navigation, footer, SEO components

Data Fetching

Build-time Notion API calls, static data loading

Server Components

Components that don’t require client-side JavaScript

Astro Component Example

src/layouts/Layout.astro
---
import Head from '@components/Head.astro';
import Navigation from '@components/Navigation.astro';

interface Props {
  title: string;
  description: string;
  path: string;
}

const { title, description, path } = Astro.props;
---

<!doctype html>
<html lang="en">
  <Head title={title} description={description} path={path} />
  <body>
    <Navigation />
    <main>
      <slot name="content" />
    </main>
  </body>
</html>
  • Zero JavaScript: This layout generates pure HTML with no runtime JS
  • Fast builds: Astro’s compiler is optimized for static sites
  • SEO-friendly: Server-rendered HTML is immediately indexable

Build-Time Data Fetching

Astro components can fetch data at build time:
src/pages/index.astro
---
import { getPage } from '@lib/notion-cms-page';
import { getBlock } from '@lib/notion-cms';
import { parseBlocks } from '@lib/notion-parse';
import Layout from '@layouts/Layout.astro';

const page = await getPage(import.meta.env.NOTION_PAGE_ID_HOME);
const blocks = await getBlock(page.id);
const content = parseBlocks(blocks);
---

<Layout title="Home" description="Andrew Gao's website" path="/">
  <Fragment slot="content">
    <article set:html={content} />
  </Fragment>
</Layout>
The await in the frontmatter runs once at build time, not on every request. This is what makes Astro so fast.

React: Image Optimization Only

When to Use React

Astro Image Component

React is used exclusively for Astro’s <Image /> component, which provides automatic image optimization.

Image Component in MDX

When Notion content is synced, images are converted to Astro Image components:
src/lib/notion-parse.ts:222-236
case "image":
  const imgAlt = parseRichTextBlock({ rich_text: block.image.caption }) || "image";
  const imgUrl = block.image[block.image.type].url.split("?");
  const imgSrc = imgUrl[0];
  const imgParams = new URLSearchParams(imgUrl[1]);
  
  if (!imgParams) {
    return html`<img src="${imgSrc}" alt="${imgAlt}"`;
  } else {
    return `<Image src=${imgSrc} width="${imgParams.get(
      "w",
    )}" height="${imgParams.get(
      "h",
    )}" format="webp" alt="${imgAlt}" />`;
  }
This generates MDX like:
import { Image } from 'astro:assets';

<Image 
  src={import("@assets/file.abc123.png")}
  width="1200" 
  height="800" 
  format="webp" 
  alt="Beautiful sunset" 
/>

Benefits of Astro Image

  • Converts to WebP/AVIF formats
  • Generates responsive image sizes
  • Lazy loads images by default
  • Images processed during astro build
  • Uses Sharp for fast, high-quality optimization
  • No runtime performance cost
  • TypeScript validation for image imports
  • Compile-time errors for missing images
React is only used for the Image component through Astro’s integration. No React components are directly authored in this project.

Solid.js: Interactive Components

When to Use Solid.js

Client Interactivity

Maps, navigation menus, toggles - anything requiring JavaScript

Better Performance

Solid.js has smaller bundle sizes than React (~7KB vs ~40KB)

Fine-Grained Reactivity

More efficient updates than React’s virtual DOM

Solid.js Component Example

src/components/Map.tsx
import { createSignal, onMount } from "solid-js";
import L from "leaflet";

export default function Map() {
  const [map, setMap] = createSignal<L.Map | null>(null);

  onMount(() => {
    const mapInstance = L.map("map").setView([37.7749, -122.4194], 10);
    
    L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png', {
      maxZoom: 20,
    }).addTo(mapInstance);

    setMap(mapInstance);
  });

  return <div id="map" class="h-96 w-full" />;
}

Hydration Directive

Solid components require the client:only directive to hydrate on the client:
src/pages/places.astro
---
import Layout from '@layouts/Layout.astro';
import Map from '@components/Map';
---

<Layout title="Places" description="Map of places I've visited" path="/places">
  <Fragment slot="content">
    <Map client:only="solid-js" />
  </Fragment>
</Layout>
client:only="solid-js" tells Astro to:
  1. Skip server-side rendering for this component
  2. Load Solid.js runtime on the client
  3. Hydrate the component after page load
This is necessary for components that depend on browser APIs (like Leaflet’s map).
Interactive Leaflet map with custom markers and zoom controls.Bundle size: ~35KB (Solid + Leaflet)Features:
  • Dynamic marker placement from JSON data
  • Zoom-based marker visibility
  • Custom tile provider (Stadia Maps)

Framework Decision Matrix

Use this guide to choose the right framework:
RequirementFrameworkReason
Static content pageAstroZero JS, fastest load time
Blog post layoutAstroNo interactivity needed
Image displayReactAstro Image optimization
Interactive mapSolid.jsRequires client JS, smaller bundle
Navigation menu (mobile)Solid.jsToggle state, animations
SEO metadataAstroServer-rendered <head>
Data fetchingAstroBuild-time API calls
Form validationSolid.jsClient-side interactivity

Bundle Size Comparison

Here’s how each framework impacts bundle size:

Astro Pages

~0-5KBStatic HTML with optional CSS

React (Image Only)

+0KBCompiled away at build time

Solid.js Page

~7KB + componentSolid runtime + component code

Actual Bundle Sizes

From production builds:
# Home page (Astro only)
index.html: 12KB HTML, 0KB JS

# Blog post (Astro + Images)
blog/my-post/index.html: 45KB HTML, 0KB JS

# Places page (Astro + Solid.js + Leaflet)
places/index.html: 8KB HTML, 35KB JS
Most pages ship zero JavaScript because they use pure Astro. Only interactive pages include JS bundles.

Performance Implications

Page Load Speed

1

Astro Pages (0KB JS)

  • TTFB: Less than 50ms
  • FCP: Less than 200ms
  • LCP: Less than 500ms
  • TTI: Same as FCP (no hydration)
2

Solid.js Pages (~30-40KB JS)

  • TTFB: Less than 50ms
  • FCP: Less than 200ms (HTML rendered)
  • LCP: Less than 800ms
  • TTI: Less than 1000ms (after hydration)

Why Not Use React Everywhere?

Comparing React vs. Solid.js for interactive components:
MetricReactSolid.jsWinner
Runtime size40KB7KB🏆 Solid.js
Rendering speedVirtual DOMDirect DOM🏆 Solid.js
Re-render costDiff entire treeUpdate changed signals🏆 Solid.js
EcosystemMassiveGrowingReact
Learning curveFamiliarSimilarTie
For this project’s needs (maps, navigation), Solid.js provides better performance with minimal trade-offs.

Islands Architecture

Astro uses the Islands architecture pattern:
<!-- Static Astro component -->
<Header />

<!-- Static content -->
<article>Blog post content...</article>

<!-- Interactive island -->
<CommentSection client:only="solid-js" />

<!-- Static footer -->
<Footer />
Each “island” is an independent interactive component in a sea of static HTML.
  1. Partial hydration: Only interactive components load JS
  2. Faster TTI: Less JavaScript to parse and execute
  3. Better SEO: Static HTML is immediately visible to crawlers
  4. Progressive enhancement: Site works without JS, enhanced with JS

Development Experience

TypeScript Support

All three frameworks have excellent TypeScript support:
// Astro component props
interface Props {
  title: string;
}

// Solid.js component props
type MapProps = {
  initialZoom?: number;
};

// Type-safe image imports
import type { ImageMetadata } from 'astro';
import heroImage from '@assets/hero.png';

Hot Module Replacement

During development (pnpm dev):
  • Astro: Instant HMR for content changes
  • Solid.js: Fast refresh preserves component state
  • React: Standard Fast Refresh (used minimally)

Build Times

# Development build
pnpm dev
# Notion sync: ~5-15s
# Astro dev server: ~2-3s
# Total: ~8-18s

# Production build
pnpm build
# Notion sync: ~15-30s
# Astro build: ~10-15s
# Total: ~25-45s
Incremental sync reduces subsequent builds to ~10-20s total.

Migration Guidelines

When to Convert Astro → Solid.js

Convert a component from Astro to Solid.js if:
  1. It needs client-side state management
  2. It has event handlers (click, scroll, etc.)
  3. It uses browser APIs (localStorage, geolocation)
  4. It requires animations/transitions

Conversion Example

---
const items = ['Home', 'Blog', 'Projects'];
---

<nav>
  {items.map(item => <a href={`/${item.toLowerCase()}`}>{item}</a>)}
</nav>
Only convert if the component requires client-side JavaScript. Keep components as Astro whenever possible to minimize bundle size.

Testing Strategy

Astro Components

Tested through integration tests:
import { describe, it, expect } from 'vitest';
import { getPage } from '../lib/notion-cms-page';

describe('Page rendering', () => {
  it('should fetch Notion content', async () => {
    const page = await getPage('test-id');
    expect(page).toBeDefined();
  });
});

Solid.js Components

Tested with Vitest and Solid Testing Library:
import { render } from 'solid-testing-library';
import Map from '../components/Map';

it('renders map container', () => {
  const { container } = render(() => <Map />);
  expect(container.querySelector('#map')).toBeDefined();
});

Best Practices

Default to Astro components. Only use Solid.js when interactivity is truly needed.
Use client:visible for below-the-fold components:
<Map client:visible />
Define shared types in src/types/ and import across frameworks.
Always use Astro’s <Image> component for static images, never raw <img> tags.

Next Steps

Architecture Overview

Return to the high-level architecture guide

Content Sync Process

Learn how content flows from Notion to Astro

Build docs developers (and LLMs) love