huge blog refactor
tidying up data structures; improvements to blog admin UI/UX, etc.
This commit is contained in:
parent
eaa2f6587d
commit
0c2aaa0b38
18 changed files with 432 additions and 239 deletions
|
|
@ -23,17 +23,19 @@ func Handler(app *model.AppState) http.Handler {
|
|||
}
|
||||
|
||||
type (
|
||||
blogPost struct {
|
||||
*model.BlogPost
|
||||
Author *model.Account
|
||||
}
|
||||
|
||||
blogPostGroup struct {
|
||||
blogPostCollection struct {
|
||||
Year int
|
||||
Posts []*blogPost
|
||||
Posts []*model.BlogPost
|
||||
}
|
||||
)
|
||||
|
||||
func (c *blogPostCollection) Clone() blogPostCollection {
|
||||
return blogPostCollection{
|
||||
Year: c.Year,
|
||||
Posts: slices.Clone(c.Posts),
|
||||
}
|
||||
}
|
||||
|
||||
func serveBlogIndex(app *model.AppState) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session := r.Context().Value("session").(*model.Session)
|
||||
|
|
@ -45,44 +47,36 @@ func serveBlogIndex(app *model.AppState) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
collections := []*blogPostGroup{}
|
||||
collectionPosts := []*blogPost{}
|
||||
collectionYear := -1
|
||||
collections := []*blogPostCollection{}
|
||||
collection := blogPostCollection{
|
||||
Posts: []*model.BlogPost{},
|
||||
Year: -1,
|
||||
}
|
||||
for i, post := range posts {
|
||||
author, err := controller.GetAccountByID(app.DB, post.AuthorID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve author of blog %s: %v\n", post.ID, err)
|
||||
continue
|
||||
if i == 0 {
|
||||
collection.Year = post.PublishDate.Year()
|
||||
}
|
||||
|
||||
if collectionYear == -1 {
|
||||
collectionYear = post.CreatedAt.Year()
|
||||
if post.PublishDate.Year() != collection.Year {
|
||||
clone := collection.Clone()
|
||||
collections = append(collections, &clone)
|
||||
collection = blogPostCollection{
|
||||
Year: post.PublishDate.Year(),
|
||||
Posts: []*model.BlogPost{},
|
||||
}
|
||||
}
|
||||
|
||||
authoredPost := blogPost{
|
||||
BlogPost: post,
|
||||
Author: author,
|
||||
}
|
||||
collection.Posts = append(collection.Posts, post)
|
||||
|
||||
if post.CreatedAt.Year() != collectionYear || i == len(posts) - 1 {
|
||||
if i == len(posts) - 1 {
|
||||
collectionPosts = append([]*blogPost{&authoredPost}, collectionPosts...)
|
||||
collections = append(collections, &collection)
|
||||
}
|
||||
collections = append(collections, &blogPostGroup{
|
||||
Year: collectionYear,
|
||||
Posts: slices.Clone(collectionPosts),
|
||||
})
|
||||
collectionPosts = []*blogPost{}
|
||||
collectionYear = post.CreatedAt.Year()
|
||||
}
|
||||
|
||||
collectionPosts = append(collectionPosts, &authoredPost)
|
||||
}
|
||||
|
||||
type blogsData struct {
|
||||
core.AdminPageData
|
||||
TotalPosts int
|
||||
Collections []*blogPostGroup
|
||||
Collections []*blogPostCollection
|
||||
}
|
||||
|
||||
err = templates.BlogsTemplate.Execute(w, blogsData{
|
||||
|
|
@ -108,7 +102,11 @@ func serveBlogPost(app *model.AppState) http.Handler {
|
|||
|
||||
post, err := controller.GetBlogPost(app.DB, blogID)
|
||||
if err != nil {
|
||||
fmt.Printf("can't find blog with ID %s\n", blogID)
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog %s: %v\n", blogID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if post == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,19 +109,8 @@ func adminIndexHandler(app *model.AppState) http.Handler {
|
|||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var latestBlogPost *BlogPost = nil
|
||||
if len(blogPosts) > 0 {
|
||||
author, err := controller.GetAccountByID(app.DB, blogPosts[0].AuthorID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to pull latest blog post author %s: %v\n", blogPosts[0].AuthorID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
latestBlogPost = &BlogPost{
|
||||
BlogPost: blogPosts[0],
|
||||
Author: author,
|
||||
}
|
||||
}
|
||||
var latestBlogPost *model.BlogPost = nil
|
||||
if len(blogPosts) > 0 { latestBlogPost = blogPosts[0] }
|
||||
blogCount, err := controller.GetBlogPostCount(app.DB, false)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to pull blog post count: %v\n", err)
|
||||
|
|
@ -137,7 +126,7 @@ func adminIndexHandler(app *model.AppState) http.Handler {
|
|||
ArtistCount int
|
||||
Tracks []*model.Track
|
||||
TrackCount int
|
||||
BlogPost *BlogPost
|
||||
LatestBlogPost *model.BlogPost
|
||||
BlogCount int
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +138,7 @@ func adminIndexHandler(app *model.AppState) http.Handler {
|
|||
ArtistCount: artistCount,
|
||||
Tracks: tracks,
|
||||
TrackCount: trackCount,
|
||||
BlogPost: latestBlogPost,
|
||||
LatestBlogPost: latestBlogPost,
|
||||
BlogCount: blogCount,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
.blog-collection {
|
||||
margin-bottom: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5em;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ input[type="text"] {
|
|||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
color: inherit;
|
||||
color: var(--fg-3);
|
||||
background-color: var(--bg-1);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
|
@ -67,6 +67,17 @@ input[type="text"] {
|
|||
background-color: var(--bg-3);
|
||||
}
|
||||
|
||||
#blogpost #publish-date {
|
||||
padding: .4em .5em;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background-color: var(--bg-1);
|
||||
color: var(--fg-3);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
#blogpost textarea {
|
||||
width: calc(100% - 2em);
|
||||
margin: 0;
|
||||
|
|
@ -95,6 +106,12 @@ input[type="text"] {
|
|||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
#blogpost .social-post-details {
|
||||
margin: 1em 0 1em 0;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
#blogpost .blog-actions {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
const blogID = document.getElementById("blogpost").dataset.id;
|
||||
const titleInput = document.getElementById("title");
|
||||
const publishDateInput = document.getElementById("publish-date");
|
||||
const descInput = document.getElementById("description");
|
||||
const mdInput = document.getElementById("markdown");
|
||||
const blueskyActorInput = document.getElementById("bluesky-actor");
|
||||
const blueskyPostInput = document.getElementById("bluesky-post");
|
||||
const blueskyRecordInput = document.getElementById("bluesky-record");
|
||||
const fediverseAccountInput = document.getElementById("fediverse-account");
|
||||
const fediverseStatusInput = document.getElementById("fediverse-status");
|
||||
const visInput = document.getElementById("visibility");
|
||||
const saveBtn = document.getElementById("save");
|
||||
const deleteBtn = document.getElementById("delete");
|
||||
|
|
@ -12,11 +15,18 @@ saveBtn.addEventListener("click", () => {
|
|||
fetch("/api/v1/blog/" + blogID, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
title: titleInput.value,
|
||||
title: titleInput.innerText,
|
||||
publish_date: publishDateInput.value + ":00Z",
|
||||
description: descInput.value,
|
||||
markdown: mdInput.value,
|
||||
bluesky_actor: blueskyActorInput.value,
|
||||
bluesky_post: blueskyPostInput.value,
|
||||
bluesky: {
|
||||
actor: blueskyActorInput.value,
|
||||
record: blueskyRecordInput.value,
|
||||
},
|
||||
fediverse: {
|
||||
account: fediverseAccountInput.value,
|
||||
status: fediverseStatusInput.value,
|
||||
},
|
||||
visible: visInput.value === "true",
|
||||
}),
|
||||
headers: { "Content-Type": "application/json" }
|
||||
|
|
@ -53,11 +63,13 @@ deleteBtn.addEventListener("click", () => {
|
|||
});
|
||||
});
|
||||
|
||||
[titleInput, descInput, mdInput, blueskyActorInput, blueskyPostInput, visInput].forEach(input => {
|
||||
[titleInput, publishDateInput, descInput, mdInput, visInput,
|
||||
blueskyActorInput, blueskyRecordInput,
|
||||
fediverseAccountInput, fediverseStatusInput].forEach(input => {
|
||||
input.addEventListener("change", () => {
|
||||
saveBtn.disabled = false;
|
||||
});
|
||||
input.addEventListener("keypress", () => {
|
||||
saveBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="blogpost">
|
||||
<h3 class="title"><a href="/admin/blogs/{{.ID}}">{{.Title}}</a>{{if not .Visible}} <small>(Not published)</small>{{end}}</h3>
|
||||
<p class="meta">
|
||||
<span class="author"><img src="/img/favicon.png" alt="{{.Author.Username}}'s avatar" width="32" height="32"/> {{.Author.Username}}</span>
|
||||
<span class="author"><img src="/img/favicon.png" alt="" width="32" height="32"/> {{.Author.DisplayName}}</span>
|
||||
<span class="date">• {{.PrintDate}}</span>
|
||||
</p>
|
||||
<p class="description">{{.Description}}</p>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@
|
|||
>{{.Post.Title}}</div>
|
||||
</h2>
|
||||
|
||||
<label for="publish-date">Publish Date</label>
|
||||
<input type="datetime-local" name="publish-date" id="publish-date" value="{{.Post.TextPublishDate}}">
|
||||
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
|
|
@ -43,20 +46,47 @@
|
|||
rows="30"
|
||||
>{{.Post.Markdown}}</textarea>
|
||||
|
||||
<div class="social-post-details">
|
||||
<div class="social-post-item">
|
||||
<label for="bluesky-actor">Bluesky Author DID</label>
|
||||
<input
|
||||
type="text"
|
||||
name="bluesky-actor"
|
||||
id="bluesky-actor"
|
||||
placeholder="did:plc:1234abcd..."
|
||||
value="{{if .Post.BlueskyActorID}}{{.Post.BlueskyActorID}}{{end}}">
|
||||
<label for="bluesky-post">Bluesky Post ID</label>
|
||||
value="{{if .Post.Bluesky}}{{.Post.Bluesky.ActorDID}}{{end}}">
|
||||
</div>
|
||||
<div class="social-post-item">
|
||||
<label for="bluesky-record">Bluesky Post ID</label>
|
||||
<input
|
||||
type="text"
|
||||
name="bluesky-post"
|
||||
id="bluesky-post"
|
||||
name="bluesky-record"
|
||||
id="bluesky-record"
|
||||
placeholder="3m109a03..."
|
||||
value="{{if .Post.BlueskyPostID}}{{.Post.BlueskyPostID}}{{end}}">
|
||||
value="{{if .Post.Bluesky}}{{.Post.Bluesky.RecordID}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="social-post-details">
|
||||
<div class="social-post-item">
|
||||
<label for="fediverse-account">Fediverse Account</label>
|
||||
<input
|
||||
type="text"
|
||||
name="fediverse-account"
|
||||
id="fediverse-account"
|
||||
placeholder="@me@my.fediverse.place"
|
||||
value="{{if .Post.Fediverse}}{{.Post.Fediverse.AccountID}}{{end}}">
|
||||
</div>
|
||||
<div class="social-post-item">
|
||||
<label for="fediverse-status">Fediverse Status ID</label>
|
||||
<input
|
||||
type="text"
|
||||
name="fediverse-status"
|
||||
id="fediverse-status"
|
||||
placeholder="never consistent ¯\_(ツ)_/¯"
|
||||
value="{{if .Post.Fediverse}}{{.Post.Fediverse.StatusID}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="visibility">Visibility</label>
|
||||
<select name="visibility" id="visibility">
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@
|
|||
<h2><a href="/admin/blogs/">Latest Blog Post</a> <small>({{.BlogCount}} total)</small></h2>
|
||||
<a class="button new" id="create-post">Create New</a>
|
||||
</div>
|
||||
{{if .BlogPost}}
|
||||
{{block "blogpost" .BlogPost}}{{end}}
|
||||
{{if .LatestBlogPost}}
|
||||
{{block "blogpost" .LatestBlogPost}}{{end}}
|
||||
{{else}}
|
||||
<p>There are no blog posts.</p>
|
||||
{{end}}
|
||||
|
|
|
|||
121
api/blog.go
121
api/blog.go
|
|
@ -29,38 +29,23 @@ func ServeAllBlogs(app *model.AppState) http.Handler {
|
|||
}
|
||||
|
||||
type (
|
||||
BlogAuthor struct {
|
||||
ID string `json:"id"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
BlogPost struct {
|
||||
ShortBlogPost struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Author BlogAuthor `json:"author"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Author *model.BlogAuthor `json:"author"`
|
||||
PublishDate time.Time `json:"publish_date"`
|
||||
}
|
||||
)
|
||||
resPosts := []*BlogPost{}
|
||||
resPosts := []*ShortBlogPost{}
|
||||
|
||||
for _, post := range posts {
|
||||
author, err := controller.GetAccountByID(app.DB, post.AuthorID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch author for blog post %s: %v\n", post.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resPosts = append(resPosts, &BlogPost{
|
||||
resPosts = append(resPosts, &ShortBlogPost{
|
||||
ID: post.ID,
|
||||
Title: post.Title,
|
||||
Description: post.Description,
|
||||
Author: BlogAuthor{
|
||||
ID: author.Username,
|
||||
Avatar: author.AvatarURL.String,
|
||||
},
|
||||
CreatedAt: post.CreatedAt,
|
||||
Author: &post.Author,
|
||||
PublishDate: post.PublishDate,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -90,11 +75,13 @@ func ServeBlog(app *model.AppState) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
if !blog.Visible && !privileged {
|
||||
if blog == nil || (!blog.Visible && !privileged) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
blog.Author.ID = blog.Author.DisplayName
|
||||
|
||||
err = json.NewEncoder(w).Encode(blog)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to serve blog post %s: %v\n", blogID, err)
|
||||
|
|
@ -122,11 +109,13 @@ func CreateBlog(app *model.AppState) http.Handler {
|
|||
|
||||
if blog.Title == "" { blog.Title = blog.ID }
|
||||
|
||||
if !blog.CreatedAt.Equal(time.Unix(0, 0)) {
|
||||
blog.CreatedAt = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
|
||||
if blog.PublishDate.Equal(time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||
blog.PublishDate = time.Date(
|
||||
time.Now().Year(), time.Now().Month(), time.Now().Day(),
|
||||
time.Now().Hour(), time.Now().Minute(), 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
blog.AuthorID = session.Account.ID
|
||||
blog.Author.ID = session.Account.ID
|
||||
|
||||
err = controller.CreateBlogPost(app.DB, &blog)
|
||||
if err != nil {
|
||||
|
|
@ -134,18 +123,21 @@ func CreateBlog(app *model.AppState) http.Handler {
|
|||
http.Error(w, fmt.Sprintf("Post %s already exists", blog.ID), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fmt.Printf("WARN: Failed to create blog %s: %s\n", blog.ID, err)
|
||||
fmt.Printf("WARN: Failed to create blog %s: %v\n", blog.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
app.Log.Info(log.TYPE_BLOG, "Blog post \"%s\" created by \"%s\".", blog.ID, session.Account.Username)
|
||||
|
||||
blog.Author.ID = session.Account.Username
|
||||
blog.Author.DisplayName = session.Account.Username
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
err = json.NewEncoder(w).Encode(blog)
|
||||
if err != nil {
|
||||
fmt.Printf("WARN: Blog post %s created, but failed to send JSON response: %s\n", blog.ID, err)
|
||||
fmt.Printf("WARN: Blog post %s created, but failed to send JSON response: %v\n", blog.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
|
|
@ -167,20 +159,64 @@ func UpdateBlog(app *model.AppState) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
err = json.NewDecoder(r.Body).Decode(blog)
|
||||
type (
|
||||
BlueskyRecord struct {
|
||||
ActorID string `json:"actor"`
|
||||
RecordID string `json:"record"`
|
||||
}
|
||||
FediverseStatus struct {
|
||||
AccountID string `json:"account"`
|
||||
StatusID string `json:"status"`
|
||||
}
|
||||
|
||||
UpdatedBlog struct {
|
||||
Title string `json:"title"`
|
||||
PublishDate time.Time `json:"publish_date"`
|
||||
Description string `json:"description"`
|
||||
Markdown string `json:"markdown"`
|
||||
Bluesky BlueskyRecord `json:"bluesky"`
|
||||
Fediverse FediverseStatus `json:"fediverse"`
|
||||
Visible bool `json:"visible"`
|
||||
}
|
||||
)
|
||||
var updatedBlog UpdatedBlog
|
||||
|
||||
err = json.NewDecoder(r.Body).Decode(&updatedBlog)
|
||||
if err != nil {
|
||||
fmt.Printf("WARN: Failed to update blog %s: %s\n", blog.ID, err)
|
||||
fmt.Printf("WARN: Failed to update blog %s: %v\n", blog.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
blog.Title = updatedBlog.Title
|
||||
blog.PublishDate = updatedBlog.PublishDate
|
||||
blog.Description = updatedBlog.Description
|
||||
blog.Markdown = updatedBlog.Markdown
|
||||
if len(updatedBlog.Bluesky.ActorID) > 0 && len(updatedBlog.Bluesky.RecordID) > 0 {
|
||||
blog.Bluesky = &model.BlueskyRecord{
|
||||
ActorDID: updatedBlog.Bluesky.ActorID,
|
||||
RecordID: updatedBlog.Bluesky.RecordID,
|
||||
}
|
||||
} else {
|
||||
blog.Bluesky = nil
|
||||
}
|
||||
if len(updatedBlog.Fediverse.AccountID) > 0 && len(updatedBlog.Fediverse.StatusID) > 0 {
|
||||
blog.Fediverse = &model.FediverseActivity{
|
||||
AccountID: updatedBlog.Fediverse.AccountID,
|
||||
StatusID: updatedBlog.Fediverse.StatusID,
|
||||
}
|
||||
} else {
|
||||
blog.Fediverse = nil
|
||||
}
|
||||
blog.Visible = updatedBlog.Visible
|
||||
|
||||
err = controller.UpdateBlogPost(app.DB, blogID, blog)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Printf("WARN: Failed to update release %s: %s\n", blogID, err)
|
||||
fmt.Printf("WARN: Failed to update release %s: %v\n", blogID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +226,7 @@ func UpdateBlog(app *model.AppState) http.Handler {
|
|||
w.WriteHeader(http.StatusCreated)
|
||||
err = json.NewEncoder(w).Encode(blog)
|
||||
if err != nil {
|
||||
fmt.Printf("WARN: Blog post %s created, but failed to send JSON response: %s\n", blog.ID, err)
|
||||
fmt.Printf("WARN: Blog post %s updated, but failed to send JSON response: %v\n", blog.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
|
|
@ -201,17 +237,32 @@ func DeleteBlog(app *model.AppState) http.Handler {
|
|||
session := r.Context().Value("session").(*model.Session)
|
||||
blogID := r.PathValue("id")
|
||||
|
||||
rowsAffected, err := controller.DeleteBlogPost(app.DB, blogID)
|
||||
blog, err := controller.GetBlogPost(app.DB, blogID)
|
||||
if err != nil {
|
||||
fmt.Printf("WARN: Failed to delete blog post %s: %s\n", blogID, err)
|
||||
fmt.Printf("WARN: Failed to fetch blog post %s: %v\n", blogID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
if blog == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
err = controller.DeleteBlogPost(app.DB, blogID)
|
||||
if err != nil {
|
||||
fmt.Printf("WARN: Failed to delete blog post %s: %v\n", blogID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
app.Log.Info(log.TYPE_BLOG, "Blog post \"%s\" deleted by \"%s\".", blogID, session.Account.Username)
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
err = json.NewEncoder(w).Encode(blog)
|
||||
if err != nil {
|
||||
fmt.Printf("WARN: Blog post %s deleted, but failed to send JSON response: %v\n", blog.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package controller
|
|||
|
||||
import (
|
||||
"arimelody-web/model"
|
||||
"database/sql"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
|
@ -9,37 +10,96 @@ import (
|
|||
func GetBlogPost(db *sqlx.DB, id string) (*model.BlogPost, error) {
|
||||
var blog = model.BlogPost{}
|
||||
|
||||
err := db.Get(&blog, "SELECT * FROM blogpost WHERE id=$1", id)
|
||||
rows, err := db.Query(
|
||||
"SELECT post.id,post.title,post.description,post.visible," +
|
||||
"post.publish_date,post.author,post.markdown," +
|
||||
"post.bluesky_actor,post.bluesky_record," +
|
||||
"post.fediverse_account,post.fediverse_status," +
|
||||
"author.id,author.username,author.avatar_url " +
|
||||
"FROM blogpost AS post " +
|
||||
"JOIN account AS author ON post.author=author.id " +
|
||||
"WHERE post.id=$1",
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blueskyActor := sql.NullString{}
|
||||
blueskyRecord := sql.NullString{}
|
||||
fediverseAccount := sql.NullString{}
|
||||
fediverseStatus := sql.NullString{}
|
||||
|
||||
if !rows.Next() {
|
||||
return nil, nil
|
||||
}
|
||||
err = rows.Scan(
|
||||
&blog.ID, &blog.Title, &blog.Description, &blog.Visible,
|
||||
&blog.PublishDate, &blog.Author.ID, &blog.Markdown,
|
||||
&blueskyActor, &blueskyRecord,
|
||||
&fediverseAccount, &fediverseStatus,
|
||||
&blog.Author.ID, &blog.Author.DisplayName, &blog.Author.AvatarURL,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if blueskyActor.Valid && blueskyRecord.Valid {
|
||||
blog.Bluesky = &model.BlueskyRecord{
|
||||
ActorDID: blueskyActor.String,
|
||||
RecordID: blueskyRecord.String,
|
||||
}
|
||||
}
|
||||
if fediverseAccount.Valid && fediverseStatus.Valid {
|
||||
blog.Fediverse = &model.FediverseActivity{
|
||||
AccountID: fediverseAccount.String,
|
||||
StatusID: fediverseStatus.String,
|
||||
}
|
||||
}
|
||||
|
||||
return &blog, nil
|
||||
}
|
||||
|
||||
func GetBlogPosts(db *sqlx.DB, onlyVisible bool, limit int, offset int) ([]*model.BlogPost, error) {
|
||||
var blogs = []*model.BlogPost{}
|
||||
|
||||
query := "SELECT * FROM blogpost ORDER BY created_at"
|
||||
if onlyVisible {
|
||||
query = "SELECT * FROM blogpost WHERE visible=true ORDER BY created_at"
|
||||
}
|
||||
query := "SELECT post.id,post.title,post.publish_date," +
|
||||
"post.description,post.visible," +
|
||||
"author.id,author.username,author.avatar_url " +
|
||||
"FROM blogpost AS post " +
|
||||
"JOIN account AS author ON post.author=author.id"
|
||||
if onlyVisible { query += " WHERE visible=true" }
|
||||
query += " ORDER BY publish_date DESC"
|
||||
|
||||
var rows *sql.Rows
|
||||
|
||||
var err error
|
||||
if limit < 0 {
|
||||
err = db.Select(&blogs, query)
|
||||
rows, err = db.Query(query)
|
||||
} else {
|
||||
err = db.Select(&blogs, query + " LIMIT $1 OFFSET $2", limit, offset)
|
||||
rows, err = db.Query(query + " LIMIT $1 OFFSET $2", limit, offset)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// for range 4 {
|
||||
// blog := *blogs[len(blogs)-1]
|
||||
// blog.CreatedAt = blog.CreatedAt.Add(time.Hour * -5000)
|
||||
// blogs = append(blogs, &blog)
|
||||
// }
|
||||
for rows.Next() {
|
||||
blog := model.BlogPost{}
|
||||
err = rows.Scan(
|
||||
&blog.ID,
|
||||
&blog.Title,
|
||||
&blog.PublishDate,
|
||||
&blog.Description,
|
||||
&blog.Visible,
|
||||
&blog.Author.ID,
|
||||
&blog.Author.DisplayName,
|
||||
&blog.Author.AvatarURL,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blogs = append(blogs, &blog)
|
||||
}
|
||||
|
||||
return blogs, nil
|
||||
}
|
||||
|
|
@ -57,54 +117,77 @@ func GetBlogPostCount(db *sqlx.DB, onlyVisible bool) (int, error) {
|
|||
}
|
||||
|
||||
func CreateBlogPost(db *sqlx.DB, post *model.BlogPost) error {
|
||||
var blueskyActor *string
|
||||
var blueskyRecord *string
|
||||
if post.Bluesky != nil {
|
||||
blueskyActor = &post.Bluesky.ActorDID
|
||||
blueskyRecord = &post.Bluesky.RecordID
|
||||
}
|
||||
|
||||
var fediverseAccount *string
|
||||
var fediverseStatus *string
|
||||
if post.Fediverse != nil {
|
||||
fediverseAccount = &post.Fediverse.AccountID
|
||||
fediverseStatus = &post.Fediverse.StatusID
|
||||
}
|
||||
|
||||
_, err := db.Exec(
|
||||
"INSERT INTO blogpost (id,title,description,visible,author,markdown,html,bluesky_actor,bluesky_post) " +
|
||||
"VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)",
|
||||
post.ID,
|
||||
post.Title,
|
||||
post.Description,
|
||||
post.Visible,
|
||||
post.AuthorID,
|
||||
post.Markdown,
|
||||
post.HTML,
|
||||
post.BlueskyActorID,
|
||||
post.BlueskyPostID,
|
||||
"INSERT INTO blogpost (" +
|
||||
"id,title,description,visible," +
|
||||
"publish_date,author,markdown," +
|
||||
"bluesky_actor,bluesky_record," +
|
||||
"fediverse_account,fediverse_status) " +
|
||||
"VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)",
|
||||
post.ID, post.Title, post.Description, post.Visible,
|
||||
post.PublishDate, post.Author.ID, post.Markdown,
|
||||
blueskyActor, blueskyRecord,
|
||||
fediverseAccount, fediverseStatus,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateBlogPost(db *sqlx.DB, postID string, post *model.BlogPost) error {
|
||||
var blueskyActor string
|
||||
var blueskyRecord string
|
||||
if post.Bluesky != nil {
|
||||
blueskyActor = post.Bluesky.ActorDID
|
||||
blueskyRecord = post.Bluesky.RecordID
|
||||
}
|
||||
|
||||
var fediverseAccount string
|
||||
var fediverseStatus string
|
||||
if post.Fediverse != nil {
|
||||
fediverseAccount = post.Fediverse.AccountID
|
||||
fediverseStatus = post.Fediverse.StatusID
|
||||
}
|
||||
|
||||
_, err := db.Exec(
|
||||
"UPDATE blogpost SET " +
|
||||
"id=$2,title=$3,description=$4,visible=$5,author=$6,markdown=$7,html=$8,bluesky_actor=$9,bluesky_post=$10,modified_at=CURRENT_TIMESTAMP " +
|
||||
"id=$2,title=$3,description=$4,visible=$5," +
|
||||
"publish_date=$6,author=$7,markdown=$8," +
|
||||
"bluesky_actor=$9,bluesky_record=$10," +
|
||||
"fediverse_account=$11,fediverse_status=$12 " +
|
||||
"WHERE id=$1",
|
||||
postID,
|
||||
post.ID,
|
||||
post.Title,
|
||||
post.Description,
|
||||
post.Visible,
|
||||
post.AuthorID,
|
||||
post.Markdown,
|
||||
post.HTML,
|
||||
post.BlueskyActorID,
|
||||
post.BlueskyPostID,
|
||||
post.ID, post.Title, post.Description, post.Visible,
|
||||
post.PublishDate, post.Author.ID, post.Markdown,
|
||||
blueskyActor, blueskyRecord,
|
||||
fediverseAccount, fediverseStatus,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteBlogPost(db *sqlx.DB, postID string) (int64, error) {
|
||||
result, err := db.Exec(
|
||||
func DeleteBlogPost(db *sqlx.DB, postID string) error {
|
||||
_, err := db.Exec(
|
||||
"DELETE FROM blogpost "+
|
||||
"WHERE id=$1",
|
||||
postID,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
|
||||
return rowsAffected, nil
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import (
|
|||
|
||||
const BSKY_API_BASE = "https://public.api.bsky.app"
|
||||
|
||||
func FetchThreadViewPost(actorID string, postID string) (*model.ThreadViewPost, error) {
|
||||
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", actorID, postID)
|
||||
func FetchThreadViewPost(actorID string, recordID string) (*model.ThreadViewPost, error) {
|
||||
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", actorID, recordID)
|
||||
|
||||
req, err := http.NewRequest(
|
||||
http.MethodGet,
|
||||
|
|
|
|||
|
|
@ -133,12 +133,12 @@ CREATE TABLE arimelody.blogpost (
|
|||
description TEXT NOT NULL,
|
||||
visible BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
|
||||
modified_at TIMESTAMP,
|
||||
author UUID NOT NULL,
|
||||
markdown TEXT NOT NULL,
|
||||
html TEXT NOT NULL,
|
||||
bluesky_actor TEXT,
|
||||
bluesky_post TEXT
|
||||
bluesky_record TEXT,
|
||||
fediverse_account TEXT,
|
||||
fediverse_status TEXT
|
||||
);
|
||||
ALTER TABLE arimelody.blogpost ADD CONSTRAINT blogpost_pk PRIMARY KEY (id);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ CREATE TABLE arimelody.blogpost (
|
|||
description TEXT NOT NULL,
|
||||
visible BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
|
||||
modified_at TIMESTAMP,
|
||||
author UUID NOT NULL,
|
||||
markdown TEXT NOT NULL,
|
||||
html TEXT NOT NULL,
|
||||
bluesky_actor TEXT,
|
||||
bluesky_post TEXT
|
||||
bluesky_record TEXT,
|
||||
fediverse_account TEXT,
|
||||
fediverse_status TEXT
|
||||
);
|
||||
ALTER TABLE arimelody.blogpost ADD CONSTRAINT blogpost_pk PRIMARY KEY (id);
|
||||
ALTER TABLE arimelody.blogpost ADD CONSTRAINT blogpost_author_fk FOREIGN KEY (author) REFERENCES account(id) ON DELETE CASCADE;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,38 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
BlueskyRecord struct {
|
||||
ActorDID string `json:"actor"`
|
||||
RecordID string `json:"record"`
|
||||
}
|
||||
FediverseActivity struct {
|
||||
AccountID string `json:"account"`
|
||||
StatusID string `json:"status"`
|
||||
}
|
||||
|
||||
BlogAuthor struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
BlogPost struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Title string `json:"title" db:"title"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Visible bool `json:"visible" db:"visible"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
ModifiedAt sql.NullTime `json:"modified_at" db:"modified_at"`
|
||||
AuthorID string `json:"author" db:"author"`
|
||||
Markdown string `json:"markdown" db:"markdown"`
|
||||
HTML template.HTML `json:"html" db:"html"`
|
||||
BlueskyActorID *string `json:"bluesky_actor" db:"bluesky_actor"`
|
||||
BlueskyPostID *string `json:"bluesky_post" db:"bluesky_post"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Visible bool `json:"visible"`
|
||||
PublishDate time.Time `json:"publish_date"`
|
||||
Author BlogAuthor `json:"author"`
|
||||
Markdown string `json:"markdown"`
|
||||
Bluesky *BlueskyRecord `json:"bluesky"`
|
||||
Fediverse *FediverseActivity `json:"fediverse"`
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -36,16 +47,13 @@ func (b *BlogPost) TitleNormalised() string {
|
|||
}
|
||||
|
||||
func (b *BlogPost) GetMonth() string {
|
||||
return fmt.Sprintf("%02d", int(b.CreatedAt.Month()))
|
||||
return fmt.Sprintf("%02d", int(b.PublishDate.Month()))
|
||||
}
|
||||
|
||||
func (b *BlogPost) PrintDate() string {
|
||||
return b.CreatedAt.Format("2 January 2006, 15:04")
|
||||
return b.PublishDate.Format("2 January 2006, 15:04")
|
||||
}
|
||||
|
||||
func (b *BlogPost) PrintModifiedDate() string {
|
||||
if !b.ModifiedAt.Valid {
|
||||
return ""
|
||||
}
|
||||
return b.ModifiedAt.Time.Format("2 January 2006, 15:04")
|
||||
func (b *BlogPost) TextPublishDate() string {
|
||||
return b.PublishDate.Format("2006-01-02T15:04")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,11 @@ article#blog p {
|
|||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
article#blog p.no-content {
|
||||
font-style: italic;
|
||||
opacity: .66;
|
||||
}
|
||||
|
||||
article#blog sub {
|
||||
opacity: .75;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@
|
|||
<p>there are no posts! 🍃</p>
|
||||
{{end}}
|
||||
{{range .Collections}}
|
||||
<h2 id="{{.Name}}" class="collection-name">{{.Name}}</h2>
|
||||
<h2 id="{{.Year}}" class="collection-name">{{.Year}}</h2>
|
||||
{{range .Posts}}
|
||||
<article class="blog-post">
|
||||
<h3 class="blog-title"><a href="/blog/{{.ID}}">{{.Title}}</a></h3>
|
||||
<p class="blog-meta">
|
||||
<span class="blog-author"><img src="/img/favicon.png" alt="{{.Author.Username}}'s avatar" width="32" height="32"/> {{.Author.Username}}</span>
|
||||
<span class="blog-author"><img src="/img/favicon.png" alt="" width="32" height="32"/> {{.Author.DisplayName}}</span>
|
||||
<span class="blog-date">• {{.PrintDate}}</span>
|
||||
</p>
|
||||
{{if ne .Description ""}}
|
||||
|
|
|
|||
|
|
@ -84,13 +84,17 @@
|
|||
<article id="blog">
|
||||
<header>
|
||||
<h1 class="typeout"># {{.Title}}</h1>
|
||||
<p class="blog-author">by <a href="/blog?author={{.Author.Username}}">{{.Author.Username}} <img src="/img/favicon.png" alt="" aria-hidden="true" width="32" height="32"/></a></p>
|
||||
<p class="blog-date">posted {{.PrintDate}}{{if .ModifiedAt.Valid}} <span class="blog-modified-date">• updated {{.PrintModifiedDate}}</span>{{end}}</p>
|
||||
<p class="blog-author">by <a href="/blog?author={{.Author.DisplayName}}">{{.Author.DisplayName}} <img src="/img/favicon.png" alt="" aria-hidden="true" width="32" height="32"/></a></p>
|
||||
<p class="blog-date">posted {{.PrintDate}}</p>
|
||||
</header>
|
||||
|
||||
<hr>
|
||||
|
||||
{{.HTML}}
|
||||
{{if .BlogHTML}}
|
||||
{{.BlogHTML}}
|
||||
{{else}}
|
||||
<p class="no-content">No content.</p>
|
||||
{{end}}
|
||||
</article>
|
||||
|
||||
{{if ne .BlueskyURL ""}}
|
||||
|
|
|
|||
93
view/blog.go
93
view/blog.go
|
|
@ -6,7 +6,6 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"arimelody-web/controller"
|
||||
|
|
@ -19,18 +18,18 @@ import (
|
|||
)
|
||||
|
||||
type (
|
||||
BlogView struct {
|
||||
Collections []*BlogViewPostCollection
|
||||
blogView struct {
|
||||
Collections []*blogPostCollection
|
||||
}
|
||||
|
||||
BlogViewPostCollection struct {
|
||||
Name string
|
||||
Posts []*BlogPostView
|
||||
blogPostCollection struct {
|
||||
Year int
|
||||
Posts []*model.BlogPost
|
||||
}
|
||||
|
||||
BlogPostView struct {
|
||||
blogPostView struct {
|
||||
*model.BlogPost
|
||||
Author *model.Account
|
||||
BlogHTML template.HTML
|
||||
Comments []*model.ThreadViewPost
|
||||
Likes int
|
||||
Boosts int
|
||||
|
|
@ -39,6 +38,13 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func (c *blogPostCollection) Clone() blogPostCollection {
|
||||
return blogPostCollection{
|
||||
Year: c.Year,
|
||||
Posts: slices.Clone(c.Posts),
|
||||
}
|
||||
}
|
||||
|
||||
var mdRenderer = html.NewRenderer(html.RendererOptions{
|
||||
Flags: html.CommonFlags | html.HrefTargetBlank,
|
||||
})
|
||||
|
|
@ -51,7 +57,7 @@ func BlogHandler(app *model.AppState) http.Handler {
|
|||
})
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
dbPosts, err := controller.GetBlogPosts(app.DB, true, -1, 0)
|
||||
posts, err := controller.GetBlogPosts(app.DB, true, -1, 0)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
http.NotFound(w, r)
|
||||
|
|
@ -62,43 +68,33 @@ func BlogHandler(app *model.AppState) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
collections := []*BlogViewPostCollection{}
|
||||
posts := []*BlogPostView{}
|
||||
collectionYear := 0
|
||||
for i, post := range dbPosts {
|
||||
author, err := controller.GetAccountByID(app.DB, post.AuthorID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve author of blog %s: %v\n", post.ID, err)
|
||||
continue
|
||||
collections := []*blogPostCollection{}
|
||||
collection := blogPostCollection{
|
||||
Posts: []*model.BlogPost{},
|
||||
Year: -1,
|
||||
}
|
||||
|
||||
for i, post := range posts {
|
||||
if i == 0 {
|
||||
collectionYear = post.CreatedAt.Year()
|
||||
collection.Year = post.PublishDate.Year()
|
||||
}
|
||||
|
||||
if post.CreatedAt.Year() != collectionYear || i == len(dbPosts) - 1 {
|
||||
if i == len(dbPosts) - 1 {
|
||||
posts = append(posts, &BlogPostView{
|
||||
BlogPost: post,
|
||||
Author: author,
|
||||
})
|
||||
if post.PublishDate.Year() != collection.Year {
|
||||
clone := collection.Clone()
|
||||
collections = append(collections, &clone)
|
||||
collection = blogPostCollection{
|
||||
Year: post.PublishDate.Year(),
|
||||
Posts: []*model.BlogPost{},
|
||||
}
|
||||
postsCopy := slices.Clone(posts)
|
||||
collections = append(collections, &BlogViewPostCollection{
|
||||
Name: strconv.Itoa(collectionYear),
|
||||
Posts: postsCopy,
|
||||
})
|
||||
posts = []*BlogPostView{}
|
||||
collectionYear = post.CreatedAt.Year()
|
||||
}
|
||||
|
||||
posts = append(posts, &BlogPostView{
|
||||
BlogPost: post,
|
||||
Author: author,
|
||||
})
|
||||
collection.Posts = append(collection.Posts, post)
|
||||
|
||||
if i == len(posts) - 1 {
|
||||
collections = append(collections, &collection)
|
||||
}
|
||||
}
|
||||
|
||||
err = templates.BlogTemplate.Execute(w, BlogView{
|
||||
err = templates.BlogTemplate.Execute(w, blogView{
|
||||
Collections: collections,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -119,10 +115,14 @@ func ServeBlogPost(app *model.AppState, blogPostID string) http.Handler {
|
|||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post %s: %v\n", blogPostID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if blog == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !blog.Visible {
|
||||
session, err := controller.GetSessionFromRequest(app, r)
|
||||
|
|
@ -138,37 +138,32 @@ func ServeBlogPost(app *model.AppState, blogPostID string) http.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
author, err := controller.GetAccountByID(app.DB, blog.AuthorID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve author of blog %s: %v\n", blog.ID, err)
|
||||
}
|
||||
|
||||
// blog.Markdown += " <i class=\"end-mark\"></i>"
|
||||
|
||||
mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs)
|
||||
md := mdParser.Parse([]byte(blog.Markdown))
|
||||
blog.HTML = template.HTML(markdown.Render(md, mdRenderer))
|
||||
blogHTML := template.HTML(markdown.Render(md, mdRenderer))
|
||||
|
||||
comments := []*model.ThreadViewPost{}
|
||||
likeCount := 0
|
||||
boostCount := 0
|
||||
var blueskyURL string
|
||||
var blueskyPost *model.ThreadViewPost
|
||||
if blog.BlueskyActorID != nil && blog.BlueskyPostID != nil {
|
||||
blueskyPost, err = controller.FetchThreadViewPost(*blog.BlueskyActorID, *blog.BlueskyPostID)
|
||||
if blog.Bluesky != nil {
|
||||
blueskyPost, err = controller.FetchThreadViewPost(blog.Bluesky.ActorDID, blog.Bluesky.RecordID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err)
|
||||
} else {
|
||||
comments = append(comments, blueskyPost.Replies...)
|
||||
likeCount += blueskyPost.Post.LikeCount
|
||||
boostCount += blueskyPost.Post.RepostCount
|
||||
blueskyURL = fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blueskyPost.Post.Author.Handle, *blog.BlueskyPostID)
|
||||
blueskyURL = fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blueskyPost.Post.Author.Handle, blog.Bluesky.RecordID)
|
||||
}
|
||||
}
|
||||
|
||||
err = templates.BlogPostTemplate.Execute(w, BlogPostView{
|
||||
err = templates.BlogPostTemplate.Execute(w, blogPostView{
|
||||
BlogPost: blog,
|
||||
Author: author,
|
||||
BlogHTML: blogHTML,
|
||||
Comments: comments,
|
||||
Likes: likeCount,
|
||||
Boosts: boostCount,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue