okay, pretty smooth. checkpoint here

This commit is contained in:
root 2026-02-13 10:51:06 +00:00
parent f8b3194199
commit 0be0dd7456
5 changed files with 143 additions and 114 deletions

View File

@ -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)
}

View File

@ -307,3 +307,57 @@ blockquote {
font-size: 2rem; 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);
}

View File

@ -13,10 +13,7 @@ func main() {
InstallService() InstallService()
return return
case "test": case "test":
fmt.Println("Starting in TEST MODE (Caching Disabled, Cache Cleared)") fmt.Println("Starting blog server (Test Mode: Caching Disabled via architecture)")
TestMode = true
// Wipe the cache so we don't see old files
ClearCache()
StartServer() StartServer()
return return
case "addTag": case "addTag":

View File

@ -21,16 +21,16 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// GetContentPath ensures the content is cached and returns the path to the cached file. // RenderContent parses the file and returns the rendered HTML content as bytes.
func GetContentPath(file ContentFile) (string, error) { // It performs no caching.
cachePath := GetCacheFilename(file.OriginalPath) func RenderContent(file ContentFile) ([]byte, error) {
// Read Raw File // Read Raw File
raw, err := ReadRaw(file.OriginalPath) raw, err := ReadRaw(file.OriginalPath)
if err != nil { 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 var dataToWrite []byte
@ -55,7 +55,7 @@ func GetContentPath(file ContentFile) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
ctx := parser.NewContext() ctx := parser.NewContext()
if err := md.Convert(raw, &buf, parser.WithContext(ctx)); err != nil { 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 // Extract Frontmatter
@ -148,8 +148,6 @@ func GetContentPath(file ContentFile) (string, error) {
postRaw, err := ReadRaw(latestPost.OriginalPath) postRaw, err := ReadRaw(latestPost.OriginalPath)
if err == nil { if err == nil {
// Parse the post to HTML to display as preview // 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( mdPreview := goldmark.New(
goldmark.WithExtensions( goldmark.WithExtensions(
extension.GFM, extension.GFM,
@ -164,43 +162,51 @@ func GetContentPath(file ContentFile) (string, error) {
var previewBuf bytes.Buffer var previewBuf bytes.Buffer
ctxPreview := parser.NewContext() ctxPreview := parser.NewContext()
if err := mdPreview.Convert(postRaw, &previewBuf, parser.WithContext(ctxPreview)); err == nil { if err := mdPreview.Convert(postRaw, &previewBuf, parser.WithContext(ctxPreview)); err == nil {
// Extract Metadata of the post to update Page Title? // Extract Metadata of the post (unused for now, but kept for future reference)
// 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
var postMeta templates.PageMetadata var postMeta templates.PageMetadata
dPost := frontmatter.Get(ctxPreview) dPost := frontmatter.Get(ctxPreview)
if dPost != nil { if dPost != nil {
dPost.Decode(&postMeta) dPost.Decode(&postMeta)
} }
// Fallback title logic // DO NOT overwrite meta.Title for the Home Page. Keep it as "Index" or whatever index.md has.
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"
fullHTML := previewBuf.String() fullHTML := previewBuf.String()
// Simple strategy: take everything up to the first </p> or limit chars? // Extract Preview (First 3 Paragraphs?)
// Or explicit split? // Strategy: count <p> tags?
// Let's take the first paragraph. // Let's capture approx 1500 chars or 3 paragraphs, whichever is shorter/better.
// Actually, splitting by </p> is safer for HTML validation.
parts := strings.Split(fullHTML, "</p>") parts := strings.Split(fullHTML, "</p>")
if len(parts) > 0 { var previewHTML string
previewHTML := parts[0] + "</p>" limit := 3
// Add Read More Link if len(parts) < limit {
readMore := fmt.Sprintf(`<p><a href="%s">Read More...</a></p>`, latestPost.RoutePath) limit = len(parts)
htmlContent = previewHTML + readMore
} else {
htmlContent = fullHTML
} }
for i := 0; i < limit; i++ {
if strings.TrimSpace(parts[i]) != "" {
previewHTML += parts[i] + "</p>"
}
}
// Add Fade/Blur Container
// Append to existing htmlContent (which currently holds index.md content)
// Add a header for the preview?
previewBlock := fmt.Sprintf(`
<div class="latest-post-preview">
<h3>Latest: %s</h3>
<div class="preview-content">
%s
</div>
<div class="read-more-container">
<a href="%s" class="read-more-btn">Read Full Article</a>
</div>
</div>`, postMeta.Title, previewHTML, latestPost.RoutePath)
htmlContent += previewBlock
} }
} }
} }
@ -407,26 +413,7 @@ func GetContentPath(file ContentFile) (string, error) {
dataToWrite = templates.BuildFullPage([]byte(htmlContent), meta, latestPostsHTML) dataToWrite = templates.BuildFullPage([]byte(htmlContent), meta, latestPostsHTML)
} }
// 4. Return Content Object or Path? return dataToWrite, nil
// 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
} }
// loadExternalMetadata checks for .yml, .yaml, .toml files and loads them. // loadExternalMetadata checks for .yml, .yaml, .toml files and loads them.

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"path/filepath"
"strings" "strings"
) )
@ -12,9 +13,9 @@ var globalRoutes RouteMap
// StartServer initializes the routing and starts the HTTP listener. // StartServer initializes the routing and starts the HTTP listener.
func StartServer() { func StartServer() {
InitCache() // InitCache() - Removed
// Initial Scan // Initial Scan (Optional, just to valid startup)
var err error var err error
globalRoutes, err = ScanContent("content") globalRoutes, err = ScanContent("content")
if err != nil { if err != nil {
@ -31,34 +32,64 @@ func StartServer() {
} }
func handleRequest(w http.ResponseWriter, r *http.Request) { func handleRequest(w http.ResponseWriter, r *http.Request) {
// 1. Handle Test Mode (Re-scan on every request) // 1. Scan Content on Every Request (Dynamic)
if TestMode { // This ensures we always have the latest file list.
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") // In a high-traffic production env, we'd use fsnotify, but for a personal blog, this is fine.
routes, err := ScanContent("content") routes, err := ScanContent("content")
if err == nil { if err != nil {
globalRoutes = routes http.Error(w, "Failed to scan content", http.StatusInternalServerError)
} log.Printf("Scan error: %v", err)
return
} }
globalRoutes = routes
// 2. Normalize Request Path // 2. Normalize Request Path
reqPath := strings.ToLower(r.URL.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 // 3. Look up in Route Map
file, found := globalRoutes[reqPath] file, found := globalRoutes[reqPath]
if !found { if !found {
// Try default file if this is a directory?
// Global routes scan handles index.md -> /
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
// 4. Get Content Path (Ensures cache is fresh) // 4. Render Content
cachePath, err := GetContentPath(file) contentBytes, err := RenderContent(file)
if err != nil { 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) http.Error(w, "Internal Server Error", 500)
return return
} }
// 5. Serve File // 5. Serve Content
// http.ServeFile handles Content-Type, Range requests, and Caching headers automatically. // Detect Content-Type based on extension for non-markdown
http.ServeFile(w, r, cachePath) 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)
}
} }