如何在博客里随机显示嘟文

塔塔说想看于是就写了!但一来这个功能基本上就是克老师写的,我只负责提需求和改报错/设计;二来我其实不太擅长教人或者说明……唉我尽力吧!大家如果看着头晕的话可以再在星屑或者评论区戳我,或者扔给AI问问。

不太清楚大家的博客代码都是长什么样的,不过应该大差不差……?下面会直接贴出我首页正在用的代码(因为只做了misskey的,所以大部分命名都会以misskey来,长毛象版本是让克老师另外写我再测试过的),大家可以参照说明直接搬回去再改一改!

博客行宽限制,代码在博客页面上显示不完全,会需要滑动才能浏览,所以一部分比较普泛的说明文字我会放到博文正文,比较具体的我会用注释补充,方便大家复制到自己的代码编辑器时也能看。

Misskey版本

HTML

<div class="misskey-note" id="misskey-note">
  <div class="misskey-note-header">
    <span class="misskey-note-label">最近在咩……</span>
    <a
      class="misskey-note-link"
      id="misskey-note-link"
      href="https://stelpolva.moe/@bipolar"
      target="_blank"
    >
      @bipolar@stelpolva.moe ↗
    </a>
  </div>
  <p class="misskey-note-text" id="misskey-note-text">加载中……</p>
</div>

CSS

  .misskey-note {
    border: 1px solid var(--border, #ddd);
    border-radius: 10px; // 卡片四周的圆角,后面的数值越大角越圆润,想要方角可以删掉这行
    padding: 1rem 1.25rem; // 卡片内余白,两个数值分别是上下余白/左右余白
    margin: 1.5rem 0; // 卡片外余白,两个数值分别是上下余白/左右余白
    background-color: var(--background);  // 背景颜色,可改为16进制(如#000000)
  }

  .misskey-note-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 0.6rem; // 卡片头部下方的余白
    font-size: 0.85rem; // 字体大小,可改用px为单位进行设置
  }

  .misskey-note-label {
    font-style: italic; // 左上角文字显示为斜体,不需要的话可以删掉
  }

  .misskey-note-link {
    text-decoration: none; // 去掉标签链接自带的格式
    color: inherit;
  }

  .misskey-note-link:hover {
    text-decoration: underline; // 鼠标移动到上方时显示下划线,不需要的可以删掉
  }

  .misskey-note-text {
    margin: 0;
    line-height: 1.7; // 行间距,数值越大越宽
    font-size: 0.95rem;
    justify-content: space-between; // 两端对齐
  }

JS

<script>
  async function loadMisskeyNote() {
    // 对应html里面id为"misskey-note-text"的元素,即卡片部分
    const textEl = document.getElementById("misskey-note-text");
    // 对应html里面id为"misskey-note-link"的元素,即链接部分
    const linkEl = document.getElementById(
      "misskey-note-link",
    ) as HTMLAnchorElement | null;
    // 如果这俩有谁不存在的话就返回,不再执行以下代码
    if (!textEl || !linkEl) return;

    // 第一步:用用户名查 userId, 其中stelpolva.moe 是 Misskey 实例域名,bipolar 是 Misskey 用户名
    try {
      const userRes = await fetch("https://stelpolva.moe/api/users/show", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ username: "bipolar" }),
      });
      const user = await userRes.json();

      // 第二步:拉取最近 30 条嘟文(只拿有文字内容的普通嘟文,不含转嘟),其他misskey站点用户请把下面星屑域名stelpolva.moe换成对应站点
      const notesRes = await fetch("https://stelpolva.moe/api/users/notes", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          userId: user.id,
          limit: 30,
          withRenotes: false,
          withReplies: false,
        }),
      });
      const notes = await notesRes.json();

      // 只保留"真正自己写的嘟文",排除带 renote 字段的转嘟(即使带了自己的评论文字)
      const textNotes = notes.filter(
        (note: {
          text: { trim: () => { (): any; new (): any; length: number } };
          renoteId: any;
        }) => note.text && note.text.trim().length > 0 && !note.renoteId,
      );

      // 如果没有抓到嘟文或者嘟文数量为零的时候,显示下方文字。
      if (!textNotes.length) {
        textEl.textContent = "暂时没有可以显示的嘟文。";
        return;
      }

      // 随机抽取一条嘟文
      const randomNote =
        textNotes[Math.floor(Math.random() * textNotes.length)];

      // 让html显示该条随机嘟文的文字(同时转换表情符号)
      textEl.innerHTML = await renderTextWithEmojis(randomNote.text);

      // 更新链接,指向这条具体的嘟文
      linkEl.href = `https://stelpolva.moe/notes/${randomNote.id}`;
    } catch (err) {
      if (textEl) textEl.textContent = "嘟文加载失败,请稍后再试。";
      console.error("Misskey note fetch error:", err);
    }
  }

  function escapeHtml(str: string) {
    return str
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  async function renderTextWithEmojis(text: string) {
    const escaped = escapeHtml(text);

    // 找出文字里所有 :表情名: 占位符,去重
    const matches = [...escaped.matchAll(/:([a-zA-Z0-9_]+):/g)];
    const emojiNames = [...new Set(matches.map((m) => m[1]))];

    if (!emojiNames.length) return escaped;

    // 并发查询每个表情名对应的图片 URL
    const emojiMap: Record<string, string> = {};
    await Promise.all(
      emojiNames.map(async (name) => {
        try {
          const res = await fetch(
            `https://stelpolva.moe/api/emoji?name=${encodeURIComponent(name)}`,
          );
          if (!res.ok) return; // 查不到就跳过,保留原文字
          const data = await res.json();
          if (data.url) emojiMap[name] = data.url;
        } catch {
          // 单个表情查询失败不影响其他表情,静默跳过
        }
      }),
    );

    return escaped.replace(
      /:([a-zA-Z0-9_]+):/g,
      (match: any, name: string | number) => {
        const url = emojiMap[name];
        if (!url) return match;
        return `<img src="${url}" alt=":${name}:" class="misskey-emoji" style="width: 1.2em; vertical-align: middle; display: inline;" />`;
      },
    );
  }

  loadMisskeyNote();
  document.addEventListener("astro:page-load", loadMisskeyNote);
</script>

Mastodon版本

HTML

<div class="mastodon-note" id="mastodon-note">
  <div class="mastodon-note-header">
    <span class="mastodon-note-label">最近在嘟……</span>
    <a
      class="mastodon-note-link"
      id="mastodon-note-link"
      href="#"
      target="_blank"
    >
      @bipolar@moresci.sale ↗
    </a>
  </div>
  <p class="mastodon-note-text" id="mastodon-note-text">加载中……</p>
</div>

CSS

<style>
  .mastodon-note {
    border: 1px solid var(--border, #ddd);
    border-radius: 10px;
    padding: 1rem 1.25rem;
    margin: 1.5rem 0;
    background-color: var(--background);
  }
  .mastodon-note-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 0.6rem;
    font-size: 0.85rem;
    opacity: 0.6;
  }
  .mastodon-note-text {
    margin: 0;
    line-height: 1.7;
    font-size: 0.95rem;
    white-space: pre-wrap;
  }
  .mastodon-note-link {
    text-decoration: none;
    color: inherit;
  }
  .mastodon-note-link:hover {
    text-decoration: underline;
  }
</style>

JS部分

<script>
  async function loadMastodonNote() {
    const textEl = document.getElementById("mastodon-note-text");
    const linkEl = document.getElementById("mastodon-note-link");

    // 配置信息
    const INSTANCE = "https://moresci.sale"; // Mastodon 实例地址
    const USERNAME = "bipolar"; // 这里直接填写用户名

    if (!textEl || !linkEl) return;

    try {
      // 第一步:通过 lookup 接口获取用户的 ID
      const lookupRes = await fetch(
        `${INSTANCE}/api/v1/accounts/lookup?acct=${USERNAME}`,
      );
      const user = await lookupRes.json();

      // 2. 获取嘟文(包含自定义表情信息)
      const notesRes = await fetch(
        `${INSTANCE}/api/v1/accounts/${user.id}/statuses?limit=30&exclude_replies=true&exclude_reblogs=true`,
      );
      const notes = await notesRes.json();

      const textNotes = notes.filter(
        (n) => n.content && n.content.replace(/<[^>]+>/g, "").trim().length > 0,
      );
      if (!textNotes.length) return;

      const randomNote = textNotes[Math.floor(Math.random() * textNotes.length)];

      // 3. 处理内容与表情渲染
      let content = randomNote.content;

      // 将 API 返回的自定义表情数组映射为图片标签
      if (randomNote.emojis && randomNote.emojis.length > 0) {
        randomNote.emojis.forEach((emoji) => {
          // Mastodon 表情的格式通常是 :shortcode:
          const shortcode = `:${emoji.shortcode}:`;
          const imgTag = `<img src="${emoji.url}" alt="${emoji.shortcode}" style="width: 1.2em; height: 1.2em; vertical-align: middle; margin: 0 0.1em;" />`;
          // 使用全局替换,确保同一表情出现多次都能被替换
          content = content.replaceAll(shortcode, imgTag);
        });
      }

      // 将处理好的 HTML 插入容器(使用 innerHTML 以渲染 img 标签)
      textEl.innerHTML = content;
      linkEl.href = randomNote.url;
    } catch (err) {
      console.error("Mastodon fetch error:", err);
      textEl.textContent = "嘟文加载失败。";
    }
  }
  loadMastodonNote();
</script>

唔大概就是这样!!看不懂的随时再来问卷毛羊!!!祝大家装修愉快嘿嘿嘿