okay, pretty smooth. checkpoint here
This commit is contained in:
parent
f8b3194199
commit
0be0dd7456
40
cache.go
40
cache.go
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -307,3 +307,57 @@ blockquote {
|
|||
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);
|
||||
}
|
||||
5
main.go
5
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":
|
||||
|
|
|
|||
93
parse.go
93
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))
|
||||
}
|
||||
// DO NOT overwrite meta.Title for the Home Page. Keep it as "Index" or whatever index.md has.
|
||||
|
||||
// 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()
|
||||
|
||||
// Simple strategy: take everything up to the first </p> or limit chars?
|
||||
// Or explicit split?
|
||||
// Let's take the first paragraph.
|
||||
// Extract Preview (First 3 Paragraphs?)
|
||||
// Strategy: count <p> tags?
|
||||
// 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>")
|
||||
if len(parts) > 0 {
|
||||
previewHTML := parts[0] + "</p>"
|
||||
// Add Read More Link
|
||||
readMore := fmt.Sprintf(`<p><a href="%s">Read More...</a></p>`, 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] + "</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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
|
|||
59
server.go
59
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")
|
||||
// 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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue