跳转到内容

从Memos转移到Moments

更新于: 2025-03-20
LiuShen
11 分钟
4,012 字
PV --
UV --

这里是清羽AI,这篇文章介绍了从Memos转移到Moments的过程,Memos是一款受欢迎的备忘录应用,但由于API多次更新和数据库不可逆的问题,作者决定寻找替代品。Moments是一个轻量朋友圈项目,早期基于PHP,后来改为Golang,功能更强,内存占用更低。文章详细介绍了Moments的部署教程,包括Compose部署和Nginx修改,以及前端实现说说页面和轻量朋友圈的方法。Moments支持分享链接、图片、音乐、视频、书籍、电影等功能,并提供了自定义位置、标签、公开性等设置,同时支持Markdown渲染、编辑、删除说说、暗夜模式、自定义图标、信息、CSS及JS代码等功能。作者还分享了如何在前端展示Moments的数据,包括引入MetingJS和APlayer包,以及创建说说页面和JS文件。整体上,Moments是一个功能丰富、轻量级的朋友圈应用,适合替代Memos。

碎碎念

问题说明

仅有版本0.2.8及以前没有跨域功能,最新版本已经更新,请按照官方文档配置防止跨域即可,无需在nginx端进行设置了。

Memos是一款很受欢迎的备忘录应用,可以在服务器中利用Docker便捷的部署,可以在线发布说说,备忘录,并且可以利用API展示到前端,功能上很强大,但是由于API的多次更新,很难做到及时兼容,再加上每次升级后数据库都不可逆,很容易造成不可挽回的后果,给作者提建议作者似乎也有自己的想法,并不遵从大众的意见,于是只能继续使用旧版本,并且一直在找替代品。

在这期间,我找到了一些也很优秀的产品,比如My-flomo-serverNanoblinko等等,但是都因为各种原因,如内存占用,部署环境,数据存储等原因,无法再我的环境上使用,不得不说Memos这个产品本身确实很优秀,由于是Golang开发的,后端很轻,内存占用仅有30MB左右,而市面上很多同类型产品都是基于SpringBoot开发,在内存方面,Java的占用众所周知。

但是我也并不着急,毕竟Memos-0.21.0版本目前还是很适合我的,于是我也在慢慢找,在这个期间,我注意到了一个项目,Moments,这个项目的早期版本是基于世界上最好的语言:PHP,从2.1.0开始,作者听从社区的意见,修改成了Golang,内存大幅减小,功能性也更强,迭代到现在,基本功能已经实现,比如插入视频,豆瓣读书和电影,分享链接,分享音乐,分享图片,在Memos中我需要自定义这些,比如使用{bilibili 视频ID},但是在Moments,这些视频和音乐都有很完善的接口,返回数据也很直观,十分适合直接调用,于是我毫不犹豫的换了过来,最终实现了我很满意的效果。

站外引用 · 引用站外地址,不保证站点的可用性和安全性🌟 Moments - 极简朋友圈github.com@kingwrcy

这篇文章就给大家介绍一下我的修改方案,方便大家抄作业,顺便给也想要换掉Memos的朋友提供一条新路。

前期要求

硬件要求

  • 一台服务器
  • 一个可自主解析的域名

软件要求

  • docker环境
  • 反向代理工具(本文以Nginx为例)

介绍与展示

这里先给大家展示一下最终的效果,注意该教程可能仅适合部分主题,如果出现主题不适配的情况请自行适配,这里以本站主题Hexo-theme-butterfly为基础进行修改:

  1. 说说页面:

    本站链接 · 来自本站,本站可确保其安全性,请放心点击跳转清羽飞扬の日常说说LiuShen's Blog

    由于前端才是最主要的展示区域,在这里我尽可能做了最多的适配,适配了豆瓣阅读书籍卡片分享,豆瓣电影分享,图片分享,链接分享,音乐分享,Bilibili分享,Youtube分享,黑夜模式等多种适配,这里仅展示部分功能,其中bilibili视频因为目前官方版本有问题,无法添加,yoputube由于拉低网络加载速度,不予展示。其他的具体效果可以上网站我的说说页面自行查看。

    说说效果展示

    亮色模式

    暗色模式

    书籍分享

    电影分享

    音乐分享

    图片分享

    链接分享

  2. 轻量朋友圈

    本站链接 · 来自本站,本站可确保其安全性,请放心点击跳转清羽飞扬の轻量朋友圈LiuShen's Blog

    这里也没什么过多可以展示的,因为网站功能太多,无法一一展示,建议大家直接进入网站自行查看。

    轻量朋友圈

  3. 功能说明

    Moments作为一个轻量朋友圈,其功能都是分享上的部分,如下所示:

    • 分享:链接,图片,音乐,视频,书籍,电影
    • 信息:自定义位置,自定义标签,是否公开
    • 页面:Markdown渲染,编辑说说,删除说说,暗夜模式,自定义图标,信息,CSSJS代码
    • 功能:S3存储,文件查询,多用户注册,点赞,评论,API

简单介绍完毕,下面我就来教大家如何进行部署!

部署教程

Moments部署

Compose部署

官方给予了很完善的教程,这里我仅仅简单介绍一下docker-compose部署的方式,如果你想以源码等其他方式进行部署,请查看文章开头部分的github地址进行查阅。

首先,在服务器任意位置创建文件:docker-compose.yaml,填入以下内容:

version: "3"
services:
  moments:
    image: kingwrcy/moments:latest
    container_name: moments
    restart: always
    environment:
      port: 3000
      JWT_KEY: "自己随便生成点字符串"
      ENABLE_SWAGGER: "true"
    ports:
      - "3003:3000" # 自行换端口,换前面的,后面的3000不要动
    volumes:
      - ./data:/app/data
    #   - ./data/localtime:/etc/localtime:ro
    #   - ./data/timezone:/etc/timezone:ro

注意文件,我将当前文件夹下的./data文件夹挂载了进去,数据都会在里面,迁移时仅需整体打包到新服务器即可。然后执行以下两条命令,后续需要升级也仅需要执行这两个命令:

docker-compose pull
docker-compose up -d

如果网络环境不佳,可尝试替换docker源,这里找了一篇参考文章:Docker镜像加速说明,这里我推荐使用1panel的国内镜像:https://docker.1panel.live

如果一切正常,那么服务应该已经跑起来了,可以在终端输入:curl 127.0.0.1:3003,如果有输出,则代表正常。

