关于博客(或者只是笔记的替代..?)

发布于 作者: Ethan

拖延了这么久终于开始写blog了,感觉是很好的梳理思路的方式。 与其说是blog,可能就是当笔记本用了...

HTMX

本来是想用WordPress或者Jekyll套个模板结束了,但感觉没意思,正好最近(?)HTMX的hype很大,玩了一下,发现确实不错。

HTMX

先前看油管上不少博主炒作HTMX,看了下文档,简单来说就是一个极小(~3500行)的js文件,用来增强HTML,让元素有能力直接发送AJAX&局部更新,有一种时光倒流的美:

<input type="text" name="q"
    hx-get="/trigger_delay"
    hx-trigger="keyup delay:500ms changed"
    hx-target="#search-results"
    placeholder="Search...">
<div id="search-results"></div>

官网示例:输入停止500ms & 内容有变化后 发送GET到给定端点并将结果更新到id = search results的div

作者Carson Gross很有意思,极其反对复杂性,著名文章The Grug Brained Developer(很值得一读),偶像Rob Pike,写过两篇文章Worse Is BetterWorse is Better is Worse自己反对自己,并都交给学生看,看完后告诉学生都是自己写的(培养思考能力..?)这次也一样在官网自己编写文章htmx sucks

油管上针对HTMX的炒作大多数感觉来自于痛恨诸如React的解决方法的复杂性同时不得不用,或者说HTMX提供了一个介于完全不是响应式 (Plain HTML) 和 你可以做任何事(React)之间的方案。

搭建

网站的分为几个部分

  1. 静态HTML页面:通过解析编写好的markdown文件及其在头部添加的元信息,比如这个博客中的

    ---
    title: 关于博客
    date: 2025-8-17
    author: Ethan
    tags:
    
    - 记录
    - HTMX
    - 个人项目
    ---
    
    ## 关于博客
    
    拖延了这么久终于开始写blog了,感觉是很好的梳理思路的方式。
    可能最暂时就当记事本用了。
    
    ## HTMX
    

    生成html,以gin FS模式挂载

  2. tags以及时间数据:内存存储,tags通过倒排索引,时间通过排序存储,方便分页和查找

  3. 搜索:为了响应性,使用了HTMX

响应式搜索

搜索表单会向后台端点发post请求

<div class="bg-dark/60 backdrop-blur-md rounded-full p-3 shadow-lg">
    <form hx-post="/submit/search" hx-vals='{"page":"1"}' class="relative" hx-target="#main-blogs">
     <input
      type="text"
      name="query"
      placeholder="搜索文章..."
      class="w-full bg-transparent text-white placeholder-white/60 rounded-full px-6 py-3 pl-12 focus:outline-none focus:ring-2 focus:ring-primary transition-all duration-300"
     />
     <button
      type="submit"
      class="absolute left-4 top-1/2 transform -translate-y-1/2 text-white/70 hover:text-white transition-colors duration-300"
     >
      <i class="fa fa-search text-lg"></i>
     </button>
    </form>
   </div>

hx-vals给表单增加自定义字段 hx-target为目标元素替换选择器 还可以增加hx-swap用来指定替换位置(innerHTML/outerHTML等),这里默认是innerHTML

端点默认是分页的,如果搜索框为空则返回默认主页,否则返回搜索&分页后的结果 结果通过go templ模板引擎自动响应HTTP:

blogs, isBegin, isEnd, size, err := search.GetInstance().QueryByPage(query, pageNum, 10)
 if err != nil {
  blogs, isBegin, isEnd := blogstorage.GetInstance().GetBlogsByPage(1, 10)
  // 默认响应
  templ.Handler(web.MainBlogs(blogs, isBegin, isEnd, 1)).ServeHTTP(c.Writer, c.Request)
  return
 }
 //响应搜索后结果
templ.Handler(web.SearchBlogs(blogs, isBegin, isEnd, pageNum, query, size)).ServeHTTP(c.Writer, c.Request)

Templ

非常好用的go模板引擎,可以编写正常go文件,内嵌html,然后通过templ generate自动生成go文件:

//content.templ
templ Content(title string, date string, author string, content string, tags []string, thisTags []string) {
    // 引入base,下方是base的children
 @Base() {
    // 引入导航栏
  @components.Nav()
  <header class="px-4 bg-white">
   <div class="max-w-4xl mx-auto blog-content">
    <div class="mb-6 pt-6">
     for _, thisTag := range thisTags {
      <a
       href={ utils.TagToURL(thisTag) }
       class="inline-block px-3 py-1 bg-secondary/10 text-secondary rounded-full text-sm hover:bg-secondary hover:text-white transition-colors duration-300"
      >
      // 普通字段
       { thisTag }
      // 复杂表达式用 {{ ... }}
      </a>
     }
    </div>
    //...
 }}

生成后通过Render方法解析:

var rbuf strings.Builder
web.Content(ef.meta.Title, ef.meta.Date, ef.meta.Author, content, ts.GetAllTags(), ef.meta.Tags).Render(context.Background(), &rbuf)
extractedFiles[i].content = rbuf.String()

或者如上文可以直接响应HTTP

搜索

采用的是https://github.com/blevesearch/bleve 貌似默认是TF-IDF,支持多语言分词

func (s *SearchStorage) QueryByPage(q string, page int, pageSize int) ([]*types.Blog, bool, bool, int, error) {
 query := bleve.NewMatchQuery(q)
 // 按照content查询
 query.SetField("content")
 searchReq := bleve.NewSearchRequest(query)
 searchRes, err := s.index.Search(searchReq)
 if err != nil {
  return nil, false, false, 0, fmt.Errorf("failed to query by page: %v", err)
 }

 bs := make([]*types.Blog, 0, len(searchRes.Hits))
 for _, hit := range searchRes.Hits {
    //处理命中
  doc, err := s.index.Document(hit.ID)
  if err != nil || doc == nil {
   return nil, false, false, 0, fmt.Errorf("doc %s not found: %v", hit.ID, err)
  }

  // 从blog读取JSON并反序列化
  // 存储序列化JSON是因为如果存储是对象,存入时会将对象的字段递归全解出来,比较难处理(或者我看的不仔细...)
  var blogJSON string
  for _, field := range doc.Fields {
   if field.Name() == "blog" {
    blogJSON = string(field.Value())
    break
   }
  }

  if blogJSON == "" {
   return nil, false, false, 0, fmt.Errorf("doc %s has no blog field", hit.ID)
  }

  // 反序列化
  var blog types.Blog
  if err := json.Unmarshal([]byte(blogJSON), &blog); err != nil {
   return nil, false, false, 0, fmt.Errorf("failed to unmarshal blog: %v", err)
  }
  bs = append(bs, &blog)
 }

 // 分页处理
 res := utils.Paginate(bs, page, pageSize)
 return res.Data, !res.HasPrev, !res.HasNext, len(searchRes.Hits), nil
}

未来功能

  1. 搜索时标题不在里面,可增加标题字段&给高权重
  2. 支持图片显示
  3. 支持文章结构展示&跳转

图片测试

test image