style is bit terrible, everything works tho

This commit is contained in:
root 2026-02-13 08:18:35 +00:00
parent 1c93ab8607
commit d4dbec8a84
12 changed files with 414 additions and 160 deletions

1
.gitignore vendored
View File

@ -3,5 +3,4 @@ vendor
*.lock
.vscode
cache
content
blog

View File

@ -10,11 +10,8 @@ import (
)
var (
memoryCache = make(map[string][]byte)
cacheMutex sync.RWMutex
cacheDir = "cache"
// TestMode disables caching when set to true
cacheMutex sync.Mutex
TestMode bool
)
@ -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)
}

237
content/default.css Normal file
View File

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

View File

@ -0,0 +1 @@
# I'm quite fond of lyrics. Particicularly, I like interpol lyrics.

11
content/index.md Normal file
View File

@ -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__
|||

1
content/index.yml Normal file
View File

@ -0,0 +1 @@
title: Blog Homepage

1
go.mod
View File

@ -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
View File

@ -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=

122
parse.go
View File

@ -4,7 +4,9 @@ package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"git.else-if.org/jess/blog/templates"
@ -15,69 +17,81 @@ 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)
// 1. Check Cache Freshness (if not in TestMode)
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
}
}
// 2. Read Raw
// 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
}
}
// Read Raw File
raw, err := ReadRaw(file.OriginalPath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
return "", fmt.Errorf("failed to read file: %w", err)
}
// If not markdown (e.g. css, images), return raw bytes immediately
var dataToWrite []byte
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.
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(),
),
)
// 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)
return "", fmt.Errorf("failed to parse markdown: %w", err)
}
// 5. Extract Frontmatter
// We retrieve the metadata using the context after parsing
// Extract Frontmatter
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
if meta.Title == "" {
base := filepath.Base(file.OriginalPath)
meta.Title = strings.TrimSuffix(base, filepath.Ext(base))
}
// 6. Special Handling for Index (Dynamic Components)
htmlContent := buf.String()
// 3. Post-Process for Index (Dynamic Content Injection)
if file.RoutePath == "/" {
// Generate the list of posts
// Generate posts data
var posts []templates.PostSnippet
for _, f := range AllContent {
if f.IsMarkdown && f.RoutePath != "/" {
@ -92,27 +106,57 @@ func GetContent(file ContentFile) ([]byte, error) {
}
}
/**
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()
// 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
// 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)
}
// 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.
// _-_- 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)
})
}
}
// 7. Build Full Page (HTML Shell)
finalPage := templates.BuildFullPage([]byte(htmlContent), meta)
// Build Full Page
dataToWrite = templates.BuildFullPage([]byte(htmlContent), meta)
}
// 8. Cache
_ = StoreCache(file.OriginalPath, finalPage)
// 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)
}
return finalPage, nil
return cachePath, nil
}

View File

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

View File

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

8
templates/tags.go Normal file
View File

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