通过反向代理将其添加到某个域名中,这里就不再多说了,各大面板都有极其完备的反代文档。

Nginx修改

在开头的Waring提到,Moments在跨域上没有进行任何设置,也就是默认的不允许跨域,这可能会导致无法在网站上使用API展示,如果你是1panel部署的服务,可以在网站管理中找到网站目录,点击进入:

网站目录

返回到上一级目录,也就是域名名称的文件夹下,找到Proxy文件夹,编辑里面的root.conf文件为如下内容:

# 代理配置
location / {
    # 跨域设置
    add_header Access-Control-Allow-Origin *;  # 允许所有域名访问,你也可以指定具体域名
    add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';  # 允许的 HTTP 方法
    add_header Access-Control-Allow-Headers 'Origin, X-Requested-With, Content-Type, Accept, Authorization';  # 允许的请求头

    # 处理 OPTIONS 请求,预检请求
    if ($request_method = 'OPTIONS') {
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
        add_header Access-Control-Allow-Headers 'Origin, X-Requested-With, Content-Type, Accept, Authorization';
        add_header Access-Control-Max-Age 1728000;
        add_header Content-Type 'text/plain charset=UTF-8';
        add_header Content-Length 0;
        return 204;
    }

    # 原代理设置
    proxy_pass http://127.0.0.1:3003;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $http_connection;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_http_version 1.1;
    add_header X-Cache $upstream_cache_status;
    add_header Cache-Control no-cache;
    proxy_ssl_server_name off;
    proxy_ssl_name $proxy_host;
    add_header Strict-Transport-Security "max-age=31536000";
}

下面是原代理设置,可以仅仅复制上面部分内容,下面保持不变,注意端口不要出问题。

如果是宝塔面板,可以在网站设置的配置文件找到Nginx配置文件,依照以上配置进行添加请求头部分,也可以通过最下面其他设置中的跨域访问CORS配置部分快捷的进行设置:

宝塔设置

如果你是兰亭雷池进行的外部Nginx,找到目录/data/safeline/resources/nginx/custom_params,在里面找到你的对应的配置,每个网站有一个唯一ID,为递增排序,如果不知道对应哪一个,可以查看上一目录中的sites-enable目录下的文件,其中有一定的信息可以助你分辨是否为对应配置。

编辑工具说明

当然这里的编辑工具可以使任何可以编辑的页面,可视化是最方便的,这里是由于腾讯云无法通过root直接登录,打开的可视化界面权限不够,所以我选择在页面中使用管理员权限打开vim进行编辑。

比如我在这里是backend_29文件,在该目录下打开命令行并执行sudo vi ./backend_29,打开vim,点击i进行编辑,粘贴下面的内容:

add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods * always;
add_header Access-Control-Allow-Headers * always;

编辑完成后,点击esc退出编辑,输入:wq进行保存,然后执行以下命令,检查配置文件是否运行正常:

docker exec safeline-tengine nginx -t
# 下面是正常输出,不要复制进去了
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

如果输出正常,执行命令重启Nginx

docker exec safeline-tengine nginx -s reload

此时跨域配置应该就结束了,下面教大家如何将其数据在前端进行展示。

笨蛋声明

Nginx配置部分我也不是很懂,只是我通过这样的方式实践后是可行的,如果有更加优秀的配置欢迎留言!

前端实现

说说页面

由于该项目利用了MetingJSAPlayer,所以请提前引入这两个包,Hexo-theme-butterfly中虽然有内置的两个包,仅需修改配置文件即可开启,但是版本比较老,这里我建议自行引入最新版本,在配置中引入以下文件,注意css和js应该是分开引入的:

<link
  rel="stylesheet"
  href="https://fastly.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css"
  media="all"
  onload='this.media="all"'
/>
<script src="https://fastly.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js"></script>
<script src="https://fastly.jsdelivr.net/npm/meting@2.0.1/dist/Meting.min.js"></script>

新建页面shuoshuo,在文件内写入一下内容:

---
title: 日常哔哔,键盘侠的日常吐槽
aside: false
---

<div id="talk"></div>
<div class="limit">- 只展示最近30条说说 -</div>
<script src="/js/shuoshuo.js" no-pjax></script>

其中的JS文件地址清自行修改,自行创建,并写入以下内容:

