在 Halo 博客中使用自定义页面接入「友链自助提交插件(link-submit)」API

31

很多 Halo 站长希望访客能自主提交友链申请,并通过后台审核后自动发布到友链列表。但很多主题没有适配这个功能,或者设计上没有入口按钮。

本文将教你如何使用 Halo 的自定义页面功能,结合开源插件 友链自助提交(link-submit),来实现一个 独立、可复用的前端友链提交表单,不依赖主题源码修改。

📌 此方案适用于所有支持自定义页面的 Halo 博客版本,结合插件 link-submit 的 REST API 实现。

🧠 背景简介:什么是 “友链自助提交插件”

插件仓库地址:
👉 https://github.com/chengzhongxue/link-submit

这个插件提供:

  • 插件端友链自助提交的 UI(可自动弹出)
  • 后端 API 接口,可通过代码自定义前端提交
  • 后台管理审核流程
  • 分组、修改、重复校验等功能

我们并不是用插件自动提供的弹窗,而是在自定义页面中自己写一个提交表单,通过 API 直接提交数据。这样更灵活、更易定制。

📦 所需条件

  1. 安装并启用 Halo 博客
  2. 已安装并启用插件:友链自助提交(link-submit)
    • 安装方式可以通过 Halo 应用市场或手动上传 Plugin。
  3. Halo 支持 自定义页面(Custom Page)
  4. 你需要知道插件的 REST API 路径(稍后说明)

🛠 API 端点概览

友链自助提交插件在后台暴露了几个关键 API 端点(版本可能更新,请以插件版本为准):

功能 API 路径
获取友链分组 /apis/anonymous.link.submit.kunkunyu.com/v1alpha1/linkgroups
提交友链申请 /apis/anonymous.link.submit.kunkunyu.com/v1alpha1/linksubmits/-/submit

以上为插件提供的匿名访问 API,可直接在前端发起请求,无需登录态。
我们将在自定义页面中调用这两个 API 注册分组列表并提交数据。

✨ 页面实现思路

整体流程如下:

🎯 页面加载 → 请求分组 API → 填充表单下拉分组
🧾 用户填写其他字段 → 点击提交按钮
📤 表单序列化成 JSON → 调用提交 API
🪄 根据返回结果显示成功 / 失败提示

🧩 自定义页面代码(可直接复制)

建议直接在 Halo 后台新建一个自定义 HTML 页面,再将以下代码复制进去:

