summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHimanshu Sardana <himanshusardana2005@gmail.com>2026-03-25 09:21:19 +0000
committerHimanshu Sardana <himanshusardana2005@gmail.com>2026-03-25 09:21:19 +0000
commitaf0332e7d805f7a4cb1086b66f61ad706a082768 (patch)
tree24622ad88fa1edf636e5c7fac5ac3f5d8e002e6d
parent7cfdc43226930b21bae2e02581672a338a9c0789 (diff)
refactor: split into cmd/internal
-rw-r--r--cmd/main.go82
-rw-r--r--content/test.md310
-rw-r--r--go.mod2
-rw-r--r--home.html364
-rw-r--r--internal/build/build.go240
-rw-r--r--layout.html2
-rw-r--r--main.go277
7 files changed, 623 insertions, 654 deletions
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 0000000..afef7bb
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+
+ internal "github.com/HimanshuSardana/kite/internal/build"
+)
+
+var themeName = "gruvbox"
+
+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 main() {
+ args := os.Args
+ if len(args) > 1 {
+ switch args[1] {
+ case "serve":
+ copyFile("./themes/"+themeName+"/style.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)
+ }
+ case "build":
+ internal.Build()
+ case "list-themes":
+ themeList := make([]string, 0)
+ themes, err := os.ReadDir("../themes/")
+ if err != nil {
+ log.Fatal("Error:", err)
+ }
+ for _, theme := range themes {
+ if theme.IsDir() {
+ themeList = append(themeList, string(theme.Name()))
+ }
+ }
+ default:
+ showHelpMessage()
+ }
+ } else {
+ showHelpMessage()
+ }
+}
+
+func showHelpMessage() {
+ fmt.Println(`
+Usage: kite <SUBCOMMAND>
+
+SUBCOMMANDS:
+build
+serve
+`)
+}
diff --git a/content/test.md b/content/test.md
deleted file mode 100644
index a30fca0..0000000
--- a/content/test.md
+++ /dev/null
@@ -1,310 +0,0 @@
----
-title: "Writing a Static Site Generator in Golang"
----
-
-I recently decided to write a static site generator as a small toy project to get more familiar with the Go standard library. I’ve always liked learning languages by building something practical, and a static site generator seemed like a perfect mix of file handling, templating, and content processing.
-
-Another reason was infrastructure. My blog used to run on a small VPS from Contabo, but after they raised their prices I decided to move everything to a Raspberry Pi sitting on my desk. Since the Pi isn’t particularly powerful, I wanted the site to be as lean as possible — no runtime rendering, no databases, just plain static files served over HTTP.
-
-> NOTE
-> I decided to call it **Kite**.
-
-The idea behind Kite is simple: take Markdown files, convert them to HTML, wrap them in a template, and write them to an output directory. That’s it.
-
----
-
-## Project Structure
-
-The generator assumes a simple structure:
-
-```
-.
-├── content/
-│ ├── index.md
-│ └── blog/
-│ └── first-post.md
-├── output/
-├── layout.html
-└── main.go
-```
-
-* **content/** contains Markdown files
-* **layout.html** is the HTML template
-* **output/** is where generated pages go
-
-Each Markdown file gets converted into an HTML file with the same relative path.
-
-For example:
-
-```
-content/blog/post.md
-```
-
-becomes:
-
-```
-output/blog/post.html
-```
-
----
-
-## The Page Structure
-
-First we define a simple struct that represents the data passed to our template.
-
-```go
-type Page struct {
- Title string
- Content template.HTML
-}
-```
-
-* **Title** will eventually hold the page title (hardcoded for now).
-* **Content** contains the rendered HTML.
-
-Notice the use of `template.HTML`. This tells Go’s template engine that the content is already trusted HTML and should not be escaped.
-
----
-
-## Walking the Content Directory
-
-The core of the generator uses `filepath.WalkDir` to recursively traverse the `content/` directory.
-
-```go
-err := filepath.WalkDir(contentDir, func(path string, d fs.DirEntry, err error) error {
-```
-
-For every file encountered, we:
-
-1. Ignore directories
-2. Check if the file ends with `.md`
-3. Convert it to HTML
-4. Render it into a template
-5. Write the result to the output directory
-
-Filtering Markdown files is straightforward:
-
-```go
-if strings.HasSuffix(d.Name(), ".md") {
- fmt.Println("Processing:", path)
-```
-
----
-
-## Converting Markdown to HTML
-
-For Markdown parsing I used the `gomarkdown/markdown` package.
-
-```go
-func convertToHtml(path string) []byte {
- md, err := os.ReadFile(path)
- if err != nil {
- log.Fatalf("Error reading %s: %s", path, err)
- }
-
- html := markdown.ToHTML(md, nil, nil)
- return html
-}
-```
-
-This function simply:
-
-1. Reads the Markdown file
-2. Converts it to HTML
-3. Returns the result
-
-No configuration, no extensions — just the default behavior.
-
----
-
-## Creating the Page Object
-
-After conversion we construct the page object:
-
-```go
-htmlContent := convertToHtml(path)
-
-newPage := Page{
- Title: "hello", // you can customize this later
- Content: template.HTML(htmlContent),
-}
-```
-
-Right now the title is hardcoded, but later this could be extracted from:
-
-* frontmatter
-* the first heading
-* metadata in the Markdown file
-
----
-
-## Parsing the Layout Template
-
-Next we load the HTML template that wraps the content.
-
-```go
-tmpl, err := template.ParseFiles("./layout.html")
-if err != nil {
- log.Fatalf("Error parsing template: %s", err)
-}
-```
-
-This happens once per file in the current version. It could be optimized by parsing the template only once before the directory walk, but for a small site this overhead is negligible.
-
----
-
-## Computing the Output Path
-
-One important step is preserving the directory structure.
-
-```go
-relPath, err := filepath.Rel(contentDir, path)
-```
-
-This converts something like:
-
-```
-content/blog/post.md
-```
-
-into:
-
-```
-blog/post.md
-```
-
-Then we swap the extension:
-
-```go
-outputFilePath := filepath.Join(
- outputDir,
- strings.Replace(relPath, ".md", ".html", 1),
-)
-```
-
-Result:
-
-```
-output/blog/post.html
-```
-
----
-
-## Ensuring Directories Exist
-
-Before writing the file we make sure the directory exists:
-
-```go
-err = os.MkdirAll(filepath.Dir(outputFilePath), 0o755)
-```
-
-This creates any missing folders in the path.
-
----
-
-## Writing the Final HTML
-
-Finally we create the output file and render the template:
-
-```go
-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)
-}
-```
-
-The template receives the `Page` struct and generates the final HTML.
-
----
-
-## Example Layout Template
-
-A minimal `layout.html` might look like this:
-
-```html
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="utf-8">
- <title>{{ .Title }}</title>
-</head>
-<body>
- <main>
- {{ .Content }}
- </main>
-</body>
-</html>
-```
-
-When the generator runs, the Markdown content gets injected into `{{ .Content }}`.
-
----
-
-## Running the Generator
-
-Just run:
-
-```
-go run main.go
-```
-
-Example output:
-
-```
-Processing: content/index.md
-Processing: content/blog/first-post.md
-All files processed!
-```
-
-And the generated files appear in `output/`.
-
----
-
-## Why This Is Nice
-
-Even this tiny generator already has a few advantages:
-
-* **Extremely fast builds**
-* **No runtime dependencies**
-* **Very small hosting requirements**
-* **Full control over the pipeline**
-
-Perfect for something like a Raspberry Pi server.
-
----
-
-## Future Improvements
-
-Kite is intentionally minimal right now, but there are plenty of directions to take it:
-
-* Frontmatter support (YAML/TOML)
-* Automatic title extraction
-* Template caching
-* Asset copying (CSS, images)
-* Incremental builds
-* Live reload during development
-* RSS feed generation
-* Tag and category pages
-
-The nice part is that Go’s standard library already provides most of the building blocks needed.
-
----
-
-## Final Thoughts
-
-Writing a static site generator is one of those projects that’s small enough to finish but complex enough to teach useful concepts:
-
-* filesystem traversal
-* templating
-* content pipelines
-* project structure
-
-And the result is something genuinely useful.
-
-Kite might stay a simple tool, or it might slowly grow features as I need them. Either way, it’s already doing exactly what I wanted: generating a lightweight blog that my Raspberry Pi can serve effortlessly.
diff --git a/go.mod b/go.mod
index 28d182c..9f43bd1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module kite
+module github.com/HimanshuSardana/kite
go 1.25.0
diff --git a/home.html b/home.html
index afdbbcb..0e963fe 100644
--- a/home.html
+++ b/home.html
@@ -4,8 +4,141 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ .SiteTitle }}</title>
- <link rel="stylesheet" href="/style.css" />
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+ <link
+ href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500&family=Geist+Mono:wght@400;500&display=swap"
+ rel="stylesheet"
+ />
<style>
+ :root {
+ --bg: #f9f8f6;
+ --surface: #ffffff;
+ --border: #e4e3df;
+ --text: #111110;
+ --text-2: #5c5b57;
+ --text-3: #a8a7a2;
+ --warm: #b85c38;
+ --serif: "Instrument Serif", Georgia, serif;
+ --sans: "Geist", system-ui, sans-serif;
+ --mono: "Geist Mono", ui-monospace, monospace;
+ --ease: cubic-bezier(0.16, 1, 0.3, 1);
+ }
+
+ *,
+ *::before,
+ *::after {
+ box-sizing: border-box;
+ }
+
+ html {
+ scroll-behavior: smooth;
+ }
+
+ body {
+ margin: 0;
+ background: var(--bg);
+ color: var(--text);
+ font-family: var(--sans);
+ -webkit-font-smoothing: antialiased;
+ min-height: 100vh;
+ }
+
+ /* ── Layout ── */
+ .page {
+ max-width: 640px;
+ margin: 0 auto;
+ padding: 0 2rem;
+ }
+
+ /* ── Nav ── */
+ nav {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ padding: 2rem 0 0;
+ gap: 2rem;
+ }
+
+ nav a {
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--text-3);
+ text-decoration: none;
+ transition: color 120ms ease;
+ }
+
+ nav a:hover {
+ color: var(--text);
+ }
+
+ /* ── Hero ── */
+ .hero {
+ padding: 5rem 0 4rem;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .hero-name {
+ font-family: var(--serif);
+ font-size: clamp(2.8rem, 8vw, 4.5rem);
+ font-weight: 400;
+ line-height: 1.05;
+ letter-spacing: -0.01em;
+ margin: 0 0 1.2rem;
+ color: var(--text);
+ animation: fadeUp 0.7s var(--ease) both;
+ }
+
+ .hero-name em {
+ font-style: italic;
+ color: var(--text-2);
+ }
+
+ .hero-role {
+ font-family: var(--mono);
+ font-size: 0.7rem;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--text-3);
+ margin: 0 0 1.4rem;
+ animation: fadeUp 0.7s 0.08s var(--ease) both;
+ }
+
+ .hero-bio {
+ font-size: 0.975rem;
+ line-height: 1.8;
+ color: var(--text-2);
+ margin: 0;
+ max-width: 420px;
+ animation: fadeUp 0.7s 0.15s var(--ease) both;
+ }
+
+ /* ── Writing section ── */
+ .writing-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 2.8rem 0 1.6rem;
+ }
+
+ .writing-label {
+ font-family: var(--mono);
+ font-size: 0.64rem;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--text-3);
+ white-space: nowrap;
+ }
+
+ .writing-rule {
+ flex: 1;
+ height: 1px;
+ background: var(--border);
+ }
+
+ /* ── Post list ── */
.post-list {
margin: 0;
padding: 0;
@@ -22,119 +155,208 @@
}
.post-item {
- display: grid;
- gap: 0 1.8rem;
+ display: flex;
align-items: baseline;
- padding: 0.85rem 0;
+ gap: 1.5rem;
+ padding: 1rem 0;
text-decoration: none;
color: inherit;
- transition: opacity var(--transition);
- }
-
- .post-item:first-child {
- border-top: 1px solid var(--border-soft);
+ border-bottom: 1px solid var(--border);
+ transition: opacity 140ms ease;
+ animation: fadeUp 0.5s var(--ease) both;
}
.post-item:hover {
- opacity: 0.6;
- text-decoration: none;
- }
-
- .post-item:hover .post-title {
- text-decoration-color: var(--text);
+ opacity: 0.55;
}
.post-date {
font-family: var(--mono);
- font-size: 0.68rem;
- letter-spacing: 0.06em;
- color: var(--text-muted);
+ font-size: 0.63rem;
+ letter-spacing: 0.05em;
+ color: var(--text-3);
white-space: nowrap;
- padding-top: 0.08em;
+ flex-shrink: 0;
+ padding-top: 0.1em;
+ min-width: 5.5rem;
+ }
+
+ .post-right {
+ flex: 1;
+ min-width: 0;
}
.post-title {
font-family: var(--serif);
- font-size: 0.97rem;
+ font-size: 1.05rem;
font-weight: 400;
color: var(--text);
- line-height: 1.5;
- text-decoration-line: underline;
- text-decoration-color: transparent;
- text-underline-offset: 3px;
- text-decoration-thickness: 1px;
- transition: text-decoration-color var(--transition);
+ line-height: 1.45;
+ display: block;
}
.post-tags {
display: flex;
- gap: 0.35rem;
- margin-top: 0.3rem;
+ gap: 0.3rem;
+ margin-top: 0.4rem;
flex-wrap: wrap;
}
+ .pill {
+ display: inline-block;
+ font-family: var(--mono);
+ font-size: 0.58rem;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ padding: 0.15rem 0.45rem;
+ background: transparent;
+ border: 1px solid var(--border);
+ border-radius: 100px;
+ color: var(--text-3);
+ }
+
+ /* ── Empty state ── */
.empty {
font-family: var(--mono);
- font-size: 0.75rem;
- color: var(--text-muted);
+ font-size: 0.72rem;
+ color: var(--text-3);
letter-spacing: 0.06em;
- padding: 2rem 0;
+ padding: 2.5rem 0;
+ }
+
+ /* ── Footer ── */
+ footer {
+ padding: 2.5rem 0 3rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 0.8rem;
+ font-family: var(--mono);
+ font-size: 0.65rem;
+ letter-spacing: 0.05em;
+ color: var(--text-3);
+ }
+
+ footer a {
+ color: inherit;
+ text-decoration: underline;
+ text-underline-offset: 3px;
+ text-decoration-color: var(--border);
+ transition: color 120ms ease;
+ }
+
+ footer a:hover {
+ color: var(--text);
+ }
+
+ /* ── Animations ── */
+ @keyframes fadeUp {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
+ /* stagger post items */
+ .post-item:nth-child(1) {
+ animation-delay: 0.2s;
+ }
+ .post-item:nth-child(2) {
+ animation-delay: 0.27s;
+ }
+ .post-item:nth-child(3) {
+ animation-delay: 0.34s;
+ }
+ .post-item:nth-child(4) {
+ animation-delay: 0.41s;
+ }
+ .post-item:nth-child(5) {
+ animation-delay: 0.48s;
+ }
+ .post-item:nth-child(6) {
+ animation-delay: 0.55s;
+ }
+
+ /* ── Mobile ── */
@media (max-width: 500px) {
+ .page {
+ padding: 0 1.4rem;
+ }
+ .hero {
+ padding: 3.5rem 0 3rem;
+ }
.post-item {
- grid-template-columns: 1fr;
+ flex-direction: column;
gap: 0.2rem;
}
+ .post-date {
+ min-width: unset;
+ }
+ footer {
+ flex-direction: column;
+ }
+ }
+
+ :focus-visible {
+ outline: 2px solid var(--text);
+ outline-offset: 3px;
+ border-radius: 2px;
}
</style>
</head>
<body>
- <nav>
- <a class="logo" href="/">{{ .SiteTitle }}</a>
- <ul>
- <li><a href="/about">About</a></li>
- <li><a href="/feed.xml">RSS</a></li>
- </ul>
- </nav>
+ <div class="page">
+ <nav>
+ <a href="/about">About</a>
+ <a href="/feed.xml">RSS</a>
+ </nav>
- <div class="site-header">
- <h1>{{ .AuthorName }}</h1>
- <p class="role">{{ .AuthorRole }}</p>
- <p class="intro">{{ .AuthorBio }}</p>
- </div>
+ <header class="hero">
+ <h1 class="hero-name">{{ .AuthorName }}</h1>
+ <p class="hero-role">{{ .AuthorRole }}</p>
+ <p class="hero-bio">{{ .AuthorBio }}</p>
+ </header>
- <main>
- <p class="section-label">Writing</p>
+ <main>
+ <div class="writing-header">
+ <span class="writing-label">Writing</span>
+ <span class="writing-rule"></span>
+ </div>
- {{ if .Posts }}
- <ul class="post-list">
- {{ range .Posts }}
- <li>
- <a class="post-item" href="/{{ .Slug }}/">
- <span class="post-date">{{ .Date }}</span>
- <span>
- <span class="post-title">{{ .Title }}</span>
- {{ if .Tags }}
- <span class="post-tags">
- {{ range .Tags }}
- <span class="pill">{{ . }}</span>
+ {{ if .Posts }}
+ <ul class="post-list">
+ {{ range .Posts }}
+ <li>
+ <a class="post-item" href="/{{ .Slug }}/">
+ <span class="post-right">
+ <span class="post-title">{{ .Title }}</span>
+ {{ if .Tags }}
+ <span class="post-tags">
+ {{ range .Tags }}
+ <span class="pill">{{ . }}</span>
+ {{ end }}
+ </span>
{{ end }}
</span>
- {{ end }}
- </span>
- </a>
- </li>
+ <span class="post-date">{{ .Date }}</span>
+ </a>
+ </li>
+ {{ end }}
+ </ul>
+ {{ else }}
+ <p class="empty">No posts yet.</p>
{{ end }}
- </ul>
- {{ else }}
- <p class="empty">No posts yet.</p>
- {{ end }}
- </main>
+ </main>
- <footer>
- <span>© {{ .Year }} {{ .AuthorName }}</span>
- <span>Built with <a href="#">kite</a></span>
- </footer>
+ <footer>
+ <span>© {{ .Year }} {{ .AuthorName }}</span>
+ <span>Built with <a href="#">kite</a></span>
+ </footer>
+ </div>
</body>
</html>
diff --git a/internal/build/build.go b/internal/build/build.go
new file mode 100644
index 0000000..1fe85b3
--- /dev/null
+++ b/internal/build/build.go
@@ -0,0 +1,240 @@
+package build
+
+import (
+ "fmt"
+ "html/template"
+ "io/fs"
+ "log"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "gopkg.in/yaml.v2"
+
+ "github.com/adrg/frontmatter"
+ "github.com/gomarkdown/markdown"
+ "github.com/gomarkdown/markdown/ast"
+ "github.com/gomarkdown/markdown/html"
+ "github.com/gomarkdown/markdown/parser"
+)
+
+var (
+ themeName = "gruvbox"
+ contentDir = "./content"
+ outputDir = "./output"
+)
+
+type Frontmatter struct {
+ Title string `yaml:"title"`
+ Date string `yaml:"date"`
+ Tags []string `yaml:"tags"`
+}
+
+type Page struct {
+ Title string
+ Content template.HTML
+ TOC []TOCItem
+}
+
+type TOCItem struct {
+ Level int
+ Text string
+ ID string
+}
+
+type PostSummary struct {
+ Title string
+ Slug string
+ Date string
+ Tags []string
+}
+
+type HomePage struct {
+ SiteTitle string `yaml:"siteTitle"`
+ AuthorName string `yaml:"authorName"`
+ AuthorRole string `yaml:"authorRole"`
+ AuthorBio string `yaml:"authorBio"`
+ Year int
+ Posts []PostSummary
+}
+
+var posts = make([]Post, 0)
+
+type Post struct {
+ Title string
+}
+
+func Build() {
+ 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("./themes/" + themeName + "/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 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
+}
+
+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")
+ }
+ }
+
+ config, err := os.ReadFile("config.yaml")
+ if err != nil {
+ panic(err)
+ }
+ var data HomePage
+ err = yaml.Unmarshal(config, &data)
+ data.Posts = summaries
+ data.Year = time.Now().Year()
+
+ if err != nil {
+ panic(err)
+ }
+
+ tmpl, err := template.ParseFiles("./themes/" + themeName + "/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)
+}
diff --git a/layout.html b/layout.html
index 68772ef..97bdb4b 100644
--- a/layout.html
+++ b/layout.html
@@ -180,8 +180,6 @@
Contents
</button>
- <p class="toc-title" aria-hidden="true">Contents</p>
-
<div class="toc-body collapsed" id="toc-body">
<ul>
{{ range .TOC }}
diff --git a/main.go b/main.go
index 70962cc..449b7c4 100644
--- a/main.go
+++ b/main.go
@@ -2,279 +2,16 @@ 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"
- "gopkg.in/yaml.v3"
)
-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"`
-}
-
-var themeName = "gruvbox"
-
-func main() {
- contentDir := "./content"
- outputDir := "./output"
-
- posts := make([]Post, 0)
-
- args := os.Args
- if len(args) > 1 {
- switch args[1] {
- case "serve":
- copyFile("./themes/"+themeName+"/style.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("./themes/" + themeName + "/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
- Date string
- Tags []string
-}
-
-type HomePage struct {
- SiteTitle string `yaml:"siteTitle"`
- AuthorName string `yaml:"authorName"`
- AuthorRole string `yaml:"authorRole"`
- AuthorBio string `yaml:"authorBio"`
- Year int
- Posts []PostSummary
-}
-
-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")
- }
- }
-
- config, err := os.ReadFile("config.yaml")
- if err != nil {
- panic(err)
- }
- var data HomePage
- err = yaml.Unmarshal(config, &data)
- data.Posts = summaries
- data.Year = time.Now().Year()
-
- if err != nil {
- panic(err)
- }
-
- tmpl, err := template.ParseFiles("./themes/" + themeName + "/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()
+func showHelpMessage() {
+ fmt.Println(`
+Usage: kite <SUBCOMMAND>
- if err := tmpl.Execute(f, data); err != nil {
- log.Fatalf("Error rendering home page: %s", err)
- }
- fmt.Println("Home page written to", outPath)
+SUBCOMMANDS:
+build
+serve
+`)
}