commit bc6fd537d8c40759062e3872d96b2dda18bf0cc9
parent d855dccdb065ec1d0ca48a2c739c8a338322b6f2
Author: Damian Myrda <damian@prime8.dev>
Date: Wed, 21 Oct 2026 01:56:13 +0000
feat!: remove the blog
Drop blog content, loader, handlers, routes, templates, styles and
scripts on the web side, the blog tab in the TUI, blog URLs in the
sitemap and the content/ copy in the Dockerfile. yaml.v3 goes with it.
Tabs are now about, experience, projects.
BREAKING CHANGE: /blog and /api/blog/* are gone.
Diffstat:
26 files changed, 22 insertions(+), 1297 deletions(-)
diff --git a/Dockerfile b/Dockerfile
@@ -23,7 +23,6 @@ ENV GIT_CLONE_BASE=https://prime8.dev
COPY --from=build /server ./server
COPY --from=build /app/static ./static
-COPY --from=build /app/content ./content
ENV SSH_PORT=22
ENV TERM=xterm-256color
diff --git a/cmd/server/handlers/blog.go b/cmd/server/handlers/blog.go
@@ -1,74 +0,0 @@
-package handlers
-
-import (
- "net/http"
-
- "git.prime8.dev/moncheeta/prime8dev/internal/config"
- "git.prime8.dev/moncheeta/prime8dev/internal/content"
- "git.prime8.dev/moncheeta/prime8dev/internal/utils"
- "git.prime8.dev/moncheeta/prime8dev/internal/web"
- "github.com/go-chi/chi/v5"
-)
-
-func (h *Handler) BlogListHandler(w http.ResponseWriter, r *http.Request) {
- about := config.GetAbout()
- blogs, err := content.LoadBlogs()
- if err != nil {
- http.Error(w, "Failed to load blogs", http.StatusInternalServerError)
- return
- }
-
- data := web.BaseData("Blog", "blog", about)
- data.Blogs = blogs
-
- if utils.IsHTMXRequest(r) {
- h.renderTemplate(w, r, "blog", data, fragmentCacheMaxAgeSeconds)
- return
- }
-
- h.renderTemplate(w, r, "layout", data, fullPageCacheMaxAgeSeconds)
-}
-
-func (h *Handler) BlogPageHandler(w http.ResponseWriter, r *http.Request) {
- id := chi.URLParam(r, "id")
-
- about := config.GetAbout()
- blogs, err := content.LoadBlogs()
- if err != nil {
- http.Error(w, "Failed to load blogs", http.StatusInternalServerError)
- return
- }
-
- blog, err := content.GetBlogByID(id)
- if err != nil {
- http.Error(w, "Failed to load blog", http.StatusInternalServerError)
- return
- }
- if blog == nil {
- http.NotFound(w, r)
- return
- }
-
- data := web.BaseData("Blog", "blog", about)
- data.Blogs = blogs
- data.SelectedBlogID = id
- data.Blog = blog
-
- h.renderTemplate(w, r, "layout", data, fullPageCacheMaxAgeSeconds)
-}
-
-func (h *Handler) BlogDetailHandler(w http.ResponseWriter, r *http.Request) {
- id := chi.URLParam(r, "id")
- blog, err := content.GetBlogByID(id)
- if err != nil {
- http.Error(w, "Failed to load blog", http.StatusInternalServerError)
- return
- }
- if blog == nil {
- http.Error(w, "Blog not found", http.StatusNotFound)
- return
- }
-
- data := web.Data{Blog: blog}
- h.renderTemplate(w, r, "blog_detail", data, fragmentCacheMaxAgeSeconds)
-}
diff --git a/cmd/server/handlers/sitemap.go b/cmd/server/handlers/sitemap.go
@@ -16,11 +16,10 @@ func (h *Handler) SitemapHandler(w http.ResponseWriter, r *http.Request) {
xml.WriteString(`<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`)
- blogs, _ := content.LoadBlogs()
projects, _ := content.LoadProjects(h.gitService)
experience := config.GetExperience()
- routes := []string{"/", "/projects", "/experience", "/blog"}
+ routes := []string{"/", "/experience", "/projects"}
for _, route := range routes {
fmt.Fprintf(&xml, "<url><loc>%s%s</loc></url>", config.SiteURL, route)
}
@@ -33,10 +32,6 @@ func (h *Handler) SitemapHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(&xml, "<url><loc>%s/experience/%s</loc></url>", config.SiteURL, exp.ID)
}
- for _, b := range blogs {
- fmt.Fprintf(&xml, "<url><loc>%s/blog/%s</loc></url>", config.SiteURL, b.ID)
- }
-
xml.WriteString("</urlset>")
_, _ = w.Write(xml.Bytes())
}
diff --git a/cmd/server/main.go b/cmd/server/main.go
@@ -53,12 +53,9 @@ func run() error {
r.Get("/projects/{id}/raw/*", h.ProjectRawHandler)
r.Get("/experience", h.ExperienceListHandler)
r.Get("/experience/{id}", h.ExperiencePageHandler)
- r.Get("/blog", h.BlogListHandler)
- r.Get("/blog/{id}", h.BlogPageHandler)
r.Get("/sitemap.xml", h.SitemapHandler)
r.Get("/api/projects/{id}", h.ProjectDetailHandler)
- r.Get("/api/blog/{id}", h.BlogDetailHandler)
if config.WakaTimeEnabled {
r.Get("/api/wakatime", h.WakaTimeHandler)
}
diff --git a/content/blogs/building-my-portfolio.md b/content/blogs/building-my-portfolio.md
@@ -1,42 +0,0 @@
----
-id: building-my-portfolio
-title: building a tui-inspired portfolio
-description: how i built this terminal-style portfolio with astro
-date: 2026-01-31
-tags:
- - Astro
- - TypeScript
- - TUI
- - Web Development
- - Portfolio
----
-
-Once I got used to the terminal, **I couldn't live without it**. I love the **simplicity** of the interface and the fact that it's **keyboard native** (no need to reach for the mouse). Anything can be reached within a few key presses, which is one of the characteristics that I love.
-
-## the stack
-
-I built this website using **Astro** using server-side rendering. I tried to keep it as simple as possible, no over-engineering. Don't need WASM, a full-fledged database, or a complex UI framework. Details about me are stored in json files for ease of access (no need to scour the entire project to make a change), and my blogs are stored in a directory of markdown files.
-
-## design decisions
-
-Some characteristics that I've observed in terminal user interfaces I've transferred over to my website:
-
-- **Keyboard-first navigation**: `Arrow keys` allows navigation throughout the UI. `Tab` and the number keys (`1-4`) can be used to switch between tabs instantly. `Enter` intuitively opens blog posts and `Escape` closes them.
-- **No stale content**: One thing I don't want to worry about it updating my website every time something changes. This is a design pattern that is followed in the entire project.
- - Project details are fetched from my [self-hosted GitLab instance](https://git.prime8.dev).
- - Blog metadata is taken from the file stats, not a hard coded value.
- - Coding stats are pulled from [my Wakatime account](https://wakatime.com/@moncheeta).
-- **Personality**: One thing I want to make sure of is that this website is unique, but also derives some of its elements from existing terminal applications.
- - Boot animation adds a feeling of life to the website, even though it's entirely static.
- - Top tab bar is an idea taken from the countless GUI applications that have it.
- - Bottom status bar is inspired by many terminal applications that use a similar setup.
-
-These characteristics make this website feel like the terminal applications that I'm in love with, while differentiating it to make it mine.
-
-## conclusion
-
-Building this portfolio was a chance for me to combine two things **I love**: terminal user interfaces and modern web development. The result is something that feels **personal**, a site that actually reflects how I actually work.
-
-If you're thinking about building your own portfolio, my advice is simple: **make it yours**. Pick a style that exemplifies you and add the features you'd actually want to use.
-
-The source code is available at [git.prime8.dev](https://git.prime8.dev/moncheeta/prime8dev)
diff --git a/go.mod b/go.mod
@@ -18,9 +18,9 @@ require (
github.com/charmbracelet/wish v1.4.7
github.com/evanw/esbuild v0.27.4
github.com/joho/godotenv v1.5.1
+ github.com/muesli/termenv v0.16.0
github.com/romance-dev/ascii-art v1.0.1
golang.org/x/crypto v0.36.0
- gopkg.in/yaml.v3 v3.0.1
)
require (
@@ -61,7 +61,6 @@ require (
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
- github.com/muesli/termenv v0.16.0 // indirect
github.com/nathan-fiscaletti/consolesize-go v0.0.0-20210105204122-a87d9f614b9d // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
diff --git a/go.sum b/go.sum
@@ -175,7 +175,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/internal/content/blogs.go b/internal/content/blogs.go
@@ -1,147 +0,0 @@
-package content
-
-import (
- "errors"
- "log/slog"
- "os"
- "path/filepath"
- "sort"
- "strings"
- "time"
-
- "git.prime8.dev/moncheeta/prime8dev/internal/utils"
- "gopkg.in/yaml.v3"
-)
-
-type Blog struct {
- ID string
- Title string
- Description string
- Date time.Time
- Markdown string
- HTML string
- URL string
- Image string
- Tags []string
-}
-
-type frontmatter struct {
- ID string `yaml:"id"`
- Title string `yaml:"title"`
- Description string `yaml:"description"`
- Date string `yaml:"date"`
- URL string `yaml:"url"`
- Image string `yaml:"image"`
- Tags []string `yaml:"tags"`
-}
-
-var (
- blogsList = loadBlogsOnStartup()
- blogsByID = mapBlogsByID(blogsList)
-)
-
-func GetBlogByID(id string) (*Blog, error) {
- id = strings.TrimSpace(id)
- if id == "" {
- return nil, nil
- }
-
- b, ok := blogsByID[id]
- if !ok {
- return nil, nil
- }
-
- copy := b
- return ©, nil
-}
-
-func LoadBlogs() ([]Blog, error) {
- copyBlogs := make([]Blog, len(blogsList))
- copy(copyBlogs, blogsList)
- return copyBlogs, nil
-}
-
-func loadBlogsOnStartup() []Blog {
- blogs, err := loadBlogsFromDisk()
- if err != nil {
- slog.Warn("failed to load blogs on startup", "error", err)
- return []Blog{}
- }
-
- return blogs
-}
-
-func loadBlogsFromDisk() ([]Blog, error) {
- paths, err := filepath.Glob("./content/blogs/*.md")
- if err != nil {
- return nil, err
- }
-
- blogs := make([]Blog, 0, len(paths))
- for _, p := range paths {
- b, err := parseBlogFile(p)
- if err != nil {
- return nil, err
- }
- blogs = append(blogs, b)
- }
-
- sort.Slice(blogs, func(i, j int) bool {
- return blogs[i].Date.After(blogs[j].Date)
- })
-
- return blogs, nil
-}
-
-func mapBlogsByID(blogs []Blog) map[string]Blog {
- byID := make(map[string]Blog, len(blogs))
- for _, b := range blogs {
- byID[b.ID] = b
- }
- return byID
-}
-
-func parseBlogFile(path string) (Blog, error) {
- raw, err := os.ReadFile(path)
- if err != nil {
- return Blog{}, err
- }
-
- parts := strings.SplitN(string(raw), "---", 3)
- if len(parts) < 3 {
- return Blog{}, errors.New("invalid blog frontmatter format")
- }
-
- var fm frontmatter
- if err := yaml.Unmarshal([]byte(parts[1]), &fm); err != nil {
- return Blog{}, err
- }
-
- date, err := time.Parse("2006-01-02", fm.Date)
- if err != nil {
- return Blog{}, err
- }
-
- markdown := strings.TrimSpace(parts[2])
- html := ""
- if fm.URL == "" && markdown != "" {
- rendered, err := utils.MarkdownToHTML(markdown)
- if err != nil {
- slog.Warn("failed to render blog markdown", "blog_id", fm.ID, "path", path, "error", err)
- return Blog{}, err
- }
- html = rendered
- }
-
- return Blog{
- ID: fm.ID,
- Title: fm.Title,
- Description: fm.Description,
- Date: date,
- Markdown: markdown,
- HTML: html,
- URL: fm.URL,
- Image: fm.Image,
- Tags: fm.Tags,
- }, nil
-}
diff --git a/internal/tui/blog_markdown_render.go b/internal/tui/blog_markdown_render.go
@@ -1,86 +0,0 @@
-package tui
-
-import (
- "strconv"
- "strings"
-
- tea "github.com/charmbracelet/bubbletea"
-)
-
-type blogMarkdownRenderedMsg struct {
- blogID string
- width int
- rendered string
-}
-
-func (m portfolioModel) cachedBlogMarkdown(blogID string, width int) (string, bool) {
- byWidth, ok := m.blogMarkdownRender[blogID]
- if !ok {
- return "", false
- }
-
- rendered, ok := byWidth[width]
- return rendered, ok
-}
-
-func (m *portfolioModel) setCachedBlogMarkdown(blogID string, width int, rendered string) {
- byWidth := m.blogMarkdownRender[blogID]
- if byWidth == nil {
- byWidth = make(map[int]string)
- m.blogMarkdownRender[blogID] = byWidth
- }
-
- byWidth[width] = rendered
-}
-
-func blogMarkdownLoadKey(blogID string, width int) string {
- return blogID + ":" + strconv.Itoa(width)
-}
-
-func (m *portfolioModel) maybeRenderSelectedBlogCmd() tea.Cmd {
- if m.activeTab != tabBlog || len(m.blogs) == 0 {
- return nil
- }
-
- blog := m.blogs[m.blogIndex]
- markdown := normalizeBlogMarkdown(blog.Markdown, blog.Description)
- if strings.TrimSpace(markdown) == "" {
- return nil
- }
-
- width := m.blogMarkdownWidth()
- if _, ok := m.cachedBlogMarkdown(blog.ID, width); ok {
- return nil
- }
-
- loadKey := blogMarkdownLoadKey(blog.ID, width)
- if m.blogMarkdownLoads[loadKey] {
- return nil
- }
-
- m.blogMarkdownLoads[loadKey] = true
- return renderBlogMarkdownCmd(blog.ID, markdown, width)
-}
-
-func (m *portfolioModel) maybeRenderOpenBlogCmd() tea.Cmd {
- if !m.blogOpen {
- return nil
- }
-
- return m.maybeRenderSelectedBlogCmd()
-}
-
-func renderBlogMarkdownCmd(blogID string, markdown string, width int) tea.Cmd {
- return func() tea.Msg {
- rendered := renderMarkdown(markdown, width)
- if rendered == "" {
- rendered = markdown
- }
-
- return blogMarkdownRenderedMsg{
- blogID: blogID,
- width: width,
- rendered: rendered,
- }
- }
-}
diff --git a/internal/tui/content.go b/internal/tui/content.go
@@ -6,8 +6,6 @@ import (
"strings"
appconfig "git.prime8.dev/moncheeta/prime8dev/internal/config"
- appcontent "git.prime8.dev/moncheeta/prime8dev/internal/content"
- "github.com/charmbracelet/lipgloss"
)
func (m portfolioModel) aboutDetail() string {
@@ -322,159 +320,3 @@ func (m portfolioModel) experiencePage() string {
}
return strings.Join(rows, "\n")
}
-
-func (m portfolioModel) blogDetail() string {
- if len(m.blogs) == 0 {
- return "~/blog is empty"
- }
- b := m.blogs[m.blogIndex]
- parts := []string{renderPrompt(m.about.Handle, "cat ~/blog/"+b.ID+".md")}
- parts = append(parts, renderBlogFrontmatter(b)...)
- if m.blogOpen {
- width := m.markdownWidth()
- if rendered, ok := m.cachedBlogMarkdown(b.ID, width); ok && rendered != "" {
- parts = append(parts, "", rendered)
- } else {
- fallback := normalizeBlogMarkdown(b.Markdown, b.Description)
- if strings.TrimSpace(fallback) != "" {
- parts = append(parts, "", fallback, "", dimStyle.Render("formatting markdown..."))
- } else {
- parts = append(parts, "", infoStyle.Render("rendering markdown..."))
- }
- }
- }
- return strings.Join(parts, "\n")
-}
-
-func renderBlogFrontmatter(b appcontent.Blog) []string {
- lines := []string{
- renderBlogMetaLine("title", b.Title, titleStyle),
- renderBlogMetaLine("description", b.Description, rowStyle),
- renderBlogMetaLine("date", b.Date.Format("2006-01-02"), infoStyle),
- }
-
- tags := strings.TrimSpace(renderTagList(b.Tags))
- if tags != "" {
- lines = append(lines, dimStyle.Render(padRunesRight("tags:", 12)+" ")+tags)
- }
-
- return lines
-}
-
-func renderBlogMetaLine(label string, value string, valueStyle lipgloss.Style) string {
- key := dimStyle.Render(padRunesRight(label+":", 12) + " ")
- return key + valueStyle.Render(value)
-}
-
-func normalizeBlogMarkdown(markdown string, description string) string {
- lines := strings.Split(markdown, "\n")
- for i := range lines {
- trimmed := strings.TrimSpace(lines[i])
- if trimmed == "" {
- continue
- }
-
- if strings.EqualFold(trimmed, strings.TrimSpace(description)) {
- lines[i] = "# " + trimmed
- }
- break
- }
-
- return strings.Join(lines, "\n")
-}
-
-func (m portfolioModel) blogMarkdownWidth() int {
- return m.markdownWidth()
-}
-
-func (m portfolioModel) blogPage() string {
- if len(m.blogs) == 0 {
- return "~/blog is empty"
- }
-
- if m.blogOpen {
- return m.blogDetail()
- }
-
- modeW, dateW, nameW, descW := m.blogTableWidths()
- markerW := 2
- header := blogHeaderLine(markerW, modeW, dateW, nameW, descW)
-
- rows := []string{
- renderPrompt(m.about.Handle, "ls -l ~/blog"),
- header,
- }
- for i, b := range m.blogs {
- rows = append(rows, blogDataLine(i == m.blogIndex, b, markerW, modeW, dateW, nameW, descW))
- }
-
- return strings.Join(rows, "\n")
-}
-
-func (m portfolioModel) blogTableWidths() (int, int, int, int) {
- viewW, _ := m.detailSize()
- available := max(viewW-5, 24)
-
- modeW := 10
- dateW := 10
- remaining := available - modeW - dateW - 3
- if remaining < 14 {
- nameW := max(4, remaining/2)
- descW := max(4, remaining-nameW)
- return modeW, dateW, nameW, descW
- }
-
- nameW := min(40, max(12, remaining/2))
- descW := max(8, remaining-nameW)
-
- maxName := 0
- maxDesc := 0
- for _, b := range m.blogs {
- nameLen := len([]rune(b.ID + ".md"))
- descLen := len([]rune(b.Description))
- if nameLen > maxName {
- maxName = nameLen
- }
- if descLen > maxDesc {
- maxDesc = descLen
- }
- }
-
- if maxName > 0 && maxName < nameW {
- spare := nameW - maxName
- nameW = maxName
- descW += spare
- }
- if maxDesc > 0 && maxDesc < descW {
- descW = maxDesc
- }
-
- return modeW, dateW, max(nameW, 8), max(descW, 8)
-}
-
-func blogHeaderLine(markerW int, modeW int, dateW int, nameW int, descW int) string {
- marker := lipgloss.NewStyle().Width(markerW).Render("")
- mode := dimStyle.Width(modeW).Render("mode")
- date := dimStyle.Width(dateW).Render("date")
- name := dimStyle.Width(nameW).Render("name")
- desc := dimStyle.Width(descW).Render("desc")
- return lipgloss.JoinHorizontal(lipgloss.Top, marker, mode, " ", date, " ", name, " ", desc)
-}
-
-func blogDataLine(selected bool, b appcontent.Blog, markerW int, modeW int, dateW int, nameW int, descW int) string {
- marker := ""
- if selected {
- marker = ">"
- }
-
- markerCol := rowStyle.Width(markerW).Render(marker)
- if selected {
- markerCol = selectedRowStyle.Width(markerW).Render(marker)
- }
- modeCol := blogModeStyle.Width(modeW).Render("-rw-r--r--")
- dateCol := dimStyle.Width(dateW).Render(b.Date.Format("2006-01-02"))
- nameCol := blogNameStyle.Width(nameW).Render(fitRunes(b.ID+".md", nameW))
- descCol := dimStyle.Width(descW).Render(fitRunes(b.Description, max(descW, 4)))
-
- return lipgloss.JoinHorizontal(lipgloss.Top, markerCol, modeCol, " ", dateCol, " ", nameCol, " ", descCol)
-}
diff --git a/internal/tui/layout.go b/internal/tui/layout.go
@@ -55,13 +55,11 @@ func (m portfolioModel) sidebarWidth(bodyWidth int) int {
func (m portfolioModel) detailContent() string {
switch m.activeTab {
- case tabAbout:
- return m.aboutDetail()
- case tabProjects:
- return m.projectDetail()
case tabExperience:
return m.experiencePage()
+ case tabProjects:
+ return m.projectDetail()
default:
- return m.blogPage()
+ return m.aboutDetail()
}
}
diff --git a/internal/tui/styles.go b/internal/tui/styles.go
@@ -3,16 +3,14 @@ package tui
import "github.com/charmbracelet/lipgloss"
var (
- colorFg = lipgloss.Color("250")
- colorDim = lipgloss.Color("242")
- colorBorder = lipgloss.Color("239")
- colorAccent = lipgloss.Color("179")
- colorBlue = lipgloss.Color("74")
- colorWebBlue = lipgloss.Color("68")
- colorLightBlue = lipgloss.Color("75")
- colorCyan = lipgloss.Color("73")
- colorMagenta = lipgloss.Color("133")
- colorFooterBg = lipgloss.Color("235")
+ colorFg = lipgloss.Color("250")
+ colorDim = lipgloss.Color("242")
+ colorBorder = lipgloss.Color("239")
+ colorAccent = lipgloss.Color("179")
+ colorBlue = lipgloss.Color("74")
+ colorCyan = lipgloss.Color("73")
+ colorMagenta = lipgloss.Color("133")
+ colorFooterBg = lipgloss.Color("235")
headerStyle = lipgloss.NewStyle().
BorderBottom(true).
@@ -53,8 +51,6 @@ var (
dimStyle = lipgloss.NewStyle().Foreground(colorDim)
titleStyle = lipgloss.NewStyle().Foreground(colorAccent).Bold(true)
linkStyle = lipgloss.NewStyle().Foreground(colorBlue)
- blogModeStyle = lipgloss.NewStyle().Foreground(colorWebBlue)
- blogNameStyle = lipgloss.NewStyle().Foreground(colorLightBlue)
infoStyle = lipgloss.NewStyle().Foreground(colorCyan)
footerTimeStyle = lipgloss.NewStyle().Foreground(colorCyan).Background(colorFooterBg)
footerWakaStyle = lipgloss.NewStyle().Foreground(colorMagenta).Background(colorFooterBg)
diff --git a/internal/tui/types.go b/internal/tui/types.go
@@ -14,15 +14,14 @@ import (
const (
tabAbout = iota
- tabProjects
tabExperience
- tabBlog
+ tabProjects
minWindowWidth = 80
minWindowHeight = 24
)
-var tabLabels = []string{"about", "projects", "experience", "blog"}
+var tabLabels = []string{"about", "experience", "projects"}
type tickMsg time.Time
@@ -38,14 +37,11 @@ type portfolioModel struct {
projects []appconfig.Project
experience []appconfig.Experience
- blogs []appcontent.Blog
activeTab int
projectIndex int
projectReadmeScroll bool
experienceIndex int
- blogIndex int
- blogOpen bool
width int
height int
@@ -72,9 +68,6 @@ type portfolioModel struct {
projectMarkdownLoads map[string]bool
projectPrefetchDone bool
- blogMarkdownRender map[string]map[int]string
- blogMarkdownLoads map[string]bool
-
wakaHours string
wakaWeekly *external.WeeklyStats
wakaLoaded bool
@@ -87,7 +80,6 @@ func newPortfolioModel(user string) portfolioModel {
about := appconfig.GetAbout()
exp := appconfig.GetExperience()
- blogs, _ := appcontent.LoadBlogs()
projects, _ := appcontent.LoadProjects(gitService)
bootTitle := about.Name
if bootTitle == "" {
@@ -111,7 +103,6 @@ func newPortfolioModel(user string) portfolioModel {
aboutASCIILoads: make(map[int]bool),
projects: projects,
experience: exp,
- blogs: blogs,
now: time.Now(),
width: 80,
height: 24,
@@ -130,9 +121,7 @@ func newPortfolioModel(user string) portfolioModel {
projectReadmeRender: make(map[string]map[int]string),
projectMarkdownLoads: make(map[string]bool),
- blogMarkdownRender: make(map[string]map[int]string),
- blogMarkdownLoads: make(map[string]bool),
- wakaTime: external.NewWakaTime(cache.New(5*time.Minute, 10*time.Minute)),
+ wakaTime: external.NewWakaTime(cache.New(5*time.Minute, 10*time.Minute)),
}
m.syncBootFrame(m.bootStartedAt)
m.syncViewport()
diff --git a/internal/tui/update.go b/internal/tui/update.go
@@ -61,15 +61,6 @@ func (m portfolioModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
return m, nil
- case blogMarkdownRenderedMsg:
- delete(m.blogMarkdownLoads, blogMarkdownLoadKey(msg.blogID, msg.width))
- m.setCachedBlogMarkdown(msg.blogID, msg.width, msg.rendered)
- if m.activeTab == tabBlog && m.blogOpen {
- if m.blogIndex >= 0 && m.blogIndex < len(m.blogs) && m.blogs[m.blogIndex].ID == msg.blogID {
- m.syncViewport()
- }
- }
- return m, nil
case aboutASCIIRenderedMsg:
m.applyAboutASCIIRendered(msg)
if m.activeTab == tabAbout {
@@ -107,7 +98,6 @@ func (m *portfolioModel) refreshDataCmd() tea.Cmd {
m.maybeLoadProjectReadmeCmd(),
m.maybePrefetchProjectReadmesCmd(),
m.maybeRenderSelectedProjectMarkdownCmd(),
- m.maybeRenderSelectedBlogCmd(),
m.maybeLoadWakaFooterCmd(),
m.maybeRenderAboutASCIICmd(),
m.maybeRenderAboutNameCmd(),
@@ -138,10 +128,6 @@ func (m *portfolioModel) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
return nil
}
- if m.consumeBlogBack(msg) {
- return nil
- }
-
if tab, ok := tabIndexFromKey(msg.String()); ok {
return m.activateTab(tab)
}
@@ -186,7 +172,6 @@ func (m *portfolioModel) activateTab(tab int) tea.Cmd {
m.activeTab = tab
m.projectReadmeScroll = false
- m.blogOpen = false
m.syncViewport()
return m.refreshDataCmd()
@@ -206,20 +191,6 @@ func (m *portfolioModel) consumeProjectReadmeBack(msg tea.KeyMsg) bool {
return true
}
-func (m *portfolioModel) consumeBlogBack(msg tea.KeyMsg) bool {
- if m.activeTab != tabBlog || !m.blogOpen {
- return false
- }
-
- if msg.String() != "left" && !key.Matches(msg, m.keys.Back) {
- return false
- }
-
- m.blogOpen = false
- m.syncViewport()
- return true
-}
-
func (m *portfolioModel) handleOpenKey() tea.Cmd {
switch m.activeTab {
case tabProjects:
@@ -228,10 +199,6 @@ func (m *portfolioModel) handleOpenKey() tea.Cmd {
}
m.projectReadmeScroll = !m.projectReadmeScroll
return nil
- case tabBlog:
- m.blogOpen = !m.blogOpen
- m.syncViewport()
- return m.maybeRenderOpenBlogCmd()
default:
return nil
}
@@ -244,12 +211,6 @@ func (m *portfolioModel) handleBackKey() tea.Cmd {
return nil
}
- if m.activeTab == tabBlog && m.blogOpen {
- m.blogOpen = false
- m.syncViewport()
- return nil
- }
-
return tea.Quit
}
@@ -275,7 +236,7 @@ func tabIndexFromKey(keyString string) (int, bool) {
}
func (m *portfolioModel) moveSelection(delta int) tea.Cmd {
- if m.activeTab == tabAbout || (m.activeTab == tabBlog && m.blogOpen) || (m.activeTab == tabProjects && m.projectReadmeScroll) {
+ if m.activeTab == tabAbout || (m.activeTab == tabProjects && m.projectReadmeScroll) {
scrollViewport(&m.viewport, delta)
return nil
}
@@ -295,13 +256,6 @@ func (m *portfolioModel) moveSelection(delta int) tea.Cmd {
}
m.experienceIndex = wrapIndex(m.experienceIndex+delta, len(m.experience))
m.syncViewport()
- case tabBlog:
- if len(m.blogs) == 0 {
- scrollViewport(&m.viewport, delta)
- return nil
- }
- m.blogIndex = wrapIndex(m.blogIndex+delta, len(m.blogs))
- m.syncViewport()
}
return nil
diff --git a/internal/tui/view.go b/internal/tui/view.go
@@ -65,7 +65,7 @@ func (m portfolioModel) renderHelpBody(width int, height int) string {
{key: "▲ ▼", desc: "switch list items"},
{key: "shift + ▲ ▼", desc: "scroll item details"},
{key: "tab", desc: "cycle tabs"},
- {key: "1-4", desc: "switch tabs"},
+ {key: "1-3", desc: "switch tabs"},
{key: "enter", desc: "open item"},
{key: "?", desc: "toggle help"},
}
diff --git a/internal/web/renderer.go b/internal/web/renderer.go
@@ -9,7 +9,6 @@ import (
"strings"
"git.prime8.dev/moncheeta/prime8dev/internal/config"
- "git.prime8.dev/moncheeta/prime8dev/internal/content"
"git.prime8.dev/moncheeta/prime8dev/internal/external"
"git.prime8.dev/moncheeta/prime8dev/internal/utils"
)
@@ -31,9 +30,6 @@ type Data struct {
SelectedProjectID string
Project *config.Project
ProjectReadme template.HTML
- Blogs []content.Blog
- SelectedBlogID string
- Blog *content.Blog
WakaStats *external.WakaTimeStats
Tabs []Tab
SiteURL string
@@ -98,16 +94,15 @@ func (r *Renderer) Render(name string, data Data) (string, error) {
func DefaultTabs() []Tab {
return []Tab{
{ID: "about", Label: "About", Num: 1},
- {ID: "projects", Label: "Projects", Num: 2},
- {ID: "experience", Label: "Experience", Num: 3},
- {ID: "blog", Label: "Blog", Num: 4},
+ {ID: "experience", Label: "Experience", Num: 2},
+ {ID: "projects", Label: "Projects", Num: 3},
}
}
func BaseData(title, activeTab string, about config.About) Data {
return Data{
Title: title,
- Description: fmt.Sprintf("Portfolio and blog of %s", about.Name),
+ Description: fmt.Sprintf("Portfolio of %s", about.Name),
ActiveTab: activeTab,
ContentTemplate: activeTab,
About: about,
diff --git a/internal/web/scripts/app.js b/internal/web/scripts/app.js
@@ -1,6 +1,4 @@
import { initAbout } from "./modules/about.js";
-import { initBlog } from "./modules/blog.js";
-import { initBlogDetail } from "./modules/blogDetail.js";
import { initExperience } from "./modules/experience.js";
import { initHeader, updateAppTitle } from "./modules/header.js";
import { initHelp } from "./modules/help.js";
@@ -18,8 +16,6 @@ function initApp() {
initAbout();
initProjects();
initExperience();
- initBlog();
- initBlogDetail();
updateAppTitle();
};
initDynamicContent();
@@ -30,7 +26,6 @@ function initApp() {
switch (e.target.id) {
case "content":
case "project-detail":
- case "blog-post-content":
initDynamicContent();
break;
default:
diff --git a/internal/web/scripts/modules/blog.js b/internal/web/scripts/modules/blog.js
@@ -1,188 +0,0 @@
-import { isEditableTarget } from "./dom.js";
-import { clearAppTitleDetail, setAppTitleDetail, updateAppTitle } from "./header.js";
-
-const blogHandlers = {
- keydown: null,
- popstate: null,
-};
-
-export function initBlog() {
- const section = document.getElementById("blog");
- if (!section) return;
-
- const listView = section.querySelector("#blog-list-view");
- const postView = section.querySelector("#blog-post-view");
- const backBtn = section.querySelector(".blog-back-btn");
- const listItems = Array.from(section.querySelectorAll(".blog-list-item"));
- if (!listView || !postView || listItems.length === 0) return;
-
- let selectedIndex = 0;
- const hasPrefilledPost = Boolean(document.getElementById("blog-post-content")?.children.length);
- let postOpen = !postView.classList.contains("hidden") || window.location.pathname.startsWith("/blog/");
- window.isBlogPostOpen = () => postOpen;
-
- if (postOpen) {
- listView.classList.add("hidden");
- postView.classList.remove("hidden");
- } else {
- listView.classList.remove("hidden");
- postView.classList.add("hidden");
- }
-
- function pathBlogID() {
- return decodeURIComponent(window.location.pathname.match(/^\/blog\/([^/]+)$/)?.[1] || "");
- }
-
- function itemByBlogID(blogID) {
- if (!blogID) return null;
- return listItems.find((item) => item.dataset.id === blogID) || null;
- }
-
- function select(index) {
- selectedIndex = index;
- listItems.forEach((item, i) => item.classList.toggle("selected", i === index));
- listItems[index]?.scrollIntoView({ behavior: "smooth", block: "nearest" });
-
- const detail = listItems[index]?.querySelector(".blog-list-title")?.textContent?.trim() || "";
- setAppTitleDetail("blog", detail);
- updateAppTitle("blog");
-
- const selectedItem = listItems[index];
- if (selectedItem) {
- selectedItem.dispatchEvent(new Event("blog:selected", { bubbles: true }));
- }
- }
-
- function openPost(blogID, updateHistory = true) {
- postOpen = true;
- listView.classList.add("hidden");
- postView.classList.remove("hidden");
- postView.scrollTop = 0;
-
- if (blogID && updateHistory) {
- const path = `/blog/${encodeURIComponent(blogID)}`;
- if (window.location.pathname !== path) {
- history.replaceState({ tab: "blog" }, "", path);
- }
- }
-
- updateAppTitle("blog");
- }
-
- function closePost(updateHistory = true) {
- postOpen = false;
- listView.classList.remove("hidden");
- postView.classList.add("hidden");
- clearAppTitleDetail("blog");
-
- if (updateHistory && window.location.pathname !== "/blog") {
- history.replaceState({ tab: "blog" }, "", "/blog");
- }
-
- updateAppTitle("blog");
- }
-
- listItems.forEach((item, index) => {
- if (item.dataset.boundClick === "1") return;
- item.dataset.boundClick = "1";
-
- item.addEventListener("click", () => {
- select(index);
- openPost(item.dataset.id, true);
- });
- });
-
- if (backBtn && backBtn.dataset.boundClick !== "1") {
- backBtn.dataset.boundClick = "1";
- backBtn.addEventListener("click", () => closePost(true));
- }
-
- if (blogHandlers.keydown) {
- document.removeEventListener("keydown", blogHandlers.keydown);
- }
-
- blogHandlers.keydown = (e) => {
- if (!section.isConnected) return;
- if (window.isLightboxOpen?.()) return;
- if (isEditableTarget(e.target)) return;
-
- if (!postOpen) {
- switch (e.key) {
- case "ArrowUp":
- e.preventDefault();
- if (selectedIndex > 0) select(selectedIndex - 1);
- break;
- case "ArrowDown":
- e.preventDefault();
- if (selectedIndex < listItems.length - 1) select(selectedIndex + 1);
- break;
- case "ArrowRight":
- case "Enter":
- e.preventDefault();
- listItems[selectedIndex]?.click();
- break;
- default:
- break;
- }
- } else {
- switch (e.key) {
- case "ArrowLeft":
- case "Escape":
- e.preventDefault();
- closePost(true);
- break;
- case "ArrowUp":
- e.preventDefault();
- postView.scrollBy({ top: -80, behavior: "smooth" });
- break;
- case "ArrowDown":
- e.preventDefault();
- postView.scrollBy({ top: 80, behavior: "smooth" });
- break;
- default:
- break;
- }
- }
- };
-
- document.addEventListener("keydown", blogHandlers.keydown);
-
- if (blogHandlers.popstate) {
- window.removeEventListener("popstate", blogHandlers.popstate);
- }
-
- blogHandlers.popstate = () => {
- if (!section.isConnected) return;
-
- const currentID = pathBlogID();
- if (!currentID) {
- closePost(false);
- return;
- }
-
- const item = itemByBlogID(currentID);
- if (!item) return;
-
- const idx = Number(item.dataset.index || 0);
- select(idx);
- openPost(currentID, false);
-
- if (!document.getElementById("blog-post-content")?.children.length) {
- item.click();
- }
- };
-
- window.addEventListener("popstate", blogHandlers.popstate);
-
- const pathID = pathBlogID();
- const selectedFromPath = itemByBlogID(pathID);
- const initialItem = selectedFromPath || section.querySelector(".blog-list-item.selected") || listItems[0];
- if (!initialItem) return;
-
- const initialIndex = Number(initialItem.dataset.index || 0);
- select(initialIndex);
-
- if (postOpen && !hasPrefilledPost) {
- initialItem.click();
- }
-}
diff --git a/internal/web/scripts/modules/blogDetail.js b/internal/web/scripts/modules/blogDetail.js
@@ -1,60 +0,0 @@
-import { setAppTitleDetail, updateAppTitle } from "./header.js";
-
-export function initBlogDetail() {
- const heading = document.querySelector(".blog-article-title")?.textContent?.trim() || "";
- if (heading) {
- setAppTitleDetail("blog", heading);
- updateAppTitle("blog");
- }
-
- const shareButtons = document.querySelectorAll(".blog-share-btn");
- if (shareButtons.length === 0) return;
-
- shareButtons.forEach((btn) => {
- if (btn.dataset.boundClick === "1") return;
- btn.dataset.boundClick = "1";
-
- btn.addEventListener("click", async (e) => {
- e.stopPropagation();
- const button = e.currentTarget;
- const blogId = button.dataset.blogId;
- const title = button.dataset.blogTitle;
- const url = `${window.location.origin}/blog/${blogId}`;
-
- if (navigator.share) {
- try {
- await navigator.share({ title, url });
- } catch {
- const copied = await copyToClipboard(url);
- if (copied) showCopiedState(button);
- }
- } else {
- const copied = await copyToClipboard(url);
- if (copied) showCopiedState(button);
- }
- });
- });
-}
-
-function showCopiedState(button) {
- const originalText = button.textContent;
- button.textContent = "[Copied!]";
- button.classList.add("copied");
- window.setTimeout(() => {
- button.textContent = originalText;
- button.classList.remove("copied");
- }, 2000);
-}
-
-async function copyToClipboard(text) {
- if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
- return false;
- }
-
- try {
- await navigator.clipboard.writeText(text);
- return true;
- } catch {
- return false;
- }
-}
diff --git a/internal/web/scripts/modules/header.js b/internal/web/scripts/modules/header.js
@@ -43,8 +43,6 @@ export function initHeader() {
if (isEditableTarget(e.target)) return;
if (typeof window.isLightboxOpen === "function" && window.isLightboxOpen()) return;
- const isBlogPostOpen = typeof window.isBlogPostOpen === "function" && window.isBlogPostOpen();
-
const current = currentFromPath(order, fallback);
const index = order.indexOf(current);
@@ -58,8 +56,6 @@ export function initHeader() {
return;
}
- if (isBlogPostOpen) return;
-
switch (e.key) {
case "Tab": {
e.preventDefault();
@@ -98,7 +94,6 @@ export function initHeader() {
setActiveTabUI(currentFromPath(order, fallback));
break;
case "project-detail":
- case "blog-post-content":
updateAppTitle();
break;
default:
@@ -146,15 +141,6 @@ export function updateAppTitle(activeTab = "") {
selectedListTitle(".list-item[data-id]", ".list-title", projectID);
}
break;
- case "blog":
- if (/^\/blog\/[^/]+$/.test(path)) {
- const blogID = path.match(/^\/blog\/([^/]+)$/)?.[1] || "";
- detailLabel =
- detailTitles.blog ||
- document.querySelector(".blog-article-title")?.textContent?.trim() ||
- selectedListTitle(".blog-list-item[data-id]", ".blog-list-title", blogID);
- }
- break;
default:
break;
}
diff --git a/internal/web/styles/blog.css b/internal/web/styles/blog.css
@@ -1,197 +0,0 @@
-#blog {
- width: 100%;
-}
-
-.blog-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- width: 100%;
- gap: 8px;
-}
-
-.blog-list-view {
- display: flex;
- align-items: flex-start;
- justify-content: center;
- height: 100%;
- width: 100%;
- overflow-y: auto;
- padding: 24px 28px;
-}
-
-.blog-list-view.hidden {
- display: none;
-}
-
-.blog-fs-shell {
- width: 100%;
- max-width: 860px;
- border: 1px solid var(--border);
- background: var(--bg-alt);
-}
-
-.blog-fs-header {
- color: var(--accent);
- border-bottom: 1px solid var(--border);
- padding: 10px 14px;
- font-size: var(--fs-14);
-}
-
-.blog-list-columns {
- display: grid;
- grid-template-columns: 92px 112px minmax(220px, 1.2fr) minmax(0, 0.8fr) 24px;
- gap: 10px;
- color: var(--fg-dim);
- border-bottom: 1px solid var(--border);
- padding: 8px 14px;
- font-size: var(--fs-12);
-}
-
-.blog-list-container {
- width: 100%;
- padding: 8px;
-}
-
-.blog-list-item {
- display: grid;
- grid-template-columns: 92px 112px minmax(220px, 1.2fr) minmax(0, 0.8fr) 24px;
- align-items: center;
- gap: 10px;
- padding: 10px 6px;
- border: 1px solid var(--border);
- margin-bottom: 8px;
- cursor: pointer;
- transition: background 0.15s ease, border-color 0.15s ease;
-}
-
-.blog-list-item:hover {
- background: var(--selection);
- border-color: var(--accent);
-}
-
-.blog-list-item.selected {
- background: var(--bg-alt);
- border-color: var(--accent);
-}
-
-.blog-list-date {
- color: var(--fg-dim);
- font-size: var(--fs-13);
-}
-
-.blog-list-mode {
- color: var(--blue);
- font-size: var(--fs-12);
-}
-
-.blog-list-file {
- color: var(--light-blue);
- font-weight: 500;
- font-size: var(--fs-14);
- text-decoration: underline;
- text-decoration-color: transparent;
- transition: text-decoration-color 0.15s;
-}
-
-.blog-list-name {
- min-width: 0;
-}
-
-.blog-list-title {
- display: none;
-}
-
-.blog-list-desc {
- color: var(--fg-dim);
- font-size: var(--fs-12);
- max-width: 34ch;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.blog-list-arrow {
- color: var(--fg-dim);
- font-size: var(--fs-13);
- justify-self: end;
-}
-
-.blog-list-item:hover .blog-list-file,
-.blog-list-item.selected .blog-list-file {
- text-decoration-color: currentColor;
-}
-
-.blog-post-view {
- display: flex;
- justify-content: center;
- height: 100%;
- width: 100%;
- overflow-y: auto;
- padding: 32px;
-}
-
-.blog-post-view.hidden {
- display: none;
-}
-
-.blog-post-container {
- max-width: 760px;
- width: 100%;
-}
-
-.blog-back-btn {
- background: transparent;
- border: 1px solid var(--border);
- color: var(--fg-dim);
- padding: 6px 12px;
- cursor: pointer;
- font-family: inherit;
- font-size: var(--fs-13);
- margin-bottom: 14px;
-}
-
-.blog-back-btn:hover {
- background: var(--bg-alt);
- color: var(--accent);
- border-color: var(--accent);
-}
-
-@media (max-width: 768px) {
- .blog-list-view {
- padding: 12px;
- }
-
- .blog-fs-header {
- font-size: var(--fs-12);
- }
-
- .blog-list-columns,
- .blog-list-item {
- grid-template-columns: 98px minmax(0, 1fr) 18px;
- }
-
- .blog-list-columns .col-mode,
- .blog-list-columns .col-desc,
- .blog-list-mode {
- display: none;
- }
-
- .blog-list-date {
- font-size: var(--fs-11);
- }
-
- .blog-list-desc {
- display: none;
- }
-
- .blog-list-container {
- padding: 6px;
- }
-
- .blog-post-view {
- padding: 16px;
- }
-}
diff --git a/internal/web/styles/blog_detail.css b/internal/web/styles/blog_detail.css
@@ -1,149 +0,0 @@
-.blog-article-header {
- margin-bottom: 18px;
- padding-bottom: 16px;
- border-bottom: 1px solid var(--border);
-}
-
-.blog-article-header-top {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 16px;
-}
-
-.blog-article-title {
- font-size: var(--fs-24);
- font-weight: 500;
- color: var(--accent);
- margin: 0 0 8px;
- line-height: 1.3;
- overflow-wrap: anywhere;
-}
-
-.blog-share-btn {
- background: transparent;
- border: 1px solid var(--border);
- color: var(--fg-dim);
- padding: 6px 12px;
- cursor: pointer;
- font-family: inherit;
- font-size: var(--fs-12);
-}
-
-.blog-share-btn:hover {
- background: var(--bg-alt);
- color: var(--accent);
- border-color: var(--accent);
-}
-
-.blog-share-btn.copied {
- color: var(--green);
- border-color: var(--green);
-}
-
-.blog-article-meta {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.blog-article-date {
- color: var(--fg-dim);
- font-size: var(--fs-13);
-}
-
-.blog-share-btn-mobile {
- display: none;
-}
-
-.blog-article-content {
- font-size: var(--fs-14);
- line-height: 1.7;
- color: var(--fg);
- padding-bottom: 32px;
-}
-
-.blog-article-content p {
- margin-bottom: 16px;
-}
-
-.blog-article-content h2 {
- font-size: var(--fs-20);
- color: var(--accent);
- margin: 32px 0 12px;
-}
-
-.blog-article-content h3 {
- font-size: var(--fs-17);
- color: var(--accent);
- margin: 24px 0 10px;
-}
-
-.blog-article-content pre {
- display: block;
- background: var(--bg-alt);
- border: 1px solid var(--border);
- padding: 16px;
- margin: 16px 0;
- overflow-x: auto;
- font-size: var(--fs-13);
-}
-
-.blog-article-content code {
- background: var(--bg-alt);
- padding: 2px 6px;
- font-size: var(--fs-13);
-}
-
-.blog-article-content pre code {
- background: transparent;
- padding: 0;
- font-size: inherit;
-}
-
-.blog-article-content a {
- color: var(--light-blue);
-}
-
-.blog-article-content ul,
-.blog-article-content ol {
- margin: 12px 0;
- padding-left: 24px;
-}
-
-.blog-article-content table {
- width: 100%;
- border-collapse: collapse;
- margin: 16px 0;
- font-size: var(--fs-13);
-}
-
-.blog-article-content th,
-.blog-article-content td {
- border: 1px solid var(--border);
- padding: 10px 14px;
- text-align: left;
-}
-
-.blog-article-content th {
- background: var(--bg-alt);
- color: var(--accent);
-}
-
-@media (max-width: 768px) {
- .blog-article-title {
- font-size: var(--fs-20);
- }
-
- .blog-share-btn-desktop {
- display: none;
- }
-
- .blog-share-btn-mobile {
- display: block;
- }
-
- .blog-article-content {
- font-size: var(--fs-13);
- }
-}
diff --git a/internal/web/templates/layout.gohtml b/internal/web/templates/layout.gohtml
@@ -48,8 +48,6 @@
{{template "projects" .}}
{{else if eq .ContentTemplate "experience"}}
{{template "experience" .}}
- {{else if eq .ContentTemplate "blog"}}
- {{template "blog" .}}
{{end}}
</main>
{{template "footer" .}}
diff --git a/internal/web/templates/partials/blog.gohtml b/internal/web/templates/partials/blog.gohtml
@@ -1,46 +0,0 @@
-{{define "blog"}}
-<section id="blog" class="page">
- {{if eq (len .Blogs) 0}}
- <div class="blog-empty">
- <p class="color-dim">~/blog is empty</p>
- <p class="color-dim">check back soon</p>
- </div>
- {{else}}
- <div id="blog-list-view" class="blog-list-view{{if .Blog}} hidden{{end}}">
- <div class="blog-fs-shell">
- <div class="blog-fs-header">~/blog $ ls -l</div>
- <div class="blog-list-columns">
- <span class="col-mode">mode</span>
- <span>date</span>
- <span>name</span>
- <span class="col-desc">desc</span>
- <span class="col-arrow"></span>
- </div>
- <div class="blog-list-container" preload-images="true">
- {{range $i, $blog := .Blogs}}
- <div class="blog-list-item{{if $.SelectedBlogID}}{{if eq $blog.ID $.SelectedBlogID}} selected{{end}}{{else}}{{if eq $i 0}} selected{{end}}{{end}}" data-index="{{$i}}" data-id="{{$blog.ID}}" hx-get="/api/blog/{{$blog.ID}}" hx-target="#blog-post-content" hx-swap="innerHTML" preload="blog:selected">
- <span class="blog-list-mode">-rw-r--r--</span>
- <span class="blog-list-date">{{$blog.Date.Format "2006-01-02"}}</span>
- <span class="blog-list-name">
- <span class="blog-list-file">{{$blog.ID}}.md</span>
- <span class="blog-list-title">{{$blog.Title}}</span>
- </span>
- <span class="blog-list-desc" title="{{$blog.Description}}">{{$blog.Description}}</span>
- <span class="blog-list-arrow">-></span>
- </div>
- {{end}}
- </div>
- </div>
- </div>
-
- <div id="blog-post-view" class="blog-post-view{{if not .Blog}} hidden{{end}}">
- <div class="blog-post-container">
- <button class="blog-back-btn">cd ..</button>
- <div id="blog-post-content">{{if .Blog}}{{template "blog_detail" .}}{{end}}</div>
- </div>
- </div>
- {{end}}
-</section>
-
-<style>{{inlineCSS "blog.css"}}</style>
-{{end}}
diff --git a/internal/web/templates/partials/blog_detail.gohtml b/internal/web/templates/partials/blog_detail.gohtml
@@ -1,28 +0,0 @@
-{{define "blog_detail"}}
-{{if .Blog}}
-<article class="blog-article" data-id="{{.Blog.ID}}" data-blog-id="{{.Blog.ID}}">
- <header class="blog-article-header">
- <div class="blog-article-header-top">
- <h1 class="blog-article-title">~/blog/{{.Blog.ID}}.md</h1>
- <button class="blog-share-btn blog-share-btn-desktop" data-blog-id="{{.Blog.ID}}" data-blog-title="{{.Blog.ID}}.md">[Share]</button>
- </div>
- <div class="blog-article-meta">
- <time class="blog-article-date" datetime="{{.Blog.Date.Format "2006-01-02"}}">{{.Blog.Date.Format "2006-01-02"}}</time>
- <button class="blog-share-btn blog-share-btn-mobile" data-blog-id="{{.Blog.ID}}" data-blog-title="{{.Blog.ID}}.md">[Share]</button>
- </div>
- </header>
-
- <div class="blog-article-content" data-blog-id="{{.Blog.ID}}">
- {{if .Blog.URL}}
- <a class="external-link" href="{{.Blog.URL}}" target="_blank" rel="noopener noreferrer">Read on external site -></a>
- {{else if .Blog.HTML}}
- {{toHTML .Blog.HTML}}
- {{else}}
- <p class="color-dim">Content not available</p>
- {{end}}
- </div>
-</article>
-
-<style>{{inlineCSS "blog_detail.css"}}</style>
-{{end}}
-{{end}}
diff --git a/internal/web/templates/partials/header.gohtml b/internal/web/templates/partials/header.gohtml
@@ -1,5 +1,5 @@
{{define "header"}}
-<header id="tabs" data-active-tab="{{.ActiveTab}}" data-tabs="about,projects,experience,blog">
+<header id="tabs" data-active-tab="{{.ActiveTab}}" data-tabs="about,experience,projects">
<nav class="tab-nav" aria-label="main navigation" preload="preload:init" preload-images="true">
{{range .Tabs}}
<a href="{{tabPath .ID}}" class="tab{{if eq .ID $.ActiveTab}} active{{end}}" data-tab="{{.ID}}" data-label="{{.ID}}" hx-get="{{tabPath .ID}}" hx-target="#content" hx-push-url="true">