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.

<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.

Attributes

AttributeRequiredMeaning
data-keyyesYour site's public key (pk_…, shown on the site page)
data-apiyesAPI origin to load content from — always set it; the snippet above pins it to https://api.dailysmith.com
data-base-pathnoPath 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-headnoSet to "false" to keep the SDK out of your <head> entirely — see "If your site manages its own head" below.
data-baas-themeno"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.

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

How to do that on common setups:

Netlify_redirects:

/blog/*   /blog/index.html   200

Vercelvercel.json:

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

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 and static 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:

// 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 CustomEvents that bubble up from the container, so you can listen on document:

EventDetailNotes
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
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 — the per-blog theme is injected as one small inline <style> element.

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-csshttps://api.dailysmith.com/v1/sdk/md2.css) and injects your blog's theme palette 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 at build time — its output doubles as the shell.

Verify it works

  1. 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.

  2. 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.

  3. 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 — 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 — bake HTML files at deploy time; the prerendered pages double as the shell for every post URL.
enro