Why your views are zero
If every post reads 0 views, almost nothing is broken. A CMSKite integration is two halves — your server fetches the content, and the reader’s browser reports the view — and it is entirely possible to build the first half and never notice the second is missing. The site works. Nothing errors. Nobody is counted.
Last updated
The usual cause, in one paragraph
Fetching a post is not reading a post, so we never count it as one. A build fetches all 200 of your posts on deploy; a cached page is fetched once and read ten thousand times; a crawler fetches every post exactly once. Counting requests would be wrong in all four directions, so a view is reported by the page a person actually opened, and by nothing else.
That report is a separate snippet from the code that fetches your content. If it was never added, every number stays at zero forever and there is no error anywhere to find.
Check it in ten seconds
Open your project in the dashboard. If anything is missing, the top of the project page says so and names the one thing to fix. It checks whether the project has a key, whether any site has ever used it, and whether views are arriving.
If you are working with an AI assistant through the CMSKite MCP server, ask it to run check_integration — it reads the same checks and will tell you which half is missing.
- No API key — nothing outside the dashboard can read this project yet.
- The key has never been used — the site is not fetching your content. Check the key is in the right environment variable.
- No views are being reported — this page is the one you want. The content half works; the tracking half is missing.
React and Next.js
npm install cmskite'use client'
import { useTrackView } from 'cmskite/react'
/**
* Reports one view for this post, once. Safe to render on every navigation:
* the hook reports the first mount only, and nothing here can throw.
*/
export function TrackView({ postId }: { postId: string }) {
useTrackView(postId, { apiKey: process.env.NEXT_PUBLIC_CMSKITE_KEY! })
return null
}Then render it inside the page that shows one post, with that post’s id. The key must be the NEXT_PUBLIC_ one — this runs in the browser, and a key without the prefix is undefined there.
const post = await cms.posts.findBySlug(slug)
if (!post) notFound()
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.body }} />
<TrackView postId={post.id} />
</article>
)Plain HTML, PHP, WordPress, or anything else
No package needed. This is one HTTP request from the reader’s browser. Paste it at the end of the page that shows a single post, and fill in the two values at the top.
<script>
(function () {
var POST_ID = 'PASTE_THE_POST_ID' // the post's id, like post_01h… NOT the slug
var KEY = 'PASTE_THE_PROJECT_KEY' // your project's read-only key
if (!POST_ID || !KEY) return
// One view per post per tab. A refresh is not a second reader.
try {
var seen = 'cmskite:v:' + POST_ID
if (sessionStorage.getItem(seen)) return
sessionStorage.setItem(seen, '1')
} catch (e) {
// Private mode. Count the view rather than lose it.
}
var url = 'https://api.cmskite.com/v1/blog/events?key=' + encodeURIComponent(KEY)
var body = JSON.stringify({
events: [{ type: 'view', postId: POST_ID, path: location.pathname }]
})
// text/plain is deliberate — see the note below.
var type = 'text/plain;charset=UTF-8'
try {
if (navigator.sendBeacon && navigator.sendBeacon(url, new Blob([body], { type: type }))) return
} catch (e) {}
try {
fetch(url, { method: 'POST', headers: { 'content-type': type }, body: body, keepalive: true })
.catch(function () {})
} catch (e) {}
})()
</script>In PHP or a template, print the post id into the snippet rather than hard-coding it:
<script>
var POST_ID = <?= json_encode($post['id']) ?>
var KEY = <?= json_encode(getenv('CMSKITE_PUBLIC_KEY')) ?>
// …the rest of the snippet above, unchanged
</script>Counting clicks too
Same endpoint, type "click", plus a nonce. The nonce is what tells us two presses of the same link are two clicks rather than one reported twice — leave it out and they collapse into one.
function trackClick(postId, label) {
var url = 'https://api.cmskite.com/v1/blog/events?key=' + encodeURIComponent(KEY)
var body = JSON.stringify({
events: [{
type: 'click',
postId: postId,
target: label,
nonce: String(Math.random()).slice(2),
}]
})
navigator.sendBeacon(url, new Blob([body], { type: 'text/plain;charset=UTF-8' }))
}Still zero after all that
- Open a post in your browser, then open the Network tab and reload. You should see a request to api.cmskite.com/v1/blog/events. If there is none, the snippet is not running — check it is on the single-post page and not only on the index.
- If the request is there and red, read the status. 401 means the key is wrong or missing. 403 means this domain is not in the project’s allowed origins.
- If the request is there and returns 202, the view was accepted. Figures are rolled up, so give it a few minutes before deciding it did not count.
- Check you sent post.id and not post.slug. This is the commonest one, and it looks identical to everything working.
- Ad blockers and privacy extensions block analytics requests, including this one. Your own visits may not be counted.