From 0be0dd7456e26590c720d6d29e8a47a984cf3ed2 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 13 Feb 2026 10:51:06 +0000 Subject: [PATCH] okay, pretty smooth. checkpoint here --- cache.go | 40 ------------------- content/default.css | 54 ++++++++++++++++++++++++++ main.go | 5 +-- parse.go | 95 +++++++++++++++++++-------------------------- server.go | 63 ++++++++++++++++++++++-------- 5 files changed, 143 insertions(+), 114 deletions(-) delete mode 100644 cache.go diff --git a/cache.go b/cache.go deleted file mode 100644 index a7c063e..0000000 --- a/cache.go +++ /dev/null @@ -1,40 +0,0 @@ -// cache.go -package main - -import ( - "crypto/md5" - "encoding/hex" - "os" - "path/filepath" - "sync" -) - -var ( - cacheDir = "cache" - cacheMutex sync.Mutex - TestMode bool -) - -// InitCache ensures the cache directory exists -func InitCache() { - if _, err := os.Stat(cacheDir); os.IsNotExist(err) { - os.Mkdir(cacheDir, 0755) - } -} - -// ClearCache wipes the cache directory (used for Test Mode) -func ClearCache() { - os.RemoveAll(cacheDir) - InitCache() -} - -// GetCacheFilename generates the hashed filename preserving the extension -func GetCacheFilename(key string) string { - hash := md5.Sum([]byte(key)) - ext := filepath.Ext(key) - // Default to .html if no extension (e.g. for processed markdown) - if ext == "" || ext == ".md" { - ext = ".html" - } - return filepath.Join(cacheDir, hex.EncodeToString(hash[:])+ext) -} diff --git a/content/default.css b/content/default.css index 2e16ab0..651915b 100644 --- a/content/default.css +++ b/content/default.css @@ -306,4 +306,58 @@ blockquote { .main-header h1 { font-size: 2rem; } +} + +/* Latest Post Preview Section */ +.latest-post-preview { + margin-top: 4rem; + padding-top: 2rem; + border-top: 1px solid var(--sidebar-border); +} + +.latest-post-preview h3 { + margin-top: 0; + margin-bottom: 2rem; + font-size: 1.5rem; + text-transform: uppercase; + letter-spacing: 1px; +} + +.preview-content { + position: relative; + max-height: 400px; + /* Limit height for the fade effect */ + overflow: hidden; + margin-bottom: 2rem; + + /* Fade Effect at the bottom */ + -webkit-mask-image: linear-gradient(to bottom, black 60%, transparent 100%); + mask-image: linear-gradient(to bottom, black 60%, transparent 100%); +} + +.read-more-container { + text-align: center; + margin-top: -2rem; + /* Pull up to cover the faded bottom area slightly or just sit below */ + position: relative; + z-index: 10; +} + +.read-more-btn { + display: inline-block; + padding: 10px 20px; + border: 1px solid var(--text-color); + text-decoration: none; + text-transform: uppercase; + font-weight: bold; + font-size: 0.9rem; + transition: all 0.3s ease; + background: var(--bg-color); + color: var(--text-color); +} + +.read-more-btn:hover { + background: var(--text-color); + color: var(--bg-color); + border-color: var(--text-color); } \ No newline at end of file diff --git a/main.go b/main.go index 259c9b8..d2a8ce6 100644 --- a/main.go +++ b/main.go @@ -13,10 +13,7 @@ func main() { InstallService() return case "test": - fmt.Println("Starting in TEST MODE (Caching Disabled, Cache Cleared)") - TestMode = true - // Wipe the cache so we don't see old files - ClearCache() + fmt.Println("Starting blog server (Test Mode: Caching Disabled via architecture)") StartServer() return case "addTag": diff --git a/parse.go b/parse.go index 8b0ee1e..191a90f 100644 --- a/parse.go +++ b/parse.go @@ -21,16 +21,16 @@ import ( "gopkg.in/yaml.v3" ) -// GetContentPath ensures the content is cached and returns the path to the cached file. -func GetContentPath(file ContentFile) (string, error) { - cachePath := GetCacheFilename(file.OriginalPath) +// RenderContent parses the file and returns the rendered HTML content as bytes. +// It performs no caching. +func RenderContent(file ContentFile) ([]byte, error) { // Read Raw File raw, err := ReadRaw(file.OriginalPath) if err != nil { - return "", fmt.Errorf("failed to read file: %w", err) + return nil, fmt.Errorf("failed to read file: %w", err) } var dataToWrite []byte @@ -55,7 +55,7 @@ func GetContentPath(file ContentFile) (string, error) { var buf bytes.Buffer ctx := parser.NewContext() if err := md.Convert(raw, &buf, parser.WithContext(ctx)); err != nil { - return "", fmt.Errorf("failed to parse markdown: %w", err) + return nil, fmt.Errorf("failed to parse markdown: %w", err) } // Extract Frontmatter @@ -148,8 +148,6 @@ func GetContentPath(file ContentFile) (string, error) { postRaw, err := ReadRaw(latestPost.OriginalPath) if err == nil { // Parse the post to HTML to display as preview - // We need to parse it fully to get the HTML content - // Re-using the MD parser config from above would be cleaner but let's instantiate for now mdPreview := goldmark.New( goldmark.WithExtensions( extension.GFM, @@ -164,43 +162,51 @@ func GetContentPath(file ContentFile) (string, error) { var previewBuf bytes.Buffer ctxPreview := parser.NewContext() if err := mdPreview.Convert(postRaw, &previewBuf, parser.WithContext(ctxPreview)); err == nil { - // Extract Metadata of the post to update Page Title? - // User said: "the [title] be big text embedded in the black area." - // So if we are previewing the latest post, the Home Page Title should probably match the Post Title on the Home Page? - // Or should it say "Latest: Title"? - // The main header h1 is where {{Title}} goes. - // Getting metadata from preview context + // Extract Metadata of the post (unused for now, but kept for future reference) var postMeta templates.PageMetadata dPost := frontmatter.Get(ctxPreview) if dPost != nil { dPost.Decode(&postMeta) } - // Fallback title logic - if postMeta.Title == "" { - base := filepath.Base(latestPost.OriginalPath) - postMeta.Title = strings.TrimSuffix(base, filepath.Ext(base)) - } - - // Update the meta for the HOME PAGE render - meta.Title = postMeta.Title // Use the post's title - - // Extract Preview (First Paragraph?) - // "preview of the latest post, then read more" + // DO NOT overwrite meta.Title for the Home Page. Keep it as "Index" or whatever index.md has. + fullHTML := previewBuf.String() - // Simple strategy: take everything up to the first

