……我好像今年年初刚说半年内绝对不再装修。

博客文章末尾添加点赞按钮

看到了象友的赛博小屋装修笔记,对文章点赞按钮功能一见钟情,遂立刻打开电脑抄作业。结果从第一步的命令行就没看懂,想着反正都是cloudflare的功能,说不定直接在网页的Dashboard仪表板中也操作,对照着研究了一会,很顺利地实现了。cf你是我我永远的博客老家我赛博小屋的魔术工房……

参考文献

Hugo Stack主题装修笔记Part3↗
<open-heart>❤️</open-heart>↗

准备工作:Cloudflare

首先你需要拥有一个cloudflare账号。

1.在左侧栏找到存储和数据库→KV界面,创建一个KV命名空间。

2.在左侧栏继续找到Compute(Workers)→Workers和Pages界面,点击创建新Worker。直接选默认的Hello world模板就行。

3.给Worker取名,我这里显示不可用是因为我已经创建了,走个截图过场(?

4.点进刚创建的Worker进入控制面板,在设置→绑定中添加KV命名空间。如下图所示。

5.然后切换到Worker控制面板的部署页面,点击右上角左数第三个按钮,进入代码编辑页面。直接把Worker.js里原来的代码改成下面这个,并将env.KVKV改成KV命名空间的ID。感谢白石京老师第三夏尔博客里分享的魔改代码!

const instruction = `.^⋁^.
'. .'
  \`

dddddddddzzzz
OpenHeart protocol API

https://api.oh.dddddddddzzzz.org

Test with example.com as <domain>.

GET /<domain>/<uid> to look up reactions for <uid> under <domain>

POST /<domain>/<uid> to send an emoji

<uid> must not contain a forward slash.
<domain> owner has the right to remove data under its domain scope.

----- Test in CLI -----
Send emoji:
curl -d '<emoji>' -X POST 'https://api.oh.dddddddddzzzz.org/example.com/uid'

Get all emoji counts for /example.com/uid:
curl 'https://api.oh.dddddddddzzzz.org/example.com/uid'
`;

export default {
  async fetch(request, env) {
    if (request.method == 'OPTIONS') {
      return new Response(null, { headers });
    }
    if (request.method === 'GET') {
      if (url(request).pathname === '/') {
        return new Response(instruction, { headers });
      } else {
        return handleGet(request, env);
      }
    }
    if (request.method === 'POST') return handlePost(request, env);
  },
};

const headers = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET,POST",
  "Access-Control-Max-Age": "86400",
};

function error(text, code = 400) {
  return new Response(text, { headers, status: code });
}

async function handleGet(request, env) {
  const [domain, ...uidParts] = url(request).pathname.slice(1).split('/');
  const uid = uidParts ? uidParts.join('/') : null;
  if (!domain || !uid) {
    return error('Domain or UID missing.');
  }

  const list = {};
  const emojis = ["❤️", "👍", "😂", "🎉"]; // Add expected emojis here

  // Fetch counts for each emoji directly
  for (const emoji of emojis) {
    const key = `${domain}:${uid}:${emoji}`;
    const value = await env.KV.get(key);
    if (value) {
      list[emoji] = Number(value);
    }
  }

  return new Response(
    JSON.stringify(list, null, 2), // Return only the found counts
    { headers: { ...headers, "Content-Type": "application/json;charset=UTF-8" } }
  );
}

function url(request) {
  return new URL(request.url);
}

async function handlePost(request, env) {
  const urlObject = url(request);
  const path = urlObject.pathname.slice(1);
  if (path === '') return error('Pathname missing');

  const [domain, ...uidParts] = path.split('/');
  const uid = uidParts ? uidParts.join('/') : '';
  if (uid.length < 1) return error('UID required.');

  const id = [encodeURI(domain), uid].join(':');
  const emoji = ensureEmoji(await request.text());
  if (!emoji) return error('Request body should contain an emoji');

  const key = `${id}:${emoji}`;
  const currentCount = Number(await env.KV.get(key) || 0);
  await env.KV.put(key, (currentCount + 1).toString());

  const redirection = urlObject.searchParams.get('redirect');
  if (redirection !== null) {
    headers['Location'] = redirection || request.headers.get('Referer');
    return new Response('recorded', { headers, status: 303 });
  } else {
    return new Response('recorded', { headers });
  }
}