function renderTalks() {
  const talkContainer = document.querySelector("#talk");
  if (!talkContainer) return;
  talkContainer.innerHTML = "";
  const generateIconSVG = () => {
    return `<svg viewBox="0 0 512 512"xmlns="http://www.w3.org/2000/svg"class="is-badge icon"><path d="m512 268c0 17.9-4.3 34.5-12.9 49.7s-20.1 27.1-34.6 35.4c.4 2.7.6 6.9.6 12.6 0 27.1-9.1 50.1-27.1 69.1-18.1 19.1-39.9 28.6-65.4 28.6-11.4 0-22.3-2.1-32.6-6.3-8 16.4-19.5 29.6-34.6 39.7-15 10.2-31.5 15.2-49.4 15.2-18.3 0-34.9-4.9-49.7-14.9-14.9-9.9-26.3-23.2-34.3-40-10.3 4.2-21.1 6.3-32.6 6.3-25.5 0-47.4-9.5-65.7-28.6-18.3-19-27.4-42.1-27.4-69.1 0-3 .4-7.2 1.1-12.6-14.5-8.4-26-20.2-34.6-35.4-8.5-15.2-12.8-31.8-12.8-49.7 0-19 4.8-36.5 14.3-52.3s22.3-27.5 38.3-35.1c-4.2-11.4-6.3-22.9-6.3-34.3 0-27 9.1-50.1 27.4-69.1s40.2-28.6 65.7-28.6c11.4 0 22.3 2.1 32.6 6.3 8-16.4 19.5-29.6 34.6-39.7 15-10.1 31.5-15.2 49.4-15.2s34.4 5.1 49.4 15.1c15 10.1 26.6 23.3 34.6 39.7 10.3-4.2 21.1-6.3 32.6-6.3 25.5 0 47.3 9.5 65.4 28.6s27.1 42.1 27.1 69.1c0 12.6-1.9 24-5.7 34.3 16 7.6 28.8 19.3 38.3 35.1 9.5 15.9 14.3 33.4 14.3 52.4zm-266.9 77.1 105.7-158.3c2.7-4.2 3.5-8.8 2.6-13.7-1-4.9-3.5-8.8-7.7-11.4-4.2-2.7-8.8-3.6-13.7-2.9-5 .8-9 3.2-12 7.4l-93.1 140-42.9-42.8c-3.8-3.8-8.2-5.6-13.1-5.4-5 .2-9.3 2-13.1 5.4-3.4 3.4-5.1 7.7-5.1 12.9 0 5.1 1.7 9.4 5.1 12.9l58.9 58.9 2.9 2.3c3.4 2.3 6.9 3.4 10.3 3.4 6.7-.1 11.8-2.9 15.2-8.7z"fill="#1da1f2"></path></svg>`;
  };
  const waterfall = a => {
    function b(a, b) {
      var c = window.getComputedStyle(b);
      return parseFloat(c["margin" + a]) || 0;
    }

    function c(a) {
      return a + "px";
    }

    function d(a) {
      return parseFloat(a.style.top);
    }

    function e(a) {
      return parseFloat(a.style.left);
    }

    function f(a) {
      return a.clientWidth;
    }

    function g(a) {
      return a.clientHeight;
    }

    function h(a) {
      return d(a) + g(a) + b("Bottom", a);
    }

    function i(a) {
      return e(a) + f(a) + b("Right", a);
    }

    function j(a) {
      a = a.sort(function (a, b) {
        return h(a) === h(b) ? e(b) - e(a) : h(b) - h(a);
      });
    }

    function k(b) {
      f(a) != t &&
        (b.target.removeEventListener(b.type, arguments.callee), waterfall(a));
    }
    "string" == typeof a && (a = document.querySelector(a));
    var l = [].map.call(a.children, function (a) {
      return ((a.style.position = "absolute"), a);
    });
    a.style.position = "relative";
    var m = [];
    l.length &&
      ((l[0].style.top = "0px"),
      (l[0].style.left = c(b("Left", l[0]))),
      m.push(l[0]));
    for (var n = 1; n < l.length; n++) {
      var o = l[n - 1],
        p = l[n],
        q = i(o) + f(p) <= f(a);
      if (!q) break;
      ((p.style.top = o.style.top),
        (p.style.left = c(i(o) + b("Left", p))),
        m.push(p));
    }
    for (; n < l.length; n++) {
      j(m);
      var p = l[n],
        r = m.pop();
      ((p.style.top = c(h(r) + b("Top", p))),
        (p.style.left = c(e(r))),
        m.push(p));
    }
    j(m);
    var s = m[0];
    a.style.height = c(h(s) + b("Bottom", s));
    var t = f(a);
    window.addEventListener
      ? window.addEventListener("resize", k)
      : (document.body.onresize = k);
  };

  const fetchAndRenderTalks = () => {
    const url = "https://mm.liushen.fun/api/memo/list";
    const cacheKey = "talksCache";
    const cacheTimeKey = "talksCacheTime";
    const cacheDuration = 30 * 60 * 1000; // 半个小时 (30 分钟)

    const cachedData = localStorage.getItem(cacheKey);
    const cachedTime = localStorage.getItem(cacheTimeKey);
    const currentTime = new Date().getTime();

    // 判断缓存是否有效
    if (cachedData && cachedTime && currentTime - cachedTime < cacheDuration) {
      const data = JSON.parse(cachedData);
      renderTalks(data); // 使用缓存渲染数据
    } else {
      if (talkContainer) {
        talkContainer.innerHTML = "";
        fetch(url, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            size: 30,
          }),
        })
          .then(res => res.json())
          .then(data => {
            if (data.code === 0 && data.data && Array.isArray(data.data.list)) {
              // 缓存数据
              localStorage.setItem(cacheKey, JSON.stringify(data.data.list));
              localStorage.setItem(cacheTimeKey, currentTime.toString());
              renderTalks(data.data.list); // 渲染数据
            }
          })
          .catch(error => {
            console.error("Error fetching data:", error);
          });
      }
    }

    // 渲染函数
    function renderTalks(list) {
      // 确保 data 是一个数组
      if (Array.isArray(list)) {
        let items = list.map(item => formatTalk(item, url));
        items.forEach(item =>
          talkContainer.appendChild(generateTalkElement(item))
        );
        waterfall("#talk");
      } else {
        console.error("Data is not an array:", list);
      }
    }
  };

  const formatTalk = (item, url) => {
    let date = formatTime(new Date(item.createdAt).toString());
    let content = item.content;
    let imgs = item.imgs ? item.imgs.split(",") : [];
    let text = content;
    content = text
      .replace(/\[(.*?)\]\((.*?)\)/g, `<a href="$2">@$1</a>`)
      .replace(/- \[ \]/g, "⚪")
      .replace(/- \[x\]/g, "⚫");
    // 保留换行符,转换 \n 为 <br>
    content = content.replace(/\n/g, "<br>");
    // 将content用一个类包裹,便于后续处理
    content = `<div class="talk_content_text">${content}</div>`;
    if (imgs.length > 0) {
      const imgDiv = document.createElement("div");
      imgDiv.className = "zone_imgbox";
      imgs.forEach(e => {
        const imgLink = document.createElement("a");
        imgLink.href = e;
        imgLink.setAttribute("data-fancybox", "gallery");
        imgLink.className = "fancybox";
        imgLink.setAttribute("data-thumb", e);
        const imgTag = document.createElement("img");
        imgTag.src = e;
        imgLink.appendChild(imgTag);
        imgDiv.appendChild(imgLink);
      });
      content += imgDiv.outerHTML;
    }

    // 外链分享功能
    if (item.externalUrl) {
      const externalUrl = item.externalUrl;
      const externalTitle = item.externalTitle;
      const externalFavicon = item.externalFavicon;

      const externalContainer = `
            <div class="shuoshuo-external-link">
                <a class="external-link" href="${externalUrl}" target="_blank" rel="external nofollow noopener noreferrer">
                    <div class="external-link-left" style="background-image: url(${externalFavicon})"></div>
                    <div class="external-link-right">
                        <div class="external-link-title">${externalTitle}</div>
                        <div>点击跳转<i class="fa-solid fa-angle-right"></i></div>
                    </div>
                </a>
            </div>`;

      content += externalContainer;
    }

    const ext = JSON.parse(item.ext || "{}");

    if (ext.music && ext.music.id) {
      const music = ext.music;
      const musicUrl = music.api
        .replace(":server", music.server)
        .replace(":type", music.type)
        .replace(":id", music.id);
      content += `
            <meting-js server="${music.server}" type="${music.type}" id="${music.id}" api="${music.api}"></meting-js>
        `;
    }

    if (ext.doubanMovie && ext.doubanMovie.id) {
      const doubanMovie = ext.doubanMovie;
      const doubanMovieUrl = doubanMovie.url;
      const doubanTitle = doubanMovie.title;
      // const doubanDesc = doubanMovie.desc || '暂无描述';
      const doubanImage = doubanMovie.image;
      const doubanDirector = doubanMovie.director || "未知导演";
      const doubanRating = doubanMovie.rating || "暂无评分";
      // const doubanReleaseDate = doubanMovie.releaseDate || '未知上映时间';
      // const doubanActors = doubanMovie.actors || '未知演员';
      const doubanRuntime = doubanMovie.runtime || "未知时长";

      content += `
                <a class="douban-card" href="${doubanMovieUrl}" target="_blank">
                    <div class="douban-card-bgimg" style="background-image: url('${doubanImage}');"></div>
                    <div class="douban-card-left">
                        <div class="douban-card-img" style="background-image: url('${doubanImage}');"></div>
                    </div>
                    <div class="douban-card-right">
                        <div class="douban-card-item"><span>电影名: </span><strong>${doubanTitle}</strong></div>
                        <div class="douban-card-item"><span>导演: </span><span>${doubanDirector}</span></div>
                        <div class="douban-card-item"><span>评分: </span><span>${doubanRating}</span></div>
                        <div class="douban-card-item"><span>时长: </span><span>${doubanRuntime}</span></div>
                    </div>
                </a>
            `;
    }

    if (ext.doubanBook && ext.doubanBook.id) {
      const doubanBook = ext.doubanBook;
      const bookUrl = doubanBook.url;
      const bookTitle = doubanBook.title;
      // const bookDesc = doubanBook.desc;
      const bookImage = doubanBook.image;
      const bookAuthor = doubanBook.author;
      const bookRating = doubanBook.rating;
      const bookPubDate = doubanBook.pubDate;

      const bookTemplate = `
                <a class="douban-card" href="${bookUrl}" target="_blank">
                    <div class="douban-card-bgimg" style="background-image: url('${bookImage}');"></div>
                        <div class="douban-card-left">
                            <div class="douban-card-img" style="background-image: url('${bookImage}');"></div>
                        </div>
                        <div class="douban-card-right">
                            <div class="douban-card-item">
                                <span>书名: </span><strong>${bookTitle}</strong>
                            </div>
                            <div class="douban-card-item">
                                <span>作者: </span><span>${bookAuthor}</span>
                            </div>
                            <div class="douban-card-item">
                                <span>出版年份: </span><span>${bookPubDate}</span>
                            </div>
                            <div class="douban-card-item">
                                <span>评分: </span><span>${bookRating}</span>
                            </div>
                        </div>
                </a>
            `;

      content += bookTemplate;
    }

    if (ext.video && ext.video.type) {
      const videoType = ext.video.type;
      const videoUrl = ext.video.value;
      if (videoType === "bilibili") {
        // Bilibili 视频模板
        // 从形如https://www.bilibili.com/video/BV1VGAPeAEMQ/?vd_source=91b3158d27d98ff41f842508c3794a13 的链接中提取视频 BV1VGAPeAEMQ
        const biliTemplate = `
                <div style="position: relative; padding: 30% 45%; margin-top: 10px;">
                    <iframe 
                        style="position: absolute; width: 100%; height: 100%; left: 0; top: 0; border-radius: 12px;" 
                        src="${videoUrl}&autoplay=0"
                        scrolling="no" 
                        frameborder="no" 
                        allowfullscreen>
                    </iframe>
                </div>
            `;
        // 将模板插入到 DOM 中
        content += biliTemplate;
      } else if (videoType === "youtube") {
        // YouTube 视频模板
        // 从形如https://youtu.be/2V6lvCUPT8I?si=DVhUas6l6qlAr6Ru的链接中提取视频 ID2V6lvCUPT8I
        const youtubeTemplate = `
                <div style="position: relative; padding: 30% 45%; margin-top: 10px;">
                    <iframe width="100%"
                        style="position: absolute; width: 100%; height: 100%; left: 0; top: 0; border-radius: 12px;"
                        src="${videoUrl}"
                        title="YouTube video player" 
                        frameborder="0" 
                        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 
                        referrerpolicy="strict-origin-when-cross-origin" 
                        allowfullscreen>
                    </iframe>
                </div>
            `;
        // 将模板插入到 DOM 中
        content += youtubeTemplate;
      }
    }

    return {
      content: content,
      user: item.user.nickname || "匿名",
      avatar:
        item.user.avatarUrl ||
        "https://p.liiiu.cn/i/2024/03/29/66061417537af.png",
      date: date,
      location: item.location || "陕西西安",
      tags: item.tags
        ? item.tags.split(",").filter(tag => tag.trim() !== "")
        : ["无标签"],
      text: content.replace(
        /\[(.*?)\]\((.*?)\)/g,
        "[链接]" + `${imgs.length ? "[图片]" : ""}`
      ),
    };
  };

  const generateTalkElement = item => {
    const talkItem = document.createElement("div");
    talkItem.className = "talk_item";

    const talkMeta = document.createElement("div");
    talkMeta.className = "talk_meta";

    const avatar = document.createElement("img");
    avatar.className = "no-lightbox avatar";
    avatar.src = item.avatar;

    const info = document.createElement("div");
    info.className = "info";

    const talkNick = document.createElement("span");
    talkNick.className = "talk_nick";
    talkNick.innerHTML = `${item.user} ${generateIconSVG()}`;

    const talkDate = document.createElement("span");
    talkDate.className = "talk_date";
    talkDate.textContent = item.date;

    const talkContent = document.createElement("div");
    talkContent.className = "talk_content";
    talkContent.innerHTML = item.content;

    const talkBottom = document.createElement("div");
    talkBottom.className = "talk_bottom";

    const TagContainer = document.createElement("div");

    const talkTag = document.createElement("span");
    talkTag.className = "talk_tag";
    talkTag.textContent = `🏷️${item.tags}`;

    const locationTag = document.createElement("span");
    locationTag.className = "location_tag";
    locationTag.textContent = `🌍${item.location}`;

    TagContainer.appendChild(talkTag);
    TagContainer.appendChild(locationTag);

    const commentLink = document.createElement("a");
    commentLink.href = "javascript:;";
    commentLink.onclick = () => goComment(item.text);
    const commentIcon = document.createElement("span");
    commentIcon.className = "icon";
    const commentIconInner = document.createElement("i");
    commentIconInner.className = "fa-solid fa-message fa-fw";
    commentIcon.appendChild(commentIconInner);
    commentLink.appendChild(commentIcon);

    talkMeta.appendChild(avatar);
    info.appendChild(talkNick);
    info.appendChild(talkDate);
    talkMeta.appendChild(info);
    talkItem.appendChild(talkMeta);
    talkItem.appendChild(talkContent);
    talkBottom.appendChild(TagContainer);
    talkBottom.appendChild(commentLink);
    talkItem.appendChild(talkBottom);

    return talkItem;
  };

  const goComment = e => {
    const match = e.match(/<div class="talk_content_text">([\s\S]*?)<\/div>/);
    const textContent = match ? match[1] : "";
    const n = document.querySelector(".atk-textarea");
    n.value = `> ${textContent}\n\n`;
    n.focus();
    btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
    // const n = document.querySelector(".atk-textarea");
    // n.value = `> ${e}\n\n`;
    // n.focus();
    // btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
  };

  const formatTime = time => {
    const d = new Date(time);
    const ls = [
      d.getFullYear(),
      d.getMonth() + 1,
      d.getDate(),
      d.getHours(),
      d.getMinutes(),
      d.getSeconds(),
    ];
    const r = ls.map(a => (a.toString().length === 1 ? "0" + a : a));
    return `${r[0]}-${r[1]}-${r[2]} ${r[3]}:${r[4]}`;
  };

  fetchAndRenderTalks();
}

