Connecting a website

A client website shows Headra content by fetching it from the API with JavaScript (or at build time) and putting it into the page. The website can be plain HTML on any hosting, a Bootstrap Studio export, or a React, Vue, Next or Nuxt app.

How it fits together

  1. Headra (https://app.headracms.com) stores the content and answers API requests with JSON.
  2. The client website loads two small scripts, asks the API for content with a public key, and renders it into its own HTML and CSS.
  3. Search engines read the sitemap and robots.txt, which Headra serves on the client's API domain. Nothing extra is uploaded to the client's hosting.

What you need

API addresshttps://app.headracms.com
Public API keyCreate one under API keys (kind: public). It's read-only and only sees published content, so it's safe in website code.
Filesconfig.js and headra.js, plus your own HTML pages. The starter kit below has everything.

Option A: start from the starter kit

A complete, working mini-site in plain HTML and JavaScript, with no build step and no framework: a magazine-style blog (cards, category chips, search, pages, articles with related posts) with a light/dark switcher, styled in one plain CSS file.

FileWhat it does
index.html Blog page: latest posts with search, category filter and pages. Download
post.html One post: title, author, image, body, categories and tags. Sets SEO tags and counts the view. Download
collection.html A list of any content type (collection.html?type=…). Download
item.html One item of any type, every field rendered (item.html?type=…&slug=…). Download
config.js Settings: API address, public key, menu and which fields are rich text. Edit this one. Download
headra.js The Headra client: fetches content, renders menus, sets SEO tags, counts views. No changes needed. Download
styles.css Basic styles. Replace with your own design. Download
README.md Notes for developers. Download

Download all as a .zip

  1. Put the files in the website's folder.
  2. Open config.js and paste the public key in place of hcms_pub_REPLACE_ME.
  3. Open index.html in a browser. Posts, the menu and the footer links to the sitemap appear.
  4. Restyle styles.css and the HTML to match the design, or copy the parts you need into the real site (Option B).

Option B: add content to an existing site (e.g. Bootstrap Studio)

Every page that shows content needs the two scripts, in this order, just before </body>:

<script src="config.js"></script>
<script src="headra.js"></script>
<script src="latest-posts.js"></script>   <!-- your own code for this page -->

In Bootstrap Studio, add config.js, headra.js and your own script under Design → JavaScript, in that order. Then add an empty element with an id where the content should go, e.g. a div with id latest-posts.

Publishing on Bootstrap Studio's own hosting with a custom domain? Bootstrap Studio hosting covers the whole setup, including Cloudflare and the sitemap.

A list of posts: latest-posts.js

(async () => {
    const { api, esc, image, field, date } = Headra;
    const result = await api('/api/posts', { per_page: 6, fields: 'excerpt,featured_image' });
    document.querySelector('#latest-posts').innerHTML = result.data.map((post) => `
        <div class="col-md-4">
            <a href="post.html?slug=${encodeURIComponent(post.slug)}">${image(post.fields.featured_image, 'medium')}</a>
            <h3>${esc(post.title)}</h3>
            <p class="text-muted">${esc(date(post.published_at))}</p>
            ${field('excerpt', post.fields.excerpt)}   // rich text (basic): inserted as HTML
        </div>`).join('');
})();

One post, with SEO tags and view counting: post.js

(async () => {
    const { api, esc, image, field, seo, track, query } = Headra;
    const result = await api(`/api/posts/${encodeURIComponent(query.get('slug'))}`);
    if (!result) { document.querySelector('#post').innerHTML = '<p>Not found.</p>'; return; }
    const post = result.data;
    seo(post);                      // <title>, description, author, Open Graph, Twitter, JSON-LD
    document.querySelector('#post').innerHTML = `
        <h1>${esc(post.title)}</h1>
        ${image(post.fields.featured_image, 'large')}
        ${field('body', post.fields.body)}`;   // rich text: inserted as HTML
    track('posts', post.slug);      // counts a view for Analytics
})();

A menu

Headra.chrome();   // renders config.menu into <nav data-menu> and the footer into <footer data-footer>

To build your own markup instead, use (await Headra.api('/api/menus/header')).data.items. Each item has label, url, target and children.

What headra.js gives you

api(path, params)GET from the API with the key. Returns JSON, or null on 404.
esc(value)Escapes text for HTML. Use it on every value except rich text.
image(media, size)An <img> with srcset and lazy loading. Sizes: thumbnail, medium, large.
field(name, value)Renders any field value (rich text, images, galleries, links, lists).
seo(item)Sets the page's title, meta and social tags, and its structured data.
track(type, slug)Counts a page view. Free: it doesn't use the API allowance.
chrome()Renders the configured menu and the footer.
query, date(iso), link(type, slug)Small helpers for the page's URL parameters, dates and links.

Option C: React, Vue, Next, Nuxt or a build step

No library is needed: it's a JSON API. Send the key in the X-Api-Key header.

const res = await fetch('https://app.headracms.com/api/posts?per_page=10', {
    headers: { 'X-Api-Key': process.env.HEADRA_PUBLIC_KEY },
});
const { data, meta } = await res.json();

For static generation (Next getStaticProps, Nuxt generate, Eleventy, …), fetch at build time and rebuild when content changes. Never use a private key in browser code.

Useful queries

/api/posts?page=2&per_page=12Paging (meta.total_pages in the reply)
?search=penaltyFull-text search
?category=news, ?tag=launchFilter by category or tag slug (comma = any of)
?sort=-published_at, ?sort=titleSort (- = descending)
?filter[field]=valueFilter on a custom field (also [gte], [lt], [in] …)
?fields=excerpt,featured_imageOnly return these fields (smaller, faster)
/api/categories, /api/tags, /api/authorsTaxonomy and authors
/api/redirects/resolve?path=/old-pageOn a 404 page: is there a redirect?

The full reference is docs/api.md in the Headra repository.

Rules to follow

  • Escape everything except rich text fields. Plain text can contain < and must be shown as text (esc(), or your framework's normal binding). Rich text is cleaned by Headra and is safe to insert as HTML.
  • Videos in rich text arrive as <figure class="video video-full"> (or video-half, plus any classes the editor added) around a 16:9 box (<div class="ratio ratio-16x9 embed-responsive embed-responsive-16by9">) holding a YouTube or Vimeo <iframe>. Bootstrap 5 and 4 sites size the box already, so they only need .video-half { width: 50%; }. Other sites add: .video .ratio { position: relative; padding-top: 56.25%; } .video iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0; } .video-half { width: 50%; }
  • Only public keys belong in website code.
  • Links: items carry url, built from the website's site URL and the type's URL pattern. Make the site's real page addresses match those patterns, so the sitemap and canonical links point to real pages. Hosting that only serves plain files can use a query string, e.g. /post.html?slug={slug}.
  • Caching: responses are cached by Headra and refresh within seconds of a change (scheduled posts within 5 minutes). Browsers may reuse a response for 60 seconds.
  • Errors come back as {"error": {"status", "message"}}. A 404 means not found or not published.

Launch checklist

  1. The website's site URL is set to the live address (Account → Websites).
  2. Content types that have pages have a URL pattern matching the site's real addresses.
  3. A public key is in config.js (and no other kind of key is anywhere in the site).
  4. An API domain is added, and sitemap.xml loads on it. The sitemap is submitted to Search Console and Bing.
  5. Pages call seo() (or set the tags themselves) and track().
  6. Check a page in the browser's developer tools: no errors in the console, and the API requests return 200.