# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

Jacob Dawang's personal website and blog at [jacobdawang.com](https://www.jacobdawang.com), built with [Quarto](https://quarto.org/). Blog posts are data analysis pieces focused on Edmonton housing, zoning, and transit, written in R using Quarto documents (`.qmd`).

## Build commands

```bash
# Preview site locally (live reload)
quarto preview

# Render the full site
quarto render
```

Always render a single post from within the post's directory. This allows for the post to be rendered within its own environment. For example:
```bash
# Render a single post (from post directory)
cd blog/2026/nodes-corridors
quarto render blog/2026/nodes-corridors/index.qmd
```

Deployment is automatic via GitHub Actions on push to `main`: renders with Quarto and deploys to Cloudflare Pages. Deployment to a preview url is automatic via GitHub actions on pushes to open pull requests.

## Architecture

- **`_quarto.yml`**: Site-level config (theme, navbar, footer, extensions). `execute: freeze: true` is set globally in `_quarto.yml`. During a full project render (`quarto render`), code is never re-executed — stored outputs in `_freeze/` are reused. Single-document renders always re-execute regardless of freeze.
- **`blog/_metadata.yml`**: Defaults for all blog posts (freeze, echo/message/warning suppression, giscus comments, title-block-banner).
- **`_freeze/`**: Cached execution results (JSON + figures). Committed to the repo so CI doesn't need R packages installed.
- **`_site/`**: Build output. Gitignored — not committed.
- **`_extensions/`**: Quarto extensions (fontawesome, iconify, social-embeds).
- **`custom_styles.scss` / `custom_styles_dark.scss`**: Theme overrides for light/dark modes on top of the cosmo Bootstrap theme.
- **`data/`**: Shared datasets (geojson, csv, zip) referenced across blog posts.

## Blog post structure

Each post lives at `blog/YYYY/post-slug/index.qmd`. The frontmatter pattern:

```yaml
---
title: "Post title"
date: "YYYY-MM-DD"
author: "Jacob Dawang"
categories: [category1, category2, category3]
description: "few sentence summary"
link-external-newwindow: true
link-external-icon: false
---
```

Posts use R code chunks with knitr. Common libraries: `tidyverse`, `sf`, `gt`, `ggplot2`, `cancensus`, `mountainmathHelpers`, `jdawangHelpers`. Data paths typically reference `../../../data` relative to the post via `here()`.

## Creating a new post

1. Create the directory `blog/YYYY/post-slug/` and `index.qmd` with the frontmatter pattern above.
2. From within the post directory, initialize renv. Use `restart = FALSE` to avoid renv attempting to restart the R session (which exits a subprocess):
   ```bash
   cd blog/YYYY/post-slug
   Rscript -e "renv::init(restart = FALSE)"
   ```
3. Install required packages:
   ```bash
   Rscript -e "renv::install(c('tidyverse', 'sf', ...))"
   Rscript -e "renv::install_github('jdawang/jdawangHelpers')"
   ```
4. Write the post, then render from within the post directory:
   ```bash
   cd blog/YYYY/post-slug
   quarto render index.qmd
   ```
5. Snapshot the environment:
   ```bash
   Rscript -e "renv::snapshot()"
   ```
6. Commit these files:
   - `index.qmd`
   - `renv.lock`
   - `.Rprofile`
   - `.gitignore`
   - `renv/activate.R`, `renv/settings.json`, `renv/.gitignore`
   - `_freeze/blog/YYYY/post-slug/` (generated by render)
   - Any static assets (e.g. `.png` files checked in manually)

For a complete example of post structure, see `blog/2026/zbr-two-year-review/`.

**Inline R expressions and cache**: When `execute: cache: true` is active, chunks containing `library()` calls can be cached, which prevents packages from loading and breaks inline R expressions (`` `r expr` ``). Any post using inline R must set `execute: cache: false` in its frontmatter. Note: `freeze` (Quarto-level, caches entire document outputs in `_freeze/`, only during project renders) and `cache` (knitr-level, caches individual chunk outputs in `*_cache/`, during all renders) are distinct mechanisms.

## jdawangHelpers package

`jdawangHelpers` is a companion R package providing shared utilities for blog posts. Source is at GitHub: `jdawang/jdawangHelpers`. It covers:

- **Edmonton building permit processing** — `clean_edmonton_bp_columns()`, `filter_edmonton_residential()`, `add_edmonton_project_type()`, `add_edmonton_suite_info()`, `add_edmonton_neighbourhood_type()`
- **Transit distance calculations** — `add_transit_distance()`, `add_ecdf_by_distance()`
- **ggplot2 themes** — `theme_jd()` (dark base theme, magma palette), `theme_map()`, `theme_map_dark()`. `theme_jd()` bakes in the viridis magma palette as the default discrete fill/colour scale — to use it, map a variable to `fill` or `colour` in `aes()`. Do not use `scale_fill_manual` with hardcoded colours unless intentionally overriding the palette.
- **Map layers** — `layers_map_base()` (requires `mountainmathHelpers`), `layers_transit_ecdf()`
- **GT table helpers**

