拖延了这么久终于开始写blog了,感觉是很好的梳理思路的方式。 与其说是blog,可能就是当笔记本用了...
HTMX
本来是想用WordPress或者Jekyll套个模板结束了,但感觉没意思,正好最近(?)HTMX的hype很大,玩了一下,发现确实不错。
先前看油管上不少博主炒作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 Better和Worse is Better is Worse自己反对自己,并都交给学生看,看完后告诉学生都是自己写的(培养思考能力..?)这次也一样在官网自己编写文章htmx sucks。
油管上针对HTMX的炒作大多数感觉来自于痛恨诸如React的解决方法的复杂性同时不得不用,或者说HTMX提供了一个介于完全不是响应式 (Plain HTML) 和 你可以做任何事(React)之间的方案。
搭建
网站的分为几个部分
-
静态HTML页面:通过解析编写好的markdown文件及其在头部添加的元信息,比如这个博客中的
--- title: 关于博客 date: 2025-8-17 author: Ethan tags: - 记录 - HTMX - 个人项目 --- ## 关于博客 拖延了这么久终于开始写blog了,感觉是很好的梳理思路的方式。 可能最暂时就当记事本用了。 ## HTMX生成html,以gin FS模式挂载
-
tags以及时间数据:内存存储,tags通过倒排索引,时间通过排序存储,方便分页和查找
-
搜索:为了响应性,使用了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
}
未来功能
- 搜索时标题不在里面,可增加标题字段&给高权重
支持图片显示支持文章结构展示&跳转
图片测试
