style is bit terrible, everything works tho
This commit is contained in:
parent
1c93ab8607
commit
d4dbec8a84
|
|
@ -3,5 +3,4 @@ vendor
|
||||||
*.lock
|
*.lock
|
||||||
.vscode
|
.vscode
|
||||||
cache
|
cache
|
||||||
content
|
|
||||||
blog
|
blog
|
||||||
59
cache.go
59
cache.go
|
|
@ -10,12 +10,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
memoryCache = make(map[string][]byte)
|
cacheDir = "cache"
|
||||||
cacheMutex sync.RWMutex
|
cacheMutex sync.Mutex
|
||||||
cacheDir = "cache"
|
TestMode bool
|
||||||
|
|
||||||
// TestMode disables caching when set to true
|
|
||||||
TestMode bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitCache ensures the cache directory exists
|
// InitCache ensures the cache directory exists
|
||||||
|
|
@ -31,8 +28,8 @@ func ClearCache() {
|
||||||
InitCache()
|
InitCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCacheFilename generates the hashed filename preserving the extension
|
// GetCacheFilename generates the hashed filename preserving the extension
|
||||||
func getCacheFilename(key string) string {
|
func GetCacheFilename(key string) string {
|
||||||
hash := md5.Sum([]byte(key))
|
hash := md5.Sum([]byte(key))
|
||||||
ext := filepath.Ext(key)
|
ext := filepath.Ext(key)
|
||||||
// Default to .html if no extension (e.g. for processed markdown)
|
// 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)
|
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 (
|
require (
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
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
|
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/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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
go.abhg.dev/goldmark/frontmatter v0.3.0 h1:ZOrMkeyyYzhlbenFNmOXyGFx1dFE8TgBWAgZfs9D5RA=
|
go.abhg.dev/goldmark/frontmatter v0.3.0 h1:ZOrMkeyyYzhlbenFNmOXyGFx1dFE8TgBWAgZfs9D5RA=
|
||||||
|
|
|
||||||
212
parse.go
212
parse.go
|
|
@ -4,7 +4,9 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.else-if.org/jess/blog/templates"
|
"git.else-if.org/jess/blog/templates"
|
||||||
|
|
@ -15,104 +17,146 @@ import (
|
||||||
"go.abhg.dev/goldmark/frontmatter"
|
"go.abhg.dev/goldmark/frontmatter"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetContent is the primary entry point for retrieving parsed content.
|
// GetContentPath ensures the content is cached and returns the path to the cached file.
|
||||||
func GetContent(file ContentFile) ([]byte, error) {
|
func GetContentPath(file ContentFile) (string, error) {
|
||||||
// 1. Ask Cache
|
cachePath := GetCacheFilename(file.OriginalPath)
|
||||||
if data, found := CheckCache(file.OriginalPath); found {
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Read Raw
|
// 1. Check Cache Freshness (if not in TestMode)
|
||||||
raw, err := ReadRaw(file.OriginalPath)
|
if !TestMode {
|
||||||
if err != nil {
|
cacheInfo, errCache := os.Stat(cachePath)
|
||||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
origInfo, errOrig := os.Stat(file.OriginalPath)
|
||||||
}
|
|
||||||
|
|
||||||
// If not markdown (e.g. css, images), return raw bytes immediately
|
if errCache == nil && errOrig == nil && cacheInfo.ModTime().After(origInfo.ModTime()) {
|
||||||
if !file.IsMarkdown {
|
return cachePath, nil
|
||||||
_ = 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 title is missing, try to grab the filename
|
// 2. Regenerate Content
|
||||||
if meta.Title == "" {
|
cacheMutex.Lock()
|
||||||
base := filepath.Base(file.OriginalPath)
|
defer cacheMutex.Unlock()
|
||||||
meta.Title = strings.TrimSuffix(base, filepath.Ext(base))
|
|
||||||
|
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)
|
// Read Raw File
|
||||||
htmlContent := buf.String()
|
raw, err := ReadRaw(file.OriginalPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if file.RoutePath == "/" {
|
var dataToWrite []byte
|
||||||
// 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))
|
|
||||||
|
|
||||||
posts = append(posts, templates.PostSnippet{
|
if !file.IsMarkdown {
|
||||||
Title: title,
|
dataToWrite = raw
|
||||||
URL: f.RoutePath,
|
} else {
|
||||||
Date: f.ModTime,
|
// 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)
|
// Build Full Page
|
||||||
dirLink := templates.RenderDirectoryLink()
|
dataToWrite = templates.BuildFullPage([]byte(htmlContent), meta)
|
||||||
|
|
||||||
// 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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Build Full Page (HTML Shell)
|
// 4. Write to Cache
|
||||||
finalPage := templates.BuildFullPage([]byte(htmlContent), meta)
|
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
|
return cachePath, nil
|
||||||
_ = StoreCache(file.OriginalPath, finalPage)
|
|
||||||
|
|
||||||
return finalPage, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
server.go
14
server.go
|
|
@ -22,7 +22,6 @@ func StartServer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SINGLE ENTRY POINT
|
// SINGLE ENTRY POINT
|
||||||
// We handle all routing manually to ensure strict control.
|
|
||||||
http.HandleFunc("/", handleRequest)
|
http.HandleFunc("/", handleRequest)
|
||||||
|
|
||||||
fmt.Println("Server is online at http://localhost:8080")
|
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)
|
// 1. Handle Test Mode (Re-scan on every request)
|
||||||
if TestMode {
|
if TestMode {
|
||||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
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")
|
routes, err := ScanContent("content")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
globalRoutes = routes
|
globalRoutes = routes
|
||||||
|
|
@ -49,20 +46,19 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
// 3. Look up in Route Map
|
// 3. Look up in Route Map
|
||||||
file, found := globalRoutes[reqPath]
|
file, found := globalRoutes[reqPath]
|
||||||
if !found {
|
if !found {
|
||||||
// If not found in our map, it doesn't exist. 404.
|
|
||||||
// This prevents the "Index Fallback" bug.
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Get Content (From Cache or Parse)
|
// 4. Get Content Path (Ensures cache is fresh)
|
||||||
content, err := GetContent(file)
|
cachePath, err := GetContentPath(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error serving %s: %v", file.OriginalPath, err)
|
log.Printf("Error serving %s: %v", file.OriginalPath, err)
|
||||||
http.Error(w, "Internal Server Error", 500)
|
http.Error(w, "Internal Server Error", 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Render
|
// 5. Serve File
|
||||||
RenderPage(w, content, file)
|
// http.ServeFile handles Content-Type, Range requests, and Caching headers automatically.
|
||||||
|
http.ServeFile(w, r, cachePath)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"html/template"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -15,17 +15,21 @@ type PostSnippet struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderLatestPosts generates the HTML for the list of recent posts.
|
// RenderLatestPosts generates the HTML for the list of recent posts.
|
||||||
// It accepts a list of posts (already sorted).
|
|
||||||
func RenderLatestPosts(posts []PostSnippet) string {
|
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
|
var buf bytes.Buffer
|
||||||
buf.WriteString(`<div class="latest-posts"><h2>Latest Posts</h2><ul>`)
|
if err := t.Execute(&buf, posts); err != nil {
|
||||||
|
return ""
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.WriteString(`</ul></div>`)
|
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +38,6 @@ func RenderDirectoryLink() string {
|
||||||
return `<div class="directory-link"><a href="/archive">View All Posts</a></div>`
|
return `<div class="directory-link"><a href="/archive">View All Posts</a></div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderSiteHeadline generates the top banner.
|
func RenderTopBanner(content string) string {
|
||||||
func RenderSiteHeadline(text string) string {
|
return content
|
||||||
return fmt.Sprintf(`<div class="site-headline"><h1>%s</h1></div>`, text)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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