Typical pipeline in posts:

```r
bp |>
  clean_edmonton_bp_columns(crs = 4326) |>
  filter_edmonton_residential() |>
  add_edmonton_project_type() |>
  add_edmonton_suite_info() |>
  add_edmonton_neighbourhood_type() |>
  add_transit_distance(transit_stops)
```

**Always use jdawangHelpers functions** for building permit processing, transit calculations, themes, and map layers. Do not copy code patterns from posts that predate jdawangHelpers — those implement manually what the package now handles. When in doubt, check the package source before writing new data processing code. Use `blog/2026/mli-select-edmonton/` as the canonical reference for jdawangHelpers usage. Use `blog/2026/zbr-two-year-review/` as the reference for writing style and tone.

Install from GitHub before using in a post:

```r
renv::install_github("jdawang/jdawangHelpers")
```

In the first R chunk, along with imports, always load the following options like the following. These set important API keys and cache paths for the cancensus and jdawangHelpers packages:

```r
options(
  cancensus.api_key = Sys.getenv("CM_API_KEY"),
  cancensus.cache_path = Sys.getenv("CM_CACHE_PATH"),
  tongfen.cache_path = Sys.getenv("TONGFEN_CACHE_PATH"),
  nextzen_API_key = Sys.getenv("NEXTZEN_API_KEY")
)
```

## jdawangHelpers package

The `jdawangHelpers` package ([github.com/jdawang/jdawangHelpers](https://github.com/jdawang/jdawangHelpers)) contains reusable helpers for this blog. Always install it from GitHub, never from a local path:

```r
remotes::install_github("jdawang/jdawangHelpers")
```

Use its functions instead of writing equivalent code inline. Key exports:

**Themes**
- `theme_jd(mode)` — ggplot2 theme with dark/light mode, viridis magma palette, Source Sans Pro font
- `theme_map(mode)` — map-specific theme (no axes/grids, transparent panel)

**Plot layers**
- `layers_map_base(roads_type, mode)` — water + roads base layers for maps (wraps `mountainmathHelpers`)
- `layers_transit_ecdf(colour_var, x_max)` — ECDF step-plot layers for cumulative units by LRT distance

**GT table helpers**
- `opt_stylize_jd(data, mode)` — style a GT table to match `theme_jd()`
- `finalize_gt(gt_tbl, source, interactive)` — add source note, sub missing values, optional interactivity

**Building permits**
- `clean_edmonton_bp_columns(bp, crs)` — rename/clean columns from the Edmonton Open Data shapefile
- `filter_edmonton_residential(bp)` — filter to residential building types
- `add_edmonton_project_type(bp)` — classify permits into project type categories
- `add_edmonton_suite_info(bp)` — extract secondary suite counts and backyard home flags

**Neighbourhood classification**
- `add_edmonton_neighbourhood_type(bp)` — classify permits as Downtown/Mature/Between mature and Henday/Outside Henday (uses bundled `mature_neighbourhood` and `henday` spatial datasets)

**Transit**
- `load_edmonton_transit_stops(gtfs_path, ...)` — load Edmonton LRT stops from a GTFS zip
- `add_transit_distance(data, transit_stops)` — add `distance_from_lrt` column (km)
- `make_transit_buffers(transit_stops, radii_km)` — concentric buffer rings around stops
- `add_ecdf_by_distance(data, group_var, weight_var)` — compute weighted ECDF by transit distance

**Constants**
- `EDMONTON_RESIDENTIAL_BUILDING_TYPES` — character vector of residential building type strings
- `CAPTION_COE`, `CAPTION_COE_SC`, `CAPTION_TORONTO` — standard caption strings for plots/tables

## R package management

Posts have their own `renv.lock` for reproducibility. To restore packages for a specific post:

```bash
cd blog/2026/nodes-corridors
Rscript -e "renv::restore()"
```

See "Creating a new post" above for the full workflow including renv setup, rendering, and what to commit.

If you come across an error like "[package name or function name] not found", that is most likely due to not installing the package. Install it and try again.

## Freeze behaviour

With `execute: freeze: true` set globally, full project renders reuse outputs from `_freeze/` without re-executing code. Single-document renders always re-execute. To force re-execution during a project render, delete the post's freeze directory first:

```bash
rm -rf _freeze/blog/2026/nodes-corridors
cd blog/2026/nodes-corridors
quarto render index.qmd
```

The `blog/2022/election-maps-2022/index.qmd` is explicitly excluded from rendering in `_quarto.yml`.
