Author SHA1 Message Date
dsh c5218f35a1 fix: cache-bust static asset URLs (?v=2)
Browsers cached the pre-fix 404 responses for /static/js/markdown.js and
/static/css/markdown.css (responses carry Cache-Control max-age=14400),
so users kept seeing broken pages after redeploy. A versioned query
string forces clients to refetch the assets; bump ?v= whenever the
static files change.
2026-08-18 06:25:01 -04:00
dsh bd3a875403 fix: embed static assets into the binary and never render blank content
The merged markdown-rendering change served /static from a disk directory,
but the live deployment only replaced the binary and templates, so
/static/js/markdown.js and /static/css/markdown.css 404'd and every
article/comment rendered empty (BlogMD was undefined and threw).

- main.go: serve /static from an embedded FS (go:embed) — deployments
  now only need to replace the executable; no static dir to copy.
- install_linux.sh: drop the static directory copy (no longer needed).
- article.html / comment_list.html: if BlogMD is unavailable, fall back
  to plain (HTML-escaped) text for the article body, comment bodies and
  the comment preview, so content is never blank.
- README: document the static/ directory.

Verified locally: /static/* serve 200 even with the disk directory
removed; normal render path (25 checks) and the fallback path both pass.
2026-08-18 06:18:08 -04:00
kevin 5f21c73044 Merge pull request '完善前端 Markdown 渲染:排版样式、代码高亮、标题锚点、图片灯箱、复制按钮' (#1) from dsh/go_blog:feat/markdown-rendering into main
Reviewed-on: kevin/go_blog#1
2026-08-18 06:12:12 -04:00
6 changed files with 54 additions and 14 deletions
+3
View File
@@ -120,6 +120,9 @@ go_blog/
│ └── upload_validator.go # 上传文件校验
├── i18n/
│ └── i18n.go # 中英文翻译映射 + Accept-Language 检测
├── static/
│ ├── css/markdown.css # Markdown 排版样式(go:embed 编入二进制)
│ └── js/markdown.js # 前端 Markdown 渲染器(BlogMD
├── templates/
│ ├── layouts/base.html # 公共布局(导航栏 + 头像下拉菜单 + 页脚)
│ ├── pages/
+1 -5
View File
@@ -40,15 +40,11 @@ install -m 0755 -o root -g root "${SCRIPT_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/$
rm -rf "${INSTALL_DIR}/templates"
cp -a "${SCRIPT_DIR}/templates" "${INSTALL_DIR}/templates"
chown -R root:root "${INSTALL_DIR}/templates"
rm -rf "${INSTALL_DIR}/static"
cp -a "${SCRIPT_DIR}/static" "${INSTALL_DIR}/static"
chown -R root:root "${INSTALL_DIR}/static"
# static 资源已通过 go:embed 编入二进制,无需单独拷贝
chown "${SERVICE_USER}:${SERVICE_USER}" "${INSTALL_DIR}"
chmod 0755 "${INSTALL_DIR}"
find "${INSTALL_DIR}/templates" -type d -exec chmod 0755 {} \;
find "${INSTALL_DIR}/templates" -type f -exec chmod 0644 {} \;
find "${INSTALL_DIR}/static" -type d -exec chmod 0755 {} \;
find "${INSTALL_DIR}/static" -type f -exec chmod 0644 {} \;
SOCKET_PATH="${SOCKET_DIR}/web.sock"
+16 -2
View File
@@ -1,10 +1,13 @@
package main
import (
"embed"
"flag"
"fmt"
"io/fs"
"log"
"net"
"net/http"
"os"
"github.com/gin-contrib/sessions"
@@ -17,6 +20,13 @@ import (
"go_blog/models"
)
// staticFiles embeds the static assets (Markdown CSS/JS) into the binary so a
// deployment only needs to replace the executable — no separate static
// directory has to be copied to the server.
//
//go:embed static
var staticFiles embed.FS
func main() {
// 0. Parse command-line flags.
configFlag := flag.String("config", "", "path to config file (default: OS-aware path)")
@@ -49,8 +59,12 @@ func main() {
// 6. Serve uploaded files (avatars etc.) from the storage path.
router.Static("/uploads", cfg.Path)
// 6b. Serve bundled static assets (CSS/JS for Markdown rendering).
router.Static("/static", "./static")
// 6b. Serve bundled static assets (embedded into the binary).
staticFS, err := fs.Sub(staticFiles, "static")
if err != nil {
log.Fatalf("Failed to open embedded static assets: %v", err)
}
router.StaticFS("/static", http.FS(staticFS))
// 6. Global session middleware.
router.Use(sessions.Sessions("blog_session", store))
+6 -1
View File
@@ -83,7 +83,12 @@
<script>
(function () {
document.querySelectorAll('.comment-body[data-md]').forEach(function (el) {
BlogMD.renderInto(el, el.getAttribute('data-md') || '');
var raw = el.getAttribute('data-md') || '';
if (window.BlogMD) {
BlogMD.renderInto(el, raw);
} else {
el.textContent = raw;
}
});
})();
</script>
+6 -3
View File
@@ -111,13 +111,16 @@
{{define "markdown_assets"}}
{{/* Markdown rendering assets: marked (pinned UMD build), DOMPurify,
highlight.js and the shared renderer + styles. Include on any page that
renders Markdown (articles, comments, editor previews). */}}
renders Markdown (articles, comments, editor previews).
Local static files carry a ?v= cache-buster: bump it whenever
static/js/markdown.js or static/css/markdown.css changes, otherwise
browsers may keep serving stale cached copies. */}}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.12.0/build/styles/github.min.css">
<link rel="stylesheet" href="/static/css/markdown.css">
<link rel="stylesheet" href="/static/css/markdown.css?v=2">
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.4.13/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.12.0/build/highlight.min.js"></script>
<script src="/static/js/markdown.js"></script>
<script src="/static/js/markdown.js?v=2"></script>
{{end}}
{{define "footer"}}
+22 -3
View File
@@ -144,12 +144,27 @@
<script>
// Article body: render Markdown client-side through the shared BlogMD
// pipeline (marked + DOMPurify + highlight.js, see /static/js/markdown.js).
BlogMD.renderInto(document.getElementById('articleBody'), {{.Article.Content}});
// If the renderer failed to load, fall back to plain text so the article
// is never blank.
(function () {
var body = document.getElementById('articleBody');
var content = {{.Article.Content}};
if (window.BlogMD) {
BlogMD.renderInto(body, content);
} else {
body.textContent = content;
}
})();
// Comments are rendered from the escaped data-md attribute.
document.querySelectorAll('.comment-body[data-md]').forEach(function (el) {
if (el.dataset.rendered) return;
BlogMD.renderInto(el, el.getAttribute('data-md') || '');
var raw = el.getAttribute('data-md') || '';
if (window.BlogMD) {
BlogMD.renderInto(el, raw);
} else {
el.textContent = raw;
}
el.dataset.rendered = '1';
});
@@ -191,7 +206,11 @@
var previewing = previewBox.classList.toggle('hidden') === false;
textarea.classList.toggle('hidden', previewing);
if (previewing) {
previewBox.innerHTML = BlogMD.render(textarea.value);
if (window.BlogMD) {
previewBox.innerHTML = BlogMD.render(textarea.value);
} else {
previewBox.textContent = textarea.value;
}
togglePreview.textContent = '{{index .Tr "comments_edit"}}';
} else {
togglePreview.textContent = '{{index .Tr "comments_preview"}}';