package main
import (
"fmt"
"html/template"
"io"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
_ "net/http/pprof"
"github.com/adrg/frontmatter"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
)
type TOCItem struct {
Level int
Text string
ID string
}
type Page struct {
Title string
Content template.HTML
TOC []TOCItem
}
type Post struct {
Title string
}
type Frontmatter struct {
Title string `yaml:"title"`
Date string `yaml:"date"`
Tags []string `yaml:"tags"`
}
func main() {
contentDir := "./content"
outputDir := "./output"
posts := make([]Post, 0)
themeName := "modern-light"
args := os.Args
if len(args) > 1 {
switch args[1] {
case "serve":
copyFile("./themes/"+themeName+".css", "./output/style.css")
fs := http.FileServer(http.Dir("./output/"))
http.Handle("/", fs)
log.Println("Serving on http://localhost:8000")
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.Fatalf("Error occured %s\n", err)
}
}
}
summaries := make([]PostSummary, 0)
err := filepath.WalkDir(contentDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if strings.HasSuffix(d.Name(), ".md") {
fmt.Println("Processing:", path)
matter, htmlContent, toc := convertToHtml(path)
slug := strings.TrimSuffix(d.Name(), ".md")
summaries = append(summaries, PostSummary{
Title: matter.Title,
Slug: slug,
Date: matter.Date,
Tags: matter.Tags,
})
posts = append(posts, Post{Title: matter.Title})
// fmt.Println("Appended post", posts)
newPage := Page{
Title: matter.Title,
Content: template.HTML(htmlContent),
TOC: toc,
}
tmpl, err := template.ParseFiles("./layout.html")
if err != nil {
log.Fatalf("Error parsing template: %s", err)
}
relPath, err := filepath.Rel(contentDir, path)
if err != nil {
log.Fatalf("Error computing relative path: %s", err)
}
// test.md -> test.html
// test.md -> test/index.html
outputFilePath := filepath.Join(outputDir, strings.Replace(relPath, ".md", "/index.html", 1))
err = os.MkdirAll(filepath.Dir(outputFilePath), 0o755)
if err != nil {
log.Fatalf("Error creating directories: %s", err)
}
outputFile, err := os.Create(outputFilePath)
if err != nil {
log.Fatalf("Error creating output file: %s", err)
}
defer outputFile.Close()
err = tmpl.Execute(outputFile, newPage)
if err != nil {
log.Fatalf("Error generating output content: %s", err)
}
}
return nil
})
if err != nil {
log.Fatalf("Error walking directory: %s", err)
}
fmt.Println("All files processed!")
fmt.Println(posts)
renderHomePage(summaries, outputDir)
}
func convertToHtml(path string) (Frontmatter, []byte, []TOCItem) {
md, err := os.ReadFile(path)
if err != nil {
log.Fatalf("Error reading %s: %s", path, err)
}
var matter Frontmatter
rest, err := frontmatter.Parse(strings.NewReader(string(md)), &matter)
if err != nil {
log.Fatalf("Error parsing frontmatter: %s", err)
}
extensions := parser.CommonExtensions | parser.AutoHeadingIDs
p := parser.NewWithExtensions(extensions)
doc := p.Parse(rest)
var toc []TOCItem
ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
if heading, ok := node.(*ast.Heading); ok && entering {
text := extractText(heading)
id := string(heading.HeadingID)
toc = append(toc, TOCItem{
Level: heading.Level,
Text: text,
ID: id,
})
}
return ast.GoToNext
})
renderer := html.NewRenderer(html.RendererOptions{
Flags: html.CommonFlags,
})
output := markdown.Render(doc, renderer)
return matter, output, toc
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil {
return err
}
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}
func extractText(h *ast.Heading) string {
var text string
ast.WalkFunc(h, func(node ast.Node, entering bool) ast.WalkStatus {
if leaf, ok := node.(*ast.Text); ok && entering {
text += string(leaf.Literal)
}
return ast.GoToNext
})
return text
}
type PostSummary struct {
Title string
Slug string // derived from filename, e.g. "my-post" from "my-post.md"
Date string // from frontmatter, e.g. "Mar 2026"
Tags []string // from frontmatter (optional)
}
type HomePage struct {
SiteTitle string
AuthorName string
AuthorRole string
AuthorBio string
Year int
Posts []PostSummary // sorted newest-first
}
func renderHomePage(summaries []PostSummary, outputDir string) {
sort.Slice(summaries, func(i, j int) bool {
return summaries[i].Date > summaries[j].Date
})
for i, p := range summaries {
if t, err := time.Parse("2006-01-02", p.Date); err == nil {
summaries[i].Date = t.Format("Jan 2006")
}
}
data := HomePage{
SiteTitle: "himanshu.co",
AuthorName: "Himanshu Sardana",
AuthorRole: "Student",
AuthorBio: "20 y/o linux enthusiast & software engineer",
Year: time.Now().Year(),
Posts: summaries,
}
tmpl, err := template.ParseFiles("./home.html")
if err != nil {
log.Fatalf("Error parsing home template: %s", err)
}
outPath := filepath.Join(outputDir, "index.html")
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
log.Fatalf("Error creating output dir: %s", err)
}
f, err := os.Create(outPath)
if err != nil {
log.Fatalf("Error creating index.html: %s", err)
}
defer f.Close()
if err := tmpl.Execute(f, data); err != nil {
log.Fatalf("Error rendering home page: %s", err)
}
fmt.Println("Home page written to", outPath)
}