renderTalks();

// function whenDOMReady() {
//     const talkContainer = document.querySelector('#talk');
//     talkContainer.innerHTML = '';
//     fetchAndRenderTalks();
// }
// whenDOMReady();
// document.addEventListener("pjax:complete", whenDOMReady);

自行修改js文件中的Moments地址为你的地址,在文件中,有一个gocomment函数,实现的是获取卡片中的文本内容,如果如果出现不匹配的情况,请自行修改一下类名,这里我匹配的是artalk的输入框。

然后引入样式文件,这个文件可以在配置文件中引用,也可以在页面文件中类似于shuoshuo.js一样引用,样式内容如下:

:root {
  --liushen-card-bg: #fff;
  --liushen-card-border: 1px solid #e3e8f7;
  --card-box-shadow: 0 3px 8px 6px rgba(7, 17, 27, 0.09);
  --card-hover-box-shadow: 0 3px 8px 6px rgba(7, 17, 27, 0.2);
  --liushen-card-secondbg: #f1f3f8;
  --liushen-button-hover-bg: #2679cc;
  --liushen-text: #4c4948;
  --liushen-button-bg: #f1f3f8;
  --liushen-fancybox-bg: rgba(255, 255, 255, 0.5);
}

:root,
[data-theme="dark"] {
  --liushen-card-bg: #181818;
  --liushen-card-secondbg: #30343f;
  --liushen-card-border: 1px solid #42444a;
  --card-box-shadow: 0 3px 8px 6px rgba(7, 17, 27, 0.09);
  --card-hover-box-shadow: 0 3px 8px 6px rgba(7, 17, 27, 0.2);
  --liushen-button-bg: #30343f;
  --liushen-button-hover-bg: #2679cc;
  --liushen-text: rgba(255, 255, 255, 0.702);
  --liushen-fancybox-bg: rgba(0, 0, 0, 0.5);
}

/* 卡片初始化 */
#talk .talk_item {
  width: calc(33.333% - 6px);
  background: var(--liushen-card-bg);
  border: var(--liushen-card-border);
  box-shadow: var(--card-box-shadow);
  transition: box-shadow 0.3s ease-in-out;
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  padding: 20px;
  margin-bottom: 9px;
  margin-right: 9px;
}
#talk .talk_item:hover {
  box-shadow: var(--card-hover-box-shadow);
}

@media (max-width: 900px) {
  #talk .talk_item {
    width: calc(50% - 5px);
  }
}
@media (max-width: 450px) {
  #talk .talk_item {
    width: calc(100%);
  }
}

#talk {
  position: relative;
  width: 100%;
  box-sizing: border-box;
}

#talk .talk_meta .avatar {
  margin: 0 !important;
  width: 60px;
  height: 60px;
  border-radius: 12px;
}
#talk .talk_bottom,
#talk .talk_meta {
  display: flex;
  align-items: center;
}
#talk .talk_meta {
  display: flex;
  align-items: center;
  width: 100%;
  padding-bottom: 10px;
  border-bottom: 1px dashed grey; /* 添加灰色虚线边框 */
}
#talk .talk_bottom {
  margin-top: 15px;
  padding-top: 10px;
  border-top: 1px dashed grey; /* 添加灰色虚线边框 */
  justify-content: space-between;
}
#talk .talk_meta .info {
  display: flex;
  flex-direction: column;
  margin-left: 10px;
}
#talk .talk_meta .info .talk_nick {
  color: #6dbdc3;
  font-size: 1.2rem;
}
#talk .talk_meta .info svg.is-badge.icon {
  width: 15px;
  padding-top: 3px;
}
#talk .talk_meta .info span.talk_date {
  opacity: 0.6;
}
#talk .talk_item .talk_content {
  margin-top: 10px;
}
#talk .talk_item .talk_content .zone_imgbox {
  display: flex;
  flex-wrap: wrap;
  --w: calc(25% - 8px);
  gap: 10px;
  margin-top: 10px;
}
#talk .talk_item .talk_content .zone_imgbox a {
  display: block;
  border-radius: 12px;
  width: var(--w);
  aspect-ratio: 1/1;
  position: relative;
}
#talk .talk_item .talk_content .zone_imgbox a:first-child {
  width: 100%;
  aspect-ratio: 1.8;
}
#talk .talk_item .talk_content .zone_imgbox img {
  border-radius: 10px;
  width: 100%;
  height: 100%;
  margin: 0 !important;
  object-fit: cover;
}
/* 底部 */
#talk .talk_item .talk_bottom {
  opacity: 0.9;
}
#talk .talk_item .talk_bottom .icon {
  float: right;
  transition: all 0.3s;
}
#talk .talk_item .talk_bottom .icon:hover {
  color: #49b1f5;
}
#talk .talk_item .talk_bottom span.talk_tag,
#talk .talk_item .talk_bottom span.location_tag {
  font-size: 14px;
  background-color: var(--liushen-card-secondbg);
  border-radius: 12px;
  padding: 3px 15px 3px 10px;
  transition: box-shadow 0.3s ease;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

#talk .talk_item .talk_bottom span.location_tag {
  margin-left: 5px;
}

#talk .talk_item .talk_bottom span.talk_tag:hover,
#talk .talk_item .talk_bottom span.location_tag:hover {
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
#talk .talk_item .talk_content > a {
  margin: 0 3px;
  color: #ff7d73 !important;
}
#talk .talk_item .talk_content > a:hover {
  text-decoration: none !important;
  color: #ff5143 !important;
}

@media screen and (max-width: 900px) {
  #talk .talk_item .talk_content .zone_imgbox {
    --w: calc(33% - 5px);
  }
  #talk .talk_item #post-comment {
    margin: 0 3px;
  }
}
@media screen and (max-width: 768px) {
  .zone_imgbox {
    gap: 6px;
  }
  .zone_imgbox {
    --w: calc(50% - 3px);
  }
  span.talk_date {
    font-size: 14px;
  }
}

#talk .talk_item .talk_content .douban-card {
  margin-top: 10px !important;
  text-decoration: none;
  align-items: center;
  border-radius: 12px;
  color: #faebd7;
  display: flex;
  justify-content: center;
  margin: 10px;
  max-width: 400px;
  overflow: hidden;
  padding: 15px;
  position: relative;
}

.douban-card .douban-card-bgimg {
  background-position: 50%;
  background-repeat: no-repeat;
  background-size: 100%;
  filter: blur(15px) brightness(0.6);
  height: 115%;
  position: absolute;
  width: 115%;
}

.douban-card .douban-card-left {
  align-items: center;
  display: flex;
  flex-direction: column;
  position: relative;
}

.douban-card .douban-card-left .douban-card-img {
  transition: all 0.5s ease;
  height: 130px;
  position: relative;
  width: 80px;
  background-position: 50%;
  background-repeat: no-repeat;
  background-size: 100%;
}

.douban-card .douban-card-left:hover .douban-card-img {
  filter: blur(5px) brightness(0.6);
  transform: perspective(800px) rotateX(180deg);
}

.douban-card .douban-card-right {
  color: #faebd7;
  display: flex;
  flex-direction: column;
  font-size: 14px;
  line-height: 1.5;
  margin-left: 12px;
  position: relative;
}

.douban-card .douban-card-right .douban-card-item {
  margin-top: 4px;
  max-width: 95%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 外链卡片 */
#talk .talk_item .talk_content .shuoshuo-external-link {
  /* 无下划线 */
  width: 100%;
  height: 80px;
  margin-top: 10px;
  border-radius: 12px;
  background-color: var(--liushen-card-secondbg);
  color: var(--liushen-card-text);
  border: var(--liushen-card-border);
  transition: background-color 0.3s ease-in-out;
}

.shuoshuo-external-link:hover {
  background-color: var(--liushen-button-hover-bg);
}

.shuoshuo-external-link .external-link {
  display: flex;
  color: var(--liushen-text) !important;
  width: 100%;
  height: 100%;
}

.shuoshuo-external-link .external-link:hover {
  color: white !important;
}

.shuoshuo-external-link .external-link:hover {
  text-decoration: none !important;
}

.shuoshuo-external-link .external-link-left {
  width: 60px;
  height: 60px;
  margin: 10px;
  border-radius: 12px;
  background-size: cover;
  background-position: center;
}

.shuoshuo-external-link .external-link-right {
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: calc(100% - 80px);
  padding: 10px;
}

.shuoshuo-external-link .external-link-right .external-link-title {
  font-size: 1rem;
  font-weight: 800;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.shuoshuo-external-link .external-link-right i {
  margin-left: 5px;
}

.limit {
  width: 100%;
  text-align: center;
  margin-top: 30px;
}

如果一切正常,应该就可以显示了,如果仍然有问题可以评论区讨论,我们在这里继续进行下一步:

首页轮播

本次修改我还修改了首页轮播部分的内容,由于请求的地址一致,这里我直接用了同一个缓存,可以使网站的加载速度更近一步,缓存时间为半个小时,缓存位置为localstorage,缓存数据共30条,由于首页的轮播只需要最新的内容,这里我们取到缓存后节选前五条即可。

在主题配置文件内部引入外部js,名称位置随意,内容如下:

let talkTimer = null;

const cacheKey = "talksCache";
const cacheTimeKey = "talksCacheTime";
const cacheDuration = 30 * 60 * 1000; // 缓存有效期 30分钟

function indexTalk() {
  if (talkTimer) {
    clearInterval(talkTimer);
    talkTimer = null;
  }

  if (!document.getElementById("bber-talk")) return;

  function toText(ls) {
    let text = [];
    ls.forEach(item => {
      text.push(
        item.content
          .replace(/#(.*?)\s/g, "")
          .replace(/\{(.*?)\}/g, "")
          .replace(/\!\[(.*?)\]\((.*?)\)/g, '<i class="fa-solid fa-image"></i>')
          .replace(/\[(.*?)\]\((.*?)\)/g, '<i class="fa-solid fa-link"></i>')
      );
    });
    return text;
  }

  function talk(ls) {
    let html = "";
    ls.forEach((item, i) => {
      html += `<li class="item item-${i + 1}">${item}</li>`;
    });
    let box = document.querySelector("#bber-talk .talk-list");
    box.innerHTML = html;
    talkTimer = setInterval(() => {
      box.appendChild(box.children[0]);
    }, 3000);
  }

  const cachedData = localStorage.getItem(cacheKey);
  const cachedTime = localStorage.getItem(cacheTimeKey);
  const currentTime = new Date().getTime();

  // 判断缓存是否有效
  if (cachedData && cachedTime && currentTime - cachedTime < cacheDuration) {
    const data = toText(JSON.parse(cachedData));
    talk(data.slice(0, 6)); // 使用缓存渲染数据
  } else {
    fetch("https://mm.liushen.fun/api/memo/list", {
      // 使用新的API地址
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ size: 30 }), // 限制30条数据
    })
      .then(res => res.json())
      .then(data => {
        // 确保新的API返回数据格式正确
        if (data.code === 0 && data.data && Array.isArray(data.data.list)) {
          localStorage.setItem(cacheKey, JSON.stringify(data.data.list));
          localStorage.setItem(cacheTimeKey, currentTime.toString());

          const formattedData = toText(data.data.list); // 处理数据格式
          talk(formattedData.slice(0, 6)); // 渲染数据
        }
      })
      .catch(error => console.error("Error fetching data:", error));
  }
}

