# Agent playbook: install the baas studio blog (JS SDK)

**Audience: you are an AI coding agent.** A human asked you to install their
hosted baas studio blog on their website. This file is self-contained —
follow it top to bottom. The human-facing docs live at https://dailysmith.com/docs,
but you should not need them to finish this task.

## What you are wiring

The blog is hosted by an API at `https://api.dailysmith.com`. The website embeds a
container `<div>` plus one `<script>`. The script (the SDK) fetches
server-rendered HTML fragments from the API and renders them inside the
container — on the customer's own domain, under a path prefix called the
**base path** (usually `/blog`). Readers navigate real URLs
(`/blog/my-post-slug`), so the site's server must return the embedding page
for the base path **and every path under it**. That shell rule is the one
step integrations most often miss; it is Step 3 below.

## Required inputs — collect these before editing anything

Ask the user for whatever is missing; do not guess:

1. **Publishable key** (`pk_…`) — shown on the site's page in the baas
   studio dashboard. It is public by design and safe to commit in frontend
   code.
2. **Base path** — where the blog mounts (default `/blog`). It must equal
   the blog's base path configured in the dashboard; Step 0 verifies this.
3. **The website codebase** — where it lives and how it is served
   (framework, static host, nginx, CMS, …).

Never ask for or use a secret key (`sk_…`). Delivery needs none. If the
user pastes one, tell them it is not needed here and should stay private.

Throughout this file, `PK` means the publishable key and `BASE` means the
base path with no trailing slash (e.g. `/blog`).

## Step 0 — validate the key and base path before touching code

```
curl -sS "https://api.dailysmith.com/v1/render/PK/BASE?format=fragment"
```

For `PK=pk_abc`, `BASE=/blog` that is
`https://api.dailysmith.com/v1/render/pk_abc/blog?format=fragment`.

- **200 with JSON** containing `"kind":"index"` → proceed. An empty post
  list is fine — content appears when the user publishes; no redeploy
  needed.
- **404, body mentions `unknown or unverified site key`** → the key is
  wrong, or the site has not passed domain verification. Installation
  cannot work until the user verifies the site in the dashboard
  (instructions: https://dailysmith.com/docs/verification). Stop and report this.
- **404, body mentions `no blog is mounted at this path`** → the base path
  is wrong. List the real ones:
  `curl -sS "https://api.dailysmith.com/v1/delivery/PK/blogs"` returns
  `{"blogs":[{"name":…,"base_path":…,"default_locale":…}]}`. Use the
  listed `base_path`, or have the user change it in the dashboard.

## Step 1 — decide the host's rendering model

Answer this before you edit anything. Almost every instruction below
branches on it, and getting it wrong is how a broken install passes its
own checks.

**Does the host produce the HTML for a URL at request time, or at build
time?** Decide from the build and deploy config, not the framework name —
the same framework does both depending on its adapter or output mode.

**Request-time (SSR).** A server or edge function runs per request:
SvelteKit on `adapter-node` / `-vercel` / `-cloudflare`, Next.js on a Node
or edge runtime, Astro with `output: 'server'`, Rails / Django / Laravel,
WordPress, nginx in front of an application server.

> One route that matches `BASE` and everything under it satisfies Steps 2
> and 3 together. Step 3b does not apply.

**Build-time (static export / SSG / SPA).** The deploy is a folder of
files and nothing runs per request: SvelteKit on `adapter-static`, Astro's
default static output, Next.js with `output: 'export'`, any Vite / CRA /
`nuxt generate` SPA, Hugo / Jekyll / Eleventy, anything on GitHub Pages,
and Cloudflare Pages / Netlify / Vercel projects with no server functions.

> Three *separate* things are required, and each one fails differently:
>
> 1. a **host rewrite** (Step 3a) — without it the deep URL 404s;
> 2. a **catch-all route the build actually emits** (Step 2) — without it
>    the shell is served but never hydrates, or the build fails outright;
> 3. **absolute asset URLs** (Step 3b) — without them the entire bundle
>    404s on deep URLs and the page is dead on arrival.
>
> Any one of these missing still leaves the other two looking healthy, and
> all three are invisible at `BASE` itself. Step 7 checks all three.

If you cannot tell which model applies, say so and ask. Do not assume SSR
because the repo uses an SSR-capable framework.

## Step 2 — embed the snippet at the base path

The page served at `BASE` must contain:

```html
<div data-baas-blog
     data-key="PK"
     data-base-path="BASE"
     data-api="https://api.dailysmith.com"></div>
<script src="https://api.dailysmith.com/v1/sdk/blog.js" async></script>
```

For a client-routed framework (React, Vue, SvelteKit, …), **Step 4's
runtime injection replaces that `<script>` tag** — it does not supplement
it. Render the container in the route component and wire the script per
Step 4; the served HTML will contain the container but no script tag, and
that is correct.

Rules:

- The SDK uses the **first** `[data-baas-blog]` element on the page — one
  blog container per page.
- Keep the site's own header, footer, and styles around the container. The
  blog inherits the host's font, colour and line-height, but nothing else
  — read Step 5 before you call the install done.
- `data-api` must always be set — without it the SDK falls back to the
  hosted default origin, which is wrong for this deployment.
- Strict Content-Security-Policy sites need the API origin in **four**
  directives (add them wherever the site defines its CSP — headers file,
  meta tag, or server config):

  ```
  script-src  'self' https://api.dailysmith.com          # the SDK script
  connect-src 'self' https://api.dailysmith.com          # the content fetches
  img-src     'self' data: https://api.dailysmith.com    # post hero images
  style-src   'self' https://api.dailysmith.com 'unsafe-inline'
  ```

  (`style-src`: the API origin for the rich-content stylesheet `<link>`;
  `'unsafe-inline'` for the small per-blog theme `<style>`.)

  `img-src` is the one that gets forgotten. Hero images are served from the
  API origin (`/v1/media/…`), so the common strict default `img-src 'self'
  data:` drops every hero — silently, and only on the posts that have one,
  so an index and a hero-less post both look perfectly fine.
- If the site has its own SEO/head manager, do not skip **Step 6**.

### The route must be a catch-all

Every framework line below uses a catch-all segment. That is load-bearing,
not stylistic:

> **`[...rest]`, not a plain `+page.svelte`** — a plain route serves the
> base path only. On a deep URL the host rewrite hands the framework a
> path its router does not recognise, so the router bails, the route
> component never mounts, the SDK is never injected, and the reader gets a
> served-but-dead page. It presents exactly like a broken SDK and is
> expensive to diagnose. The host rewrite and the catch-all solve
> different halves and you need both: the rewrite makes the deep URL
> return the shell at all; the catch-all makes the framework hydrate it.

Where the snippet goes, by setup:

- **Plain static hosting** — a `blog/index.html` (site chrome + snippet)
  served at `BASE`. No router involved; the rewrite is the whole story.
- **Next.js (App Router)** — `app/blog/[[...slug]]/page.tsx` (optional
  catch-all) rendering the container; inject the script per Step 4.
- **SvelteKit** — `src/routes/blog/[...rest]/+page.svelte`; inject the
  script per Step 4 if the app navigates client-side.
- **Astro** — `src/pages/blog/[...slug].astro` rendering the container and
  snippet.
- **Any CMS** — a full-width page or template mounted at `BASE`, with the
  CMS's router sending `BASE/*` to it.

Adapt the directory names when the user's base path is not `/blog`.

### Build-time hosts: the catch-all needs an explicit entry

On a prerendering build, a catch-all route with nothing linking into it is
never reached by the crawler, so the build emits no file for it — and most
prerenderers treat that as a **hard build failure**, not a warning. This
bites on a clean install, before anything has been deployed.

You are not prerendering every post. You need exactly **one** emitted HTML
file, at `BASE`; the rewrite in Step 3a covers everything deeper.

- **SvelteKit** — add `src/routes/blog/[...rest]/+page.ts`:

  ```ts
  export const prerender = true;
  export const entries = () => [{ rest: '' }];
  ```

- **Astro** — `export async function getStaticPaths() { return [{ params:
  { slug: undefined } }]; }` in the `.astro` page.
- **Next.js (`output: 'export'`)** — `export function
  generateStaticParams() { return [{ slug: [] }]; }`.

Check the emitted output directory afterwards: a file must exist at
`<out>/blog/index.html`. If it does not, nothing else in this playbook can
work.

## Step 3 — serve the shell for every blog URL

### 3a — the rewrite

A hard refresh on `BASE/any-post-slug` must return the Step 2 page with
HTTP 200 — the SDK reads the URL on load and renders the right article.
Configure whichever applies:

- **Netlify** — `_redirects`:

  ```
  /blog/*   /blog/index.html   200
  ```

- **Vercel** — `vercel.json`:

  ```json
  { "rewrites": [{ "source": "/blog/:path*", "destination": "/blog" }] }
  ```

- **Cloudflare Pages** — a `_redirects` file in the output directory, same
  syntax as Netlify.

- **nginx**:

  ```nginx
  location /blog {
      try_files $uri /blog/index.html;
  }
  ```

- **Request-time frameworks** — the catch-all route from Step 2 is the
  rewrite; confirm no more-specific route or middleware shadows `BASE/*`.
- **Build-time frameworks** — the catch-all route is **not** enough. It
  makes the client router recognise the URL; it does nothing about the
  host having no file at that path. Add the host rewrite above as well.

On a build-time host the rewrite must be **emitted by the build** (a
committed `_redirects` or `vercel.json` in the repo), not added by hand in
a dashboard, or the next pipeline change drops it.

### 3b — build-time hosts: asset URLs must be absolute

Do not skip this. It is the single most common way a static install ships
broken, and it is a direct consequence of 3a rather than an edge case.

Static builds routinely emit **relative** asset references (`_app/…`,
`./assets/…`, `assets/…`). Those resolve against the current URL. At
`BASE` they happen to resolve correctly. At `BASE/my-post` they resolve to
`BASE/_app/…` — and the rewrite you just added answers *that* with the
HTML shell, because a rewrite cannot tell an asset path from a post slug.
The browser receives `text/html` where it expected JavaScript, refuses the
file for MIME mismatch, and the app never boots. Every script, every
stylesheet, silently, and **only on deep URLs** — the base path keeps
working perfectly, which is exactly why this survives casual testing.

Fix it at the source, so the build emits root-absolute URLs:

- **SvelteKit** — `kit.paths.relative = false` in `svelte.config.js`.
- **Vite / any SPA** — `base` must be an absolute path (`'/'`), never
  `'./'`.
- **Next.js** — the default `/_next/…` is already absolute; if
  `assetPrefix` has been set to something relative, make it absolute.
- **Astro** — already absolute; no action.
- **Anything else** — same requirement, whatever that generator calls it.

This is global build config, not blog-route config. It is the one
sanctioned exception to the minimal-diff rule (see Rules) — call it out
explicitly in your report so the user knows a shared setting changed.

Then prove it rather than assuming it. Build, and inspect the emitted
shell:

```
grep -oE '(src|href)="[^"]+"' <out>/blog/index.html | sort -u
```

Every `<script>` and `<link>` URL must start with `/` or `https://`.
Anything starting with `.` or a bare filename will break on deep URLs.

### 3c — the consequence to report to the user

Do not skip this in your report: with the shell rule in place, **every URL
under `BASE` answers HTTP 200** — the real status lives in the API. The
SDK handles it (on an API 404 it renders a not-found state, sets `<meta
name="robots" content="noindex">`, and fires a cancelable `baas:notfound`
event the site can use to render its own 404), but a site that needs
unknown URLs to return a real HTTP 404 to crawlers should use the edge
proxy or prerender install instead (https://dailysmith.com/docs/install-proxy,
https://dailysmith.com/docs/install-prerender).

## Step 4 — client-side routed apps (React, Vue, SvelteKit, …)

The SDK binds when its script executes, and re-binding is **idempotent**:
re-executing the script or calling `window.baasBlog.init()` destroys the
previous instance first (listeners unbound, `<head>` restored), so route
re-entry is safe. `window.baasBlog.destroy()` tears down explicitly. Use
one of:

1. **Wire it to the blog route's lifecycle**:

   ```jsx
   // React example — same shape in any framework's mount hook
   useEffect(() => {
     if (window.baasBlog) {
       window.baasBlog.init();          // already loaded: rebind to this container
     } else {
       const s = document.createElement('script');
       s.src = 'https://api.dailysmith.com/v1/sdk/blog.js';
       s.async = true;
       document.body.appendChild(s);    // executes and binds on load
     }
     return () => window.baasBlog && window.baasBlog.destroy();
   }, []);

   return <div data-baas-blog data-key="PK"
               data-base-path="BASE" data-api="https://api.dailysmith.com" />;
   ```

2. **Hard-navigate into the blog** — if links into `BASE` are plain
   `<a>` tags opted out of the router's link interception, the static
   snippet works as-is.

### Keep the host router off the SDK's anchors

The SDK renders its own `<a data-baas-link>` elements *inside* the host
router's DOM and intercepts their clicks itself. The host router does not
know that, and any link behaviour it applies globally will fire on URLs it
cannot serve — SvelteKit's `data-sveltekit-preload-data="hover"`, for
instance, will request `/blog/<slug>/__data.json` on hover and log an
error for every card in the index. Opt the container out:

```html
<div data-baas-blog data-sveltekit-preload-data="off" …></div>
```

Do the equivalent for whatever the host router uses (Next's
`<Link prefetch>` is not involved, since the SDK emits plain anchors).

## Step 5 — style the blog into the site

"Inherits the site's typography" means font-family, colour and
line-height, and nothing more. On any designed site, the defaults below
will look broken unless you address them. Budget real time for this step.

What the SDK ships for a fragment is four presentational rules: a host
reset on `.baas-wrap.md2`, `max-width` containment for images, hero
sizing, and a background for non-rich code blocks. Everything else is the
host's job.

### The markup you are styling

| Class | Where |
|---|---|
| `.baas-wrap` | wrapper the SDK puts around every fragment (also carries `md2`) |
| `.baas-index` | index root |
| `.baas-card` | one post teaser (`h2 > a` title, `p` excerpt) |
| `.baas-pagination` | newer/older nav, when there is more than one page |
| `.baas-post` | article root on a post page |
| `.baas-back` | "← <blog name>" backlink at the top of a post |
| `.baas-hero` | hero `<img>` |
| `.baas-meta` | date · reading-time line, on cards and posts |
| `a[data-baas-link]` | in-blog links the SDK intercepts |

Rich-content components (callouts, steps, tables, charts) come from
`md2.css` and are prefixed `md2-`.

### The four things you will need to fix

- **Links are unstyled.** The fragment CSS sets no link colour anywhere,
  so the backlink, every card title, and every in-prose link render as
  browser-default blue underline. Style `.baas-wrap a`, `.baas-card h2 a`
  and `.baas-back` to the host's own link treatment.
- **The measure is whatever the host gives it.** The container inherits
  the width of the section you dropped it into; in a normal full-width
  page wrapper, articles run at roughly double a readable measure.
  Constrain `.baas-wrap` (or its parent) to the host's article measure.
- **`md2.css` brings its own type scale** — heading sizes and heading
  rhythm chosen for an unknown host. On a site with a larger display scale
  it reads wrong immediately. Override the heading sizes to the host's.
- **The hero's `border-radius: .75rem` is hard-coded** in the SDK's own
  CSS and is not tokenised. On a site that is not rounded-by-default,
  override `.baas-hero { border-radius: … }` by hand.

### Specificity: one number to beat

The SDK injects its CSS into `<head>` **at runtime**, so it always lands
after the host's build-time stylesheet. At equal specificity the later rule
wins, so a plain `.baas-hero { … }` in the host's sheet loses to the SDK's.
Order will not rescue an override; only specificity will.

Every token block baas injects — the blog palette and `md2.css`'s own host
tokens, in every colour scheme — declares its custom properties at exactly
**(0,1,0)**, on `.baas-wrap` and `[data-baas-blog]`. The light, OS-dark and
host-forced variants differ only inside `:where()`, which contributes no
specificity, so there is one number to beat and not one per scheme.

**Beat it with (0,2,0) or better** — any second class, attribute or
ancestor. `.my-blog .baas-wrap` is exactly (0,2,0) and already enough;
`:root[data-theme="dark"] .my-blog .baas-wrap` is (0,4,0) and leaves
headroom. The presentational rules (hero sizing, image and code
containment) are (0,1,0) too; the single exception is the host reset
`.baas-wrap.md2`, which is (0,2,0) by design, so overriding *that* needs
(0,3,0).

### Custom properties do not cascade like the rest

This is the least obvious thing in the whole integration and it fails
silently, so read it before you write a single token override.

A custom property declared **on** an element beats one inherited from an
ancestor, however specific the ancestor's rule. baas declares `--md2-*` on
`.baas-wrap` itself. So the obvious move — setting the tokens on your own
container, which is `.baas-wrap`'s parent — has no effect whatsoever, at any
specificity:

```css
/* does nothing: --md2-bg is re-declared on .baas-wrap, one level down */
:root[data-theme="dark"] .my-blog { --md2-bg: var(--surface-raised); }
```

Name the wrapper. Listing the container alongside it is worth doing — the
server-rendered and WordPress paths put the same tokens on the container:

```css
:root[data-theme="dark"] .my-blog[data-baas-blog],
:root[data-theme="dark"] .my-blog .baas-wrap { --md2-bg: var(--surface-raised); }
```

### The token contract

Fifteen colours and a radius. The split matters when the host has more than
one theme: **surfaces and text need a value per theme; the five hues do
not** — they are mid-tone accents and read on either canvas.

| Token | Paints | Per theme |
|---|---|---|
| `--md2-text` | text inside components | yes |
| `--md2-muted` | secondary text, captions, meta | yes |
| `--md2-bg` | component canvas — panels, table cells | yes |
| `--md2-bg-soft` | secondary fill — table headers, code panels | yes |
| `--md2-border` | every component border and rule | yes |
| `--md2-radius` | corner rounding (a CSS length, not a colour) | no |
| `--md2-info` `--md2-warning` `--md2-blocking` `--md2-success` `--md2-nit` | the five accent hues | no |
| `--md2-info-bg` … `--md2-nit-bg` | the wash behind each accent | yes |

`blocking` is the dashboard's **Danger** and `nit` its **Note**; the other
names match. MD2 also resolves a few daisyUI-named fallbacks —
`--color-base-100` (= `bg`), `--color-base-200` (= `bg-soft`),
`--color-base-300` (= `border`), `--color-base-content` (= `text`) — so set
those too if you are re-pointing surfaces wholesale.

Retheming through tokens is cleaner than overriding component rules.

### Hosts with light and dark themes

Most designed sites have two themes, usually switched with `data-theme` on
`<html>` or by `prefers-color-scheme`. Three things to know:

- **The SDK never writes `data-theme`.** The wrapper carries no theming
  attribute of its own, so the host's palette rules — including bare
  `[data-theme="light"] { … }` selectors — cannot leak into the blog
  subtree. Nothing to do; no host CSS to rewrite.
- **The blog follows the host's `data-theme` automatically.** Set on
  `<html>`, on a wrapper, or on the container, it selects the blog's dark
  or light palette, ahead of the reader's OS preference. A site that
  toggles the conventional way re-themes the blog for free.
- **Anything else needs one attribute or one CSS block.** If the site
  switches schemes another way (a `.dark` class, a custom attribute), or is
  light-only, or dark-only, put `data-baas-theme="light"` or `"dark"` on the
  container. `data-baas-theme="inherit"` stands the blog's configured
  palette down entirely so your own tokens are the only source.

When the site's tokens should drive the blog, write this once per theme.
The host keeps its own tokens as the single source of truth, so the blog
re-themes with the site forever; the blog's configured palette still
supplies the hues, so the dashboard is not made pointless:

```css
/* Both selectors: the container AND the wrapper — a custom property declared
   on .baas-wrap beats one inherited from its parent. Leading with
   :root[data-theme] puts the rule well past the (0,1,0) it has to beat to
   outrank the runtime-injected sheets. Hues are left to the blog. */
:root[data-theme="dark"] .my-blog[data-baas-blog],
:root[data-theme="dark"] .my-blog .baas-wrap {
  --md2-text: var(--text-body);
  --md2-muted: var(--text-muted);
  --md2-border: var(--border-neutral);
  --md2-bg: var(--surface-raised);
  --md2-bg-soft: var(--surface-card);
  --md2-info-bg: color-mix(in srgb, var(--md2-info) 14%, var(--surface-raised));
  /* …warning / blocking / success / nit washes the same way… */
  --color-base-100: var(--surface-raised);   /* daisy fallbacks md2 also reads */
  --color-base-200: var(--surface-card);
  --color-base-300: var(--border-neutral);
  --color-base-content: var(--text-body);
}
/* …and the mirror image for :root[data-theme="light"]. */
```

Substitute the host's real token names. If the host has no token layer,
either add one or write literal values twice.

Check the result in **both** themes on a post that uses callouts and
panels, and check the OS preference the site is not currently showing — a
palette that is correct in the default theme and wrong in the other one is
the normal failure here, and it is invisible until you toggle.

## Step 6 — if the site manages its own head

Under the base path the SDK owns `document.title`, the description and
`og:*` metas, the canonical link, and JSON-LD by default. Either keep the
site's head manager off blog routes, or set `data-baas-head="false"` on
the container and apply the metadata yourself.

The conflict that decides it: the SDK **reuses** an existing `<meta>` or
`<link rel="canonical">` node (remembering the host's value and restoring
it on `destroy()`), so those never duplicate — but it **appends** its own
`<script id="baas-jsonld">`. A host that emits its own JSON-LD therefore
ends up with two blocks on every post, with the host's breadcrumb still
describing the base path. That is the actual reason to opt out.

Opting out means consuming the `baas:page` event, whose detail is:

```
{ path, kind, title, description, canonical, json_ld, locale,
  hero_image_url }
```

Things the payload will not tell you, that you need anyway:

- **`kind`** is `"index"` or `"post"` — those two values, nothing else.
- **`hero_image_url`** is the absolute URL of the post's hero, or `""`.
  It is the only source for `og:image` / `twitter:image`; the fragment
  markup contains the same image, but if you skip this field every post
  ships a generic social card.
- **`json_ld`** is a **string** holding one bare JSON object with
  top-level `@context` and `@type: "BlogPosting"`. It is never an array
  and never uses `@graph`. Emit it verbatim inside a
  `<script type="application/ld+json">`; escape `<`, `>` and `&` if your
  framework does not. It can be `"{}"` for a post that has none, and it
  is empty on the index.
- **`title` already carries a site suffix.** The index is always
  `"<blog name> — <site name>"`; a post uses its own SEO title tag if one
  is set and otherwise `"<post title> — <site name>"`. There is no
  unsuffixed variant in the payload, so a host head manager that appends
  its own brand suffix double-suffixes every page. Strip yours on blog
  routes.
- **`description` is empty on the index** (`""`), always. A head manager
  that passes it through emits an empty meta description on the blog
  landing page; supply a fallback.

The SDK also fires `baas:navigate` after each `history.pushState` (client
routers cannot observe pushState), and cancelable `baas:notfound` /
`baas:error` events — cancel `baas:notfound` to render the site's own 404
UI. Presentational CSS still ships when head management is off; only
`<head>` metadata is handed back to you.

## Step 7 — verify

Phrase every check as "prove it works," never "prove the markup is there."
A `curl` can only tell you the shell was *served*; it cannot tell you the
shell *booted*, and both of the failures in Step 2 and Step 3b serve a
shell that looks perfectly healthy to `curl` while the page is dead in a
browser. The browser check below is therefore not optional, and it must
run at a **hard load of a deep URL** — every failure this playbook warns
about is invisible at `BASE` itself.

Drive a browser yourself if you can. If you cannot, say so plainly and
hand the user checks 3–5 as a checklist. Do not report an install as
verified on checks 1–2 alone.

1. **SDK asset reachable** — `curl -sI "https://api.dailysmith.com/v1/sdk/blog.js"` →
   200, `application/javascript`.
2. **Shell served deep** — fetch `BASE/verify-shell-check`: HTTP 200, body
   is the shell (contains `data-baas-blog`), not the site's 404 page.
   Necessary but **not sufficient** — it rules out only "the rewrite is
   missing."
3. **The blog boots on a deep URL — the real check.** Hard-load
   `BASE/<a real post slug>` in a browser (fresh navigation, not a
   client-side click) and confirm all four:

   ```js
   // every line must hold
   !!window.baasBlog                                        // SDK executed
   !!document.querySelector('[data-baas-blog] .baas-wrap')  // content replaced
   performance.getEntriesByType('resource')                 // must be []
     .filter(r => new URL(r.name).pathname.startsWith('BASE/'))
     .map(r => r.name)
   ```

   …plus **zero failed requests and zero console errors**. A non-empty
   third result, or a console error mentioning `MIME type` / `Refused to
   execute`, is the Step 3b asset failure. A served page where
   `window.baasBlog` is undefined or the container still holds its
   placeholder is the Step 2 catch-all failure.
4. **Navigation and the not-found state** — from `BASE`, click a post: the
   URL and document title change. Then hard-load
   `BASE/definitely-not-a-post`: the not-found state renders and `<meta
   name="robots" content="noindex">` is in the head.
5. **Rich content survives the click path** — click into a post that uses
   callouts or steps; they must arrive styled. The component stylesheet
   loads as `<link id="baas-md2-css">` to `https://api.dailysmith.com/v1/sdk/md2.css`.
6. **Your Step 5 overrides actually apply** — check the computed styles of
   `.baas-back`, a `.baas-card h2 a`, and `.baas-hero`. A losing override
   fails silently.
7. **Both themes, if the site has two** — toggle the site's theme with a
   post open and read the tokens off the wrapper, not off your container:

   ```js
   getComputedStyle(document.querySelector('.baas-wrap'))
     .getPropertyValue('--md2-bg')     // must change with the toggle
   ```

   A value that does not move is a token override written on the container
   only, or one that lost at (0,1,0) — both covered in Step 5. Also confirm
   the article's text is legible against the page background in each theme;
   the failure mode is a subtree painted in the palette the page is not
   using, which no console error will report.

If you are working in a git worktree or a monorepo package, a failing
build may have nothing to do with this install (missing linked deps,
untracked local config, a workspace root the tooling cannot find).
Reproduce in a plain checkout before you diagnose it as an integration
problem, and say which environment you verified in.

## Step 8 — report back to the user

Summarize: files you changed — **calling out any global build config you
had to touch for Step 3b** — how the shell rule is satisfied, which
verification checks passed and which you handed back, the every-URL-200
consequence from Step 3c, and what remains theirs: publishing posts from
the baas studio dashboard (new content appears with no redeploy), and an
optional SEO upgrade — serving full server-rendered pages via an edge
proxy (https://dailysmith.com/docs/install-proxy) or static prerender
(https://dailysmith.com/docs/install-prerender) while the SDK keeps handling
navigation.

## Rules

- Keep the diff minimal and in the codebase's existing style: the blog
  route, routing/rewrite config, and the styles from Step 5. A build-time
  host additionally requires the global asset-URL setting from Step 3b and
  the prerender entry from Step 2 — those are expected, not scope creep.
  Report them; do not silently widen further.
- Do not commit, push, or deploy without the user's explicit go-ahead.
- `pk_…` keys are public; `sk_…` keys never belong in this integration.
- If something still fails, consult
  https://dailysmith.com/docs/troubleshooting before improvising.
