From 87bc37e1248ac18be5fec8af6cc5e470b76845cb Mon Sep 17 00:00:00 2001 From: Alex Selimov Date: Thu, 19 Mar 2026 23:31:30 -0400 Subject: [PATCH 1/9] Add anonymous upvote functionality --- assets/herman.css | 45 ++++++++++++++++++++++++++++++++++++ layouts/_default/single.html | 9 ++++++++ static/upvote.js | 37 +++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 static/upvote.js diff --git a/assets/herman.css b/assets/herman.css index f2573c3..8a5ca66 100644 --- a/assets/herman.css +++ b/assets/herman.css @@ -190,6 +190,51 @@ td { transform: translateY(0%); } +.upvote { + display: flex; + flex-direction: column; + align-items: center; + width: fit-content; + gap: 0.2rem; + margin-block: var(--spacing); +} + +#upvote-btn { + background: none; + border: none; + cursor: pointer; + color: var(--color-light); + font-size: calc(var(--size) * 4); + line-height: 0.8; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 0; + opacity: 0.4; + transition: opacity 0.15s, color 0.15s; +} + +#upvote-btn:hover { + opacity: 0.8; +} + +#upvote-btn[voted] { + color: var(--color-primary); + opacity: 1; +} + +#upvote-chevron { + display: block; + line-height: 1; + margin-bottom: -0.3em; +} + +#upvote-count { + font-size: calc(var(--size) * 1.6); + line-height: 1; +} + figure { margin-inline-start: 0em; margin-inline-end: 0em; diff --git a/layouts/_default/single.html b/layouts/_default/single.html index 0c85516..7a5fd7b 100644 --- a/layouts/_default/single.html +++ b/layouts/_default/single.html @@ -18,6 +18,15 @@ #{{ lower .LinkTitle }} {{ end }}

+{{ if .Site.Params.upvotes }} +
+ +
+ +{{ end }} {{ if not .Params.hideReply }} {{ with .Site.Params.author.email }}

