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)
|
|
||||||
}
|
|
||||||
|
|
@ -306,4 +306,58 @@ blockquote {
|
||||||
.main-header h1 {
|
.main-header h1 {
|
||||||
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);
|
||||||
}
|
}
|
||||||
5
main.go
5
main.go
|
|
@ -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":
|
||||||
|
|
|
||||||
95
parse.go
95
parse.go
|
|
@ -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.
|
||||||
|
|
|
||||||
63
server.go
63
server.go
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue