Skip to content
Jpshlk.

How this site works

Eight diagrams of this site's anatomy: what runs at build time, how layouts and slots compose pages, where the data and images come from, how search works with no server, and the little bit of JavaScript that runs in your browser right now.

component (.astro file)a <slot />: a hole content falls intoscript or head element

1The big picture: it all happens at build time

Astro components are not shipped to visitors. They run once, on my machine or Netlify's, at build time. What comes out is plain HTML files. That is why the site is fast: your browser gets finished pages.

The source

src/

content/ (MDX posts, tags, projects)

pages/ (one file per route)

layouts/ + components/

npm run build

The built site

dist/

index.html, blog/…/index.html

rss.xml, sitemap, llms.txt

optimized images + one CSS file

pagefind/ (the search index, see 7)

2Slots in sixty seconds

A <slot /> is a hole. Whatever you write between a component's opening and closing tags falls through into that hole. That is the entire trick that makes layouts work.

BaseLayout.astro declares the hole

<html>
  <head>…</head>
  <body>
    <Header />
    <main>
      <slot />  ← the hole
    </main>
    <Footer />
  </body>
</html>

index.astro fills the hole

<BaseLayout title="Josh Pasholk">
  <Hero />
  <About />
  <WorkSection />
  <LatestPosts />
  <RandomQuote /> ← all of this lands
     where the slot was
</BaseLayout>

So every page gets the same head, header, and footer for free, and only has to bring the middle. Change BaseLayout.astro once and every page on the site changes.

3The homepage, as nested boxes

Boxes inside boxes literally is the architecture. Solid boxes are components; the dashed region is BaseLayout's slot, filled by whatever index.astro put between its tags.

BaseLayout

src/layouts/BaseLayout.astro (owns <html> and <head>)

theme script (is:inline)<Seo /> meta + JSON-LD<ClientRouter />Pagefind loader script

Header nav + search button and modal + ThemeToggle + mobile menu

<slot /> : filled by src/pages/index.astro

Hero components/home/Hero.astro

About components/home/About.astro

WorkSection reads the projects collection

LatestPosts

components/home/LatestPosts.astro (newest 5 posts)

PostCard × 5 each renders TagPill + FormattedDate

RandomQuote components/home/RandomQuote.astro

Footer SocialIcon row (mastodon, github, routinehub, …)

4A blog post: layouts nest, so slots nest

A post page uses two layouts. PostLayout sits inside BaseLayout's slot, and declares a slot of its own, which is where the actual writing (the rendered MDX) lands.

BaseLayout

same shell as the homepage: head, Header, Footer

<slot /> : filled by PostLayout

PostLayout

src/layouts/PostLayout.astro

post header: FormattedDate, title (view-transition name), TagPills

<slot /> : filled by <Content />, the rendered MDX

Prose wrapper typography styles for the article body

the MDX itself can import components: 11 posts pull in Link.astro and SocialIcon

prev / next post links

GitHub buttons script (data-astro-rerun)

Who fills what: the route file src/pages/blog/[...slug].astro looks up the post, calls render(post) to turn MDX into <Content />, and writes <PostLayout post={post}><Content /></PostLayout>. One file, two slots filled.

5Where the data comes from: props go down

Components never go looking for data. Data is loaded once, then handed down the tree as props (function arguments for components). One-way traffic.

MDX files

src/content/blog/*.mdx

frontmatter + body
schema validates

Typed collection

src/content.config.ts

title, date, tags…
sort + filter drafts

Helper

src/lib/posts.ts

getPublishedPosts()
props

Component

<PostCard post={post} />

renders title, date, tags

The same pipeline feeds the blog list, the tag pages, the RSS feed, the landing page's LatestPosts, and the SEO schema endpoints. One source of truth, many consumers.

6Images: three jobs, one build

There are 36 original images in src/assets/, and posts reference them with plain markdown. Nobody resizes anything by hand; the build does all three image jobs.

Originals

src/assets/

full-size photos + screenshots

referenced by 22 posts and Hero.astro

astro build

Optimized copies

dist/_astro/

resized, compressed, hashed filenames

share images: one per post, for link previews

Job one, content images: a markdown ![alt](../../assets/photo.jpg) in a post (or the <Image /> component in Hero) comes out the other side compressed and cache-friendly, automatically.

Job two, share images: PostLayout renders each post's feature image to a link-preview version at build time: capped at 1200px wide, never upscaled, and kept as PNG or JPEG because some link crawlers still choke on modern formats.

Job three, the passthrough bucket: public/ is for files that must not be touched: favicons, robots.txt, the redirects file, and the default share image. They are copied into dist/ exactly as-is.

7Search: no server, and a second kind of component

The search button in the header (Cmd+K works too) and the /search/ page run on Pagefind, which indexes the finished HTML right after the build.

Built pages

dist/**/*.html

only pages marked data-pagefind-body: posts, home, /uses/, /projects/

pagefind --site dist

Search index

dist/pagefind/

index chunks + the UI bundle, all static files

your query

Your browser

no API, no server

downloads only the tiny chunks that match

A second kind of component: everything in sections 2 through 4 was Astro components, which run at build time and disappear. The search UI is the opposite: web components, custom HTML tags that a script brings to life in your browser. The header renders just this:

<pagefind-modal-trigger instance="modal" compact></pagefind-modal-trigger>
<pagefind-modal instance="modal" reset-on-close></pagefind-modal>

The empty <pagefind-modal> builds its own input, results, and keyboard hints once the loader script (that chip in section 3's diagram) fetches the bundle. In dev there is no index yet, so a CSS rule hides the undefined tags and the search page shows a fallback note instead.

8The browser layer: what happens on a click

Almost everything above ran at build time. Here is the small amount of JavaScript that actually runs for you, the visitor, in order, when you click a link on this site.

  1. First visit: theme script runs before paint

    The is:inline script in the head reads localStorage (or the system setting) and applies the dark class before any pixel renders. No flash.

  2. You click a link

    <ClientRouter /> intercepts it, fetches the next page's HTML, and swaps the document instead of doing a full reload. This is what enables the title morph animation between the blog list and a post.

  3. astro:after-swap fires

    The new document arrived without the dark class, so the theme script's listener re-applies it mid-swap. Still no flash.

  4. astro:page-load fires

    ThemeToggle and the mobile menu re-bind their click handlers to the fresh DOM, RandomQuote picks a new quote, and the GitHub buttons script re-runs on posts that need it. The Pagefind loader also runs here: it fetches the search components once per visit and prunes anything from their registry that the swap just removed, so the search button never points at a dead modal.

The house rule that keeps all this working: on a view-transitions site, any JavaScript that touches the page must run on astro:page-load, not just once at startup.

One last thing: this page is itself a box in the section 3 diagram. You are reading it inside the very slot described in section 2. 🤙