Init v3.2

main
JiXieShi 2024-03-21 15:25:12 +08:00
commit 6e70a1d4df
49 changed files with 2115 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
.DS_Store
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
web
images
cache
go_lib

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 xusenlin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

112
README.md Normal file
View File

@ -0,0 +1,112 @@
# Blog
> Blog 是基于 go 语言开发的,不依赖第三方 go 库,适合用来学习和展示 markdown 文档的精美博客。
:chestnut: [JiXieShi](http://www.starss.cc/) (个人博客,正在使用)
---
## 项目背景
博客从最早的静态 Html+Css 主页到 Wordpress 、Typecho 、Hexo 、Hugo 一路尝试过来,虽然他们都是很优秀的开源项目,但是没有一款符合我内心真正的诉求。
他们有的要么太重了要么部署不方便要么对存储的文章不友好我就放弃了之前在Typecho上的文章同时调整分类也不那么灵活有的迁移也不方便。
自己太懒,不想自己做文章备份,不想安装数据库,快速响应也是必须的,所以才有了用 GOLANG 来写一个简单博客的想法。
当然自己的这个项目并不能和这些优秀的开源项目相比较它只是符合我自己的诉求顺便用来学习GOLANG的项目。(写文章也能增加github的活跃度逃 :))
## Blog 在做什么
简单来说Blog 会使用 git 将你的 .md 文档仓库克隆到当前目录下, 然后并行加载和解析文章关键信息并生成内容,呈现一个博客站点。
当你向你的 git 仓库提交文章的时候,你可以设置 webHooks 来自动通知 Blog 更新同步文章,或者可以在仪表盘里手动更新文章。
## 安装
你可以克隆当前仓库,然后在 JSON 文件里面配置好你文档的 Git 地址和 WebHook 秘钥,编译源代码并放入后台运行。
## 使用
- 配置好 config.json 文件,详细的配置说明文件在当前仓库根目录的《配置说明.md》
- 新建一个 git 仓库,用来存放你的文章,但是对你的目录结构有一些要求
如下:
```
|-- assets //博客静态文件,存放一些图片资源,方便显示到文档里
|-- content
| |-- GOLANG //分类目录
| |-- GOLANG基础 // 子分类目录,但是在页面上不会产生分类目录
| |--- GOLANG基础语法.md
| |-- 其他分类
| |--- xxx.md
|-- extra_nav // Blog只会有 Blog Categories 两个导航,其他导航可以放到这个目录下,比如自己的简历,作品之类的
```
content目录下的一级目录代表一个分类如果一级目录下有子级目录也不会产生新的分类,子级目录的文档也会属于一级目录的分类。
- 每一篇文章的开头可以配置一些文章的属性,具体可以看 配置说明.md
如下:
```
```json
{
"date":"2019.01.02 14:33"//最少需要
"tags": ["BLOG""其它tag"]//可以不填不过最好添加一些tag后面可以做一些好玩的东西。
"title": "文章的标题,一般不用填写,默认使用文件名"
"description": "文章描述不填写自动取正文200个字可以在app.json中配置"
"author": "JiXieShi" //文章作者,可以不用填写,现在也没有使用到
"musicId":"网易云歌单ID" //文章的配歌
}
```
```
我会根据关键字```json来解析不用担心这个会显示到文章内容里面我在解析的时候就将它去掉了。
如果不再文章前面添加这一段json也是可以的不过我不建议这么做因为这样就和V1.0版本没什么区别了,没有了文章属性,排序都是个问题。
最后mac markdown 文章编辑器推荐 Typora。
> 提示:```json 前面不能有其它字符。
- 切换主题色和搜索文章在仪表盘里,访问/admin 可以查看仪表盘,
如果不想让别人知道你的仪表盘地址可以自己配置dashboard字段。
## TODO
- [x] 1.移动端更好的适配
- [x] 2.根目录可以添加其他文件生成导航
- [x] 3.仪表盘支持切换主题色
- [x] 4.仪表盘支持搜索
- [x] 5.tags的展示
- [x] 6.仪表盘添加手动更新文章功能
- [x] 7.添加webHook支持(去掉自动更新)
- [x] 8.添加评论支持(在配置里开启所有评论都会储存在仓库的Issues)
- [x] 9.支持网易云音乐
- [x] 10.支持自定义切换主题
> 后续尽善尽美之后,我可能会提供其他漂亮的主题皮肤,也欢迎大家参与进来。
## 特性
1. 响应迅速 ---没有什么依赖得益于GOLANG的运行速度部署在阿里云的博客平均响应在50毫秒内。
2. 迁移方便 ---GOLANG交叉编译可以方便的发布二进制文件到不同的操作系统执行二进制文件并克隆博客文件即可运行你的博客。
3. 小巧精美 ---非常简单的代码方便学习和改造即使有一天你厌倦Blog你的文章也能很好的迁移和阅读。
4. 分类调整 ---随时调整你的文章分类,你可以把一个文件夹里所有的东西移动到其他分类里的某个地方,不管有多少层级,分类发生了什么翻天覆地的变化,下一次更新就能展示。
## 更新日志
### V3.2
* 加入Tag搜索和展示
### V3.1
* 去掉标题的.MD后缀
* 网易云音乐改为左下悬浮同时由音乐id改为歌单id
* 增加主题配置项(ps:虽然只有一个简单主题)
### V3.0
* 完全重构,添加了短链,美化了 URL
* 自动完成克隆工作
* 添加了网易云音乐功能
* 文件自动生成导航(导航排序通过时间可自定义)
## 感谢
## License
MIT © Richard McRichface

26
blog.go Normal file
View File

@ -0,0 +1,26 @@
package main
import (
"blog/config"
"blog/models"
"blog/routes"
"fmt"
"net/http"
"strconv"
)
func init() {
models.CompiledContent() //克隆或者更新文章、递归生成文章、导航、短链 Map、加载模板
}
func main() {
routes.InitRoute()
fmt.Printf("Versionv%v \n", config.Cfg.Version)
fmt.Printf("ListenAndServe On Port %v \n", config.Cfg.Port)
fmt.Printf("Dashboard On Path %v \n", config.Cfg.Dashboard)
fmt.Printf("UpdateArticle's GitHookUrl: %v Secret: %v \n", config.Cfg.GitHookUrl, config.Cfg.WebHookSecret)
if err := http.ListenAndServe(":"+strconv.Itoa(config.Cfg.Port), nil); err != nil {
fmt.Println("ServeErr:", err)
}
}

19
config.json Normal file
View File

@ -0,0 +1,19 @@
{
"port": 8080,
"pageSize": 3,
"descriptionLen": 200,
"author": "JiXieShi",
"icp": "",
"webHookSecret": "jixieshi",
"categoryDisplayQuantity": 6,
"utterancesRepo": "jixishi/blog_docs",
"timeLayout": "2006.01.02 15:04",
"siteName": "JiXieShi's Blog",
"htmlKeywords": "forest blog,Golang,ARM,BE6,前端,硬件",
"htmlDescription": "JiXieShi's Personal blog",
"documentGitUrl": "https://github.com/jixishi/blog_docs.git",
"themePath": "/blog",
"themeColor": "#2196f3",
"dashboard": "adminjxs",
"themeOption": ["#673ab7","#f44336","#9c27b0","#2196f3","#607d8b","#795548"]
}

109
config/main.go Normal file
View File

@ -0,0 +1,109 @@
package config
import (
"blog/utils"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
)
type Config struct {
userConfig
systemConfig
}
//
var Cfg Config
func init() {
var err error
Cfg.CurrentDir, err = os.Getwd()
if err != nil {
panic(err)
}
configFile, err := ioutil.ReadFile(Cfg.CurrentDir + "/config.json")
if err != nil {
panic(err)
}
err = json.Unmarshal(configFile, &Cfg)
if err != nil {
panic(err)
}
if "" == Cfg.Dashboard ||
!strings.HasPrefix(Cfg.Dashboard, "/") {
Cfg.Dashboard = "/admin"
}
if "" == Cfg.ThemePath ||
!strings.HasPrefix(Cfg.ThemePath, "/") {
Cfg.ThemesDir = Cfg.CurrentDir + "/themes/blog" //+ Cfg.ThemePath
} else {
Cfg.ThemesDir = Cfg.CurrentDir + "/themes" + Cfg.ThemePath
}
repoName, err := utils.GetRepoName(Cfg.DocumentGitUrl)
if err != nil {
panic(err)
}
Cfg.AppName = "WebServer"
Cfg.Version = 3.2
Cfg.DocumentDir = Cfg.CurrentDir + "/" + repoName
Cfg.GitHookUrl = "/api/git_push_hook"
Cfg.AppRepository = "https://gitea.starss.cc/JiXieShi/Blog"
}
func Initial() {
if _, err := exec.LookPath("git"); err != nil {
fmt.Println("请先安装git")
panic(err)
}
if !utils.IsDir(Cfg.DocumentDir) {
fmt.Println("正在克隆文档仓库,请稍等...")
out, err := utils.RunCmdByDir(Cfg.CurrentDir, "git", "clone", Cfg.DocumentGitUrl)
if err != nil {
panic(err)
}
fmt.Println(out)
} else {
out, err := utils.RunCmdByDir(Cfg.DocumentDir, "git", "pull")
fmt.Println(out)
if err != nil {
panic(err)
}
}
if err := checkDocDirAndBindConfig(&Cfg); err != nil {
fmt.Println("文档缺少必要的目录")
panic(err)
}
imgDir := Cfg.CurrentDir + "/images"
if !utils.IsDir(imgDir) {
if os.Mkdir(imgDir, os.ModePerm) != nil {
panic("生成images目录失败")
}
}
}
func checkDocDirAndBindConfig(cfg *Config) error {
dirs := []string{"assets", "content", "extra_nav"}
for _, dir := range dirs {
absoluteDir := Cfg.DocumentDir + "/" + dir
if !utils.IsDir(absoluteDir) {
return errors.New("documents cannot lack " + absoluteDir + " dir")
}
}
cfg.DocumentAssetsDir = cfg.DocumentDir + "/assets"
cfg.DocumentContentDir = cfg.DocumentDir + "/content"
cfg.DocumentExtraNavDir = cfg.DocumentDir + "/extra_nav"
return nil
}

14
config/system.go Normal file
View File

@ -0,0 +1,14 @@
package config
type systemConfig struct {
AppName string
Version float32
CurrentDir string
ThemesDir string
GitHookUrl string
AppRepository string
DocumentDir string
DocumentAssetsDir string
DocumentContentDir string
DocumentExtraNavDir string
}

37
config/user.go Normal file
View File

@ -0,0 +1,37 @@
package config
type userConfig struct {
SiteName string `json:"siteName"`
Author string `json:"author"`
Icp string `json:"icp"`
TimeLayout string `json:"timeLayout"`
Port int `json:"port"`
WebHookSecret string `json:"webHookSecret"`
CategoryDisplayQuantity int `json:"categoryDisplayQuantity"`
UtterancesRepo string `json:"utterancesRepo"`
PageSize int `json:"pageSize"`
DescriptionLen int `json:"descriptionLen"`
DocumentGitUrl string `json:"documentGitUrl"`
HtmlKeywords string `json:"htmlKeywords"`
HtmlDescription string `json:"htmlDescription"`
ThemePath string `json:"themePath"`
ThemeColor string `json:"themeColor"`
ThemeOption []string `json:"themeOption"`
Dashboard string `json:"dashboard"`
}

25
controller/article.go Normal file
View File

@ -0,0 +1,25 @@
package controller
import (
"blog/models"
"net/http"
)
func Article(w http.ResponseWriter, r *http.Request) {
articleTemplate := models.Template.Article
if err := r.ParseForm(); err != nil {
articleTemplate.WriteError(w, err)
}
key := r.Form.Get("key")
path := models.ArticleShortUrlMap[key]
articleDetail, err := models.ReadArticleDetail(path)
if err != nil {
articleTemplate.WriteError(w, err)
}
articleTemplate.WriteData(w, models.BuildViewData("Article", articleDetail))
}

16
controller/category.go Normal file
View File

@ -0,0 +1,16 @@
package controller
import (
"blog/config"
"blog/models"
"net/http"
)
func Category(w http.ResponseWriter, r *http.Request) {
categoriesTemplate := models.Template.Categories
result := models.GroupByCategory(&models.ArticleList, config.Cfg.CategoryDisplayQuantity)
categoriesTemplate.WriteData(w, models.BuildViewData("Categories", result))
}

35
controller/dashboard.go Normal file
View File

@ -0,0 +1,35 @@
package controller
import (
"blog/config"
"blog/models"
"net/http"
"strconv"
)
func Dashboard(w http.ResponseWriter, r *http.Request) {
var dashboardMsg []string
dashboardTemplate := models.Template.Dashboard
if err := r.ParseForm(); err != nil {
dashboardTemplate.WriteError(w, err)
}
index, err := strconv.Atoi(r.Form.Get("theme"))
if err == nil && index < len(config.Cfg.ThemeOption) {
config.Cfg.ThemeColor = config.Cfg.ThemeOption[index]
dashboardMsg = append(dashboardMsg, "颜色切换成功!")
}
action := r.Form.Get("action")
if "updateArticle" == action {
models.CompiledContent()
dashboardMsg = append(dashboardMsg, "文章更新成功!")
}
dashboardTemplate.WriteData(w, models.BuildViewData("Dashboard", map[string]interface{}{
"msg": dashboardMsg,
}))
}

26
controller/extraNav.go Normal file
View File

@ -0,0 +1,26 @@
package controller
import (
"blog/models"
"net/http"
)
func ExtraNav(w http.ResponseWriter, r *http.Request) {
extraNavTemplate := models.Template.ExtraNav
if err := r.ParseForm(); err != nil {
extraNavTemplate.WriteError(w, err)
}
name := r.Form.Get("name")
for _, nav := range models.Navigation {
if nav.Title == name {
articleDetail, err := models.ReadArticleDetail(nav.Path)
if err != nil {
extraNavTemplate.WriteError(w, err)
}
extraNavTemplate.WriteData(w, models.BuildViewData(nav.Title, articleDetail))
return
}
}
}

35
controller/index.go Normal file
View File

@ -0,0 +1,35 @@
package controller
import (
"blog/config"
"blog/models"
"net/http"
"strconv"
)
func Index(w http.ResponseWriter, r *http.Request) {
indexTemplate := models.Template.Index
if err := r.ParseForm(); err != nil {
indexTemplate.WriteError(w, err)
}
page, err := strconv.Atoi(r.Form.Get("page"))
if err != nil {
page = 1
}
articles := models.ArticleList
search := r.Form.Get("search")
category := r.Form.Get("category")
tag := r.Form.Get("tag")
if search != "" || category != "" || tag != "" {
articles = models.ArticleSearch(&articles, search, category, tag)
}
result := models.Pagination(&articles, page, config.Cfg.PageSize)
indexTemplate.WriteData(w, models.BuildViewData("Blog", result))
}

61
controller/webhook.go Normal file
View File

@ -0,0 +1,61 @@
package controller
import (
"blog/models"
"fmt"
"net/http"
)
func GithubHook(w http.ResponseWriter, r *http.Request) {
//err := r.ParseForm()
//if err != nil {
// SedResponse(w, err.Error())
// return
//}
//
//if "" == config.Cfg.WebHookSecret || "push" != r.Header.Get("x-github-event") {
// SedResponse(w, "No Configuration WebHookSecret Or Not Pushing Events")
// log.Println("No Configuration WebHookSecret Or Not Pushing Events")
// return
//}
//
//sign := r.Header.Get("X-Hub-Signature")
//
//bodyContent, err := ioutil.ReadAll(r.Body)
//
//if err != nil {
// SedResponse(w, err.Error())
// log.Println("WebHook err:" + err.Error())
// return
//}
//
//if err = r.Body.Close(); err != nil {
// SedResponse(w, err.Error())
// log.Println("WebHook err:" + err.Error())
// return
//}
//
//mac := hmac.New(sha1.New, []byte(config.Cfg.WebHookSecret))
//mac.Write(bodyContent)
//expectedHash := "sha1=" + hex.EncodeToString(mac.Sum(nil))
//
//if sign != expectedHash {
// SedResponse(w, "WebHook err:Signature does not match")
// log.Println("WebHook err:Signature does not match")
// return
//}
SedResponse(w, "ok")
models.CompiledContent()
}
func SedResponse(w http.ResponseWriter, msg string) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_, err := w.Write([]byte(`{"msg": "` + msg + `"}`))
if err != nil {
fmt.Println(err)
}
}

4
go.mod Normal file
View File

@ -0,0 +1,4 @@
module blog
go 1.13

9
go.sum Normal file
View File

@ -0,0 +1,9 @@
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

238
models/articles.go Normal file
View File

@ -0,0 +1,238 @@
package models
import (
"blog/config"
"blog/utils"
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"sort"
"strings"
"time"
)
type Time time.Time
type Article struct {
Title string `json:"title"`
Date Time `json:"date"`
Description string `json:"description"`
Tags []string `json:"tags"`
Author string `json:"author"`
MusicId string `json:"musicId"`
Path string
ShortUrl string
Category string
}
type Articles []Article
type ArticleDetail struct {
Article
Body string
}
func initArticlesAndImages(dir string) (Articles, map[string]string, error) {
var articles Articles
shortUrlMap := make(map[string]string)
articles, err := RecursiveReadArticles(dir)
if err != nil {
return articles, shortUrlMap, err
}
sort.Sort(articles)
for i := len(articles) - 1; i >= 0; i-- {
//这里必须使用倒序的方式生成 shortUrl,因为如果有相同的文章标题,
// 倒序会将最老的文章优先生成shortUrl保证和之前的 shortUrl一样
article := articles[i]
keyword := utils.GenerateShortUrl(article.Title, func(url, keyword string) bool {
//保证 keyword 唯一
_, ok := shortUrlMap[keyword]
return !ok
})
articles[i].ShortUrl = keyword
shortUrlMap[keyword] = article.Path
}
return articles, shortUrlMap, nil
}
func ArticleSearch(articles *Articles, search string, category string, tag string) Articles {
var articleList Articles
for _, article := range *articles {
pass := true
if search != "" && strings.Index(article.Title, search) == -1 {
pass = false
}
if category != "" && strings.Index(article.Category, category) == -1 {
pass = false
}
if tag != "" {
pass = false
for _, tagx := range article.Tags {
if strings.Index(tagx, tag) != -1 {
pass = true
break
}
}
}
if pass {
articleList = append(articleList, article)
}
}
return articleList
}
func RecursiveReadArticles(dir string) (Articles, error) {
var articles Articles
dirInfo, err := os.Stat(dir)
if err != nil {
return articles, err
}
if !dirInfo.IsDir() {
return articles, errors.New("目标不是一个目录")
}
fileOrDir, err := ioutil.ReadDir(dir)
if err != nil {
return articles, err
}
for _, fileInfo := range fileOrDir {
name := fileInfo.Name()
path := dir + "/" + name
upperName := strings.ToUpper(name)
if fileInfo.IsDir() {
subArticles, err := RecursiveReadArticles(path)
if err != nil {
return articles, err
}
articles = append(articles, subArticles...)
} else if strings.HasSuffix(upperName, ".MD") {
article, err := ReadArticle(path)
if err != nil {
return articles, err
}
articles = append(articles, article)
} else if strings.HasSuffix(upperName, ".PNG") ||
strings.HasSuffix(upperName, ".GIF") ||
strings.HasSuffix(upperName, ".JPG") {
dst := config.Cfg.CurrentDir + "/images/" + name
fmt.Println(utils.IsFile(dst))
if !utils.IsFile(dst) {
_, _ = utils.CopyFile(path, dst)
}
}
}
return articles, nil
}
func ReadArticle(path string) (Article, error) {
article, _, err := readMarkdown(path)
if err != nil {
return article, err
}
return article, nil
}
func ReadArticleDetail(path string) (ArticleDetail, error) {
_, articleDetail, err := readMarkdown(path)
if err != nil {
return articleDetail, err
}
return articleDetail, nil
}
func readMarkdown(path string) (Article, ArticleDetail, error) {
var article Article
var articleDetail ArticleDetail
mdFile, err := os.Stat(path)
if err != nil {
return article, articleDetail, err
}
if mdFile.IsDir() {
return article, articleDetail, errors.New("this path is Dir")
}
markdown, err := ioutil.ReadFile(path)
if err != nil {
return article, articleDetail, err
}
markdown = bytes.TrimSpace(markdown)
article.Path = path
article.Category = GetCategoryName(path)
article.Title = strings.TrimSuffix(strings.ToUpper(mdFile.Name()), ".MD")
article.Date = Time(mdFile.ModTime())
if !bytes.HasPrefix(markdown, []byte("```json")) {
article.Description = cropDesc(markdown)
articleDetail.Article = article
articleDetail.Body = string(markdown)
return article, articleDetail, nil
}
markdown = bytes.Replace(markdown, []byte("```json"), []byte(""), 1)
markdownArrInfo := bytes.SplitN(markdown, []byte("```"), 2)
article.Description = cropDesc(markdownArrInfo[1])
if err := json.Unmarshal(bytes.TrimSpace(markdownArrInfo[0]), &article); err != nil {
article.Title = "文章[" + article.Title + "]解析 JSON 出错,请检查。"
article.Description = err.Error()
return article, articleDetail, nil
}
article.Path = path
article.Title = strings.ToUpper(article.Title)
articleDetail.Article = article
articleDetail.Body = string(markdownArrInfo[1])
return article, articleDetail, nil
}
func cropDesc(c []byte) string {
content := []rune(string(c))
contentLen := len(content)
if contentLen <= config.Cfg.DescriptionLen {
return string(content[0:contentLen])
}
return string(content[0:config.Cfg.DescriptionLen])
}
func (t *Time) UnmarshalJSON(b []byte) error {
date, err := time.ParseInLocation(`"`+config.Cfg.TimeLayout+`"`, string(b), time.Local)
if err != nil {
return nil
}
*t = Time(date)
return nil
}
func (t Time) MarshalJSON() ([]byte, error) {
return []byte(t.Format(`"` + config.Cfg.TimeLayout + `"`)), nil
}
func (t Time) Format(layout string) string {
return time.Time(t).Format(layout)
}
func (a Articles) Len() int { return len(a) }
func (a Articles) Less(i, j int) bool { return time.Time(a[i].Date).After(time.Time(a[j].Date)) }
func (a Articles) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

69
models/category.go Normal file
View File

@ -0,0 +1,69 @@
package models
import (
"blog/config"
"sort"
"strings"
)
type Category struct {
Name string
Quantity int
Articles Articles
}
type Categories []Category
func GetCategoryName(path string) string {
var categoryName string
newPath := strings.Replace(path, config.Cfg.DocumentContentDir+"/", "", 1)
if strings.Index(newPath, "/") == -1 { //文件在根目录下(content/)没有分类名称
categoryName = "未分类"
} else {
categoryName = strings.Split(newPath, "/")[0]
}
return categoryName
}
func GroupByCategory(articles *Articles, articleQuantity int) Categories {
var categories Categories
categoryMap := make(map[string]Articles)
for _, article := range *articles {
_, existedCategory := categoryMap[article.Category]
if existedCategory {
categoryMap[article.Category] = append(categoryMap[article.Category], article)
} else {
categoryMap[article.Category] = Articles{article}
}
}
for categoryName, articles := range categoryMap {
articleLen := len(articles)
var articleList Articles
if articleQuantity <= 0 {
articleList = articles
} else {
if articleQuantity > articleLen {
articleList = articles[0:articleLen]
} else {
articleList = articles[0:articleQuantity]
}
}
categories = append(categories, Category{
Name: categoryName,
Quantity: articleLen,
Articles: articleList,
})
}
sort.Sort(categories)
return categories
}
func (c Categories) Len() int { return len(c) }
func (c Categories) Less(i, j int) bool { return c[i].Quantity > c[j].Quantity }
func (c Categories) Swap(i, j int) { c[i], c[j] = c[j], c[i] }

50
models/common.go Normal file
View File

@ -0,0 +1,50 @@
package models
import (
"blog/config"
"sync"
)
var Navigation Navs
var ArticleList Articles
var ArticleShortUrlMap map[string]string //用来保证文章 shortUrl 唯一和快速定位文章
var Template HtmlTemplate
func CompiledContent() {
config.Initial() //克隆或者更新文档库
//下面是对内容的生成
wg := sync.WaitGroup{}
var err error
//导航
wg.Add(1)
go func() {
Navigation, err = initExtraNav(config.Cfg.DocumentExtraNavDir)
if err != nil {
panic(err)
}
wg.Done()
}()
//加载html模板
wg.Add(1)
go func() {
Template, err = initHtmlTemplate(config.Cfg.ThemesDir)
if err != nil {
panic(err)
}
wg.Done()
}()
//文章
wg.Add(1)
go func() {
ArticleList, ArticleShortUrlMap, err = initArticlesAndImages(config.Cfg.DocumentContentDir)
if err != nil {
panic(err)
}
wg.Done()
}()
wg.Wait()
//启用并发比之前节约4倍左右的时间
return
}

31
models/extra_nav.go Normal file
View File

@ -0,0 +1,31 @@
package models
import (
"sort"
"strings"
)
type Nav struct {
Title string
Path string
}
type Navs []Nav
func initExtraNav(dir string) (Navs, error) {
var navigation Navs
var extraNav Articles
extraNav, err := RecursiveReadArticles(dir)
if err != nil {
return navigation, err
}
sort.Sort(extraNav)
for _, article := range extraNav {
title := strings.Title(strings.ToLower(article.Title))
navigation = append(navigation, Nav{Title: title, Path: article.Path})
}
return navigation, nil
}

91
models/html_template.go Normal file
View File

@ -0,0 +1,91 @@
package models
import (
"blog/config"
"fmt"
"html/template"
"io"
)
type TemplatePointer struct {
*template.Template
}
type HtmlTemplate struct {
Article TemplatePointer
Categories TemplatePointer
Dashboard TemplatePointer
ExtraNav TemplatePointer
Index TemplatePointer
}
func (t TemplatePointer) WriteData(w io.Writer, data interface{}) {
err := t.Execute(w, data)
if err != nil {
if _, e := w.Write([]byte(err.Error())); e != nil {
fmt.Println(e)
}
}
}
func (t TemplatePointer) WriteError(w io.Writer, err error) {
if _, e := w.Write([]byte(err.Error())); e != nil {
fmt.Println(e)
}
}
func BuildViewData(title string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"Title": title,
"Data": data,
"Config": config.Cfg,
"Navs": Navigation,
}
}
func initHtmlTemplate(viewDir string) (HtmlTemplate, error) {
var htmlTemplate HtmlTemplate
tp, err := readHtmlTemplate(
[]string{"index", "extraNav", "dashboard", "categories", "article"},
viewDir)
if err != nil {
return htmlTemplate, err
}
htmlTemplate.Index = tp[0]
htmlTemplate.ExtraNav = tp[1]
htmlTemplate.Dashboard = tp[2]
htmlTemplate.Categories = tp[3]
htmlTemplate.Article = tp[4]
return htmlTemplate, nil
}
func SpreadDigit(n int) []int {
var r []int
for i := 1; i <= n; i++ {
r = append(r, i)
}
return r
}
func readHtmlTemplate(htmlFileName []string, viewDir string) ([]TemplatePointer, error) {
var htmlTemplate []TemplatePointer
head := viewDir + "/layouts/head.html"
footer := viewDir + "/layouts/footer.html"
for _, name := range htmlFileName {
tp, err := template.New(name+".html").
Funcs(template.FuncMap{"SpreadDigit": SpreadDigit}).
ParseFiles(viewDir+"/"+name+".html", head, footer)
if err != nil {
return htmlTemplate, err
}
htmlTemplate = append(htmlTemplate, TemplatePointer{tp})
}
return htmlTemplate, nil
}

48
models/pagination.go Normal file
View File

@ -0,0 +1,48 @@
package models
import (
"math"
)
type PageResult struct {
List Articles `json:"list"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
TotalPage int
}
func Pagination(articles *Articles, page int, pageSize int) PageResult {
articleLen := len(*articles)
totalPage := int(math.Floor(float64(articleLen / pageSize)))
if (articleLen % pageSize) != 0 {
totalPage++
}
result := PageResult{
Total: articleLen,
Page: page,
PageSize: pageSize,
TotalPage: totalPage,
}
if page < 1 {
result.Page = 1
}
if page > result.TotalPage {
result.Page = result.TotalPage
}
if articleLen <= result.PageSize {
result.List = (*articles)[0:articleLen]
} else {
startNum := (result.Page - 1) * result.PageSize
endNum := startNum + result.PageSize
if endNum > articleLen {
endNum = articleLen
}
result.List = (*articles)[startNum:endNum]
}
return result
}

24
routes/main.go Normal file
View File

@ -0,0 +1,24 @@
package routes
import (
"blog/config"
"blog/controller"
"net/http"
)
func InitRoute() {
http.HandleFunc("/", controller.Index)
http.HandleFunc("/blog", controller.Index)
http.HandleFunc("/categories", controller.Category)
http.HandleFunc("/article", controller.Article)
http.HandleFunc("/extra-nav", controller.ExtraNav)
http.HandleFunc(config.Cfg.GitHookUrl, controller.GithubHook)
http.HandleFunc(config.Cfg.Dashboard, controller.Dashboard)
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir(config.Cfg.ThemesDir+"/public"))))
http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir(config.Cfg.DocumentAssetsDir))))
http.Handle("/images/", http.StripPrefix("/images/", http.FileServer(http.Dir(config.Cfg.CurrentDir+"/images"))))
}

55
themes/blog/article.html Normal file
View File

@ -0,0 +1,55 @@
{{template "header" .}}
<div class="sub-title">- {{ .Title }} -</div>
<h1 style="text-align: center">{{ .Data.Title }}</h1>
<div class="article-info">
<span>
<img class="icon" src="/public/img/folder.svg" alt="">
<span>
分类于
<a class="category" href="/?category={{ .Data.Category }}">{{ .Data.Category }}</a>
</span>
</span>
<span class="divider-line"></span>
<span>
<img class="icon" src="/public/img/date.svg" alt="">
<span>发表于{{ .Data.Date.Format "2006-01-02 15:04" }}</span>
</span>
<span class="divider-line"></span>
<span>
<img class="icon" src="/public/img/tag.svg" alt="">
<span>Tags:
{{ range .Data.Tags }}
<a class="category" href="/?tag={{.}}">{{.}}</a>
{{ end }}</span>
</span>
<!-- <span id="busuanzi_container_page_pv">-->
<!-- 已浏览<span id="busuanzi_value_page_pv"></span>次-->
<!-- </span>-->
</div>
{{ if ne .Data.MusicId "" }}
<script src="https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/meting@1.2.0/dist/Meting.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/kaygb/kaygb@master/js/v3.js"></script>
<script src="https://cdn.staticfile.org/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/kaygb/kaygb@master/layer/layer.js"></script>
<div id="aplayer" class="aplayer" data-order="random" data-id="{{ .Data.MusicId }}" data-server="netease"
data-type="playlist" data-fixed="true" data-autoplay="true" data-volume="0.8"></div>
<span class="pln"><span class="pln"> </span></span>
{{ end }}
<div id="article" class="markdown">
</div>
<script>
document.getElementById('article').innerHTML = marked({{ .Data.Body }});
</script>
<script src="/public/js/prism.js"></script>
{{if ne .Config.UtterancesRepo ""}}
<script src="https://utteranc.es/client.js" repo="{{ .Config.UtterancesRepo }}" issue-term="[{{ .Data.Title }}]"
theme="github-light" crossorigin="anonymous" async>
</script>
{{end}}
{{template "footer" .}}

View File

@ -0,0 +1,17 @@
{{template "header" .}}
<div class="sub-title">- {{ .Title }} -</div>
<div class="categories">
{{range .Data}}
<div class="categories-card">
<a href="/?category={{ .Name }}">
<h3><img class="icon" src="/public/img/folder.svg" alt=""> {{ .Name }} <sub>({{ .Quantity }})</sub></h3>
</a>
<article class="categories-article">
{{range .Articles}}
<a href="/article?key={{ .ShortUrl }}">{{ .Title }}</a>
{{end}}
</article>
</div>
{{end}}
</div>
{{template "footer" .}}

View File

@ -0,0 +1,45 @@
{{template "header" .}}
<div class="sub-title">- {{ .Title }} -</div>
<p>Search</p>
<div class="item-content">
<div class="search-box">
<input id="search-input" class="search-input" type="text">
<img onclick="searchArticle()" class="search-icon" src="/public/img/search.svg" alt="">
</div>
</div>
<p>Theme</p>
<div class="item-content">
<ul class="colors">
{{range $index, $color := .Config.ThemeOption }}
<li onclick="selectColor('{{ $index }}')" class="{{ if eq $color $.Config.ThemeColor }}active{{end}}" style="background-color: {{ $color }}"></li>
{{end}}
</ul>
</div>
<p>Action</p>
<div class="item-content">
<div class="action">
<a href="{{ .Config.DashboardEntrance }}?action=updateArticle">更新文章</a>
</div>
</div>
{{ if .Data.msg }}
<div id="action-msg" class="action-msg">
{{range $msg := .Data.msg }}
<span>{{ $msg }}</span>
{{ end }}
<span class="close" onclick="document.getElementById('action-msg').remove()">x</span>
</div>
{{ end }}
<span class="action-tip">提示更新文章会执行git pull命令和你的仓库网络有关等待时间可能会稍长。</span>
<script>
function selectColor(index) {
window.location.href = '{{ .Config.DashboardEntrance }}?theme=' + index
}
function searchArticle() {
var searchKey = document.getElementById('search-input').value;
searchKey = searchKey.replace(/^\s+|\s+$/g,"")
if("" === searchKey){return}
window.location.href = '/?search=' + searchKey
}
</script>
{{template "footer" .}}

View File

@ -0,0 +1,7 @@
{{template "header" .}}
<div class="sub-title">- {{ .Title }} -</div>
<div id="about"></div>
<script>
document.getElementById('about').innerHTML = marked({{ .Data.Body }});
</script>
{{template "footer" .}}

55
themes/blog/index.html Normal file
View File

@ -0,0 +1,55 @@
{{template "header" .}}
<div class="sub-title">- {{ .Title }} -</div>
<ul class="articles">
{{range .Data.List }}
<li>
<h2>
<a class="title" href="/article?key={{ .ShortUrl }}">{{ .Title }}</a>
</h2>
<div class="article-info">
{{ if ne .Category "" }}
<span>
<img class="icon" src="/public/img/folder.svg" alt="">
<span>
分类于
<a class="category" href="/?category={{ .Category }}">{{ .Category }}</a>
</span>
</span>
<span class="divider-line"></span>
{{ end }}
<span>
<img class="icon" src="/public/img/date.svg" alt="">
<span>发表于{{ .Date.Format "2006-01-02 15:04" }}</span>
</span>
</div>
<div class="description"> {{ .Description }}...</div>
<a class="read-all" href="/article?key={{ .ShortUrl }}" rel="contents">
阅读全文 »
</a>
<div class="article-eof"></div>
</li>
{{end}}
</ul>
<ul class="pagination">
{{ range $page := SpreadDigit .Data.TotalPage }}
<li
class="{{ if eq $page $.Data.Page }}active{{end}}"
{{ if ne $page $.Data.Page }}onclick="goPage({{$page}})"{{end}}
>
<a href="javascript:;">{{ $page }}</a>
</li>
{{end}}
<li style="color: #bfbfbf;">
[{{ .Data.Total }}]
</li>
</ul>
<script>
var urlParams = currentUrlToParams();
function goPage(page) {
urlParams.page = page
window.location.href = "/" + obj2StrParams(urlParams)
}
</script>
{{template "footer" .}}

View File

@ -0,0 +1,15 @@
{{define "footer"}}
</div>
<div class="footer">
<span>{{ .Config.Icp }}</span><span class="footer-divider"> | </span>
<span>
© 2018 - 2022 {{ .Config.Author }}
Powered By <a href="{{ .Config.AppRepository }}" target="_blank"> {{ .Config.AppName }} </a>
</span>
<span id="busuanzi_container_site_pv" style='display:none'>
总访问量<span id="busuanzi_value_site_pv"></span>
</span>
</div>
</body>
</html>
{{ end}}

View File

@ -0,0 +1,69 @@
{{define "header"}}
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="shortcut icon" href="public/img/favicon.ico" type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="Keywords" content="{{ .Config.HtmlKeywords }}"/>
<meta name="description" content="{{ .Config.HtmlDescription }}"/>
<title>{{ .Title }} - {{ .Config.SiteName }}</title>
<style>
:root {
--primary: {
{
.Config.ThemeColor
}
}
}
</style>
<link rel="stylesheet" href="/public/css/app.css">
<link href="/public/css/prism.css" rel="stylesheet"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css">
<script src="/public/js/marked.min.js"></script>
<script async src="https://busuanzi.icodeq.com/busuanzi.pure.mini.js"></script>
<script>
function obj2StrParams(obj, firstStr = '?') {
let params = firstStr;
for (let p in obj) {
let isFirst = (params === '?');
let qz = isFirst ? '' : (params === '&' ? '' : '&');
params += (qz + p + '=' + obj[p])
}
return params
}
function currentUrlToParams(key = null) {
let paramsUrl = (window.location.href).split('?');
if (paramsUrl.length < 2) return key ? null : {};
let paramsArr = paramsUrl[1].split('&');
let paramsData = {}
paramsArr.forEach(r => {
let data = r.split('=')
paramsData[data[0]] = data[1]
})
if (key) return paramsData.hasOwnProperty(key) ? paramsData[key] : null;
return paramsData;
}
</script>
</head>
<body class="theme{{ .Config.ThemeColor }}">
<nav class="head">
<div class="container head-content">
<div class="logo">{{ .Config.SiteName }}</div>
<div class="nav">
<a href="/blog">Blog</a>
<a href="/categories">Categories</a>
{{range $nav := .Navs }}
<a href="/extra-nav?name={{ $nav.Title }}">{{ $nav.Title }}</a>
{{end}}
</div>
</div>
</nav>
<div class="post-warp">
{{ end }}

View File

@ -0,0 +1,477 @@
*{
box-sizing: border-box;
}
html {
font-family: josefin sans,-apple-system,BlinkMacSystemFont,helvetica neue,pingfang sc,hiragino sans gb,STHeiti,microsoft yahei,wenquanyi micro hei,Arial,Verdana,sans-serif;
}
html::-webkit-scrollbar {
width: 8px;
height: 8px
}
html::-webkit-scrollbar-thumb {
height: 40px;
background-color: #eee;
border-radius: 0
}
html::-webkit-scrollbar-thumb:hover {
background-color: #ddd
}
pre{
padding: 1em;
margin: .5em 0;
overflow: auto;
background: #f5f2f0;
}
body{
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
}
a{
color: #161209;
text-decoration: none;
transition: color .2s ease,border-color .46s ease,background .46s ease,opacity .46s ease;
}
a:hover{
color: #673ab7;
color: var(--primary,#673ab7);
}
p{
line-height: 2em;
}
blockquote{
text-indent: 2em;
position: relative;
margin: 20px 0;
}
blockquote::before{
position: absolute;
left: -32px;
top: -10px;
content: '“';
font-family: fantasy;
font-size: 40px;
color: #673ab7;
color: var(--primary,#673ab7);
}
@media screen and (max-width: 860px){
body{
padding: 6px;
}
}
@font-face {
font-family: josefin sans;
font-style: normal;
font-weight: 400;
src: url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.eot);
src: local("Josefin Sans"),local("JosefinSans-Normal"),url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.eot?#iefix) format("embedded-opentype"),url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.woff2) format("woff2"),url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.woff) format("woff"),url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.ttf) format("truetype"),url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.svg#JosefinSans) format("svg")
}
.container {
width: auto;
max-width: 1200px;
text-align: center;
margin: 0 auto;
}
.head{
width: 100%;
height: 64px;
}
.head-content{
display: flex;
height: 100%;
align-items: center;
justify-content: space-between;
}
@media screen and (max-width: 860px){
.head-content{
padding-top: 10px;
flex-direction: column;
}
}
.nav a{
padding: 0 8px;
}
.sub-title {
position: relative;
width: 100%;
text-align: right;
font-size: 32px;
margin-bottom: 82px;
}
@media screen and (max-width: 860px){
.sub-title {
display: none;
}
}
.post-warp{
position: relative;
width: 100%;
overflow: hidden;
max-width: 780px;
margin: 0 auto;
min-height: calc(100vh - 120px);
padding: 46px 0;
}
@media screen and (max-width: 860px){
.post-warp{
min-height: calc(100vh - 132px);
}
}
.markdow img{
width: 100%;
border-radius: 4px;
}
.footer{
color: #999;
display: flex;
height: 54px;
line-height: 1;
align-items: center;
justify-content: center;
}
.footer-divider{
margin: 0 10px;
}
@media screen and (max-width: 860px){
.footer{
flex-direction: column;
}
.footer span{
text-align: center;
padding: 4px 0;
}
.footer-divider{
display: none;
}
}
.footer a{
color: #999;
margin: 0 8px;
}
pre{
border-radius: 3px;
font-size: 14px;
line-height: 1.6;
overflow: auto;
}
hr{
border: none;
overflow: hidden;
}
hr:after{
content: '';
display: block;
margin: 30px auto;
width: 10%;
height: 1px;
background: #ccc;
text-align: center;
}
/*Categories*/
.categories{
display: flex;
width: 100%;
justify-content: space-between;
flex-wrap:wrap;
}
.categories-card{
width: 50%;
margin-bottom: 46px;
min-height: 250px;
padding: 0 16px;
}
@media screen and (max-width: 860px){
.categories-card{
width: 100%;
}
}
.icon{
width: 16px;
height: 16px;
}
.categories-article{
text-indent: 2em;
}
.categories-article a{
display: block;
margin-bottom: 10px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/*Pagination*/
.pagination{
display: flex;
margin: 60px 0;
width: 100%;
justify-content: center;
padding: 0;
}
.pagination li{
list-style-type: none;
margin: 0 10px;
width: 20px;
}
.pagination li a {
text-align: center;
display: block;
width: 100%;
color: #bfbfbf;
padding-bottom: 6px;
}
.pagination li a:hover,.pagination .active a{
border-bottom: 3px solid #673ab7;
border-bottom: 3px solid var(--primary,#673ab7);
}
/*Index*/
.articles{
margin: 0;
padding: 0;
min-height: 400px;
}
.articles li{
padding-bottom: 8px;
list-style-type: none;
margin: 40px 0;
text-align: center;
}
.articles li:after{
content: '';
display: block;
margin: 60px auto;
width: 10%;
height: 1px;
background: #ccc;
text-align: center;
}
.articles li:last-child:after{
display: none;
}
.read-all{
display: inline-block;
padding: 4px 20px;
font-size: 14px;
color: #fff;
background: #673ab7;
border: 2px solid #673ab7;
background: var(--primary,#673ab7);
border: 2px solid var(--primary,#673ab7);
text-decoration: none;
border-radius: 2px;
transition-property: background-color;
transition-duration: 0.2s;
transition-timing-function: ease-in-out;
transition-delay: 0s;
}
.read-all:hover{
background: #fff;
}
.articles li .title{
color: #555;
font-size: 20px;
font-weight: normal;
}
.articles li .title:hover{
color: #673ab7;
color: var(--primary,#673ab7);
}
.article-info{
margin:20px 0 60px;
color: #999;
font-size: 14px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
flex-flow: wrap;
}
.article-info img{
position: relative;
top: 2px;
width: 16px;
height: 16px;
}
.article-img img{
position: relative;
width: 300px;
height: 200px;
border-radius:3%;
object-fit:cover;
}
.article-info .divider-line{
width: 2px;
height: 16px;
background: #999;
margin: 0 14px;
}
.article{
padding: 10px 0;
margin: 10px 0;
}
.articles .description{
color: #555;
font-size: 16px;
text-align: left;
line-height: 2;
text-indent: 2em;
margin-bottom: 40px;
}
@media screen and (max-width: 860px){
.articles .description{
padding: 0 10px;
}
}
.sub{
font-size: 14px;
}
/*dashboard*/
.item-content{
width: 100%;
height: 120px;
display: flex;
justify-content: center;
align-items: center;
}
.search-box{
height: 40px;
display: flex;
border-radius: 2px;
align-items: center;
justify-content: space-between;
border: 1px solid #673ab7;
border: 1px solid var(--primary,#673ab7);
width: 320px;
}
.search-input{
text-align: center;
flex: 1;
border: none;
height: 30px;
padding: 0 10px;
}
.search-input:focus,.search-input:active{
border: none;
outline:none;
}
.search-icon{
cursor: pointer;
margin: 0 10px;
width: 20px;
height: 20px;
}
.colors{
display: flex;
flex-wrap: wrap;
list-style: none;
margin: 0;
padding: 0;
}
.colors li{
width: 40px;
height: 40px;
margin: 10px 20px;
border-radius: 2px;
cursor: pointer;
position: relative;
}
.colors li.active:before{
content: '';
width: 10px;
height: 10px;
background-color: #fff;
display: block;
position: absolute;
top: 15px;
left: 15px;
border-radius: 2px;
}
.action{
display: flex;
flex-wrap: wrap;
list-style: none;
margin: 0;
padding: 0;
}
.action a{
padding: 8px 16px;
border-radius: 2px;
opacity: .8;
color: #fff;
background-color: #673ab7;
background-color: var(--primary,#673ab7);
font-size: 14px;
}
.action a:hover{
opacity: 1;
box-shadow: 0 0 4px rgba(0,0,0,.2);
}
.action-tip{
font-size: 14px;
color: #673ab7;
color: var(--primary,#673ab7);
}
.action-msg{
z-index: 1400;
position: fixed;
top: 24px;
left: auto;
right: 24px;
color: #fff;
display: flex;
flex-direction: column;
padding: 12px 16px;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
opacity: 1;
min-width: 288px;
border-radius: 4px;
background-color: #673ab7;
background-color: var(--primary,#673ab7);
box-shadow: 0 3px 5px -1px rgba(0,0,0,0.2), 0 6px 10px 0 rgba(0,0,0,0.14), 0 1px 18px 0 rgba(0,0,0,0.12);
}
.action-msg .close{
color: #fff;
cursor: pointer;
position: absolute;
top: auto;
font-size: 16px;
right: 24px;
}

View File

@ -0,0 +1,5 @@
/* PrismJS 1.28.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+c+cpp+cmake+go+java+json+json5+kotlin+lua+markup-templating+nginx+php+python+verilog+yaml&plugins=line-highlight+show-language+toolbar+copy-to-clipboard */
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)}
div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}

Binary file not shown.

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1556811784413" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3571" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M508.34940208 792.26591763c-22.11718637 0-41.00975882-7.11308325-56.67650371-21.35017245-15.66674489-14.23102103-25.65006222-35.48046222-29.95116562-63.7422554l-58.0600415 4.9152c3.68579318 37.8919443 18.47993837 68.30307555 44.38850372 91.23703467 25.90613808 22.94002725 59.33677985 34.4064 100.29920711 34.4064 49.96634548 0 89.39353125-18.22507615 118.26942103-54.68008297 23.75558637-29.69144889 35.63459318-64.91704889 35.63459319-105.67437273 0-42.80107615-13.51740682-77.82035911-40.54979319-105.06027616-27.03238637-27.23263525-60.21059318-40.85562785-99.53097956-40.85562784-29.69751703 0-58.05882785 9.06338608-85.09121422 27.9025588L461.34916741 437.96859259H640.64474075v-52.18607407H417.11236741l-43.62027616 231.3505754 51.91543467 6.7744806c8.18835911-12.90209659 19.45448297-23.38542933 33.79230341-31.47912534 14.3317523-8.08520059 30.41113125-12.12901452 48.22721422-12.12901451 28.672 0 51.86324859 9.11799941 69.57981393 27.3406483 17.71171082 18.22993067 26.57242075 43.11540622 26.57242074 74.65035851 0 33.17699318-9.21508978 59.80281363-27.64648297 79.87139319-18.43260682 20.07100682-40.96242725 30.10408297-67.58339317 30.10408296z" fill="#999"></path><path d="M894.81762133 50.82074075H131.55259733C85.31694933 50.82074075 47.17985185 88.05732503 47.17985185 134.29418667v756.1337363C47.17985185 936.66478459 85.31694933 974.39288889 131.55259733 974.39288889h763.41551408C941.20497303 974.39288889 978.03377778 936.66478459 978.03377778 890.42792297V134.29418667c0-46.23686163-36.98050845-83.47344592-83.21615645-83.47344592zM732.27377778 117.57037037c30.16233718 0 54.61333333 24.45099615 54.61333333 54.61333333s-24.45099615 54.61333333-54.61333333 54.61333333-54.61333333-24.45099615-54.61333333-54.61333333 24.45099615-54.61333333 54.61333333-54.61333333z m-439.33392593 0c30.16233718 0 54.61333333 24.45099615 54.61333333 54.61333333s-24.45099615 54.61333333-54.61333333 54.61333333-54.61333333-24.45099615-54.61333333-54.61333333 24.45099615-54.61333333 54.61333333-54.61333333zM905.216 890.42792297c0 6.01110755-4.23556741 11.14718815-10.24667497 11.14718814H131.55259733c-6.01110755 0-11.5549677-5.13486697-11.5549677-11.14718814V276.55585185h785.21837037v613.87207112z" fill="#999"></path></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1558276399452" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2757" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M511.939185 874.030645C312.344934 874.030645 149.969355 711.655066 149.969355 511.939185c0-199.594251 162.375579-361.96983 361.96983-361.96983 199.594251 0 361.96983 162.375579 361.96983 361.96983 0.12163 199.715881-162.253949 362.09146-361.96983 362.09146z m0-677.963654C337.76553 196.066991 195.945362 337.76553 195.945362 512.060815s141.698539 315.993823 315.993823 315.993823S827.933009 686.356099 827.933009 512.060815 686.23447 196.066991 511.939185 196.066991z" fill="#5B5C5C" p-id="2758"></path><path d="M554.144673 333.265233c-17.636299-35.759116-28.461337-70.666825-33.934672-90.978976V498.681554c9.000594 3.40563 15.325336 11.919705 15.325336 22.014966 0 13.014372-10.581779 23.596152-23.596152 23.596151s-23.596152-10.581779-23.596151-23.596151c0-10.581779 7.05452-19.460744 16.663261-22.379855V242.042998c-5.473334 20.190521-16.298373 55.21986-34.056301 91.222235-27.73156 56.071267-62.274379 105.452904-62.274379 105.452905L430.93384 741.089441s21.041929 23.474522 81.613493 23.474521c60.693194 0 81.613493-23.474522 81.613494-23.474521l22.136595-302.371303c0.12163 0-34.42119-49.381637-62.152749-105.452905z" fill="#5B5C5C" p-id="2759"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M853.333333 938.666667H170.666667c-72.533333 0-128-55.466667-128-128V213.333333c0-72.533333 55.466667-128 128-128h213.333333c12.8 0 25.6 8.533333 34.133333 17.066667L490.666667 213.333333H853.333333c72.533333 0 128 55.466667 128 128v469.333334c0 72.533333-55.466667 128-128 128zM170.666667 170.666667c-25.6 0-42.666667 17.066667-42.666667 42.666666v597.333334c0 25.6 17.066667 42.666667 42.666667 42.666666h682.666666c25.6 0 42.666667-17.066667 42.666667-42.666666V341.333333c0-25.6-17.066667-42.666667-42.666667-42.666666h-384c-12.8 0-25.6-8.533333-34.133333-17.066667L362.666667 170.666667H170.666667z" fill="#999"></path></svg>

After

Width:  |  Height:  |  Size: 922 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566293108350" class="icon" viewBox="0 0 1030 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2103" width="48.28125" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M738.618409 646.405992c96.606263 96.606263 192.944918 192.944918 290.889218 290.621611-31.310063 29.169204-62.352519 57.803193-93.662582 86.972397-93.662582-93.662582-190.001237-190.001237-286.875107-286.875107-104.099269 71.451169-215.691545 95.535833-336.917687 66.901844-96.87387-22.746627-175.015224-75.732887-233.621239-156.282708-120.690927-165.648966-98.747122-390.439162 42.81718-530.130212 149.860131-147.719272 377.861615-153.339027 534.947145-33.450922C814.08369 205.389036 876.436208 448.644141 738.618409 646.405992zM728.984544 407.700212C728.984544 230.008915 585.814598 86.036146 408.658514 86.036146 231.502431 86.036146 86.994448 230.276522 86.994448 407.164998c0 178.226513 143.972768 322.466888 321.664066 321.664066C587.152634 728.293849 728.984544 585.926725 728.984544 407.700212z" p-id="2104"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1711012507479" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1625" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M648.32 715.072l-124.992 125.056a224 224 0 0 1-316.8 0l-22.656-22.656a224 224 0 0 1 0-316.8L308.48 376.064l63.488 63.232-124.736 124.8a134.4 134.4 0 0 0-6.528 183.04l6.528 7.04 22.656 22.592a134.4 134.4 0 0 0 183.04 6.528l7.04-6.528 124.928-124.992 63.36 63.296z m-44.288-295.04a44.8 44.8 0 0 1 0 63.36L484.736 602.56a44.8 44.8 0 1 1-63.36-63.36l119.296-119.232a44.8 44.8 0 0 1 63.36 0z m236.032-213.504a224 224 0 0 1 0 316.8l-123.84 123.84-63.488-63.296 123.968-123.904a134.4 134.4 0 0 0 6.592-183.04l-6.592-7.04-22.592-22.592a134.4 134.4 0 0 0-183.04-6.592l-7.04 6.592-124.224 124.16-63.488-63.232 124.352-124.288a224 224 0 0 1 316.8 0l22.592 22.592z" p-id="1626"></path></svg>

After

Width:  |  Height:  |  Size: 1012 B

6
themes/blog/public/js/marked.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

15
utils/cmd.go Normal file
View File

@ -0,0 +1,15 @@
package utils
import (
"os/exec"
)
func RunCmdByDir(dir string, cmdName string, arg ...string) (string, error) {
cmd := exec.Command(cmdName, arg ...)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
return "", err
}
return string(out), nil
}

13
utils/cmd_test.go Normal file
View File

@ -0,0 +1,13 @@
package utils_test
import (
"blog/utils"
"testing"
)
func TestRunCmdByDir(t *testing.T) {
_, err := utils.RunCmdByDir("./", "ping", "127.0.0.1")
if err != nil {
t.Error("run cmd error", err)
}
}

65
utils/file.go Normal file
View File

@ -0,0 +1,65 @@
package utils
import (
"errors"
"fmt"
"io"
"os"
)
func IsDir(name string) bool {
if info, err := os.Stat(name); err == nil {
return info.IsDir()
}
return false
}
func IsFile(filename string) bool {
existed := true
if _, err := os.Stat(filename); os.IsNotExist(err) {
existed = false
}
return existed
}
func MakeDir(dir string) error {
if !IsDir(dir) {
return os.MkdirAll(dir, os.ModePerm)
}
return nil
}
func RemoveDir(dir string) error {
if !IsDir(dir) {
return errors.New("cannot delete without directory")
}
return os.RemoveAll(dir)
}
func CopyFile(src, dst string) (int64, error) {
sourceFileStat, err := os.Stat(src)
if err != nil {
return 0, err
}
if !sourceFileStat.Mode().IsRegular() {
return 0, fmt.Errorf("%s is not a regular file", src)
}
source, err := os.Open(src)
if err != nil {
return 0, err
}
defer source.Close()
destination, err := os.Create(dst)
if err != nil {
return 0, err
}
defer destination.Close()
nBytes, err := io.Copy(destination, source)
return nBytes, err
}

19
utils/git.go Normal file
View File

@ -0,0 +1,19 @@
package utils
import (
"errors"
"strings"
)
//通过git url 返回仓库的名字
func GetRepoName(gitUrl string) (string, error) {
if !strings.HasSuffix(gitUrl, ".git") {
return "", errors.New("git URL must end with .git")
}
noSuffixUrl := strings.TrimSuffix(gitUrl, ".git")
urlArr := strings.Split(noSuffixUrl, "/")
return urlArr[ len(urlArr)-1 ], nil
}

14
utils/git_test.go Normal file
View File

@ -0,0 +1,14 @@
package utils_test
import (
"blog/utils"
"testing"
)
func TestGetRepoName(t *testing.T) {
url := "http://jsit205.vaiwan.cn/JiXieShi/blog.git"
name, err := utils.GetRepoName(url)
if err != nil || name != "blog" {
t.Error("repository name error")
}
}

43
utils/short_url.go Normal file
View File

@ -0,0 +1,43 @@
package utils
import (
"crypto/md5"
"fmt"
"strconv"
)
const charset = "A0a12B3b4CDc56Ede7FGf8Hg9IhJKiLjkMNlmOPnQRopqrSstTuvUVwxWXyYzZ"
func generateCharset(url, hexMd5 string, len, sectionNum int, cb func(url, keyword string) bool) string {
for i := 0; i < sectionNum; i++ {
sectionHex := hexMd5[i*8 : 8+i*8]
bits, _ := strconv.ParseUint(sectionHex, 16, 32)
bits = bits & 0x3FFFFFFF
keyword := ""
for j := 0; j < len; j++ {
idx := bits & 0x3D
keyword = keyword + string(charset[idx])
bits = bits >> 5
}
if cb(url, keyword) {
return keyword
}
}
return ""
}
// 起初生成6位的短码当四组6位短码都重复时再生成8位的短码因此总共会有8个短码供你选择。
func GenerateShortUrl(url string, cb func(url, keyword string) bool) string {
if url == "" || cb == nil {
return ""
}
hexMd5 := fmt.Sprintf("%x", md5.Sum([]byte(url)))
sections := len(hexMd5) / 8
keyword := generateCharset(url, hexMd5, 6, sections, cb)
if keyword == "" {
return generateCharset(url, hexMd5, 8, sections, cb)
}
return keyword
}

14
utils/short_url_test.go Normal file
View File

@ -0,0 +1,14 @@
package utils_test
import (
"blog/utils"
"testing"
)
func TestGenerateShortUrl(t *testing.T) {
url := "http://jsit205.vaiwan.cn/JiXieShi/blog.git"
shortUrl := utils.GenerateShortUrl(url, func(url, keyword string) bool { return true })
if shortUrl != "ItMIz7" {
t.Error("generate short URL error")
}
}

35
配置说明.md Normal file
View File

@ -0,0 +1,35 @@
## config.json 配置说明
- "port": 服务器监听的端口号,
- "pageSize": 首页每一页的文章数量,
- "descriptionLen": 文章没有配置description字段时默认取文章内容多少个字作为描述,
- "author": 博客作者,网站底部展示,
- "icp": 网站的备案号,
- "webHookSecret": 博客文章更新勾子的密钥,这里要和你在仓库设置的密钥一样,
- "categoryDisplayQuantity": 在分类页面下,每个分类下最多展示多少篇文章,
- "utterancesRepo": 是否开启utterances评论留空没有评论否则填写评论存储的仓库name/repo,
- "timeLayout": 解析时间的格式保持和你文章里面的date字段一样除非了解Golang的时间解析否则不要修改,
- "siteName": 网站的名字,
- "documentGitUrl": 你文章的git地址应用会把文章克隆在当前目录下,必须公开并且以.git结尾,
- "htmlKeywords": 网页里面的htmlKeywords,
- "htmlDescription": 网页里面的htmlDescription,
- "themePath": 主题路径,留空使用/blog,
- "themeColor": 博客的主题颜色,
- "dashboardEntrance": 网站仪表盘的访问路径,留空使用/admin,
- "themeOption": 网站可选择的主题颜色
## MD 文章支持的字段
- "title": 文章标题,不填使用文件名,
- "date": 文章日期,排序使用(导航文章使用这个来排序),
- "description": 文章描述
- "tags": 文章 tag
- "author": 文章作者
- "musicId": 网易云的歌单ID
> 文章的这些字段全部可以为空,但是没有日期会默认使用文件生成的日期,那样每次迁移文档时间都是不可控的。
## 其他
开启utterances评论之后utterancesRepo指向的仓库必须是公开并且可以被评论的具体使用请访问 https://utteranc.es
webHook的地址: 你的域名/api/git_push_hook