<div id="link-submit-wrapper"><style>
    /* 样式隔离 */
    #link-submit-wrapper {
      max-width: 800px;
      margin: 20px auto;
      padding: 25px;
      background: #fff;
      border-radius: 12px;
      box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
      color: #333;
      font-family: sans-serif;
    }

    #link-submit-wrapper .form-title {
      font-size: 1.25rem;
      font-weight: 600;
      margin-bottom: 20px;
      border-bottom: 1px solid #eee;
      padding-bottom: 10px;
    }

    #link-submit-wrapper .form-grid {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 15px;
    }

    #link-submit-wrapper .full-row {
      grid-column: span 2;
    }

    #link-submit-wrapper .form-group {
      display: flex;
      flex-direction: column;
    }

    #link-submit-wrapper label {
      font-size: 14px;
      margin-bottom: 6px;
      font-weight: 500;
      color: #555;
    }

    /* 修复下拉框选中无变化的问题 */
    #link-submit-wrapper select,
    #link-submit-wrapper input,
    #link-submit-wrapper textarea {
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 6px;
      font-size: 14px;
      background-color: #fff !important;
      color: #333 !important;
      outline: none;
    }

    #link-submit-wrapper select {
      appearance: auto !important;
      -webkit-appearance: menulist !important;
      cursor: pointer;
    }

    #link-submit-wrapper textarea {
      height: 80px;
      resize: vertical;
    }

    #link-submit-wrapper .footer-row {
      display: flex;
      justify-content: space-between;
      align-items: flex-end;
      margin-top: 10px;
    }

    #link-submit-wrapper .hint {
      font-size: 12px;
      color: #999;
      margin-top: 4px;
    }

    #link-submit-wrapper .btn-submit {
      background: #1a1c22;
      color: #fff;
      padding: 10px 30px;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      font-weight: bold;
    }

    #link-submit-wrapper .btn-submit:disabled {
      background: #666;
      cursor: not-allowed;
    }

    @media (max-width: 600px) {
      #link-submit-wrapper .form-grid {
        grid-template-columns: 1fr;
      }

      #link-submit-wrapper .full-row {
        grid-column: span 1;
      }
    }
  </style>

  <div class="form-title">友链提交</div>

  <form id="linkSubmitForm">
    <div class="form-grid">
      <div class="form-group">
        <label>类型</label>
        <select name="type" required="">
          <option value="add">添加</option>
          <option value="update">修改</option>
        </select>
      </div>

      <div class="form-group">
        <label>网站分组</label>
        <select name="groupName" id="groupNameSelect" required="">
          <option value="">加载中...</option>
        </select>
      </div>

      <div class="form-group">
        <label>网站名称</label>
        <input type="text" name="displayName" required="">
      </div>

      <div class="form-group">
        <label>网址</label>
        <input type="url" name="url" required="">
      </div>

      <div class="form-group">
        <label>Logo</label>
        <input type="text" name="logo">
      </div>

      <div class="form-group">
        <label>RSS</label>
        <input type="text" name="rssUrl">
      </div>

      <div class="form-group full-row">
        <label>描述</label>
        <textarea name="description"></textarea>
      </div>

      <div class="form-group full-row">
        <div class="footer-row">
          <div style="flex: 1; margin-right: 20px;">
            <label>联系邮箱</label>
            <input type="email" name="email" required="" style="width: 100%;">
            <div class="hint">用于接收审核结果通知</div>
          </div>

          <button type="submit" class="btn-submit" id="btnSubmit">
            提交申请
          </button>
        </div>
      </div>
    </div></form>

  <script>
    (function () {
      const form = document.getElementById('linkSubmitForm');
      const groupSelect = document.getElementById('groupNameSelect');
      const typeSelect = form.querySelector('select[name="type"]');
      const btnSubmit = document.getElementById('btnSubmit');

      // 加载分组
      async function loadGroups() {
        try {
          const response = await fetch(
            '/apis/anonymous.link.submit.kunkunyu.com/v1alpha1/linkgroups'
          );
          const items = await response.json();
          groupSelect.innerHTML = '';

          if (Array.isArray(items) && items.length > 0) {
            items.forEach((item) => {
              const opt = document.createElement('option');
              opt.value = item.groupName;
              opt.textContent = item.displayName;
              groupSelect.appendChild(opt);
            });
          } else {
            groupSelect.innerHTML = '<option value="">暂无分组</option>';
          }
        } catch (e) {
          groupSelect.innerHTML = '<option value="">接口异常</option>';
        }
      }

      // 提交表单
      form.addEventListener('submit', async (e) => {
        e.preventDefault();
        btnSubmit.disabled = true;
        btnSubmit.innerText = '提交中...';

        const formData = new FormData(form);
        const payload = Object.fromEntries(formData.entries());

        try {
          const res = await fetch(
            '/apis/anonymous.link.submit.kunkunyu.com/v1alpha1/linksubmits/-/submit',
            {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
              },
              body: JSON.stringify(payload),
            }
          );

          if (res.ok) {
            alert('提交成功!请耐心等待后台审核。');
            form.reset();
          } else {
            const err = await res.json();
            alert('提交失败:' + (err.detail || '未知异常'));
          }
        } catch (err) {
          alert('网络连接错误');
        } finally {
          btnSubmit.disabled = false;
          btnSubmit.innerText = '提交申请';
        }
      });

      loadGroups();
    })();
  </script></div>