// pjax注释掉上面的 indexTalk(); 使用如下方法:
function whenDOMReady() {
  indexTalk();
}

whenDOMReady();
document.addEventListener("pjax:complete", whenDOMReady);

注意自行修改其中第42行的API地址为你自己的。

然后在主题文件中添加以下的样式:

/* maintop */

#main_top {
  display: flex;
  justify-content: center;
  z-index: 1;
  max-width: 1200px;
  margin: 20px auto;
  width: 100%;
  padding: 0 15px;
  margin-top: 40px;
  margin-bottom: 0px;
}

.hide-aside #main_top {
  width: 80%;
}

.hide-aside #main_top #bber-talk {
  max-width: 936px;
}

@media screen and (min-width: 2000px) {
  .hide-aside #main_top #bber-talk {
    max-width: 80%;
  }

  #main_top {
    max-width: 70%;
  }
}

@media screen and (max-width: 1210px) {
  .hide-aside #main_top {
    padding: 0 12px;
  }
}

@media screen and (max-width: 900px) {
  .hide-aside #main_top {
    width: 100%;
    padding: 0 15px;
  }
}

@media screen and (max-width: 768px) {
  .hide-aside #main_top {
    padding: 0 5px;
  }

  div#main_top {
    margin-top: 20px;
    padding: 0 5px;
  }
}

#bber-talk {
  /* border-radius: 8px; */
  /* background: var(--card-bg); */
  /* box-shadow: none; */
  box-sizing: border-box;
  /* transition: all .3s ease-in-out; */
  cursor: pointer;
  width: 100%;
  min-height: 50px;
  padding: 0.5rem 1rem;
  display: flex;
  align-items: center;
  overflow: hidden;
  font-weight: 700;
}

#bber-talk,
#bber-talk a {
  color: var(--font-color);
}

#bber-talk svg.icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
  font-size: 20px;
}

#bber-talk .item i {
  margin-left: 5px;
}

#bber-talk > i {
  font-size: 1.1rem;
}

#bber-talk .talk-list {
  flex: 1;
  max-height: 32px;
  font-size: 16px;
  padding: 0;
  margin: 0;
  overflow: hidden;
}

#bber-talk .talk-list:hover {
  color: var(--default-bg-color);
  transition: all 0.2s ease-in-out;
}

#bber-talk .talk-list li {
  list-style: none;
  width: 100%;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  margin-left: 10px;
}

@media screen and (min-width: 770px) {
  #bber-talk .talk-list {
    text-align: center;
    margin-right: 20px;
  }
}

此时我们还缺少一个插入点,我们需要确保我们的轮播条在最顶部的位置轮播,创建文件themes\butterfly\layout\includes\others\memos_home.pug,写入以下内容:

if (is_home())
  #main_top
    #bber-talk.cardHover.bb_talk_swipper(onclick=`pjax.loadUrl("/shuoshuo/")`)
      svg.icon(t='1660960757124', viewBox='0 0 1024 1024', version='1.1', xmlns='http://www.w3.org/2000/svg', p-id='3946', width='200', height='200')
        path(d='M526.432 924.064c-20.96 0-44.16-12.576-68.96-37.344L274.752 704H192c-52.928 0-96-43.072-96-96V416c0-52.928 43.072-96 96-96h82.752l182.624-182.624c24.576-24.576 47.744-37.024 68.864-37.024C549.184 100.352 576 116 576 160v704c0 44.352-26.72 60.064-49.568 60.064zM192 384c-17.632 0-32 14.368-32 32v192c0 17.664 14.368 32 32 32h96c8.48 0 16.64 3.36 22.624 9.376l192.064 192.096c3.392 3.36 6.496 6.208 9.312 8.576V174.016a145.824 145.824 0 0 0-9.376 8.608l-192 192C304.64 380.64 296.48 384 288 384h-96zM687.584 730.368a31.898 31.898 0 0 1-18.656-6.016c-14.336-10.304-17.632-30.304-7.328-44.672l12.672-17.344C707.392 617.44 736 578.624 736 512c0-69.024-25.344-102.528-57.44-144.928-5.664-7.456-11.328-15.008-16.928-22.784-10.304-14.336-7.04-34.336 7.328-44.672 14.368-10.368 34.336-7.04 44.672 7.328 5.248 7.328 10.656 14.464 15.968 21.504C764.224 374.208 800 421.504 800 512c0 87.648-39.392 141.12-74.144 188.32l-12.224 16.736c-6.272 8.704-16.064 13.312-26.048 13.312z', p-id='3947')
        path(d='M796.448 839.008a31.906 31.906 0 0 1-21.088-7.936c-13.28-11.648-14.624-31.872-2.976-45.152C836.608 712.672 896 628.864 896 512s-59.392-200.704-123.616-273.888c-11.648-13.312-10.304-33.504 2.976-45.184 13.216-11.648 33.44-10.336 45.152 2.944C889.472 274.56 960 373.6 960 512s-70.528 237.472-139.488 316.096c-6.368 7.232-15.2 10.912-24.064 10.912z', p-id='3948')
      ul.talk-list 说说加载中。。。

创建了元素,我们需要通过一个注入点引入这个文件,将其插入到主页中,打开文件themes\butterfly\layout\includes\layout.pug,按照下面的位置插入我们的文件引用:

- var htmlClassHideAside = theme.aside.enable && theme.aside.hide ? 'hide-aside' : ''
- page.aside = is_archive() ? theme.aside.display.archive: is_category() ? theme.aside.display.category : is_tag() ? theme.aside.display.tag : page.aside
- var hideAside = !theme.aside.enable || page.aside === false ? 'hide-aside' : ''
- var pageType = is_post() ? 'post' : 'page'
- pageType = page.type ? pageType + ' type-' + page.type : pageType

doctype html
html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside)
  head
    include ./head.pug
  body
    if theme.preloader.enable
      !=partial('includes/loading/index', {}, {cache: true})

    if theme.background
      #web_bg

    !=partial('includes/sidebar', {}, {cache: true})

    #body-wrap(class=pageType)
      include ./header/index.pug
+     include ./others/memos_home.pug

      main#content-inner.layout(class=hideAside)
        if body
          div!= body
        else
          block content
          if theme.aside.enable && page.aside !== false
            include widget/index.pug

      - const footerBg = theme.footer_img
      - const footer_bg = footerBg ? footerBg === true ? bg_img : getBgPath(footerBg) : ''
      footer#footer(style=footer_bg)
        !=partial('includes/footer', {}, {cache: true})

    include ./rightside.pug
    !=partial('includes/rightmenu', {}, {cache: true})
    include ./additional-js.pug