or limit chars? - // Or explicit split? - // Let's take the first paragraph. + // Extract Preview (First 3 Paragraphs?) + // Strategy: count

tags? + // Let's capture approx 1500 chars or 3 paragraphs, whichever is shorter/better. + // Actually, splitting by

is safer for HTML validation. + parts := strings.Split(fullHTML, "

") - if len(parts) > 0 { - previewHTML := parts[0] + "

" - // Add Read More Link - readMore := fmt.Sprintf(`

Read More...

`, latestPost.RoutePath) - htmlContent = previewHTML + readMore - } else { - htmlContent = fullHTML + var previewHTML string + limit := 3 + if len(parts) < limit { + limit = len(parts) } + + for i := 0; i < limit; i++ { + if strings.TrimSpace(parts[i]) != "" { + previewHTML += parts[i] + "

" + } + } + + // Add Fade/Blur Container + // Append to existing htmlContent (which currently holds index.md content) + // Add a header for the preview? + + previewBlock := fmt.Sprintf(` +
+

Latest: %s

+
+ %s +
+ +
`, postMeta.Title, previewHTML, latestPost.RoutePath) + + htmlContent += previewBlock } } } @@ -407,26 +413,7 @@ func GetContentPath(file ContentFile) (string, error) { dataToWrite = templates.BuildFullPage([]byte(htmlContent), meta, latestPostsHTML) } - // 4. Return Content Object or Path? - // The function signature returns (string, error), which is the path to the cached file. - // Since we are disabling cache, we should probably change the architecture to return ([]byte, error) - // or write to a temporary file if the rest of the app expects a file path. - // The rest of the app (server.go) does http.ServeFile(w, r, cachePath). - // So we MUST return a file path. - - // For now, we will continue to write to the "cache" location, but since we removed the read check, - // it basically acts as a "render to temp file" on every request. - // This satisfies the "disable caching" requirement (always fresh) while keeping the file-based serving architecture. - - tmpPath := cachePath + ".tmp" - if err := os.WriteFile(tmpPath, dataToWrite, 0644); err != nil { - return "", fmt.Errorf("failed to write cache: %w", err) - } - if err := os.Rename(tmpPath, cachePath); err != nil { - return "", fmt.Errorf("failed to commit cache: %w", err) - } - - return cachePath, nil + return dataToWrite, nil } // loadExternalMetadata checks for .yml, .yaml, .toml files and loads them. diff --git a/server.go b/server.go index f6b4968..9364eb4 100644 --- a/server.go +++ b/server.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "path/filepath" "strings" ) @@ -12,9 +13,9 @@ var globalRoutes RouteMap // StartServer initializes the routing and starts the HTTP listener. func StartServer() { - InitCache() + // InitCache() - Removed - // Initial Scan + // Initial Scan (Optional, just to valid startup) var err error globalRoutes, err = ScanContent("content") if err != nil { @@ -31,34 +32,64 @@ func StartServer() { } func handleRequest(w http.ResponseWriter, r *http.Request) { - // 1. Handle Test Mode (Re-scan on every request) - if TestMode { - w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") - routes, err := ScanContent("content") - if err == nil { - globalRoutes = routes - } + // 1. Scan Content on Every Request (Dynamic) + // This ensures we always have the latest file list. + // In a high-traffic production env, we'd use fsnotify, but for a personal blog, this is fine. + routes, err := ScanContent("content") + if err != nil { + http.Error(w, "Failed to scan content", http.StatusInternalServerError) + log.Printf("Scan error: %v", err) + return } + globalRoutes = routes // 2. Normalize Request Path reqPath := strings.ToLower(r.URL.Path) - + + // Handle /Favicon.svg directly if needed, or rely on it being in content? + // If Favicon.svg is in content/Favicon.svg, it will be in routes as /favicon.svg + // 3. Look up in Route Map file, found := globalRoutes[reqPath] if !found { + // Try default file if this is a directory? + // Global routes scan handles index.md -> / http.NotFound(w, r) return } - // 4. Get Content Path (Ensures cache is fresh) - cachePath, err := GetContentPath(file) + // 4. Render Content + contentBytes, err := RenderContent(file) if err != nil { - log.Printf("Error serving %s: %v", file.OriginalPath, err) + log.Printf("Error rendering %s: %v", file.OriginalPath, err) http.Error(w, "Internal Server Error", 500) return } - // 5. Serve File - // http.ServeFile handles Content-Type, Range requests, and Caching headers automatically. - http.ServeFile(w, r, cachePath) + // 5. Serve Content + // Detect Content-Type based on extension for non-markdown + if !file.IsMarkdown { + ext := strings.ToLower(filepath.Ext(file.OriginalPath)) + switch ext { + case ".css": + w.Header().Set("Content-Type", "text/css") + case ".js": + w.Header().Set("Content-Type", "application/javascript") + case ".svg": + w.Header().Set("Content-Type", "image/svg+xml") + case ".png": + w.Header().Set("Content-Type", "image/png") + case ".jpg", ".jpeg": + w.Header().Set("Content-Type", "image/jpeg") + } + } else { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + } + + // Disable browser caching to ensure changes are seen immediately + w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") + + if _, err := w.Write(contentBytes); err != nil { + log.Printf("Error writing response: %v", err) + } }