blog visitor frontend (pretty much) done!

This commit is contained in:
ari melody 2025-06-24 01:32:30 +01:00
parent 3d64333b4f
commit faf6095d16
Signed by: ari
GPG key ID: CF99829C92678188
16 changed files with 903 additions and 463 deletions

View file

@ -5,7 +5,9 @@ import (
"html/template"
"net/http"
"os"
"time"
"slices"
"strconv"
"strings"
"arimelody-web/controller"
"arimelody-web/model"
@ -16,14 +18,26 @@ import (
"github.com/gomarkdown/markdown/parser"
)
type BlogView struct {
*model.Blog
Comments []*model.ThreadViewPost
Likes int
Reposts int
BlueskyURL string
MastodonURL string
}
type (
BlogView struct {
Collections []*BlogViewPostCollection
}
BlogViewPostCollection struct {
Name string
Posts []*BlogPostView
}
BlogPostView struct {
*model.BlogPost
Author *model.Account
Comments []*model.ThreadViewPost
Likes int
Boosts int
BlueskyURL string
MastodonURL string
}
)
var mdRenderer = html.NewRenderer(html.RendererOptions{
Flags: html.CommonFlags | html.HrefTargetBlank,
@ -31,41 +45,105 @@ var mdRenderer = html.NewRenderer(html.RendererOptions{
func BlogHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
blog := model.Blog{
Title: "hello world!",
Description: "lorem ipsum yadda yadda something boobies babababababababa",
Visible: true,
Date: time.Now(),
AuthorID: "ari",
Markdown:
`
**i'm ari!** (she/her) 🏳🏳🌈💫🦆🇮🇪
if strings.Count(r.URL.Path, "/") > 1 {
http.NotFound(w, r)
return
}
welcome to my blog!
if len(r.URL.Path) > 1 {
ServeBlogPost(app, r.URL.Path[1:]).ServeHTTP(w, r)
return
}
i'm a musician, developer, streamer, youtuber, and probably a bunch of other things i forgot to mention!
dbPosts, err := controller.GetBlogPosts(app.DB, true, -1, 0)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
return
}
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog posts: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
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
}
## code block test
if i == 0 {
collectionYear = post.CreatedAt.Year()
}
~~~ c
#include <stdio.h>
if post.CreatedAt.Year() != collectionYear || i == len(dbPosts) - 1 {
if i == len(dbPosts) - 1 {
posts = append(posts, &BlogPostView{
BlogPost: post,
Author: author,
})
}
postsCopy := slices.Clone(posts)
collections = append(collections, &BlogViewPostCollection{
Name: strconv.Itoa(collectionYear),
Posts: postsCopy,
})
posts = []*BlogPostView{}
collectionYear = post.CreatedAt.Year()
}
int main(int argc, char *argv[]) {
printf("hello world!~\n");
return 0;
}
~~~
posts = append(posts, &BlogPostView{
BlogPost: post,
Author: author,
})
}
## aridoodle
this is `+"`"+`aridoodle`+"`"+`. please take care of her.
![aridoodle](/img/aridoodle.png)
`,
BlueskyActorID: "did:plc:yct6cvgfipngizry5umzkxr3",
BlueskyPostID: "3llsudsx7pc2u",
err = templates.BlogTemplate.Execute(w, BlogView{
Collections: collections,
})
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Error rendering blog post: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
})
}
func ServeBlogPost(app *model.AppState, blogPostID string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
blog, err := controller.GetBlogPost(app.DB, blogPostID)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
return
}
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if !blog.Visible {
session, err := controller.GetSessionFromRequest(app, r)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if session == nil || session.Account == nil {
http.NotFound(w, r)
return
}
}
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>"
@ -74,23 +152,32 @@ this is `+"`"+`aridoodle`+"`"+`. please take care of her.
blog.HTML = template.HTML(markdown.Render(md, mdRenderer))
comments := []*model.ThreadViewPost{}
blueskyPost, err := controller.FetchThreadViewPost(blog.BlueskyActorID, blog.BlueskyPostID)
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 := 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 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)
}
}
err = templates.BlogTemplate.Execute(w, BlogView{
Blog: &blog,
Comments: blueskyPost.Replies,
Likes: blueskyPost.Post.LikeCount,
Reposts: blueskyPost.Post.RepostCount,
BlueskyURL: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blog.BlueskyActorID, blog.BlueskyPostID),
MastodonURL: "#",
err = templates.BlogPostTemplate.Execute(w, BlogPostView{
BlogPost: blog,
Author: author,
Comments: comments,
Likes: likeCount,
Boosts: boostCount,
BlueskyURL: blueskyURL,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error rendering blog post: %v\n", err)
fmt.Fprintf(os.Stderr, "WARN: Error rendering blog post: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

View file

@ -1,15 +1,15 @@
{{define "head"}}
<title>{{.Title}} - ari melody 💫</title>
<title>blog - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<meta name="description" content="{{.Description}}">
<meta name="description" content="thoughts from your local SPACEGIRL 💫">
<meta property="og:title" content="{{.Title}}">
<meta property="og:title" content="ari melody blog 💫">
<meta property="og:type" content="article">
<meta property="og:url" content="www.arimelody.me/blog/{{.Date.Year}}/{{.GetMonth}}/{{.TitleNormalised}}">
<meta property="og:image" content="https://www.arimelody.me/img/favicon.png">
<meta property="og:url" content="https://arimelody.space/blog/">
<meta property="og:image" content="https://arimelody.space/img/favicon.png">
<meta property="og:site_name" content="ari melody">
<meta property="og:description" content="{{.Description}}">
<meta property="og:description" content="thoughts from your local SPACEGIRL 💫">
<link rel="stylesheet" href="/style/main.css">
<link rel="stylesheet" href="/style/index.css">
@ -18,138 +18,34 @@
{{define "content"}}
<main>
<div id="blog-sidebar">
<ul>
<li>
<a href="#copy-link" id="blog-copy-link" title="copy link">
<div class="dark-only">
<img src="/img/blog/copy-link-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/copy-link-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
{{if ne .BlueskyURL ""}}
<li>
<a href="{{.BlueskyURL}}" id="blog-share-bsky" title="share on bluesky">
<div class="dark-only">
<img src="/img/blog/bluesky-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/bluesky-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
<li>
<a href="{{.BlueskyURL}}" id="blog-like" title="like this post">
<div class="dark-only">
<img src="/img/blog/like-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/like-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
<li>
<a href="{{.BlueskyURL}}" id="blog-boost" title="boost this post">
<div class="dark-only">
<img src="/img/blog/boost-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/boost-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
<li>
<a href="#comments" id="blog-comments" title="comments">
<div class="dark-only">
<img src="/img/blog/comment-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/comment-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
{{end}}
</ul>
</div>
<div id="blog-container">
<article class="blog">
<h1 class="typeout">{{.Title}}</h1>
<p class="blog-date">Posted by <a href="/blog/{{.AuthorID}}">{{.AuthorID}}</a> @ {{.PrintDate}}</p>
<h1 class="typeout"># blog</h1>
<p class="">thoughts from your local SPACEGIRL 💫</p>
<hr>
{{.HTML}}
<hr>
</article>
{{if ne .BlueskyURL ""}}
<hr>
<div id="interactions">
<span class="likes">❤️ {{.Likes}}</span>
<span class="reposts">🔁 {{.Reposts}}</span>
</div>
<p class="comment-callout">
join the conversation on
<a class="bluesky" href="{{.BlueskyURL}}" target="_blank">Bluesky 🦋</a>
<!-- TODO: mastodon support -->
<!--
and
<a class="mastodon" href="{{.MastodonURL}}" target="_blank">Mastodon 🐘</a>
-->
</p>
<div id="comments">
{{range .Comments}}
{{template "comment" .}}
{{end}}
</div>
<div id="posts">
{{if eq (len .Collections) 0}}
<p>there are no posts! 🍃</p>
{{end}}
{{range .Collections}}
<h2 id="{{.Name}}" class="collection-name">{{.Name}}</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-date">&bull; {{.PrintDate}}</span>
</p>
{{if ne .Description ""}}
<p class="blog-description">{{.Description}}</p>
{{end}}
</article>
{{end}}
{{end}}
<script type="module" src="/script/blog.js"></script>
</div>
<!-- <button type="submit" class="link-button" id="load-more">load more</button> -->
<script src="/script/blog.js" type="module" defer></script>
</main>
{{end}}
{{define "comment"}}
<article class="comment">
<div class="comment-hover">
<div class="comment-header">
<a href="https://bsky.app/profile/{{.Post.Author.DID}}" target="_blank">
<img class="avatar" src="{{.Post.Author.Avatar}}" alt="{{.Post.Author.DisplayName}}'s avatar" width="32" height="32">
<span class="display-name">{{.Post.Author.DisplayName}}</span>
<span class="handle">@{{.Post.Author.Handle}}</span>
</a>
</div>
<a class="comment-body" href="{{.Post.BskyURL}}" target="_blank">
<div>
<p class="comment-text">{{.Post.Record.Text}}</p>
{{if .Post.HasImage}}
<p class="comment-images">
{{range .Post.Embed.Media.Images}}
<a href="{{.Fullsize}}" target="_blank">[image]</a>
{{end}}
</p>
{{end}}
</div>
<div class="comment-footer">
<span>{{.Post.LikeCount}} like{{if ne .Post.LikeCount 1}}s{{end}}</span>
&bull;
<span>{{.Post.RepostCount}} repost{{if ne .Post.RepostCount 1}}s{{end}}</span>
&bull;
<span class="comment-date">{{.Post.Record.CreatedAtPrint}}</span>
</div>
</a>
</div>
<div class="comment-replies">
{{range .Replies}}
{{template "comment" .}}
{{end}}
</div>
</article>
{{end}}

178
view/blogpost.html Normal file
View file

@ -0,0 +1,178 @@
{{define "head"}}
<title>{{.Title}} - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<meta name="description" content="{{.Description}}">
<meta property="og:title" content="{{.Title}}">
<meta property="og:type" content="article">
<meta property="og:url" content="https://arimelody.space/blog/{{.ID}}">
<meta property="og:image" content="https://arimelody.space/img/favicon.png">
<meta property="og:site_name" content="ari melody">
<meta property="og:description" content="{{.Description}}">
<link rel="stylesheet" href="/style/main.css">
<link rel="stylesheet" href="/style/index.css">
<link rel="stylesheet" href="/style/blogpost.css">
{{end}}
{{define "content"}}
<main>
<div id="blog-sidebar">
<ul>
<li>
<a href="#copy-link" id="blog-copy-link" title="copy link">
<div class="dark-only">
<img src="/img/blog/copy-link-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/copy-link-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
{{if ne .BlueskyURL ""}}
<li>
<a href="{{.BlueskyURL}}" id="blog-share-bsky" title="share on bluesky">
<div class="dark-only">
<img src="/img/blog/bluesky-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/bluesky-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
<hr>
<li>
<a href="{{.BlueskyURL}}" id="blog-like" title="like this post">
<div class="dark-only">
<img src="/img/blog/like-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/like-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
<li>
<a href="{{.BlueskyURL}}" id="blog-boost" title="boost this post">
<div class="dark-only">
<img src="/img/blog/boost-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/boost-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
<li>
<a href="#comments" id="blog-comments" title="comments">
<div class="dark-only">
<img src="/img/blog/comment-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/comment-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
{{end}}
</ul>
</div>
<div id="blog-container">
<article class="blog">
<div 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="{{.Author.Username}}'s avatar" width="32" height="32"/></a></p>
<p class="blog-date">posted {{.PrintDate}}{{if .ModifiedAt.Valid}} <span class="blog-modified-date">&bull; updated {{.PrintModifiedDate}}</span>{{end}}</p>
</div>
<hr>
<div id="blog-content">
{{.HTML}}
</div>
</article>
{{if ne .BlueskyURL ""}}
<hr>
<div id="interactions">
<button class="likes" aria-label="{{.Likes}} likes">
<div class="dark-only">
<img src="/img/blog/like-dark.svg" alt="" width="32" height="32">
</div>
<div class="light-only">
<img src="/img/blog/like-light.svg" alt="" width="32" height="32">
</div>
{{.Likes}}
</button>
<button class="boosts" aria-label="{{.Boosts}} boosts">
<div class="dark-only">
<img src="/img/blog/boost-dark.svg" alt="" width="32" height="32">
</div>
<div class="light-only">
<img src="/img/blog/boost-light.svg" alt="" width="32" height="32">
</div>
{{.Boosts}}
</button>
<p class="comment-callout">
join the conversation on
<a class="bluesky" href="{{.BlueskyURL}}">Bluesky 🦋</a>
<!-- TODO: mastodon support -->
<!--
and
<a class="mastodon" href="{{.MastodonURL}}">Mastodon 🐘</a>
-->
</p>
</div>
<div id="comments">
{{range .Comments}}
{{template "comment" .}}
{{end}}
</div>
{{end}}
<script type="module" src="/script/blogpost.js"></script>
</div>
</main>
{{end}}
{{define "comment"}}
<article class="comment">
<div class="comment-hover">
<div class="comment-header">
<a href="https://bsky.app/profile/{{.Post.Author.Handle}}" target="_blank">
<img class="avatar" src="{{.Post.Author.Avatar}}" alt="{{.Post.Author.DisplayName}}'s avatar" width="32" height="32">
<span class="display-name">{{.Post.Author.DisplayName}}</span>
<span class="handle">@{{.Post.Author.Handle}}</span>
</a>
</div>
<div class="comment-body" target="_blank">
<div>
<p class="comment-text">{{.Post.Record.Text}}</p>
{{if .Post.HasImage}}
<p class="comment-images">
{{range .Post.Embed.Media.Images}}
<a href="{{.Fullsize}}" target="_blank">[image]</a>
{{end}}
</p>
{{end}}
</div>
<div class="comment-footer">
<span class="comment-footer-static">
<span>{{.Post.LikeCount}} like{{if ne .Post.LikeCount 1}}s{{end}}</span>
&bull;
<span>{{.Post.RepostCount}} boost{{if ne .Post.RepostCount 1}}s{{end}}</span>
&bull;
</span>
<a href="{{.Post.BskyURL}}" class="comment-date">{{.Post.Record.CreatedAtPrint}}</a>
</div>
</div>
</div>
<div class="comment-replies">
{{range .Replies}}
{{template "comment" .}}
{{end}}
</div>
</article>
{{end}}

View file

@ -21,11 +21,11 @@
<a href="/" preload="mouseover">home</a>
</li>
<li>
<a href="/music" preload="mouseover">music</a>
<a href="/music/" preload="mouseover">music</a>
</li>
<li>
<!-- coming later! -->
<span title="coming later!">blog</span>
<a href="/blog/" preload="mouseover">blog</a>
</li>
<li>
<!-- coming later! -->