initial commit - added base, need to fix templ errors
This commit is contained in:
24
go.mod
Normal file
24
go.mod
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
module git.valxntine.dev/valxntine/blog
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||||
|
github.com/a-h/templ v0.3.943 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/cli/browser v1.3.0 // indirect
|
||||||
|
github.com/fatih/color v1.16.0 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/natefinch/atomic v1.0.1 // indirect
|
||||||
|
github.com/yuin/goldmark v1.7.13 // indirect
|
||||||
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
|
golang.org/x/tools v0.35.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
tool github.com/a-h/templ/cmd/templ
|
35
go.sum
Normal file
35
go.sum
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||||
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||||
|
github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY=
|
||||||
|
github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||||
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
||||||
|
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
||||||
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
|
||||||
|
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
||||||
|
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||||
|
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
|
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
|
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||||
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||||
|
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
156
main.go
Normal file
156
main.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.valxntine.dev/valxntine/blog/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
fs := http.FileServer(http.Dir("./static/"))
|
||||||
|
mux.Handle("/static/", http.StripPrefix("/static/", fs))
|
||||||
|
|
||||||
|
mux.HandleFunc("/", handleHome)
|
||||||
|
mux.HandleFunc("/posts/", handlePost)
|
||||||
|
mux.HandleFunc("/tags/", handleTag)
|
||||||
|
|
||||||
|
handler := loggingMiddleware(mux)
|
||||||
|
|
||||||
|
log.Println("Serving on :8080")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
func loggingMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("%s %s %s", r.Method, r.URL.Path, r.RemoteAddr)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHome(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := 1
|
||||||
|
if p := r.URL.Query().Get("page"); p != "" {
|
||||||
|
if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
|
||||||
|
page = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
posts := GetPaginatedPosts(page, 5)
|
||||||
|
total := GetTotalPages(5)
|
||||||
|
|
||||||
|
d := models.Data{
|
||||||
|
Title: "~/valxntine/blog",
|
||||||
|
Subtitle: "Thought from the Forge",
|
||||||
|
Posts: posts,
|
||||||
|
CurrentPage: page,
|
||||||
|
TotalPages: total,
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Header.Get("HX-Request") == "true" {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
err := templates.PostList(
|
||||||
|
d.Posts,
|
||||||
|
d.CurrentPage,
|
||||||
|
d.TotalPages,
|
||||||
|
).Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
err := templates.HomePage(d).Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
slug := strings.TrimPrefix(r.URL.Path, "/posts/")
|
||||||
|
if slug == "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
post := GetPostBySlug(slug)
|
||||||
|
if post == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Header.Get("HX-Request") == "true" {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
err := templates.PostDetail(*post).Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
// err := templates.Layout(post.Title + " - ~/valxntine/blog") {
|
||||||
|
// @templates.Header("~/valxntine/blog", "Thoughts from the Forge")
|
||||||
|
// @templates.Nav()
|
||||||
|
// <main>
|
||||||
|
// @templates.PostDetail(*post)
|
||||||
|
// </main>
|
||||||
|
// @templates.Footer()
|
||||||
|
// }.Render(r.Context(), w)
|
||||||
|
// if err != nil {
|
||||||
|
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTag(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tag := strings.TrimPrefix(r.URL.Path, "/tags/")
|
||||||
|
if tag == "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := 1
|
||||||
|
if p := r.URL.Query().Get("page"); p != "" {
|
||||||
|
if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
|
||||||
|
page = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
posts := GetPostsByTag(tag, page, 5)
|
||||||
|
total := GetTotalPagesByTag(tag, 5)
|
||||||
|
|
||||||
|
d := models.Data{
|
||||||
|
Title: "~/valxntine/blog",
|
||||||
|
Subtitle: fmt.Sprintf("Posts tagged with '%s'", tag),
|
||||||
|
Posts: posts,
|
||||||
|
CurrentPage: page,
|
||||||
|
TotalPages: total,
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Header.Get("HX-Request") == "true" {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
err := templates.PostList(d.Posts, d.CurrentPage, d.TotalPages).Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
err := templates.HomePage(d).Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
22
models/models.go
Normal file
22
models/models.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
ID string
|
||||||
|
Title string
|
||||||
|
Slug string
|
||||||
|
Excerpt string
|
||||||
|
Content string
|
||||||
|
Tags []string
|
||||||
|
PublishedAt time.Time
|
||||||
|
WordCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Data struct {
|
||||||
|
Title string
|
||||||
|
Subtitle string
|
||||||
|
Posts []Post
|
||||||
|
CurrentPage int
|
||||||
|
TotalPages int
|
||||||
|
}
|
370
posts.go
Normal file
370
posts.go
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.valxntine.dev/valxntine/blog/models"
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/extension"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed posts/*.md
|
||||||
|
var postsFS embed.FS
|
||||||
|
|
||||||
|
var (
|
||||||
|
postCache []models.Post
|
||||||
|
postCacheTime time.Time
|
||||||
|
cacheValid = false
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostMeta struct {
|
||||||
|
Title string
|
||||||
|
Date string
|
||||||
|
Tags []string
|
||||||
|
Excerpt string
|
||||||
|
WordCount int
|
||||||
|
Draft bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var md = goldmark.New(
|
||||||
|
goldmark.WithExtensions(
|
||||||
|
extension.GFM,
|
||||||
|
extension.Table,
|
||||||
|
extension.Strikethrough,
|
||||||
|
extension.Linkify,
|
||||||
|
extension.TaskList,
|
||||||
|
),
|
||||||
|
goldmark.WithParserOptions(
|
||||||
|
parser.WithAutoHeadingID(),
|
||||||
|
),
|
||||||
|
goldmark.WithRendererOptions(
|
||||||
|
html.WithHardWraps(),
|
||||||
|
html.WithXHTML(),
|
||||||
|
html.WithUnsafe(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseFrontmatter(content string) (PostMeta, string, error) {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
|
||||||
|
if len(lines) < 2 || lines[0] != "---" {
|
||||||
|
return PostMeta{}, content, fmt.Errorf("no frontmatter found")
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := PostMeta{}
|
||||||
|
var endID int
|
||||||
|
|
||||||
|
for i := 1; i < len(lines); i++ {
|
||||||
|
if lines[i] == "---" {
|
||||||
|
endID = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if endID == 0 {
|
||||||
|
return meta, content, fmt.Errorf("frontmatter not closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < endID; i++ {
|
||||||
|
line := strings.TrimSpace(lines[i])
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
k := strings.TrimSpace(parts[0])
|
||||||
|
v := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' {
|
||||||
|
v = v[1 : len(v)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(k) {
|
||||||
|
case "title":
|
||||||
|
meta.Title = v
|
||||||
|
case "date":
|
||||||
|
meta.Date = v
|
||||||
|
case "excerpt":
|
||||||
|
meta.Excerpt = v
|
||||||
|
case "tags":
|
||||||
|
if v != "" {
|
||||||
|
tags := strings.Split(v, ",")
|
||||||
|
for _, t := range tags {
|
||||||
|
meta.Tags = append(meta.Tags, strings.TrimSpace(t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "draft":
|
||||||
|
meta.Draft = strings.ToLower(v) == "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body := strings.Join(lines[endID+1:], "\n")
|
||||||
|
return meta, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSlug(fn string) string {
|
||||||
|
name := filepath.Base(fn)
|
||||||
|
name = strings.TrimSuffix(name, filepath.Ext(name))
|
||||||
|
|
||||||
|
if len(name) > 11 && name[4] == '-' && name[7] == '-' && name[10] == '-' {
|
||||||
|
name = name[11:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func countWords(c string) int {
|
||||||
|
sc := bufio.NewScanner(strings.NewReader(c))
|
||||||
|
sc.Split(bufio.ScanWords)
|
||||||
|
count := 0
|
||||||
|
for sc.Scan() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func load() ([]models.Post, error) {
|
||||||
|
if cacheValid && time.Since(postCacheTime) < 10*time.Minute {
|
||||||
|
return postCache, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var posts []models.Post
|
||||||
|
|
||||||
|
entries, err := postsFS.ReadDir("posts")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read posts dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c, err := postsFS.ReadFile(filepath.Join("posts", entry.Name()))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, body, err := parseFrontmatter(string(c))
|
||||||
|
if err != nil {
|
||||||
|
meta = PostMeta{
|
||||||
|
Title: strings.TrimSuffix(entry.Name(), ".md"),
|
||||||
|
Date: time.Now().Format("2006-01-02"),
|
||||||
|
}
|
||||||
|
body = string(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta.Draft {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
published, err := time.Parse("2006-01-02", meta.Date)
|
||||||
|
if err != nil {
|
||||||
|
published = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
var htmlC strings.Builder
|
||||||
|
|
||||||
|
if err := md.Convert([]byte(body), &htmlC); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
excerpt := meta.Excerpt
|
||||||
|
if excerpt == "" {
|
||||||
|
plain := stripHTML(htmlC.String())
|
||||||
|
words := strings.Fields(plain)
|
||||||
|
if len(words) > 30 {
|
||||||
|
excerpt = strings.Join(words[:30], " ") + "..."
|
||||||
|
} else {
|
||||||
|
excerpt = plain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count := meta.WordCount
|
||||||
|
if count == 0 {
|
||||||
|
count = countWords(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
slug := generateSlug(entry.Name())
|
||||||
|
|
||||||
|
post := models.Post{
|
||||||
|
ID: slug,
|
||||||
|
Title: meta.Title,
|
||||||
|
Slug: slug,
|
||||||
|
Excerpt: excerpt,
|
||||||
|
Content: htmlC.String(),
|
||||||
|
Tags: meta.Tags,
|
||||||
|
PublishedAt: published,
|
||||||
|
WordCount: count,
|
||||||
|
}
|
||||||
|
|
||||||
|
posts = append(posts, post)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(posts, func(i, j int) bool {
|
||||||
|
return posts[i].PublishedAt.After(posts[j].PublishedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
postCache = posts
|
||||||
|
postCacheTime = time.Now()
|
||||||
|
cacheValid = true
|
||||||
|
|
||||||
|
return posts, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripHTML(s string) string {
|
||||||
|
in := false
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
for _, r := range s {
|
||||||
|
if r == '<' {
|
||||||
|
in = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == '>' {
|
||||||
|
in = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !in {
|
||||||
|
sb.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPaginatedPosts(page, perPage int) []models.Post {
|
||||||
|
posts, err := load()
|
||||||
|
if err != nil {
|
||||||
|
return []models.Post{}
|
||||||
|
}
|
||||||
|
|
||||||
|
start := (page - 1) * perPage
|
||||||
|
end := start + perPage
|
||||||
|
|
||||||
|
if start >= len(posts) {
|
||||||
|
return []models.Post{}
|
||||||
|
}
|
||||||
|
if end > len(posts) {
|
||||||
|
end = len(posts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTotalPages(per int) int {
|
||||||
|
posts, err := load()
|
||||||
|
if err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return (len(posts) + per - 1) / per
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPostBySlug(slug string) *models.Post {
|
||||||
|
posts, err := load()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, post := range posts {
|
||||||
|
if post.Slug == slug {
|
||||||
|
return &post
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPostsByTag(tag string, page, per int) []models.Post {
|
||||||
|
posts, err := load()
|
||||||
|
if err != nil {
|
||||||
|
return []models.Post{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var filtered []models.Post
|
||||||
|
for _, post := range posts {
|
||||||
|
for _, t := range post.Tags {
|
||||||
|
if t == tag {
|
||||||
|
filtered = append(filtered, post)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start := (page - 1) * per
|
||||||
|
end := start + per
|
||||||
|
|
||||||
|
if start >= len(filtered) {
|
||||||
|
return []models.Post{}
|
||||||
|
}
|
||||||
|
if end > len(filtered) {
|
||||||
|
end = len(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTotalPagesByTag(tag string, per int) int {
|
||||||
|
posts, err := load()
|
||||||
|
if err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for _, post := range posts {
|
||||||
|
for _, t := range post.Tags {
|
||||||
|
if t == tag {
|
||||||
|
count++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (count + per - 1) / per
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllPosts() []models.Post {
|
||||||
|
posts, err := load()
|
||||||
|
if err != nil {
|
||||||
|
return []models.Post{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllTags() []string {
|
||||||
|
posts := GetAllPosts()
|
||||||
|
if len(posts) == 0 {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
set := make(map[string]bool)
|
||||||
|
for _, post := range posts {
|
||||||
|
for _, tag := range post.Tags {
|
||||||
|
set[tag] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tags []string
|
||||||
|
for tag := range set {
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(tags)
|
||||||
|
return tags
|
||||||
|
}
|
32
posts/hello.md
Normal file
32
posts/hello.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: "Hello, Forge"
|
||||||
|
date: "2025-08-17"
|
||||||
|
excerpt: "The Hammer Strikes..."
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Hello, Forge
|
||||||
|
|
||||||
|
Welcome.
|
||||||
|
|
||||||
|
This is my home.
|
||||||
|
|
||||||
|
A place to show you all the things, write some code, and explore new features of Go and other languages I enjoy writing.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Valentine struct {
|
||||||
|
Age int
|
||||||
|
Hobbies []string
|
||||||
|
Job string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
valentine := Valentine{
|
||||||
|
Age: 32,
|
||||||
|
Hobbies: []string{"Lifting heavy things", "Walking Dwight", "Writing Go"},
|
||||||
|
Job: "Senior Software Engineer",
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Sprintf("%+v\n", valentine)
|
||||||
|
}
|
||||||
|
```
|
402
static/style.css
Normal file
402
static/style.css
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', 'Courier New', monospace;
|
||||||
|
background-color: #fafafa;
|
||||||
|
color: #2d3748;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border: 2px solid #4a5568;
|
||||||
|
background-color: #f7fafc;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #718096;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
background-color: #edf2f7;
|
||||||
|
border: 1px solid #cbd5e0;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a {
|
||||||
|
color: #3182ce;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-right: 20px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a:visited {
|
||||||
|
color: #805ad5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a:hover {
|
||||||
|
background-color: #bee3f8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-posts {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post:hover {
|
||||||
|
border-color: #cbd5e0;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title a {
|
||||||
|
color: #2b6cb0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title a:visited {
|
||||||
|
color: #805ad5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: #2c5282;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-meta {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #718096;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-excerpt {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-more {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-more a {
|
||||||
|
color: #3182ce;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-more a:hover {
|
||||||
|
background-color: #ebf8ff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 48px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #3182ce;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-right: 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-detail {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-detail .post-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.header h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-excerpt {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle animations and modern touches */
|
||||||
|
.blink {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: -10px 0 0 -10px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-top: 2px solid #3182ce;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination Styles */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f7fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a {
|
||||||
|
color: #3182ce;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a:hover {
|
||||||
|
background-color: #bee3f8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-info {
|
||||||
|
color: #718096;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Post Detail Styles */
|
||||||
|
.post-detail {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-detail .post-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-detail .post-meta {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #718096;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 32px 0 16px 0;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 24px 0 12px 0;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content p {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content code {
|
||||||
|
background-color: #f7fafc;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content pre {
|
||||||
|
background-color: #1a202c;
|
||||||
|
color: #f7fafc;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content pre code {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-footer {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-footer a {
|
||||||
|
color: #3182ce;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-footer a:hover {
|
||||||
|
background-color: #ebf8ff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tag Links */
|
||||||
|
a[href^="/tags/"] {
|
||||||
|
background-color: #ebf8ff;
|
||||||
|
color: #2b6cb0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href^="/tags/"]:hover {
|
||||||
|
background-color: #bee3f8;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTMX Transitions */
|
||||||
|
.htmx-settling {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-swapping {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
11
templates/footer.templ
Normal file
11
templates/footer.templ
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
templ Footer() {
|
||||||
|
<footer class="footer">
|
||||||
|
<p>
|
||||||
|
© 2025 Valentine Bott | Built with Go + Templ + HTMX |
|
||||||
|
<a href="https://git.valxntine.dev/valxntine/blog">Source Code</a> |
|
||||||
|
Last updated: <span class="blink">●</span> Now-ish
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
}
|
40
templates/footer_templ.go
Normal file
40
templates/footer_templ.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.943
|
||||||
|
package templates
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func Footer() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<footer class=\"footer\"><p>© 2025 Valentine Bott | Built with Go + Templ + HTMX | <a href=\"https://git.valxntine.dev/valxntine/blog\">Source Code</a> | Last updated: <span class=\"blink\">●</span> Now-ish</p></footer>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
8
templates/header.templ
Normal file
8
templates/header.templ
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
templ Header(title, subtitle string) {
|
||||||
|
<div class="header">
|
||||||
|
<h1>{ title }</h1>
|
||||||
|
<div class="subtitle">{ subtitle }</div>
|
||||||
|
</div>
|
||||||
|
}
|
66
templates/header_templ.go
Normal file
66
templates/header_templ.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.943
|
||||||
|
package templates
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func Header(title, subtitle string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"header\"><h1>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 5, Col: 15}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h1><div class=\"subtitle\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 6, Col: 36}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
12
templates/home.templ
Normal file
12
templates/home.templ
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
templ HomePage(data models.Data) {
|
||||||
|
@Layout(data.Title) {
|
||||||
|
@Header(data.Title, data.Subtitle)
|
||||||
|
@Nav()
|
||||||
|
<main>
|
||||||
|
@PostList(data.Posts, data.CurrentPage, data.TotalPages)
|
||||||
|
</main>
|
||||||
|
@Footer()
|
||||||
|
}
|
||||||
|
}
|
82
templates/home_templ.go
Normal file
82
templates/home_templ.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.943
|
||||||
|
package templates
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func HomePage(data models.Data) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = Header(data.Title, data.Subtitle).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = Nav().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <main>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = PostList(data.Posts, data.CurrentPage, data.TotalPages).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</main>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = Footer().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout(data.Title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
25
templates/layout.templ
Normal file
25
templates/layout.templ
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
templ Layout(title string) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>{ title }</title>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{ children... }
|
||||||
|
<script>
|
||||||
|
document.body.addEventListener('htmx:beforeRequest', function(e) {
|
||||||
|
e.detail.elt.classList.add('loading');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener('htmx:afterRequest', function(e) {
|
||||||
|
e.detail.elt.classList.remove('loading');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
61
templates/layout_templ.go
Normal file
61
templates/layout_templ.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.943
|
||||||
|
package templates
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func Layout(title string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 9, Col: 18}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><script src=\"https://unpkg.com/htmx.org@1.9.6\"></script></head><body>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<script>\n document.body.addEventListener('htmx:beforeRequest', function(e) {\n e.detail.elt.classList.add('loading');\n });\n\n document.body.addEventListener('htmx:afterRequest', function(e) {\n e.detail.elt.classList.remove('loading');\n });\n </script></body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
11
templates/nav.templ
Normal file
11
templates/nav.templ
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
templ Nav() {
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/" hx-get="/" hx-target="main" hx-push-url="true">home</a>
|
||||||
|
<a href="/tags" hx-get="/tags" hx-target="main" hx-push-url="true">tags</a>
|
||||||
|
<a href="/about" hx-get="/about" hx-target="main" hx-push-url="true">about</a>
|
||||||
|
<a href="/archive" hx-get="/archive" hx-target="main" hx-push-url="true">archive</a>
|
||||||
|
<a href="mailto:valentine.bott@gmail.com">Contact Me</a>
|
||||||
|
</nav>
|
||||||
|
}
|
40
templates/nav_templ.go
Normal file
40
templates/nav_templ.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.943
|
||||||
|
package templates
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func Nav() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<nav class=\"nav\"><a href=\"/\" hx-get=\"/\" hx-target=\"main\" hx-push-url=\"true\">home</a> <a href=\"/tags\" hx-get=\"/tags\" hx-target=\"main\" hx-push-url=\"true\">tags</a> <a href=\"/about\" hx-get=\"/about\" hx-target=\"main\" hx-push-url=\"true\">about</a> <a href=\"/archive\" hx-get=\"/archive\" hx-target=\"main\" hx-push-url=\"true\">archive</a> <a href=\"mailto:valentine.bott@gmail.com\">Contact Me</a></nav>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
29
templates/pagination.templ
Normal file
29
templates/pagination.templ
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
templ Pagination(currentPage, totalPages int) {
|
||||||
|
<div class="pagination">
|
||||||
|
if currentPage > 1 {
|
||||||
|
<a href={ templ.URL(fmt.Sprintf("/?page=%d", currentPage-1)) }
|
||||||
|
hx-get={ fmt.Sprintf("/?page=%d", currentPage-1) }
|
||||||
|
hx-target="#post-list"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<- Previous
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="page-info">
|
||||||
|
Page { fmt.Sprintf("%d", currentPage) }
|
||||||
|
</span>
|
||||||
|
|
||||||
|
if currentPage < totalPages {
|
||||||
|
<a href={ templ.URL(fmt.Sprintf("/?page=%d", currentPage+1)) }
|
||||||
|
hx-get={ fmt.Sprintf("/?page=%d", currentPage+1) }
|
||||||
|
hx-target="#post-list"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
Next ->
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
29
templates/post.templ
Normal file
29
templates/post.templ
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
templ PostDetail(post models.Post) {
|
||||||
|
<article class="post-detail">
|
||||||
|
<h1 class="post-title">{ post.Title }</h1>
|
||||||
|
<div class="post-meta">
|
||||||
|
Published: { post.PublishedAt.Format("January 2, 2006") } |
|
||||||
|
Tags:
|
||||||
|
for i, tag := range post.Tags {
|
||||||
|
if i > 0 {
|
||||||
|
,
|
||||||
|
}
|
||||||
|
<a href={ templ.URL("/tags/" + tag) }>{ tag }</a>
|
||||||
|
}
|
||||||
|
| { fmt.Sprintf("%d", post.WordCount) } words
|
||||||
|
</div>
|
||||||
|
<div class="post-content">
|
||||||
|
{ templ.Raw(post.Content) }
|
||||||
|
</div>
|
||||||
|
<div class="post-footer">
|
||||||
|
<a href="/"
|
||||||
|
hx-get="/"
|
||||||
|
hx-target="main"
|
||||||
|
hx-push-url="true">
|
||||||
|
<- Back to Posts
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
}
|
57
templates/posts.templ
Normal file
57
templates/posts.templ
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
templ PostList(posts []models.Post, page, totalPages int) {
|
||||||
|
<div id="post-list">
|
||||||
|
<ul class="blog-posts">
|
||||||
|
for _, post := range posts {
|
||||||
|
@PostListItem(post)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
if totalPages > 1 {
|
||||||
|
@Pagination(page, totalPages)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ PostListItem(post models.Post) {
|
||||||
|
<li class="blog-post">
|
||||||
|
<div class="post-title">
|
||||||
|
<a href={ templ.URL("/posts/" + post.Slug) }
|
||||||
|
hx-get={ "/posts/" + post.Slug }
|
||||||
|
hx-target="main"
|
||||||
|
hx-push-url="true">
|
||||||
|
{ post.Title }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="post-meta">
|
||||||
|
Posted: { post.PublishedAt.Format("2006-01-02") } |
|
||||||
|
Tags:
|
||||||
|
for i, tag := range post.Tags {
|
||||||
|
if i > 0 {
|
||||||
|
,
|
||||||
|
}
|
||||||
|
<a href={ templ.URL("/tags/" + tag) }
|
||||||
|
hx-get={ "/tags/" + tag }
|
||||||
|
hx-target="main"
|
||||||
|
hx-push-url="true">
|
||||||
|
{ tag }
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
| Words: { fmt.Sprintf("%d", post.WordCount) }
|
||||||
|
</div>
|
||||||
|
<div class="post-excerpt">
|
||||||
|
{ post.Excerpt }
|
||||||
|
</div>
|
||||||
|
<div class="read-more">
|
||||||
|
<a href={ templ.URL("/posts/" + post.Slug) }
|
||||||
|
hx-get={ "/posts/" + post.Slug }
|
||||||
|
hx-target="main"
|
||||||
|
hx-push-url=true>
|
||||||
|
[Read more...]
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
Reference in New Issue
Block a user