summaryrefslogtreecommitdiff
path: root/internal/post
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-09 20:44:58 +0300
committerPaul Buetow <paul@buetow.org>2026-04-09 20:44:58 +0300
commit3e61d09873065f5342efc414ee3ea0d5fdc4c767 (patch)
tree7d0ac51cfb41b4774db6292deeb0cc3dce93cf07 /internal/post
parent51f95f88ca78471a50b3fc62dbcea8edb609dc80 (diff)
add snonux static microblog generator
Full Go implementation with: - txt/md/image/audio input processing, URL auto-linking in .txt files - Paginated HTML output with Atom feed - 11 visual themes: neon, terminal, synthwave, minimal, brutalist, paper, aurora, matrix, ocean, retro, glass (selectable via --theme flag) - Keyboard navigation (j/k/arrows, Enter modal, h/l page nav) - Shared nav templates (navhints, navmodal, navscript) across all themes - Magefile build automation; integration test suite covering all themes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/post')
-rw-r--r--internal/post/post.go84
1 files changed, 84 insertions, 0 deletions
diff --git a/internal/post/post.go b/internal/post/post.go
new file mode 100644
index 0000000..cdef546
--- /dev/null
+++ b/internal/post/post.go
@@ -0,0 +1,84 @@
+// Package post defines the Post data model and its JSON serialisation format.
+// Each post is stored as post.json inside its own directory under outdir/posts/.
+// This allows pages to be re-generated without re-processing the original inputs.
+package post
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+// Type enumerates the supported input content types.
+type Type string
+
+const (
+ TypeText Type = "text"
+ TypeMarkdown Type = "markdown"
+ TypeImage Type = "image"
+ TypeAudio Type = "audio"
+)
+
+// Post represents a single microblog entry.
+// It is persisted as post.json in outdir/posts/<ID>/.
+type Post struct {
+ // ID is the unique directory-name-safe timestamp, e.g. "2026-04-09-143022".
+ ID string `json:"id"`
+
+ // Timestamp is the moment the post was processed (UTC).
+ Timestamp time.Time `json:"timestamp"`
+
+ // PostType determines how the content was generated and how it should be rendered.
+ PostType Type `json:"type"`
+
+ // Content is the pre-rendered HTML snippet for this post (without outer post-card wrapper).
+ Content string `json:"content"`
+
+ // Assets lists filenames (not paths) of any asset files stored alongside post.json.
+ Assets []string `json:"assets,omitempty"`
+}
+
+// Save writes the post as post.json into dir.
+func (p *Post) Save(dir string) error {
+ data, err := json.MarshalIndent(p, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal post %s: %w", p.ID, err)
+ }
+
+ path := filepath.Join(dir, "post.json")
+ if err := os.WriteFile(path, data, 0o644); err != nil {
+ return fmt.Errorf("write post.json for %s: %w", p.ID, err)
+ }
+
+ return nil
+}
+
+// Load reads and parses post.json from dir.
+func Load(dir string) (*Post, error) {
+ path := filepath.Join(dir, "post.json")
+
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("read post.json in %s: %w", dir, err)
+ }
+
+ var p Post
+ if err := json.Unmarshal(data, &p); err != nil {
+ return nil, fmt.Errorf("unmarshal post.json in %s: %w", dir, err)
+ }
+
+ return &p, nil
+}
+
+// NewID generates a unique post ID from the given time.
+// Format: YYYY-MM-DD-HHmmss, optionally suffixed with -N for collisions.
+func NewID(t time.Time, suffix int) string {
+ base := t.UTC().Format("2006-01-02-150405")
+ if suffix == 0 {
+ return base
+ }
+
+ return fmt.Sprintf("%s-%d", base, suffix)
+}