1507 字
8 分钟

为你的 Fuwari 添加 Artalk 评论系统

TIP

本喂饭级教程用的是 1Panel 使用 Docker Compose 部署 Artalk 。

有一说一,1Panel 真香。

用 Fuwari 的小伙伴可以火速来前来围观,为你的博客添加 Artalk 评论系统。

ArtalkJS
/
Artalk
Waiting for api.github.com...
00K
0K
0K
Waiting...

Q :为什么选择使用 Artalk 作为我的评论系统?

来自 Artalk :

Artalk 是一款简单易用但功能丰富的评论系统,你可以开箱即用地部署并置入任何博客、网站、Web 应用。

  • 🍃 前端 ~40KB,纯天然 Vanilla JS
  • 🍱 后端 Golang,高效轻量跨平台
  • 🐳 通过 Docker 一键部署,方便快捷
  • 🌈 开源程序,自托管,隐私至上

Artalk 功能完善,轻量高效,同时还支持缓存,并且不用登录游客也能发表评论。

萌樱觉得,确实是一个很不错的评论系统。(虽然没用过别的

安装 Artalk#

  • 打开 1Panel ➡️ 容器 ➡️ 编排 ➡️ 创建编排
  • 来源:编辑,文件夹填 Artalk ,然后在下方输入 docker-compose.yml 的内容,保存即可
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 组件#

src/components/Artalk.astro
---
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 配置即可。

WARNING

server: “你的博客地址” 末尾不能有 /

https://example.com/

✅️ https://example.com

src/config.ts
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",
},
};

修改布局#

src/pages/posts/[...slug].astro
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) : "#"}
src/content/config.ts
const postsCollection = defineCollection({
schema: z.object({
// 其他代码
category: z.string().optional().nullable().default(""),
lang: z.string().optional().default(""),
disableComments: z.boolean().optional().default(false),
// 其他代码
}),
});

修改创建文章脚本(可选)#

scripts/new-post.js
image: ''
tags: []
category: ''
draft: false
lang: ''
disableComments: false

食用方法#

WARNING

本地环境会出现 Artalk Error

接入 Artalk 之后,就可以在文章中看到评论区了。

如果需要关闭评论,把 disableComments 改为 true 就可以了。

如果修改了new-post.js,那么在创建文章的时候就会自动填入 disableComments 来控制是否显示评论区。

最后,希望大家食用愉快~

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

为你的 Fuwari 添加 Artalk 评论系统
https://bytemoe.com/posts/fuwari-add-artalk-comment-system/
作者
MoeSakuraW
发布于
2025-12-29
许可协议
CC BY-NC-SA 4.0

目录