添加第22行,注意不要抄前面的加号,缩进与上面的include对齐即可。这样我们的首页轮播也实现了,如果有样式不对的地方请自行微调。

额外教程

Meting

由于Moments的音乐部分需要使用MetingJS,如果用默认的服务可能会很慢,非常影响速度,所以我建议自建,这里我找到的项目是:

站外引用 · 引用站外地址,不保证站点的可用性和安全性Meting-API:Meting API 的容器化与部署github.com@xizeyoupan

这个项目支持多种部署方式,除了源码部署,还可以通过Docker部署,Deno平台部署以及Vercel一键部署,速度上大家自行判断。

由于作者使用的为轻量化框架Deno,对于X-Forwarded请求头或transparent proxy并不支持,所以实际有用的只有X-Forwarded-Host请求头,我们需要将/meting的流量都转发到/上,我们需要自己修改Nginx配置文件,这里官方是有介绍的,只需要在Nginx配置文件中添加一个转发即可,详情请见上方链接,这里我只说明雷池的修改方式:

首先在终端中cd到目录/data/safeline/resources/nginx/custom_params,通过vim打开对应的配置文件,比如这里我对应的配置文件ID为49:

root@VM-4-11-ubuntu:/home/ubuntu# cd /data/safeline/resources/nginx/custom_params
root@VM-4-11-ubuntu:/data/safeline/resources/nginx/custom_params# vi ./backend_49

在打开的vim窗口中,点击字母i,输入以下的内容:

location ^~ /meting/ {
    proxy_pass http://[你的源地址IP]:3040/;
    proxy_set_header X-Forwarded-Host $scheme://$host:$server_port/meting;
}

输入完成后,点击esc,输入:wq强制保存并退出。

同样通过以下命令检查并重启Nginx

root@VM-4-11-ubuntu:/data/safeline/resources/nginx/custom_params# docker exec safeline-tengine nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
root@VM-4-11-ubuntu:/data/safeline/resources/nginx/custom_params# docker exec safeline-tengine nginx -s reload
root@VM-4-11-ubuntu:/data/safeline/resources/nginx/custom_params#

重启应该就实现效果了,但是访问根目录可能并没有变https,因为我们这里的配置是将/meting的数据发送到根域名并采用https,所以在音乐页面,我们可以填写的api地址如下:

https://meting.example.com/meting/api?server=:server&type=:type&id=:id&r=:r

保存后应该就可以看到后台没有http访问的报错和警告了。

编辑工具说明

当然这里的编辑工具可以使任何可以编辑的页面,可视化是最方便的,这里是由于腾讯云无法通过root直接登录,打开的可视化界面权限不够,所以我选择在页面中使用管理员权限打开vim进行编辑。

S3配置

笨蛋声明

这里我感觉稍微有点卡顿,如果错误配置可能会影响使用(可能是我太笨了QAQ),所以稍微记录一下,也希望能给别人提供一些帮助。

S3上我选择缤纷云,每个月有10GB流量免费额度,和50GB存储免费额度,放在轻量朋友圈上是包用不完的,首先创建一个存储桶,如果你不打算绑定外部地址,那就选择公开桶,但是经过测试公开桶需要余额大于零。如果你打算绑定备案域名,那就不需要公开桶,然后配置好权限访问后,如果师公开桶,你会得到一个桶的域名,比如缤纷云的话,一般格式为https://你的桶名称.s3.bitiful.net,这个就是你图片直链的域名。

站外引用 · 引用站外地址,不保证站点的可用性和安全性缤纷云:高性能对象存储+CDN七牛云存储与OSS的优秀替代品,以不到1/3的成本减轻成本负担并释放你的创造力。

如果你打算自行绑定域名到桶,那就是自己的域名了,然后去左边对象存储的AccessKey中创建一个子用户,你会获得一个子用户AccessKeySecretKey.

回到Moments后台的S3配置中,按照下图进行配置:

S3配置

注意最后一个参数,不是图片后缀名称,而是说S3服务商所支持的图片参数,比如我这里选了文件格式为avif,如果不支持他会自动返回其他格式。

这样就配置好了,后面你在友圈产生的任何图片,比如分享书籍,电影产生的背景图,分享图片产生的直链都会自动传到存储中,这个图片没有后缀名,不要被误导了。

总结

Moments可能功能不是很多,但是非常适合我个人,我也希望作者赶紧修好B站的上传,这玩意可是刚需呜呜呜。

界面上足够简单,占用比Memos还低,仅仅十几MB的存储,几乎所有服务器都能无负担的部署成功,使用SQLite存储也很好迁移,作者非常好,让大家提意见,他一点点实现,我认为这才是开源社区应该有的模样,而不是这个版本还没完善,就破坏性更新下一版本,同时会自动修改数据库格式,如果错误更新不允许回退,我个人认为这个领域霸主似乎有点德不配位,当然我可能并没有资格去评判,Time will tell, the audience will show.

我个人是偏向于不断的更新,跟上作者的更新步伐,体验最新的功能,很乐意给予反馈和贡献,我也希望可以给开源项目作者提供一定的帮助,但是如果每次更新都需要花大量的时间阅读文档重新适配API等各种东西,在精力上可能稍微有点接受不了。当然可能是我个人要求太多,能力较低,无法正确适配APIMemos抛开API不谈,用起来还是很舒服的,功能强大,同时功能也很完善,是付费产品flomo的完美自托管替代品,希望作者越做越好!

参考链接

站外引用 · 引用站外地址,不保证站点的可用性和安全性自定义站点nginx-confSafeLine-雷池-不让黑客越过半步 站外引用 · 引用站外地址,不保证站点的可用性和安全性缤纷云OSS质量变换质量变换可以对处理后的图片输出时做质量压缩,以尽可能节省传输流量。 站外引用 · 引用站外地址,不保证站点的可用性和安全性让Meting API解锁音乐开发新可能爱吃猫的鱼BLOG 站外引用 · 引用站外地址,不保证站点的可用性和安全性雷池WAF社区版安装+Nginx配置修改指南FloatSheep’s Blog

每日一图

图片来自哲风壁纸

小爷是魔,那又如何

Previous
Cloudflare/Vercel项目推荐(4)
Next
Certimate--自动化申请并部署证书到所有平台