diff --git a/internal/service/post_service.go b/internal/service/post_service.go
index f4a52f4..9edf841 100644
--- a/internal/service/post_service.go
+++ b/internal/service/post_service.go
@@ -2,6 +2,7 @@ package service
import (
"fmt"
+ "regexp"
"metazone.cc/metalab/internal/common"
"metazone.cc/metalab/internal/config"
@@ -12,11 +13,13 @@ import (
// sanitizePolicy Tiptap 输出 HTML 的消毒策略
// 前端 WYSIWYG 编辑器输出 HTML,由 bluemonday UGC 策略消毒
-// 显式放行 code/pre 的 class 属性以保留代码块样式
-// 放行 img 的 src/alt 属性以保留图片
+// 放行 code/pre 的 class/data-language 属性以保留代码高亮和语言标签
+// 放行 span 的 class 属性以保留 highlight.js 的 hljs-* 高亮
+// 放行 img 的 src/alt/title 属性以保留图片
var sanitizePolicy = func() *bluemonday.Policy {
p := bluemonday.UGCPolicy()
- p.AllowAttrs("class").OnElements("code", "pre")
+ p.AllowAttrs("class", "data-language").OnElements("code", "pre")
+ p.AllowAttrs("class").Matching(regexp.MustCompile("^hljs-")).OnElements("span")
p.AllowAttrs("src", "alt", "title").OnElements("img")
return p
}()
diff --git a/templates/MetaLab-2026/html/posts/new.html b/templates/MetaLab-2026/html/posts/new.html
index 0bae38e..c32cba0 100644
--- a/templates/MetaLab-2026/html/posts/new.html
+++ b/templates/MetaLab-2026/html/posts/new.html
@@ -35,7 +35,7 @@
/* 引用 */
.tiptap blockquote {
- border-left: 3px solid #e5e7eb;
+ border-left: 3px solid #3498db;
padding-left: 1em;
margin: 0.5em 0;
color: #6b7280;
@@ -43,16 +43,17 @@
/* 代码块 */
.tiptap pre {
- background: #1f2937;
- color: #f3f4f6;
+ background: #1a2332;
+ color: #e2e8f0;
padding: 12px 16px;
border-radius: 6px;
font-size: 13px;
line-height: 1.5;
overflow-x: auto;
margin: 0.5em 0;
+ position: relative;
}
-.tiptap pre code { background: transparent; padding: 0; color: inherit; font-size: inherit; }
+.tiptap pre code { background: transparent; padding: 0; color: inherit; font-size: inherit; white-space: pre; display: block; }
.tiptap code {
background: #f3f4f6;
padding: 2px 5px;
@@ -60,6 +61,59 @@
font-size: 0.9em;
font-family: 'SF Mono', 'Menlo', 'Monaco', monospace;
}
+/* 代码块语言标签 */
+.tiptap pre .code-lang-label {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ padding: 4px 16px;
+ font-size: 11px;
+ font-weight: 600;
+ color: #94a3b8;
+ background: #2c3e50;
+ border-radius: 6px 6px 0 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
+ pointer-events: none;
+ z-index: 1;
+}
+
+/* highlight.js 语法高亮主题 — 基于 One Dark,适配 MetaLab 深蓝背景 */
+.tiptap pre code .hljs-keyword,
+.tiptap pre code .hljs-selector-tag,
+.tiptap pre code .hljs-literal,
+.tiptap pre code .hljs-section,
+.tiptap pre code .hljs-link { color: #c792ea; }
+.tiptap pre code .hljs-string,
+.tiptap pre code .hljs-title,
+.tiptap pre code .hljs-name,
+.tiptap pre code .hljs-type,
+.tiptap pre code .hljs-attribute,
+.tiptap pre code .hljs-symbol,
+.tiptap pre code .hljs-bullet,
+.tiptap pre code .hljs-addition,
+.tiptap pre code .hljs-variable,
+.tiptap pre code .hljs-template-tag,
+.tiptap pre code .hljs-template-variable { color: #c3e88d; }
+.tiptap pre code .hljs-comment,
+.tiptap pre code .hljs-quote,
+.tiptap pre code .hljs-deletion,
+.tiptap pre code .hljs-meta { color: #676e95; }
+.tiptap pre code .hljs-number,
+.tiptap pre code .hljs-regexp,
+.tiptap pre code .hljs-built_in { color: #f78c6c; }
+.tiptap pre code .hljs-function .hljs-title,
+.tiptap pre code .hljs-doctag,
+.tiptap pre code .hljs-formula { color: #82aaff; }
+.tiptap pre code .hljs-tag,
+.tiptap pre code .hljs-subst { color: #f07178; }
+.tiptap pre code .hljs-emphasis { font-style: italic; }
+.tiptap pre code .hljs-strong { font-weight: bold; }
+.tiptap pre code .hljs-attr,
+.tiptap pre code .hljs-selector-attr,
+.tiptap pre code .hljs-selector-class,
+.tiptap pre code .hljs-selector-id,
+.tiptap pre code .hljs-selector-pseudo { color: #ffcb6b; }
/* 水平线 */
.tiptap hr { border: none; border-top: 1px solid #e5e7eb; margin: 1em 0; }
@@ -68,7 +122,7 @@
.tiptap img { max-width: 100%; height: auto; border-radius: 6px; }
/* 链接 */
-.tiptap a { color: #6366f1; text-decoration: underline; }
+.tiptap a { color: #3498db; text-decoration: underline; }
/* Placeholder */
.tiptap p.is-editor-empty:first-child::before {
@@ -104,7 +158,21 @@
transition: background 0.15s;
}
.editor-toolbar button:hover { background: #e5e7eb; }
-.editor-toolbar button.is-active { background: #1f2937; color: #fff; }
+.editor-toolbar button.is-active { background: #1a2332; color: #fff; }
+.code-lang-select {
+ height: 30px;
+ padding: 0 6px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ background: transparent;
+ color: #374151;
+ font-size: 12px;
+ cursor: pointer;
+ font-family: inherit;
+ outline: none;
+}
+.code-lang-select:hover { border-color: #e5e7eb; }
+.code-lang-select:focus { border-color: #3498db; }
.editor-toolbar .toolbar-divider {
width: 1px;
background: #e5e7eb;
@@ -143,6 +211,35 @@
+
@@ -170,6 +267,19 @@
{{template "layout/footer.html" .}}
+
diff --git a/templates/MetaLab-2026/html/posts/show.html b/templates/MetaLab-2026/html/posts/show.html
index ed9c7d4..248a427 100644
--- a/templates/MetaLab-2026/html/posts/show.html
+++ b/templates/MetaLab-2026/html/posts/show.html
@@ -47,6 +47,20 @@