style is bit terrible, everything works tho
This commit is contained in:
parent
1c93ab8607
commit
d4dbec8a84
|
|
@ -3,5 +3,4 @@ vendor
|
|||
*.lock
|
||||
.vscode
|
||||
cache
|
||||
content
|
||||
blog
|
||||
59
cache.go
59
cache.go
|
|
@ -10,12 +10,9 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
memoryCache = make(map[string][]byte)
|
||||
cacheMutex sync.RWMutex
|
||||
cacheDir = "cache"
|
||||
|
||||
// TestMode disables caching when set to true
|
||||
TestMode bool
|
||||
cacheDir = "cache"
|
||||
cacheMutex sync.Mutex
|
||||
TestMode bool
|
||||
)
|
||||
|
||||
// InitCache ensures the cache directory exists
|
||||
|
|
@ -31,8 +28,8 @@ func ClearCache() {
|
|||
InitCache()
|
||||
}
|
||||
|
||||
// getCacheFilename generates the hashed filename preserving the extension
|
||||
func getCacheFilename(key string) string {
|
||||
// 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)
|
||||
|
|
@ -41,49 +38,3 @@ func getCacheFilename(key string) string {
|
|||
}
|
||||
return filepath.Join(cacheDir, hex.EncodeToString(hash[:])+ext)
|
||||
}
|
||||
|
||||
// CheckCache looks for content in memory, then on disk.
|
||||
func CheckCache(key string) ([]byte, bool) {
|
||||
if TestMode {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
cacheMutex.RLock()
|
||||
|
||||
// 1. Check Memory
|
||||
if data, found := memoryCache[key]; found {
|
||||
cacheMutex.RUnlock()
|
||||
return data, true
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
// 2. Check Disk
|
||||
filePath := getCacheFilename(key)
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err == nil {
|
||||
// Populate memory for next time
|
||||
cacheMutex.Lock()
|
||||
memoryCache[key] = data
|
||||
cacheMutex.Unlock()
|
||||
return data, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// StoreCache saves content to memory and disk.
|
||||
func StoreCache(key string, data []byte) error {
|
||||
if TestMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
cacheMutex.Lock()
|
||||
defer cacheMutex.Unlock()
|
||||
|
||||
// 1. Save to memory
|
||||
memoryCache[key] = data
|
||||
|
||||
// 2. Save to disk
|
||||
filePath := getCacheFilename(key)
|
||||
return os.WriteFile(filePath, data, 0644)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
/* content/default.css */
|
||||
|
||||
/*
|
||||
The "27bslash6" Aesthetic:
|
||||
- Stark Black & White
|
||||
- Helvetica / Arial
|
||||
- Narrow, centered reading column
|
||||
- Zero border radius, zero shadows
|
||||
*/
|
||||
|
||||
:root {
|
||||
--bg-color: #ffffff;
|
||||
--text-color: #000000;
|
||||
--link-color: #cc0000; /* That specific "angry" red he sometimes uses, or just black */
|
||||
--meta-color: #666666;
|
||||
--font-stack: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--max-width: 680px; /* The classic narrow column */
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-stack);
|
||||
font-size: 15px; /* Slightly smaller, crisp text */
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
/* The main document container */
|
||||
body > * {
|
||||
max-width: var(--max-width);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3 {
|
||||
font-weight: bold;
|
||||
letter-spacing: -0.5px;
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #000;
|
||||
padding-bottom: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1.2rem;
|
||||
text-align: justify; /* Gives it that "formal letter" look */
|
||||
}
|
||||
|
||||
/* Links - Stark and obvious */
|
||||
a {
|
||||
color: var(--text-color);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Code blocks - Raw and industrial */
|
||||
pre {
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #ccc;
|
||||
padding: 15px;
|
||||
font-family: "Monaco", "Menlo", "Consolas", monospace;
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Monaco", "Menlo", "Consolas", monospace;
|
||||
background: #f0f0f0;
|
||||
padding: 2px 4px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
/* Blockquotes - The "Email Reply" look */
|
||||
blockquote {
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 15px;
|
||||
border-left: 4px solid #000;
|
||||
color: var(--meta-color);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Images - fit the column */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 2rem auto;
|
||||
border: 1px solid #000; /* Optional: adds to the brutalist feel */
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul, ol {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
Specific Components
|
||||
*/
|
||||
|
||||
/* Top Banner - The "System Notice" Style
|
||||
*/
|
||||
.topbanner {
|
||||
background-color: #f4f4f4; /* Very light grey, distinct from white body */
|
||||
border: 1px solid #000; /* Crisp definition */
|
||||
border-left-width: 6px; /* Thick accent, matches your sidebar theme */
|
||||
padding: 1.5rem 2rem; /* Generous padding for readability */
|
||||
margin-bottom: 3rem; /* Push the H1 down */
|
||||
font-size: 14px; /* Slightly smaller than body text */
|
||||
}
|
||||
|
||||
/* Ensure text inside the banner doesn't inherit the 'justify' from global p */
|
||||
.topbanner p {
|
||||
text-align: left;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.topbanner p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Optional: Make links inside the banner aggressive */
|
||||
.topbanner a {
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.topbanner a:hover {
|
||||
background-color: var(--link-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* The Site Headline (Top of Index) */
|
||||
.site-headline {
|
||||
margin-bottom: 3rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.site-headline h1 {
|
||||
font-size: 32px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
.sidebar {
|
||||
float: right;
|
||||
width: 30%;
|
||||
min-width: 250px;
|
||||
margin-left: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: #f9f9f9;
|
||||
border-left: 4px solid #333;
|
||||
}
|
||||
|
||||
/* The Latest Posts List */
|
||||
.latest-posts {
|
||||
margin-top: 3rem;
|
||||
border-top: 4px solid #000;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.latest-posts h2 {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
color: var(--meta-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.latest-posts ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.latest-posts li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.latest-posts .date {
|
||||
color: var(--meta-color);
|
||||
font-size: 12px;
|
||||
margin-right: 1rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.latest-posts a {
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.latest-posts a:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
/* Directory Link */
|
||||
.directory-link {
|
||||
margin: 2rem 0;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
# I'm quite fond of lyrics. Particicularly, I like interpol lyrics.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
_-_-
|
||||
_-_- Else-If.org - Blog
|
||||
_-_-
|
||||
|
||||
# Welcome to the Else-If.org Blog directory!
|
||||
|
||||
I'll come up with more to say soon!
|
||||
|
||||
__p.s. my name is Jess P.S ;D__
|
||||
|
||||
|||
|
||||
|
|
@ -0,0 +1 @@
|
|||
title: Blog Homepage
|
||||
1
go.mod
1
go.mod
|
|
@ -8,5 +8,6 @@ require go.abhg.dev/goldmark/frontmatter v0.3.0
|
|||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/tendstofortytwo/goldmark-customtag v0.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -6,6 +6,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tendstofortytwo/goldmark-customtag v0.0.1 h1:c3Wnzi98gE7BPaqio/UOQVGGJU0EMdgKwjQHo9Y7NKQ=
|
||||
github.com/tendstofortytwo/goldmark-customtag v0.0.1/go.mod h1:9KHP0WeHIafExF+Zu4CIDfzgfMo/FaEKXUO7NFmCyos=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.abhg.dev/goldmark/frontmatter v0.3.0 h1:ZOrMkeyyYzhlbenFNmOXyGFx1dFE8TgBWAgZfs9D5RA=
|
||||
|
|
|
|||
212
parse.go
212
parse.go
|
|
@ -4,7 +4,9 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.else-if.org/jess/blog/templates"
|
||||
|
|
@ -15,104 +17,146 @@ import (
|
|||
"go.abhg.dev/goldmark/frontmatter"
|
||||
)
|
||||
|
||||
// GetContent is the primary entry point for retrieving parsed content.
|
||||
func GetContent(file ContentFile) ([]byte, error) {
|
||||
// 1. Ask Cache
|
||||
if data, found := CheckCache(file.OriginalPath); found {
|
||||
return data, nil
|
||||
}
|
||||
// GetContentPath ensures the content is cached and returns the path to the cached file.
|
||||
func GetContentPath(file ContentFile) (string, error) {
|
||||
cachePath := GetCacheFilename(file.OriginalPath)
|
||||
|
||||
// 2. Read Raw
|
||||
raw, err := ReadRaw(file.OriginalPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
// 1. Check Cache Freshness (if not in TestMode)
|
||||
if !TestMode {
|
||||
cacheInfo, errCache := os.Stat(cachePath)
|
||||
origInfo, errOrig := os.Stat(file.OriginalPath)
|
||||
|
||||
// If not markdown (e.g. css, images), return raw bytes immediately
|
||||
if !file.IsMarkdown {
|
||||
_ = StoreCache(file.OriginalPath, raw)
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// 3. Configure Goldmark
|
||||
// We use the Extender pattern which is the standard way to add frontmatter support.
|
||||
// We also enable Unsafe mode to allow raw HTML tags (like <site-headline>) to pass through
|
||||
// so we can replace them later.
|
||||
md := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
&frontmatter.Extender{},
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
)
|
||||
|
||||
// 4. Parse Markdown
|
||||
var buf bytes.Buffer
|
||||
ctx := parser.NewContext()
|
||||
|
||||
if err := md.Convert(raw, &buf, parser.WithContext(ctx)); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse markdown: %w", err)
|
||||
}
|
||||
|
||||
// 5. Extract Frontmatter
|
||||
// We retrieve the metadata using the context after parsing
|
||||
var meta templates.PageMetadata
|
||||
d := frontmatter.Get(ctx)
|
||||
if d != nil {
|
||||
if err := d.Decode(&meta); err != nil {
|
||||
// If decoding fails, we just proceed without metadata
|
||||
fmt.Printf("Warning: failed to decode frontmatter for %s: %v\n", file.OriginalPath, err)
|
||||
if errCache == nil && errOrig == nil && cacheInfo.ModTime().After(origInfo.ModTime()) {
|
||||
return cachePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If title is missing, try to grab the filename
|
||||
if meta.Title == "" {
|
||||
base := filepath.Base(file.OriginalPath)
|
||||
meta.Title = strings.TrimSuffix(base, filepath.Ext(base))
|
||||
// 2. Regenerate Content
|
||||
cacheMutex.Lock()
|
||||
defer cacheMutex.Unlock()
|
||||
|
||||
if !TestMode {
|
||||
cacheInfo, errCache := os.Stat(cachePath)
|
||||
origInfo, errOrig := os.Stat(file.OriginalPath)
|
||||
if errCache == nil && errOrig == nil && cacheInfo.ModTime().After(origInfo.ModTime()) {
|
||||
return cachePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Special Handling for Index (Dynamic Components)
|
||||
htmlContent := buf.String()
|
||||
// Read Raw File
|
||||
raw, err := ReadRaw(file.OriginalPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
if file.RoutePath == "/" {
|
||||
// Generate the list of posts
|
||||
var posts []templates.PostSnippet
|
||||
for _, f := range AllContent {
|
||||
if f.IsMarkdown && f.RoutePath != "/" {
|
||||
name := filepath.Base(f.OriginalPath)
|
||||
title := strings.TrimSuffix(name, filepath.Ext(name))
|
||||
var dataToWrite []byte
|
||||
|
||||
posts = append(posts, templates.PostSnippet{
|
||||
Title: title,
|
||||
URL: f.RoutePath,
|
||||
Date: f.ModTime,
|
||||
if !file.IsMarkdown {
|
||||
dataToWrite = raw
|
||||
} else {
|
||||
// Configure Goldmark with the custom tag extension
|
||||
md := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
&frontmatter.Extender{},
|
||||
templates.SidebarTag, // ||| -> <sidebar>
|
||||
templates.TopBanner,
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Extract Frontmatter
|
||||
var meta templates.PageMetadata
|
||||
d := frontmatter.Get(ctx)
|
||||
if d != nil {
|
||||
if err := d.Decode(&meta); err != nil {
|
||||
fmt.Printf("Warning: failed to decode frontmatter for %s: %v\n", file.OriginalPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if meta.Title == "" {
|
||||
base := filepath.Base(file.OriginalPath)
|
||||
meta.Title = strings.TrimSuffix(base, filepath.Ext(base))
|
||||
}
|
||||
|
||||
htmlContent := buf.String()
|
||||
|
||||
// 3. Post-Process for Index (Dynamic Content Injection)
|
||||
if file.RoutePath == "/" {
|
||||
// Generate posts data
|
||||
var posts []templates.PostSnippet
|
||||
for _, f := range AllContent {
|
||||
if f.IsMarkdown && f.RoutePath != "/" {
|
||||
name := filepath.Base(f.OriginalPath)
|
||||
title := strings.TrimSuffix(name, filepath.Ext(name))
|
||||
|
||||
posts = append(posts, templates.PostSnippet{
|
||||
Title: title,
|
||||
URL: f.RoutePath,
|
||||
Date: f.ModTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Add More Custom tags here. Add a customTag in tags.go as well, and the implementation in a template file.
|
||||
*/
|
||||
|
||||
// ||| SideBar
|
||||
sidebarRegex := regexp.MustCompile(`(?s)<sidebar>.*?</sidebar>`)
|
||||
|
||||
if sidebarRegex.MatchString(htmlContent) {
|
||||
// Render the dynamic content
|
||||
latestPostsHTML := templates.RenderLatestPosts(posts)
|
||||
dirLink := templates.RenderDirectoryLink()
|
||||
|
||||
// Wrap in <div class="sidebar"> to match CSS
|
||||
replacement := fmt.Sprintf(`<div class="sidebar">%s%s</div>`, latestPostsHTML, dirLink)
|
||||
|
||||
// Replace the placeholder tag with the actual content
|
||||
htmlContent = sidebarRegex.ReplaceAllString(htmlContent, replacement)
|
||||
}
|
||||
|
||||
// _-_- TopBanner
|
||||
topbannerRegex := regexp.MustCompile(`(?s)<topbanner>.*?</topbanner>`)
|
||||
|
||||
if topbannerRegex.MatchString(htmlContent) {
|
||||
htmlContent = topbannerRegex.ReplaceAllStringFunc(htmlContent, func(match string) string {
|
||||
// 'match' is "<topbanner>...content...</topbanner>"
|
||||
|
||||
// Remove the known tags safely
|
||||
innerContent := strings.TrimPrefix(match, "<topbanner>")
|
||||
innerContent = strings.TrimSuffix(innerContent, "</topbanner>")
|
||||
|
||||
// Pass to template
|
||||
bannerHTML := templates.RenderTopBanner(innerContent)
|
||||
|
||||
// Return the formatted HTML
|
||||
return fmt.Sprintf(`<div class="topbanner">%s</div>`, bannerHTML)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
latestPostsHTML := templates.RenderLatestPosts(posts)
|
||||
dirLink := templates.RenderDirectoryLink()
|
||||
|
||||
// Replace custom tags if they exist in the markdown
|
||||
if strings.Contains(htmlContent, "<latest-posts>") {
|
||||
htmlContent = strings.ReplaceAll(htmlContent, "<latest-posts>", latestPostsHTML)
|
||||
} else {
|
||||
// Fallback: Append if not present
|
||||
htmlContent = htmlContent + "\n" + dirLink + "\n" + latestPostsHTML + "\n" + dirLink
|
||||
}
|
||||
|
||||
// Handle site-headline if present
|
||||
// You can add <site-headline>Your Text</site-headline> in your markdown
|
||||
// For now, we just let it pass through as HTML, or you can add specific replacement logic here.
|
||||
// Build Full Page
|
||||
dataToWrite = templates.BuildFullPage([]byte(htmlContent), meta)
|
||||
}
|
||||
|
||||
// 7. Build Full Page (HTML Shell)
|
||||
finalPage := templates.BuildFullPage([]byte(htmlContent), meta)
|
||||
// 4. Write to Cache
|
||||
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)
|
||||
}
|
||||
|
||||
// 8. Cache
|
||||
_ = StoreCache(file.OriginalPath, finalPage)
|
||||
|
||||
return finalPage, nil
|
||||
return cachePath, nil
|
||||
}
|
||||
|
|
|
|||
14
server.go
14
server.go
|
|
@ -22,7 +22,6 @@ func StartServer() {
|
|||
}
|
||||
|
||||
// SINGLE ENTRY POINT
|
||||
// We handle all routing manually to ensure strict control.
|
||||
http.HandleFunc("/", handleRequest)
|
||||
|
||||
fmt.Println("Server is online at http://localhost:8080")
|
||||
|
|
@ -35,8 +34,6 @@ 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")
|
||||
// In test mode, we refresh the routes map every time so you can add files
|
||||
// without restarting.
|
||||
routes, err := ScanContent("content")
|
||||
if err == nil {
|
||||
globalRoutes = routes
|
||||
|
|
@ -49,20 +46,19 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
|
|||
// 3. Look up in Route Map
|
||||
file, found := globalRoutes[reqPath]
|
||||
if !found {
|
||||
// If not found in our map, it doesn't exist. 404.
|
||||
// This prevents the "Index Fallback" bug.
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Get Content (From Cache or Parse)
|
||||
content, err := GetContent(file)
|
||||
// 4. Get Content Path (Ensures cache is fresh)
|
||||
cachePath, err := GetContentPath(file)
|
||||
if err != nil {
|
||||
log.Printf("Error serving %s: %v", file.OriginalPath, err)
|
||||
http.Error(w, "Internal Server Error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. Render
|
||||
RenderPage(w, content, file)
|
||||
// 5. Serve File
|
||||
// http.ServeFile handles Content-Type, Range requests, and Caching headers automatically.
|
||||
http.ServeFile(w, r, cachePath)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package templates
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -15,17 +15,21 @@ type PostSnippet struct {
|
|||
}
|
||||
|
||||
// RenderLatestPosts generates the HTML for the list of recent posts.
|
||||
// It accepts a list of posts (already sorted).
|
||||
func RenderLatestPosts(posts []PostSnippet) string {
|
||||
const tpl = `<div class="latest-posts">
|
||||
<h2>Latest Posts</h2>
|
||||
<ul>
|
||||
{{range .}}
|
||||
<li><span class="date">{{.Date.Format "Jan 02, 2006"}}</span> <a href="{{.URL}}">{{.Title}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>`
|
||||
|
||||
t := template.Must(template.New("latest").Parse(tpl))
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(`<div class="latest-posts"><h2>Latest Posts</h2><ul>`)
|
||||
|
||||
for _, p := range posts {
|
||||
dateStr := p.Date.Format("Jan 02, 2006")
|
||||
buf.WriteString(fmt.Sprintf(`<li><span class="date">%s</span> <a href="%s">%s</a></li>`, dateStr, p.URL, p.Title))
|
||||
if err := t.Execute(&buf, posts); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
buf.WriteString(`</ul></div>`)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +38,6 @@ func RenderDirectoryLink() string {
|
|||
return `<div class="directory-link"><a href="/archive">View All Posts</a></div>`
|
||||
}
|
||||
|
||||
// RenderSiteHeadline generates the top banner.
|
||||
func RenderSiteHeadline(text string) string {
|
||||
return fmt.Sprintf(`<div class="site-headline"><h1>%s</h1></div>`, text)
|
||||
func RenderTopBanner(content string) string {
|
||||
return content
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
// templates/tags.go
|
||||
package templates
|
||||
|
||||
import customtag "github.com/tendstofortytwo/goldmark-customtag"
|
||||
|
||||
// ||| -> <sidebar></sidebar>
|
||||
var SidebarTag = customtag.New("|||", "sidebar")
|
||||
var TopBanner = customtag.New("_-_-", "topbanner")
|
||||
Loading…
Reference in New Issue