function ensureEmoji(emoji) {
  const segments = Array.from(
    new Intl.Segmenter({ granularity: 'grapheme' }).segment(emoji.trim())
  );
  const parsedEmoji = segments.length > 0 ? segments[0].segment : null;

  if (/\p{Emoji}/u.test(parsedEmoji)) return parsedEmoji;
}

之后点右上角的部署按钮,跳出版本更新成功的通知就没问题了。

可选:自定义域名

在Worker设置界面的域和路由选项卡里修改,不过前提需要域名托管在cloudflare服务器上。

博客点赞按钮实装

1.在/layouts/partials目录下新建kudos.html,复制以下内容。第一行引入的链接中间改成Worker的域名,后面的{{ .Permalink }}不要漏了。我把爱心emoji改成了streamline的爱心印章!

<!-- emoji 可为多个,但必须要在前面的可识别列表里出现 -->
<open-heart  href="https://kudos.naturaleki.one/{{ .Permalink }}" emoji="❤️"> 
  <img src="https://img.naturaleki.one/lock/kudoclick.png" width="25" height="25" style="position: relative; top: 5px;">
</open-heart>

<!-- load web component -->
<script src="https://unpkg.com/open-heart-element" type="module"></script>
<!-- when the webcomponent loads, fetch the current counts for that page -->
<script>
window.customElements.whenDefined('open-heart').then(() => {
  for (const oh of document.querySelectorAll('open-heart')) {
    oh.getCount()
  }
})
// refresh component after click
window.addEventListener('open-heart', e => {
	e && e.target && e.target.getCount && e.target.getCount()
})
</script>

2.在博客文章模板下渲染这个html文件,我的默认模板为/layouts/_default/single.html,找一个喜欢的地方放就行。我额外添加了一个条件函数,这样可以在Front Matter里设置showkudos=false来隐藏About和Friends界面的点赞按钮。

{{ if and (ne .Params.showkudos false) }}
{{ partial "kudos.html" . }}
{{ end }}

3.丢入自定义css。我把数字的字体改成了像素。

// Open heart reaction style
open-heart {
    margin: 5px;
    margin-top: 0;
    display: block;  // Center alignment
    margin-left: auto;
    margin-right: auto;
    width: fit-content;
    border: 1px solid var(--color-secondary);
    border-radius: .2em;
    padding: .4em;
    font-family: 'ipx en';
  
  }
  open-heart:not([disabled]):hover,
  open-heart:not([disabled]):focus {
    border-color: var(--color-focus);
    cursor: pointer;
  }
  open-heart[disabled] {
    background: #ece9f4;
    border: 1px solid var(--color-secondary);
    cursor: not-allowed;
    color: var(--color-font);
  }
  open-heart[count]:not([count="0"])::after {
    content: attr(count);
    padding: .2em;
  }

推送一下就好了!翻到文章最下方就可以看到了,请来点点看!

恭喜我们pjax又添一个bug!

封装一下就好了,警惕强行安装pjax对博客代码的洗礼。

<script>
  // 封装 open-heart 组件初始化功能
  function initOpenHeart() {
    window.customElements.whenDefined('open-heart').then(() => {
      document.querySelectorAll('open-heart').forEach(oh => {
        oh.getCount();
      });
    });

    // 绑定 open-heart 组件的点击刷新事件
    window.addEventListener('open-heart', e => {
      e?.target?.getCount && e.target.getCount();
    });
  }

  // 在页面初次加载时调用
  initOpenHeart();

  // 监听 Pjax 完成事件并调用封装的函数
  document.addEventListener('pjax:complete', function () {
    initOpenHeart();
  });
</script>