# JS SDK

The fastest integration: two lines of HTML, no build step, no CMS. The SDK
renders the blog into a container on your existing page and handles
navigation client-side.

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

Put this on the page served at your blog's base path (`/blog` by default).
Your header, footer, and styles stay — the blog renders inside the
container and inherits your site's typography.

:::callout{severity=info title="Using an AI coding assistant?"}
This whole page exists as a playbook written for agents. Grab the prompt
from [Install with AI](/docs/install-ai), paste it into your assistant,
and it wires the snippet, configures the URL shell, and verifies delivery
for you.
:::

## Attributes

| Attribute | Required | Meaning |
|---|---|---|
| `data-key` | yes | Your site's public key (`pk_…`, shown on the site page) |
| `data-api` | yes | API origin to load content from — always set it; the snippet above pins it to `https://api.dailysmith.com` |
| `data-base-path` | no | Path the blog is mounted at; default `/blog`. Must match the blog's base path configured in the dashboard **and** the URL path this page is served under. |
| `data-baas-head` | no | Set to `"false"` to keep the SDK out of your `<head>` entirely — see "If your site manages its own head" below. |
| `data-baas-theme` | no | `"light"` or `"dark"` pins the blog to one colour scheme whatever the page around it does; `"inherit"` stands the blog's palette down so your own CSS supplies every token. Left unset, the blog follows your site's `data-theme`, then the reader's system preference. See [Theming](/docs/theming). |

The SDK uses the **first** `[data-baas-blog]` element on the page; one blog
container per page. It also exposes `window.baasBlog` with `init(el?)` and
`destroy()` for client-routed apps — see below.

## Serve the shell for every blog URL

:::callout{severity=warning title="The one requirement people miss"}
The SDK pushes real URLs (`/blog/my-post-slug`) as readers navigate. Your
server must return the page containing the snippet for **the base path and
everything under it** — otherwise a refresh or a shared link to a post hits
your 404 before the SDK can load. The SDK reads the current URL on init, so
once the shell is served, the right article renders.
:::

How to do that on common setups:

**Netlify** — `_redirects`:

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

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

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

**nginx**:

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

**Frameworks** — use a catch-all route as the blog page and put the snippet
in it: `app/blog/[[...slug]]/page.tsx` (Next.js),
`src/routes/blog/[...rest]/+page.svelte` (SvelteKit),
`src/pages/blog/[...slug].astro` (Astro).

### The trade-off: every blog URL now answers 200

The shell rewrite means `/blog/anything-at-all` returns HTTP 200 with the
shell — the real status lives one hop away, in the API. Know what happens
on that soft-404 surface:

- When the API returns **404 for a missing post**, the SDK renders a
  distinct not-found state, sets `<meta name="robots" content="noindex">`,
  clears its canonical and JSON-LD, and fires a **cancelable**
  `baas:notfound` event — call `preventDefault()` on it to render your own
  404 UI instead.
- Network failures and server errors are separate: the SDK fires
  `baas:error` with `{status}` (`0` means the fetch itself failed) and
  shows a "temporarily unavailable" message.
- If you need unknown URLs to answer with a **real HTTP 404** for
  crawlers, that's what the [edge proxy](/docs/install-proxy) and
  [static prerender](/docs/install-prerender) paths give you — the API's
  status passes through.

## Client-side routed apps (React, Vue, SvelteKit, …)

The script binds when it executes, and re-executing it (or calling
`window.baasBlog.init()`) re-binds idempotently: the previous instance is
destroyed first, so you never stack listeners. `destroy()` unbinds the
`popstate` listener, clears the container, and restores every `<head>`
node the SDK created or overwrote.

In an app that swaps routes client-side, wire it to the blog route's
lifecycle:

```jsx
// React example — the same shape works in any framework's mount hook
useEffect(() => {
  if (window.baasBlog) {
    window.baasBlog.init();            // SDK 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_YOUR_KEY"
            data-base-path="/blog" data-api="https://api.dailysmith.com" />;
```

Alternative: **hard-navigate into the blog.** If your blog links are plain
`<a>` tags that trigger a full page load (opt the blog route out of your
router's link interception), the static snippet works as-is.

One more router note: the SDK navigates with `history.pushState`, which
client routers cannot observe. It fires `baas:navigate` after every push so
analytics, breadcrumbs, or scroll managers can hook in (see Events).

## If your 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. That is the contract — don't
also manage those from your own router while a blog URL is active, or the
two systems will fight. The SDK tags every node it creates with a
`data-baas` attribute, remembers the original value of any host node it
overwrites, and puts everything back on `destroy()`.

If your site has its own SEO layer, opt out instead: set
`data-baas-head="false"` on the container and apply the metadata yourself
from the `baas:page` event, which carries everything the head needs.

## Events

All events are `CustomEvent`s that bubble up from the container, so you can
listen on `document`:

| Event | Detail | Notes |
|---|---|---|
| `baas:page` | `{path, kind, title, description, canonical, json_ld, locale, og_locale, hero_image_url}` | After every successful render (`kind` is `index` or `post`) |
| `baas:notfound` | `{path}` | Missing post (API 404). Cancelable — `preventDefault()` and render your own 404 |
| `baas:error` | `{status, path}` | Fetch failed; `status: 0` means a network error. Cancelable |
| `baas:navigate` | `{path}` | After `history.pushState` — routers can't see it, this is your hook |

```js
document.addEventListener('baas:notfound', (e) => {
  e.preventDefault();
  renderMyOwn404();
});
document.addEventListener('baas:navigate', (e) => {
  analytics.pageview(e.detail.path);
});
```

## Content-Security-Policy

A strict-CSP site needs the API origin in four directives:

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

`style-src` needs the API origin for the rich-content stylesheet (loaded as
a `<link>`), and `'unsafe-inline'` only if your blog has a
[theme palette](/docs/theming) — the per-blog theme is injected as one
small inline `<style>` element.

:::callout{severity=warning title="img-src is the one that gets forgotten"}
Hero images are served from the API origin, so the common strict default
`img-src 'self' data:` drops every hero — with no error you would notice,
and only on the posts that have one. Your index and your hero-less posts
will look perfectly fine.
:::

## What the SDK does

- Fetches server-rendered HTML fragments and injects them into the
  container (`aria-busy` is set while loading).
- Patches `document.title`, meta description, Open Graph tags, the
  canonical link, and JSON-LD per page — tagged `data-baas`, restored on
  `destroy()`.
- Intercepts in-blog links for instant History-API navigation; back/forward
  work as expected; `baas:navigate` fires for your router or analytics.
- Loads the rich-content component stylesheet once as a cacheable `<link>`
  (`#baas-md2-css` → `https://api.dailysmith.com/v1/sdk/md2.css`) and injects your
  blog's [theme palette](/docs/theming) as a small inline `<style>`
  (`#baas-theme-css`), replacing it when it changes.
- Fetches that stylesheet from the index too, once your page has finished
  loading, so opening a post never shows unstyled content while a cold
  stylesheet is in flight. A post is injected only after its styles apply,
  capped at two seconds — a stylesheet that never arrives costs the styling,
  not the post.

## Static sites and prerendered hosts

Two facts that matter when your site is prerendered or fully static:

- The rewrite rule from "Serve the shell" must be **emitted by your
  build** (a committed `_redirects` file, a `vercel.json` in the repo) —
  a rule added by hand in a dashboard disappears on the next deploy
  pipeline change.
- Until JavaScript runs, every post URL serves the prerendered shell's
  `<head>` — the shell's title and canonical, not the post's. Crawlers
  that execute JS get the right values; for guaranteed per-URL HTML, run
  the [prerender CLI](/docs/install-prerender) at build time — its output
  doubles as the shell.

## Verify it works

::::steps

:::step[Open the blog index]
Visit `/blog` on your site. The post list should render inside your page
chrome. In devtools' network tab you'll see a request to
`https://api.dailysmith.com/v1/render/pk_…/blog?format=fragment` — this is also the
check that works for SPA installs, where the script tag is injected at
runtime and never appears in the served HTML.
:::

:::step[Click into a post, then refresh]
The URL should change to `/blog/<slug>`, the document title should update —
and a hard refresh on that URL must render the same article. If the refresh
404s, revisit "Serve the shell for every blog URL" above.
:::

:::step[Check the head and the 404 state]
After opening a post, the document head should carry its title, meta
description, canonical URL, and a `#baas-jsonld` script tag. Then visit
`/blog/definitely-not-a-post`: you should see the not-found state, and the
head should carry `<meta name="robots" content="noindex">`.
:::

::::

## SEO: pair it with server-side delivery

The SDK alone renders after JavaScript loads. Modern crawlers execute JS,
but for guaranteed crawler-grade SEO serve the same content statically too:

- [Edge proxy](/docs/install-proxy) — a rewrite rule serves full SSR pages
  from your domain; the SDK then upgrades navigation client-side. (The
  proxy rule also satisfies the shell requirement above, and unknown URLs
  return real 404s.)
- [Static prerender](/docs/install-prerender) — bake HTML files at deploy
  time; the prerendered pages double as the shell for every post URL.
