1507 字
8 分钟
为你的 Fuwari 添加 Artalk 评论系统
TIP本喂饭级教程用的是 1Panel 使用 Docker Compose 部署 Artalk 。
有一说一,1Panel 真香。
用 Fuwari 的小伙伴可以火速来前来围观,为你的博客添加 Artalk 评论系统。
Waiting for api.github.com...
Q :为什么选择使用 Artalk 作为我的评论系统?
来自 Artalk :
Artalk 是一款简单易用但功能丰富的评论系统,你可以开箱即用地部署并置入任何博客、网站、Web 应用。
- 🍃 前端 ~40KB,纯天然 Vanilla JS
- 🍱 后端 Golang,高效轻量跨平台
- 🐳 通过 Docker 一键部署,方便快捷
- 🌈 开源程序,自托管,隐私至上
Artalk 功能完善,轻量高效,同时还支持缓存,并且不用登录游客也能发表评论。
萌樱觉得,确实是一个很不错的评论系统。(虽然没用过别的
安装 Artalk
- 打开 1Panel ➡️ 容器 ➡️ 编排 ➡️ 创建编排
- 来源:
编辑,文件夹填Artalk,然后在下方输入docker-compose.yml的内容,保存即可
version: "3.8" services: artalk: container_name: artalk image: artalk/artalk-go restart: unless-stopped build: context: ./ dockerfile: Dockerfile ports: - 23366:23366 # 端口号可以自定义,这里映射原来的端口 volumes: - ./data:/data environment: - TZ=Asia/Shanghai - ATK_LOCALE=zh-CN - ATK_SITE_DEFAULT=你的博客名称 # 记得改 - ATK_SITE_URL=你的博客地址 # 记得改 networks: - 1panel-network
networks: 1panel-network: external: true等进度条跑完,就算是安装成功了。
配置 Artalk
安装完成后,点击操作里面的终端连接到容器内部配置管理员账户。
artalk admin根据提示输入你的信息即可。
配置反代
- 网站 ➡️ 创建网站 ➡️ 选择反向代理
- 域名:填入你的 Artalk 域名
- 代理地址:
http://127.0.0.1:23366
保存后就能访问你的 Artalk 域名进行登录操作了。
需要配置 HTTPS 的话请自行配置,这里就不在赘述了。
WARNING登录之后千万不要手滑把账号删了,不然会登录进不去,别问我是怎么知道的。( ̄へ ̄)
接入 Artalk
IMPORTANT组件使用 CDN 加载 Artalk 资源,不一定适合所有人。 不想使用 CDN 的小伙伴请自行修改代码。
特性
- 是否开启文章评论
- 公共 CDN 加载资源
- 全适配 Fuwari 主题色
- 组件懒加载
创建 Artalk 组件
---interface Props { pageKey: string; pageTitle?: string; server: string; site: string; cdnCss?: string; cdnJs?: string;}
const {pageKey, pageTitle, server, site, cdnCss, cdnJs} = Astro.props;const serverOrigin = new URL(server).origin;
const cssUrl = cdnCss || `${server}/dist/Artalk.css`;const jsUrl = cdnJs || `${server}/dist/Artalk.js`;
const cssOrigin = new URL(cssUrl).origin;const jsOrigin = new URL(jsUrl).origin;const origins = [...new Set([serverOrigin, cssOrigin, jsOrigin])];---
{origins.map(origin => (<Fragment key={origin}> <link rel="preconnect" href={origin} crossorigin/> <link rel="dns-prefetch" href={origin}/></Fragment> ))}
<div id="artalk-comments" data-page-key={pageKey} data-page-title={pageTitle} data-server={server} data-site={site} data-css-url={cssUrl} data-js-url={jsUrl}></div>
<script> interface ArtalkInstance { destroy: () => void; setDarkMode: (dark: boolean) => void; }
interface ArtalkConfig { el: string; pageKey: string; pageTitle?: string; server: string; site: string; darkMode: boolean; }
declare global { interface Window { Artalk: { init: (config: ArtalkConfig) => ArtalkInstance; }; } }
const state = { artalkInstance: null as ArtalkInstance | null, themeObserver: null as MutationObserver | null, intersectionObserver: null as IntersectionObserver | null, currentDarkMode: null as boolean | null, scriptLoadPromise: null as Promise<void> | null, isInitializing: false, hasInitialized: false, };
function cleanup(): void { if (state.artalkInstance) { try { state.artalkInstance.destroy(); } catch { // 静默处理 } state.artalkInstance = null; }
state.themeObserver?.disconnect(); state.themeObserver = null;
state.intersectionObserver?.disconnect(); state.intersectionObserver = null;
state.currentDarkMode = null; state.isInitializing = false; state.hasInitialized = false; }
function loadCSS(url: string): void { if (document.getElementById('artalk-css')) return;
const link = document.createElement('link'); link.id = 'artalk-css'; link.rel = 'stylesheet'; link.href = url; link.media = 'print'; link.onload = () => { link.media = 'all'; }; document.head.appendChild(link); }
function loadJS(url: string): Promise<void> { if (state.scriptLoadPromise) return state.scriptLoadPromise; if (window.Artalk) return Promise.resolve();
state.scriptLoadPromise = new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = url; script.async = true; script.onload = () => resolve(); script.onerror = () => { state.scriptLoadPromise = null; reject(new Error('Failed to load Artalk.js')); }; document.body.appendChild(script); });
return state.scriptLoadPromise; }
function isDarkMode(): boolean { return document.documentElement.classList.contains('dark'); }
function setupThemeObserver(): void { if (state.themeObserver) return;
state.themeObserver = new MutationObserver(() => { if (!state.artalkInstance) return;
const dark = isDarkMode(); if (dark !== state.currentDarkMode) { state.currentDarkMode = dark; state.artalkInstance.setDarkMode(dark); } });
state.themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'], }); }
async function initArtalk(): Promise<void> { const container = document.getElementById('artalk-comments');
if (!container || state.hasInitialized || state.isInitializing) { return; }
state.isInitializing = true;
const {pageKey, pageTitle, server, site, cssUrl, jsUrl} = container.dataset;
if (!pageKey || !server || !site || !cssUrl || !jsUrl) { console.error('Artalk: Missing required configuration'); state.isInitializing = false; return; }
try { loadCSS(cssUrl); await loadJS(jsUrl);
if (!document.getElementById('artalk-comments')) { state.isInitializing = false; return; }
state.currentDarkMode = isDarkMode();
state.artalkInstance = window.Artalk.init({ el: '#artalk-comments', pageKey, pageTitle, server, site, darkMode: state.currentDarkMode, });
state.hasInitialized = true; setupThemeObserver(); } catch (error) { console.error('Artalk initialization failed:', error); } finally { state.isInitializing = false; } }
function setupLazyLoad(): void { const container = document.getElementById('artalk-comments');
if (!container || state.hasInitialized) return;
state.intersectionObserver?.disconnect();
state.intersectionObserver = new IntersectionObserver( (entries) => { if (entries[0]?.isIntersecting && !state.hasInitialized) { initArtalk(); state.intersectionObserver?.disconnect(); } }, {rootMargin: '400px 0px', threshold: 0} );
state.intersectionObserver.observe(container); }
document.addEventListener('astro:before-swap', cleanup); document.addEventListener('astro:page-load', setupLazyLoad);
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupLazyLoad, {once: true}); } else { setupLazyLoad(); }</script>
<style is:global> #artalk-comments { --atk-color-main: var(--primary, #3b82f6); --atk-color-bg: var(--card-bg, #ffffff); }
:root.dark #artalk-comments { --atk-color-bg: var(--card-bg, #1e1e1e); }
#artalk-comments a:not(.atk-send-btn) { color: var(--atk-color-main) !important; transition: opacity 0.2s ease; }
#artalk-comments a:not(.atk-send-btn):hover { opacity: 0.8; }
#artalk-comments .atk-send-btn { background: var(--atk-color-main) !important; transition: opacity 0.2s ease; }
#artalk-comments .atk-send-btn:hover { opacity: 0.9; }
#artalk-comments .atk-dropdown .atk-dropdown-item.active span, #artalk-comments .atk-dropdown .atk-dropdown-item:hover, #artalk-comments .atk-dropdown .atk-dropdown-item:hover span { color: var(--atk-color-main) !important; }
#artalk-comments .atk-text { color: var(--atk-color-main) !important; }
#artalk-comments .atk-comment-count .atk-text, #artalk-comments .atk-text .atk-comment-count-num { color: inherit !important; }
#artalk-comments .atk-list-footer { margin-bottom: 1rem; }
#artalk-comments .atk-main-editor, #artalk-comments .atk-textarea { background-color: var(--atk-color-bg) !important; }</style>修改配置文件
IMPORTANT使用 jsDelivr 公共 CDN 加载 Artalk 资源。
如果想使用其他 CDN ,修改 CDN 配置即可。
WARNINGserver: “你的博客地址” 末尾不能有
/。
export const expressiveCodeConfig: ExpressiveCodeConfig = { // Note: Some styles (such as background color) are being overridden, see the astro.config.mjs file. // Please select a dark theme, as this blog theme currently only supports dark background color theme: "github-dark",};
export const artalkConfig = { server: "你的博客地址", site: "你的博客名称", // CDN 配置 cdn: { css: "https://cdn.jsdelivr.net/npm/artalk/dist/Artalk.css", js: "https://cdn.jsdelivr.net/npm/artalk/dist/Artalk.js", },};修改布局
import ImageWrapper from "../../components/misc/ImageWrapper.astro";import PostMetadata from "../../components/PostMeta.astro";import { profileConfig, siteConfig } from "../../config";import { formatDateToYYYYMMDD } from "../../utils/date-utils";import Artalk from "@components/Artalk.astro";
<!-- 其他代码-->
{licenseConfig.enable && <License title={entry.data.title} slug={entry.slug} pubDate={entry.data.published} class="mb-6 rounded-xl license-container onload-animation"></License>}
</div> </div>
<!-- 添加评论区,只在启用评论时渲染--> {!entry.data.disableComments && ( <Artalk pageKey={Astro.url.pathname} pageTitle={entry.data.title} server={artalkConfig.server} site={artalkConfig.site} /> )}
<div class="flex flex-col md:flex-row justify-between mb-4 gap-4 overflow-hidden w-full"> <a href={entry.data.nextSlug ? getPostUrlBySlug(entry.data.nextSlug) : "#"}const postsCollection = defineCollection({ schema: z.object({
// 其他代码
category: z.string().optional().nullable().default(""), lang: z.string().optional().default(""), disableComments: z.boolean().optional().default(false),
// 其他代码 }),});修改创建文章脚本(可选)
image: ''tags: []category: ''draft: falselang: ''disableComments: false食用方法
WARNING本地环境会出现
Artalk Error。
接入 Artalk 之后,就可以在文章中看到评论区了。
如果需要关闭评论,把 disableComments 改为 true 就可以了。
如果修改了new-post.js,那么在创建文章的时候就会自动填入 disableComments 来控制是否显示评论区。
最后,希望大家食用愉快~
为你的 Fuwari 添加 Artalk 评论系统
https://bytemoe.com/posts/fuwari-add-artalk-comment-system/