diff --git a/static/upvote.js b/static/upvote.js new file mode 100644 index 0000000..5abf255 --- /dev/null +++ b/static/upvote.js @@ -0,0 +1,37 @@ +(async function () { + // Define the slug + const slug = location.pathname.replace(/^\/|\/$/g, "").replaceAll("/", "-"); + const API = "http://localhost:3000"; + + const btn = document.getElementById("upvote-btn"); + const count = document.getElementById("upvote-count"); + + // On load fetch curent vote count and whether the button should be clicked on or off + const res = await fetch(`${API}/posts/${slug}/votes`, { + credentials: "include", + }); + const data = await res.json(); + + count.textContent = data.vote_count; + if (data.voted) btn.setAttribute("voted", true); + + btn.addEventListener("click", async () => { + const alreadyVoted = btn.hasAttribute("voted"); + + const method = alreadyVoted ? "DELETE" : "POST"; + const r = await fetch(`${API}/posts/${slug}/vote`, { + method, + credentials: "include", + }); + + if (r.ok) { + const updated = await fetch(`${API}/posts/${slug}/votes`, { + credentials: "include", + }); + const d = await updated.json(); + count.textContent = d.vote_count; + if (d.voted) btn.setAttribute("voted", false); + else btn.removeAttribute("voted"); + } + }); +})(); From d8834d83b781837986c9f678a8bfdfd0b79287e7 Mon Sep 17 00:00:00 2001 From: Alex Selimov Date: Fri, 20 Mar 2026 00:01:45 -0400 Subject: [PATCH 2/9] Template api url --- layouts/_default/single.html | 1 + static/upvote.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/layouts/_default/single.html b/layouts/_default/single.html index 7a5fd7b..9b68f55 100644 --- a/layouts/_default/single.html +++ b/layouts/_default/single.html @@ -25,6 +25,7 @@ + {{ end }} {{ if not .Params.hideReply }} diff --git a/static/upvote.js b/static/upvote.js index 5abf255..131da67 100644 --- a/static/upvote.js +++ b/static/upvote.js @@ -1,7 +1,7 @@ (async function () { // Define the slug const slug = location.pathname.replace(/^\/|\/$/g, "").replaceAll("/", "-"); - const API = "http://localhost:3000"; + const API = window.UPVOTE_API; const btn = document.getElementById("upvote-btn"); const count = document.getElementById("upvote-count"); From 460fe6a820299d0f41064bae0504ea380eaa74cd Mon Sep 17 00:00:00 2001 From: Alex Selimov Date: Fri, 20 Mar 2026 08:45:33 -0400 Subject: [PATCH 3/9] Fix button color swapping --- assets/auto-theme.css | 4 ++++ assets/herman.css | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/auto-theme.css b/assets/auto-theme.css index dce83e8..ee3f9a6 100644 --- a/assets/auto-theme.css +++ b/assets/auto-theme.css @@ -104,6 +104,10 @@ li { } @media (prefers-color-scheme: light) { + #upvote-btn[voted] { + color: #61ABDA; + } + body { background-color: #ffffff; color: #1f2328; diff --git a/assets/herman.css b/assets/herman.css index 8a5ca66..277fba2 100644 --- a/assets/herman.css +++ b/assets/herman.css @@ -203,7 +203,7 @@ td { background: none; border: none; cursor: pointer; - color: var(--color-light); + color: currentColor; font-size: calc(var(--size) * 4); line-height: 0.8; padding: 0; From 41c217030c8f93fb71d2c14e7a6800e0e25a526c Mon Sep 17 00:00:00 2001 From: Alex Selimov Date: Fri, 20 Mar 2026 09:00:56 -0400 Subject: [PATCH 4/9] Update README and exampleSite --- README.md | 21 ++++++++++++++++++--- exampleSite/hugo.toml | 11 +++++++++++ theme.toml | 28 ++++++++++++++-------------- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a084e50..47ede3d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -# ᕦʕ •ᴥ•ʔᕤ Bear Cub +# ᕦʕ •ᴥ•ʔᕤ Voting Bear Cub -[![github pages](https://github.com/clente/hugo-bearcub/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/clente/hugo-bearcub/actions/workflows/gh-pages.yml) -[![MIT license](https://img.shields.io/github/license/clente/hugo-bearcub)](https://github.com/clente/hugo-bearcub/blob/main/LICENSE) +[![MIT license](https://img.shields.io/badge/license-MIT-blue)](https://forge.alexselimov.com/aselimov/hugo-bearcub/src/branch/main/LICENSE) ## Overview @@ -9,6 +8,22 @@ Blog](https://bearblog.dev) and [Hugo Bear Blog](https://github.com/janraasch/hugo-bearblog). +This is a fork of [Bear Cub](https://github.com/clente/hugo-bearcub) with the +following additions: + +- **Anonymous post upvoting**: each post can display an upvote button backed by + a configurable API endpoint. Votes are tracked anonymously via cookies so + readers can toggle their vote without creating an account. Enable with + `upvotes = true` and set `upvoteApi` to your API base URL. +- **Redesigned header**: the site header now uses a mesh SVG background for a + more distinctive look. +- **GitHub light/dark color scheme**: the syntax highlighting palette has been + replaced with colors matching the GitHub light and dark themes. +- **Herman CSS upvote styles**: upvote button styles are bundled into the Herman + CSS variant so the button integrates cleanly with the existing theme. +- **Configurable index post count**: use `postsToShowOnIndex` in `[params]` to + control how many posts appear on the home page (defaults to 3). + **Bear Cub** takes care of speed and optimization, so you can focus on writing good content. It is free, multilingual, optimized for search engines, no-nonsense, responsive, light, and fast. Really fast. diff --git a/exampleSite/hugo.toml b/exampleSite/hugo.toml index 04690c0..939c13d 100644 --- a/exampleSite/hugo.toml +++ b/exampleSite/hugo.toml @@ -75,6 +75,17 @@ enableRobotsTXT = true # information see layouts/partials/social_card.html generateSocialCard = true + # The number of posts to show on the index page. Defaults to 3 if not set. + postsToShowOnIndex = 3 + + # Enable upvoting on posts. When set to true, each post will display an upvote + # button that calls the API endpoint specified by `upvoteApi`. + upvotes = false + + # The base URL of the upvote API. Only used when `upvotes = true`. + # The API is expected to handle GET and POST requests at {upvoteApi}/{post-path}. + upvoteApi = "https://example.com/api" + # Social media. Delete any item you aren't using to make sure it won't show up # in your website's metadata. [params.social] diff --git a/theme.toml b/theme.toml index 602a351..7f6b154 100644 --- a/theme.toml +++ b/theme.toml @@ -1,29 +1,29 @@ -name = "Bear Cub" +name = "Voting Bear Cub" license = "MIT" licenselink = "https://github.com/clente/hugo-bearcub/blob/master/LICENSE" -description = "A lightweight Hugo theme based on Bear Blog and Hugo Bear Blog. It is free, multilingual, optimized for search engines, no-nonsense, responsive, light, and fast. Really fast." +description = """A lightweight Hugo theme based on Bear Blog and Hugo Bear Blog. It is free, +multilingual, optimized for search engines, no-nonsense, responsive, light, and fast. Really fast. +This version adds an upvote button that connects to an +https://forge.alexselimov.com/aselimov/upvoters server to handle anonymous upvotes. +""" # The home page of the theme, where the source can be found. -homepage = "https://github.com/clente/hugo-bearcub" - -# If you have a running demo of the theme. -demosite = "https://clente.github.io/hugo-bearcub" +homepage = "https://forge.alexselimov.com/aselimov/hugo-bearcub" tags = ["blog", "responsive", "minimal", "personal", "dark", "multilingual"] features = ["favicon", "seo", "no javascript", "rss", "social cards"] # If the theme has a single author [author] - name = "Caio Lente" - homepage = "https://lente.dev" +name = "Alex Selimov" +homepage = "https://alexselimov.com" -# If porting an existing theme [original] - author = "Jan Raasch" - homepage = "https://www.janraasch.com" - repo = "https://github.com/janraasch/hugo-bearblog" +name = "Caio Lente" +homepage = "https://lente.dev" +repo = "https://github.com/clente/hugo-bearcub" # Hugo versions the theme supports [module] - [module.hugoVersion] - min = "0.90" +[module.hugoVersion] +min = "0.90" From 665da9bec6e9ab22792552d356556e6f76f5e4fb Mon Sep 17 00:00:00 2001 From: Alex Selimov Date: Fri, 20 Mar 2026 09:06:40 -0400 Subject: [PATCH 5/9] Small README updates --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 47ede3d..faf5e82 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,9 @@ -# ᕦʕ •ᴥ•ʔᕤ Voting Bear Cub +# ʕ •ᴥ•ʔつ🗳️ Voting Bear Cub [![MIT license](https://img.shields.io/badge/license-MIT-blue)](https://forge.alexselimov.com/aselimov/hugo-bearcub/src/branch/main/LICENSE) ## Overview -🐻 A lightweight [Hugo](https://gohugo.io/) theme based on [Bear -Blog](https://bearblog.dev) and [Hugo Bear -Blog](https://github.com/janraasch/hugo-bearblog). - This is a fork of [Bear Cub](https://github.com/clente/hugo-bearcub) with the following additions: @@ -44,6 +40,11 @@ To finish off, append a line to the site configuration file: echo 'theme = "hugo-bearcub"' >> hugo.toml ``` +### Anonymous voting + +To easily setup the anonymous voting you need to run an [upvoters](https://forge.alexselimov.com/aselimov/upvoters) server. +You should be able to ship your own solution as well. + ## Features Like [Bear Blog](https://bearblog.dev), this theme: From ecceb7afa9e0973ffc98336474eefe2544b60629 Mon Sep 17 00:00:00 2001 From: Alex Selimov Date: Fri, 20 Mar 2026 13:00:23 -0400 Subject: [PATCH 6/9] Add option to hide upvotes --- layouts/_default/single.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layouts/_default/single.html b/layouts/_default/single.html index 9b68f55..d9a1118 100644 --- a/layouts/_default/single.html +++ b/layouts/_default/single.html @@ -18,7 +18,7 @@ #{{ lower .LinkTitle }} {{ end }}

-{{ if .Site.Params.upvotes }} +{{ if and .Site.Params.upvotes (not .Params.hideUpvotes) }}