How this site works
A guided tour of this site's anatomy that starts from zero. If you are not quite sure what a browser even does, start at section 1 and read straight down: every idea is explained the first time it shows up. If you build websites for a living, skim the table of contents and jump to the parts about this site. Either way, no web dev background needed.
<slot />: a hole content falls intoscript or head elementOn this page
The foundations (start here if the web is new to you)
This site
- The big picture: build time
- Slots in sixty seconds
- The homepage, as nested boxes
- A blog post: layouts nest
- Where the data comes from
- How a post gets written
- Images: three jobs, one build
- How it looks: Tailwind
- Search without a server
- Dark mode in three parts
- What happens on a click
- Analytics that do not follow you
- Comments via GitHub Discussions
- Pages for robots
- From my laptop to your screen
1What a website actually is
Your browser (Safari, Chrome, Firefox) is a program with one job: download files of plain text from other computers and turn them into the pages you see. Nearly every page on the internet, this one included, is written in three languages, and each one has exactly one job. Here is the same tiny page in all three.
HTML: structure + words
<h1>Hi, I'm Josh</h1> <p>Welcome to my site.</p> <button>Say hi</button>
Tags label what each piece is: a heading, a paragraph, a button. No styling, no behavior, just meaning.
CSS: how it looks
h1 { color: seagreen; }
p { max-width: 60ch; }
button {
border-radius: 8px;
}Rules that point at the HTML and describe its appearance: colors, sizes, spacing, rounded corners.
JavaScript: what it does
const btn = document
.querySelector('button');
btn.addEventListener('click',
() => {
alert('Hi! đ€');
});A real programming language the browser runs, for anything that reacts or changes after the page appears.
Want to go deeper? MDN Web Docs is the closest thing the web has to an official manual, and it is free. Its reference pages for HTML, CSS, and JavaScript back up everything on this page.
2What happens when you visit jpshlk.com
Typing an address and seeing a page feels instant, but there is a round trip hiding in there. Here it is in order; the whole thing usually takes a fraction of a second.
Your browser
you type jpshlk.com
first stop: find out where that name actually lives
DNS
the internet's phone book
turns the name into a machine address (an IP address)
A server
a computer that answers requests
sends back the HTML, then the CSS, images, and scripts it mentions
The page you see
rendered by your browser
HTML parsed, CSS applied, scripts run: section 1's three jobs
A server is not special hardware: it is any computer whose job is answering requests. Many sites make theirs assemble each page fresh, on demand, for every visitor. This site did all that work ahead of time (section 4), so its "server" is really a CDN, a delivery network that just hands over finished files from a data center near you (section 18). That is most of why it feels fast.
Want to go deeper? MDN explains how the web works and what DNS is in beginner terms.
3The toolbox on my side
Everything from here on mentions tools that run on my computer, not yours. Four of them do almost all the work, so here is the one-line version of each. You will recognize them for the rest of the tour.
The terminal
typing commands instead of clicking
A window where you run programs by typing their names. Nothing magic: every command on this page, like npm run build, is typed here.
Node
JavaScript, outside the browser
JavaScript was born inside browsers. Node lets it run on any computer, which is how most website build tools (Astro included) are written.
npm
an app store for shared code
npm installs the open source packages this site depends on, and runs named commands: npm run build just means "npm, run the command I named build".
Git
save points for a whole folder
Git records named snapshots of every file in a project, forever, so nothing is ever lost and any change can be undone. Its full story is in section 18.
4The big picture: it all happens at build time
Two definitions first. A framework is a kit of pre-solved problems: instead of every site owner hand-writing the machinery for layouts, navigation, image resizing, and feeds, the framework supplies the machinery and you supply the content. This site's framework is Astro.
Astro exists because of a bet about timing. Many modern frameworks ship a JavaScript app to your browser, which then assembles the page in front of you. That is work done at runtime: while you watch, on your device. Astro's bet is the opposite: do all the assembling once, ahead of time, and ship the finished result. That ahead-of-time step is the build, and "build time" just means "while that step is running".
So the Astro components you will meet below are never sent 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/
The built site
dist/
index.html, blog/…/index.html
rss.xml, sitemap, llms.txt
optimized images + one CSS file
pagefind/ (the search index, see 12)
The trade-off, honestly stated: a build-time site cannot show you a shopping cart or a personalized feed, because every visitor gets the same files. For a blog, that is no trade-off at all: the content is the same for everyone anyway.
5Slots in sixty seconds
This whole site is assembled from components: named, reusable chunks of page that you use like custom HTML tags (<Header />, <PostCard />). And the trick that makes layouts work is the <slot />: a hole. Whatever you write between a component's opening and closing tags falls through into that hole. That is the entire trick.
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 /> <WorkSection /> <LatestPosts /> ← 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. There is one escape hatch: pass BaseLayout a chromeless prop and it skips the header and footer while keeping the whole head intact. The link-in-bio page uses it to stand alone. Astro's own components guide covers this in full.
6The 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
Hero components/home/Hero.astro
WorkSection reads the projects collection
LatestPosts
components/home/LatestPosts.astro (newest 5 posts)
PostCard × 5 each renders TagPill + FormattedDate
Footer RandomQuote + SocialIcon row (github, linkedin, bluesky) + links + ©
7A 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
PostLayout
src/layouts/PostLayout.astro
breadcrumb trail: Home âș Blog, same data as the page's JSON-LD (Breadcrumbs.astro)
post header: FormattedDate, title (view-transition name), TagPills
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.
8Where the data comes from: props go down
Components never go looking for data. Data (post titles, dates, tags) is loaded once, then handed down the tree as props: the values a component receives when you use it, like arguments to a function. Writing <PostCard post={post} /> hands PostCard one post to render. One-way traffic, top to bottom.
MDX files
src/content/blog/*.mdx
frontmatter + bodyTyped collection
src/content.config.ts
title, date, tags…Helper
src/lib/posts.ts
getPublishedPosts()Component
<PostCard post={post} />
renders title, date, tagsThe 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.
The TypeScript aside: this site is written in TypeScript, which is JavaScript plus types: labels describing what shape data must have ("a post has a title, and it is text"). The "schema validates" arrow above is that idea applied to content, so a typo gets caught while I am writing, not after publishing. Astro calls this whole pipeline content collections.
9How a post gets written
Every post is one file, written in Markdown: a way of writing formatted text with punctuation instead of buttons (# makes a heading, **bold** makes bold). Strictly speaking the files are MDX, which is Markdown that is also allowed to use the components from section 5. On top of the writing sits a small settings block called frontmatter. That block is data, not prose, and it drives everything the site knows about the post.
The settings block (frontmatter)
--- title: My Great Post date: '2026-07-11' summary: 'One or two sentences.' tags: ['astro', 'apps'] draft: true ← hides it from the live site --- The actual writing starts here.
What each line feeds
title → the h1, the browser tab, search results
date → sorting, the archive, the RSS feed
summary → post cards and link previews
tags → must match a real tag page, checked at build
draft → true renders in dev only
The spell-checker: a schema in src/content.config.ts validates every settings block at build time. A misspelled tag, a bad date, a missing title: the build fails and tells me which file, before anything reaches the internet.
Publishing is deleting one line. Drafts render on my machine in npm run dev so I can preview them; remove draft: true, push, and the post is on its way (section 18 covers that trip). If its date has already arrived it goes live on the next build. If the date is still in the future the post is scheduled: it waits, invisible on the live site, and a small robot checks every morning and rebuilds the site the day it comes due.
10Images: 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. Why bother? Because images are the heaviest thing on most pages, and a phone on cell data should not have to download a four megabyte original to fill a small column of text. Shrinking them is the single biggest web performance win there is.
Originals
src/assets/
full-size photos + screenshots
referenced by 22 posts and Hero.astro
Optimized copies
dist/_astro/
resized, compressed, hashed filenames
share images: one per post, for link previews
Job one, content images: a markdown  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. And a post with no art at all gets a card invented for it: the build draws a dark 1200 by 630 title card (site name, title, date) and wires it up as that post's link preview.
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.
11How it looks: Tailwind and one CSS file
Section 1 said CSS is the looks. This site writes almost none of it by hand. Instead it uses Tailwind CSS: a big box of tiny pre-made class names, each doing exactly one thing, composed right in the markup instead of in separate stylesheets. Here is a real one, the pill-shaped chip used in the diagrams on this page.
The markup on this very page
<span class="rounded-full border bg-primary-50 px-2.5 py-0.5 font-mono text-xs text-primary-700"> theme script </span>
theme script (is:inline) ← the result
What each class means
rounded-full → fully round corners (the pill shape)
bg-primary-50 → the palest tint of the site's green
px-2.5 py-0.5 → a little padding, sideways and vertical
font-mono text-xs → small monospace type
text-primary-700 → darker green ink on top
One small file: at build time (section 4 again) Tailwind scans the whole codebase for class names it recognizes and writes a single CSS file containing only the ones actually used. Styles nobody uses never ship, and the whole site's look arrives in one small download.
Dark mode hooks in here: any class can be prefixed with dark:, as in dark:bg-zinc-900, to apply only when dark mode is on. What flips that switch is the next section. The site's design tokens live in src/styles/global.css: Tailwind v4 is configured in CSS itself, no separate config file.
12Search: 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. An index is a pre-built map from words to the pages containing them, like the one in the back of a book: that is what makes search instant with no server involved.
Built pages
dist/**/*.html
only pages marked data-pagefind-body: posts, home, /about/, /projects/
Search index
dist/pagefind/
index chunks + the UI bundle, all static files
Your browser
no API, no server
downloads only the tiny chunks that match
A second kind of component: everything in sections 5 through 7 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 6'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.
13Dark mode in three moving parts
The theme toggle looks like one button, but making it flicker-free takes three pieces working together.
One class rules everything
Dark mode is just a
darkclass on the page's root element. Every dark style in the CSS (thosedark:variants from section 11) keys off that single class, so adding or removing it re-themes the whole page instantly. No reload, no per-element logic.The no-flash trick
A tiny script at the very top of the page reads your saved choice (or your system preference) and sets the class before the first pixel renders. Without it, dark mode users would see a white flash on every visit. It re-runs after each page swap too, because the incoming page arrives without the class.
The toggle remembers
Clicking the sun/moon flips the class and saves your choice in the browser's
localStorage(a little notepad each site gets in your browser), so it sticks across visits. It also stamps adata-pf-themeattribute that the search UI (section 12) reads to switch its own dark scheme.
14The 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. One idea first: normally, clicking a link throws the whole page away and rebuilds the next one from scratch. This site uses Astro's view transitions instead: it swaps just the content and animates the difference, which is why moving around feels smooth. Here is the sequence, in order, when you click a link on this site.
First visit: theme script runs before paint
The
is:inlinescript in the head readslocalStorage(or the system setting) and applies thedarkclass before any pixel renders. No flash.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 image and title morph between a card and its post or project page. The morph names are applied just in time by a script in the layout (only when one side is a detail page), so list-to-list navigations stay a clean crossfade instead of morphing every card at once. One deliberate exception: the header opts out of the crossfade (its transition is set to none), so the site name and nav links stay planted while the little pill highlight slides underneath them to whichever link is now active. The stylesheet also reserves a permanent gutter for the scrollbar, so landing on a short page (where the scrollbar would otherwise vanish) cannot nudge the whole layout sideways mid-swap.astro:after-swapfiresThe new document arrived without the
darkclass, so the theme script's listener re-applies it mid-swap. Still no flash.astro:page-loadfiresThemeToggle and the mobile menu re-bind their click handlers to the fresh DOM, RandomQuote picks a new quote, the comments widget re-mounts on posts, 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.
15Analytics that do not follow you
Analytics means counting visits. Most sites do it with cookies (small ID tags a site stores in your browser so it can recognize you later) and scripts that follow you across the web. I want to know if anyone reads this site, not who you are. Pageviews are counted by GoatCounter, an open source, privacy-first counter. That is why there is no cookie banner here: there is nothing to consent to.
What it sees
which page was viewed
which site linked here (the referrer)
rough totals, on a public-style dashboard
What it never does
no cookies, so nothing to accept
no fingerprinting, no ad networks
no following you to other sites
Only in production: the script renders only on real production deploys. Local dev and preview deploys ship literally zero analytics bytes, not a disabled script, no script at all.
One view transitions wrinkle: page swaps are not real page loads, so automatic counting is off and each swap is counted once on astro:page-load (the same event from section 14). Otherwise reading five posts would count as one view, or one view as ten.
16Comments via GitHub Discussions
There is no comment database here either. Each post's comments live in a GitHub Discussion on this site's repo, and a small open source widget called giscus shows that discussion under the post, inside an iframe: a sandboxed little window onto another website, embedded in this page. Reading costs nothing; replying takes a GitHub sign in, which handles spam and moderation for me.
How a comment gets there
you sign in with GitHub inside the widget
your comment lands in a Discussion named after the post's URL
the first comment on a post creates its Discussion automatically
What the page pays for it
nothing until you scroll near the comments (lazy loading)
then one iframe from giscus.app, sandboxed like any other
no tracking scripts, no ads, no cookies from this site
The same view transitions wrinkle as analytics: a swapped-in page never runs scripts baked into its HTML, so the widget is created fresh on astro:page-load (section 14 again) for every post you visit.
Dark mode reaches into the iframe: the widget mounts with whatever theme the page has, and when you flip the toggle, a tiny observer notices the class change and messages the iframe so the comments flip with it.
17Pages for robots
Humans get the HTML pages. The same build also writes files meant only for machines, and hires a few robots to proofread my work.
Machine-readable outputs in dist/
generated fresh on every build
rss.xml the whole post in the feed, not a teaser, so feed readers get everything
sitemap a map of every page, for search engine crawlers
JSON-LD in every head schema.org data describing the page, the site, and me, all agreeing with each other; the visible breadcrumb links on posts, projects, and tag pages render from the same data
llms.txt a curated text index of the real content, for AI tools that read sites
Never heard of RSS? It is the old, still-great way to follow sites without an algorithm in between: a feed reader app checks each site's feed file for you and shows new posts in order, like podcast subscriptions but for reading. About Feeds is a lovely five minute introduction. The JSON-LD blocks speak schema.org, the shared vocabulary search engines use to understand pages.
The proofreading robots: validators run during every build and FAIL it on a missing h1, a dead internal link, or a page title or description that is too long or too short. A broken link cannot reach the live site because the build that contains it never finishes.
18From my laptop to your screen
Nothing here uploads files to a server by hand. The whole publish flow is: change something, commit it (ask git for one of those named snapshots from section 3, with a note saying what changed), and push (upload the new snapshots to GitHub). Everything after that is automatic.
My laptop
(or an AI session)
edit, commit, git push
GitHub
the code's home
keeps history, notifies Netlify
Netlify
npm run build
Astro builds, Pagefind indexes, validators check
A CDN near you
jpshlk.com
static files, served fast, worldwide
The cast: GitHub is the website where the code and its entire history live; the arrow labeled webhook is literally GitHub tapping Netlify on the shoulder to say something changed. Netlify then re-runs the build from section 4 on a fresh computer and copies the result to a CDN: data centers around the world, each holding a copy of the finished files, so the site loads from somewhere near you.
A push is not the only trigger: every morning a scheduled check (a GitHub Actions robot) looks for posts whose publish date has arrived and, if one is due, pokes Netlify to rebuild. That is how a finished post from section 9 can go live on its planned date while I sleep in.
Safe experiments: only the main branch publishes to jpshlk.com. Work happens on other branches, and each one gets its own private preview URL built exactly the same way, so every change is reviewed on a real deploy before it merges. This very page was checked on a preview URL before you ever saw it.
One last thing: this page is itself a box in the section 6 diagram. You are reading it inside the very slot described in section 5. đ€