Without a framework
There is an npm package, and you do not need it. CMSKite is an HTTP API with a bearer token, so any language that can make a request can use it. Every example below is complete: read the list, read one post, and report the view.
Last updated
The whole API, in two requests
Whatever you are writing, an integration is these two calls. The first runs on your server. The second runs in the reader’s browser, and is the one people forget.
curl 'https://api.cmskite.com/v1/blog/posts?limit=20' \
-H "authorization: Bearer $CMSKITE_API_KEY"
curl 'https://api.cmskite.com/v1/blog/posts/slug/hello-world' \
-H "authorization: Bearer $CMSKITE_API_KEY"curl -X POST 'https://api.cmskite.com/v1/blog/events?key=YOUR_PROJECT_KEY' \
-H 'content-type: text/plain;charset=UTF-8' \
--data '{"events":[{"type":"view","postId":"post_01h...","path":"/blog/hello-world"}]}'The key already knows which project it belongs to, so there is no project or workspace header to send — one supplied by the client is refused rather than honoured. Every response is the same envelope: success, data, and a requestId to quote if you ever need support.
PHP
<?php
function cmskite_get(string $path, array $query = []): mixed
{
$url = 'https://api.cmskite.com/v1' . $path;
if ($query) {
$url .= '?' . http_build_query($query);
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['authorization: Bearer ' . getenv('CMSKITE_API_KEY')],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
// No curl_close(). It has done nothing since PHP 8.0 and since 8.5 it
// prints a deprecation notice -- into the page, above the <!doctype>.
// A blog that cannot reach the API renders empty. It does not fatal.
if ($body === false || $status !== 200) {
error_log("CMSKite: HTTP {$status} for {$path}");
return null;
}
return json_decode($body, true)['data'] ?? null;
}
function cmskite_posts(int $limit = 20): array
{
return cmskite_get('/blog/posts', ['limit' => $limit]) ?? [];
}
function cmskite_post(string $slug): ?array
{
return cmskite_get('/blog/posts/slug/' . rawurlencode($slug));
}<?php
require __DIR__ . '/cmskite.php';
$post = cmskite_post($_GET['slug'] ?? '');
if (!$post) {
http_response_code(404);
exit('Post not found');
}
?>
<article>
<h1><?= htmlspecialchars($post['title'], ENT_QUOTES, 'UTF-8') ?></h1>
<?php
// The body is HTML the author wrote in CMSKite, so it is printed as markup.
echo $post['body'];
?>
</article>
<script>
var POST_ID = <?= json_encode($post['id']) ?>
var KEY = <?= json_encode(getenv('CMSKITE_PUBLIC_KEY')) ?>
// …the tracker from "Why your views are zero" goes here, unchanged.
</script>WordPress
No plugin. Two defines in wp-config.php and one file in your theme. Using wp_remote_get rather than curl means proxies, timeouts and filters behave the way the rest of the site does.
define('CMSKITE_API_KEY', 'your server key');
define('CMSKITE_PUBLIC_KEY', 'your browser key');<?php
// require_once get_stylesheet_directory() . '/cmskite.php'; from functions.php
function cmskite_get($path, $query = []) {
$url = 'https://api.cmskite.com/v1' . $path . ($query ? '?' . http_build_query($query) : '');
$response = wp_remote_get($url, [
'timeout' => 10,
'headers' => ['authorization' => 'Bearer ' . CMSKITE_API_KEY],
]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
error_log('CMSKite: could not fetch ' . $path);
return null;
}
return json_decode(wp_remote_retrieve_body($response), true)['data'] ?? null;
}
/** Cached, because a blog index should not make an API call per visitor. */
function cmskite_posts($limit = 20) {
$cached = get_transient('cmskite_posts_' . $limit);
if ($cached !== false) return $cached;
$posts = cmskite_get('/blog/posts', ['limit' => $limit]) ?: [];
set_transient('cmskite_posts_' . $limit, $posts, 5 * MINUTE_IN_SECONDS);
return $posts;
}
function cmskite_post($slug) {
return cmskite_get('/blog/posts/slug/' . rawurlencode($slug));
}<?php $post = cmskite_post(get_query_var('cmskite_slug')); ?>
<article>
<h1><?php echo esc_html($post['title']); ?></h1>
<?php echo wp_kses_post($post['body']); ?>
</article>A plain HTML site
<ul id="posts"></ul>
<script>
var KEY = 'your project key'
fetch('https://api.cmskite.com/v1/blog/posts?limit=20', {
headers: { authorization: 'Bearer ' + KEY }
})
.then(function (res) {
if (!res.ok) throw new Error('CMSKite ' + res.status)
return res.json()
})
.then(function (payload) {
document.getElementById('posts').innerHTML = payload.data
.map(function (post) {
var a = document.createElement('a')
a.href = 'post.html?slug=' + encodeURIComponent(post.slug)
a.textContent = post.title
return '<li>' + a.outerHTML + '</li>'
})
.join('')
})
.catch(function (err) {
document.getElementById('posts').textContent = 'Could not load posts.'
console.error(err)
})
</script>The single-post page is the same fetch against /blog/posts/slug/… — and it is where the tracker goes, inside the .then, because that is where the post id finally exists.
Python
import os
import httpx
_client = httpx.Client(
base_url="https://api.cmskite.com/v1",
timeout=10.0,
headers={"authorization": f"Bearer {os.environ['CMSKITE_API_KEY']}"},
)
def posts(limit: int = 20) -> list[dict]:
"""The published posts, newest first. Empty when the API is unreachable."""
try:
response = _client.get("/blog/posts", params={"limit": limit})
response.raise_for_status()
except httpx.HTTPError as error:
print(f"CMSKite: {error}")
return []
return response.json()["data"]
def post(slug: str) -> dict | None:
"""One post, or None when there is no such post."""
try:
response = _client.get(f"/blog/posts/slug/{slug}")
if response.status_code == 404:
return None
response.raise_for_status()
except httpx.HTTPError as error:
print(f"CMSKite: {error}")
return None
return response.json()["data"]Let an assistant do it
The CMSKite MCP server gives an AI assistant these guides directly. Point it at your project, ask it to add a blog, and it will write the right files for whatever your site is built in — then verify the result rather than assume it.
get_integration_guide framework: php | wordpress | laravel | python | html
check_integration does this project actually work?
get_content_analytics views, uniques and clicks