Adding likes to your blog
This page is the canonical, always up to date guide. Two ways to integrate: the drop-in widget (two lines of HTML) or the direct API if you prefer to build your own button. Either way, start with the setup below.
1. Get set up
- Register an account — create one here, it is free.
- Create a project — on the dashboard, give it a name like “My blog”. A project groups the domains that belong to the same site, and analytics are aggregated per project.
-
Add and verify your domain — add your domain to the project.
You will get a verification token that looks like
likes-<32 hex chars>. Prove the domain is yours with either method, then add the domain again:Option A — DNS TXT record (checked first)
likes-verification=likes-a1b2c3...32charhashAdd it on the domain itself or on its root domain — subdomains fall back to the root, and multi-part TLDs like
.com.esand.co.ukare handled correctly.Option B — HTML meta tag (fallback, checked on your homepage)
<meta name="likes-verification" content="likes-a1b2c3...32charhash"> - Integrate — paste the widget below, or call the API directly.
2. The widget recommended
Drop these two lines where you want the like button to appear:
<div id="likes"></div>
<script src="https://likeslikes.net/likes.js" defer></script>
The widget renders a heart with the like count for the current page and lets each visitor like it once. A canonical URL is used when present. If its domain is not registered with Likeslikes, the widget tries the URL being visited instead.
Options
-
data-selector— attribute on the script tag; CSS selector of the container(s), default#likes. -
data-url— like a different URL than the current page. Set it on the script tag, or per container — useful on list pages with one button per post. -
data-ignore-canonical— presence-only attribute on the script tag; ignore the page's canonical URL and use the URL being visited. -
--likeslikes-color— CSS variable that sets the heart color:
#likes { --likeslikes-color: #7c3aed; }
How the page URL is chosen
The widget uses the first available URL in this order: a container's data-url,
the script's data-url, then the canonical URL (unless ignored). If a canonical on
another origin returns Domain not found, the widget retries with the current
origin and path. Explicit data-url values do not fall back automatically.
<div id="likes"></div>
<script src="https://likeslikes.net/likes.js" data-ignore-canonical defer></script>
Hugo and republished posts
Set Hugo's baseURL to the public domain visitors use so generated links,
metadata, and sitemaps use the verified domain. To make every hosting alias count likes for
that public domain, set a fixed per-page data-url in your layout or partial:
<div id="likes" data-url="{{ .Permalink }}"></div>
<script src="https://likeslikes.net/likes.js" defer></script>
Troubleshooting "Domain not found"
When neither the canonical domain nor the current domain is registered, the widget remains
visible as a disabled counter showing 0 and logs the error in the browser
console. Register the fallback hostname or use data-url to select a verified URL.
3. The direct API
Building your own button (a static site, a Hugo template, anything)? There are only two endpoints, and you always pass the full URL of the post — the domain is extracted automatically, no IDs or keys needed.
Get the like count for a URL
curl "https://likeslikes.net/api/v1/likes?url=https://myblog.com/my-post"
Returns {"likes": 5, "is_liked": false, ...} — is_liked tells
whether the calling IP has already liked this URL.
Add a like
curl -X POST https://likeslikes.net/api/v1/likes \
-H "Content-Type: application/json" \
-d '{"url": "https://myblog.com/my-post"}'
One like per visitor per URL; duplicates return an error.
A minimal JavaScript button
<button id="like-btn">❤ <span id="like-count">…</span></button>
<script>
const API = 'https://likeslikes.net';
const url = window.location.href;
const btn = document.getElementById('like-btn');
const count = document.getElementById('like-count');
fetch(`${API}/api/v1/likes?url=${encodeURIComponent(url)}`)
.then(r => r.json())
.then(({ data }) => {
count.textContent = data.likes;
btn.disabled = data.is_liked;
});
btn.addEventListener('click', () => {
fetch(`${API}/api/v1/likes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
})
.then(r => r.json())
.then(({ data }) => {
if (data.success) {
count.textContent = data.likes;
btn.disabled = true;
}
});
});
</script>
Can I export my data?
Yes. Your data belongs to you, and you can take it with you whenever you want. On the dashboard, open a project's actions menu and choose Export data (JSON). The download includes the project, its domains, and one record per like with its URL and timestamp. Use it in another service or your own tools. Export and check your downloaded file before deleting a project; deletion cannot be undone.
Exports contain no IP addresses, IP hashes, or visitor identifiers.
Each entry in likes represents one recorded like. Two entries can have the same
URL and timestamp and still represent separate likes; count both.
JSON format version 1 contains format_version, exported_at (UTC),
project (id, name, public_id), domains (id, domain_name, created_at),
and likes (url, created_at).
URLs use HTTPS with the recorded domain and normalized path; the original scheme,
query strings, and fragments are not retained.
Record timestamps use YYYY-MM-DD HH:MM:SS in the database's timezone,
without an offset, and may be null for older records. Empty collections are arrays.
Each export is a full snapshot; adding multiple exports together would double-count likes. There is currently no import feature, and exports cannot restore IP-based duplicate checks.
Good to know
- The API only works for domains verified in a project.
- One like per visitor (IP) per URL. Raw IPs are never stored — only salted hashes.
- Rate limits: 30 requests/minute, 300/hour per IP.
- URLs on
localhostalways return 999 likes, so you can develop without verifying anything. - Queries and fragments are discarded. Host case, duplicate or trailing slashes, dot segments, and percent encoding are normalized so equivalent URLs share one counter.
- Public likes API responses are wrapped in an envelope:
{"statusCode": 200, "data": {...}}.