diff --git a/TODOS.md b/TODOS.md
index ea75b9b..bfaeb74 100644
--- a/TODOS.md
+++ b/TODOS.md
@@ -3,3 +3,8 @@
- Look at the source for 3 template languages that support mustache syntax,
and see how they implement the tempaltes, then pick the best one. One of them
should be Hugo / go.
+- [ ] Deal with the favicon
+- [ ] Icon support.
+- [ ] RSS
+- [ ] sitemap
+- [ ] robots.txt?
diff --git a/content.odin b/content.odin
index 4ee6bcf..782502c 100644
--- a/content.odin
+++ b/content.odin
@@ -7,22 +7,24 @@ import "core:os"
import "core:strings"
Page_Type :: enum {
+ Home,
Post,
Standalone,
}
Page :: struct {
- type: Page_Type,
- slug: string,
- permalink: string,
- title: string,
- date: string,
- draft: bool,
- is_starred: bool,
- menu: string,
- body: string,
- body_html: string,
- bundle_dir: string,
+ type: Page_Type,
+ slug: string,
+ permalink: string,
+ title: string,
+ description: string,
+ date: string,
+ draft: bool,
+ is_starred: bool,
+ menu: string,
+ body: string,
+ body_html: string,
+ bundle_dir: string,
}
// walk_content reads the content directory and returns all non-draft pages
@@ -30,6 +32,7 @@ Page :: struct {
walk_content :: proc(content_path: string, include_drafts: bool) -> []Page {
pages: [dynamic]Page
+ collect_home(&pages, content_path)
collect_standalone(&pages, content_path)
posts_path := fmt.tprintf("%s/posts", content_path)
@@ -51,6 +54,27 @@ walk_content :: proc(content_path: string, include_drafts: bool) -> []Page {
return pages[:]
}
+collect_home :: proc(pages: ^[dynamic]Page, content_path: string) {
+ html_path := fmt.tprintf("%s/index.html", content_path)
+ if os.exists(html_path) {
+ page, ok := load_page(html_path, .Home, "")
+ if ok {
+ page.permalink = "/"
+ append(pages, page)
+ }
+ return
+ }
+
+ md_path := fmt.tprintf("%s/index.md", content_path)
+ if os.exists(md_path) {
+ page, ok := load_page(md_path, .Home, "")
+ if ok {
+ page.permalink = "/"
+ append(pages, page)
+ }
+ }
+}
+
collect_standalone :: proc(pages: ^[dynamic]Page, content_path: string) {
entries, err := os.read_all_directory_by_path(content_path, context.allocator)
if err != nil {
@@ -60,14 +84,17 @@ collect_standalone :: proc(pages: ^[dynamic]Page, content_path: string) {
defer os.file_info_slice_delete(entries, context.allocator)
for entry in entries {
- if !strings.has_suffix(entry.name, ".md") {
+ if !is_content_file(entry.name) {
+ continue
+ }
+ if entry.name == "index.md" || entry.name == "index.html" {
continue
}
if entry.type != .Regular {
continue
}
- slug := entry.name[:len(entry.name) - 3]
+ slug := strip_extension(entry.name)
page, ok := load_page(entry.fullpath, .Standalone, slug)
if ok {
append(pages, page)
@@ -86,16 +113,19 @@ collect_posts :: proc(pages: ^[dynamic]Page, posts_path: string) {
for entry in entries {
switch entry.type {
case .Regular:
- if !strings.has_suffix(entry.name, ".md") {
+ if !is_content_file(entry.name) {
continue
}
- slug := entry.name[:len(entry.name) - 3]
+ slug := strip_extension(entry.name)
page, ok := load_page(entry.fullpath, .Post, slug)
if ok {
append(pages, page)
}
case .Directory:
- index_path := fmt.tprintf("%s/index.md", entry.fullpath)
+ index_path := fmt.tprintf("%s/index.html", entry.fullpath)
+ if !os.exists(index_path) {
+ index_path = fmt.tprintf("%s/index.md", entry.fullpath)
+ }
if !os.exists(index_path) {
continue
}
@@ -105,7 +135,6 @@ collect_posts :: proc(pages: ^[dynamic]Page, posts_path: string) {
append(pages, page)
}
case .Undetermined, .Symlink, .Named_Pipe, .Socket, .Block_Device, .Character_Device:
- // Do nothing
}
}
}
@@ -131,17 +160,25 @@ load_page :: proc(
return
}
- page.type = page_type
- page.slug = slug
- page.title = fm.title
- page.date = fm.date
- page.draft = fm.draft
- page.is_starred = fm.isStarred
- page.menu = fm.menu
- page.body = strings.clone(body)
- page.body_html = cm.markdown_to_html_from_string(body, {.Unsafe})
+ page.type = page_type
+ page.slug = slug
+ page.title = fm.title
+ page.description = fm.description
+ page.date = fm.date
+ page.draft = fm.draft
+ page.is_starred = fm.isStarred
+ page.menu = fm.menu
+ page.body = strings.clone(body)
+
+ if strings.has_suffix(file_path, ".html") {
+ page.body_html = strings.clone(body)
+ } else {
+ page.body_html = cm.markdown_to_html_from_string(body, {.Unsafe})
+ }
switch page_type {
+ case .Home:
+ page.permalink = "/"
case .Post:
page.permalink = fmt.aprintf("/posts/%s/", slug)
case .Standalone:
@@ -152,3 +189,37 @@ load_page :: proc(
return
}
+is_content_file :: proc(name: string) -> bool {
+ return strings.has_suffix(name, ".md") || strings.has_suffix(name, ".html")
+}
+
+strip_extension :: proc(name: string) -> string {
+ dot := strings.last_index(name, ".")
+ if dot < 0 {
+ return name
+ }
+ return name[:dot]
+}
+
+// copy_static_assets copies non-content files from the content root to the output directory.
+copy_static_assets :: proc(content_path: string, output_dir: string) {
+ entries, err := os.read_all_directory_by_path(content_path, context.allocator)
+ if err != nil {
+ return
+ }
+ defer os.file_info_slice_delete(entries, context.allocator)
+
+ for entry in entries {
+ if entry.type != .Regular {
+ continue
+ }
+ if is_content_file(entry.name) {
+ continue
+ }
+
+ dest := fmt.tprintf("%s/%s", output_dir, entry.name)
+ if err := os.copy_file(dest, entry.fullpath); err != nil {
+ fmt.eprintfln("thor: cannot copy %s: %v", entry.name, err)
+ }
+ }
+}
diff --git a/frontmatter.odin b/frontmatter.odin
index 8b0c4a4..cb7e26f 100644
--- a/frontmatter.odin
+++ b/frontmatter.odin
@@ -6,6 +6,7 @@ import "core:strings"
Frontmatter :: struct {
title: string,
+ description: string,
date: string,
publishDate: string,
draft: bool,
@@ -44,8 +45,9 @@ parse_frontmatter :: proc(content: string) -> (fm: Frontmatter, body: string, ok
return
}
- fm.title = json_get_string(obj, "title")
- fm.date = json_get_string(obj, "date")
+ fm.title = json_get_string(obj, "title")
+ fm.description = json_get_string(obj, "description")
+ fm.date = json_get_string(obj, "date")
fm.publishDate = json_get_string(obj, "publishDate")
fm.draft = json_get_bool(obj, "draft")
fm.isStarred = json_get_bool(obj, "isStarred")
diff --git a/main.odin b/main.odin
index 6a5e494..1508938 100644
--- a/main.odin
+++ b/main.odin
@@ -20,7 +20,7 @@ main :: proc() {
pages := walk_content(opt.content_dir, opt.drafts)
- render_site(pages, opt.output_dir)
+ render_site(pages, opt.content_dir, opt.output_dir)
}
load_default_options :: proc(opt: ^Options) {
diff --git a/render.odin b/render.odin
index ae90900..0036a6d 100644
--- a/render.odin
+++ b/render.odin
@@ -4,45 +4,58 @@ import "core:fmt"
import "core:os"
import "core:strings"
-SITE_TITLE :: "One Idiot Developer"
-SITE_DESC :: "This is fine...right?"
-
-@(rodata)
MONTHS: [12]string = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
}
-HEADER :: `
-
-`
-
-render_site :: proc(pages: []Page, output_dir: string) {
+render_site :: proc(pages: []Page, content_path: string, output_dir: string) {
sort_pages_by_date(pages)
+ // Find home page and extract site title
+ home: Page
+ has_home := false
+ site_title := "Site"
+
for page in pages {
- html := render_page_html(page)
+ if page.type == .Home {
+ home = page
+ has_home = true
+ site_title = page.title
+ break
+ }
+ }
+
+ // Copy static assets (avatar, favicon, etc.)
+ copy_static_assets(content_path, output_dir)
+
+ // Render individual content pages (skip home)
+ for page in pages {
+ if page.type == .Home {
+ continue
+ }
+ html := render_page_html(page, site_title)
write_page(output_dir, page.permalink, html)
}
- home_html := render_home_html(pages)
- write_file(fmt.tprintf("%s/index.html", output_dir), home_html)
+ // Render home page
+ if has_home {
+ home_html := render_home_html(home, pages, site_title)
+ write_file(fmt.tprintf("%s/index.html", output_dir), home_html)
+ }
- posts_html := render_posts_html(pages)
+ // Render posts list page
+ posts_html := render_posts_html(pages, site_title)
write_page(output_dir, "/posts/", posts_html)
- fmt.printfln("Rendered %d pages to %s", len(pages) + 2, output_dir)
+ total := len(pages) + 1
+ if !has_home {
+ total += 1
+ }
+ fmt.printfln("Rendered %d pages to %s", total, output_dir)
}
-render_page_html :: proc(page: Page) -> string {
+render_page_html :: proc(page: Page, site_title: string) -> string {
body := fmt.aprintf(
`
@@ -55,15 +68,18 @@ render_page_html :: proc(page: Page) -> string {
render_date(page),
page.body_html,
)
- title := fmt.tprintf("%s | %s", page.title, SITE_TITLE)
+ title := fmt.tprintf("%s | %s", page.title, site_title)
return render_chrome(title, body)
}
-render_home_html :: proc(pages: []Page) -> string {
+render_home_html :: proc(home: Page, pages: []Page, site_title: string) -> string {
items: [dynamic]string
defer delete(items)
for page in pages {
+ if page.type == .Home {
+ continue
+ }
append(&items, render_post_item(page))
}
@@ -72,24 +88,20 @@ render_home_html :: proc(pages: []Page) -> string {
body := fmt.aprintf(
`
-
- %s
- %s
-
+%s
`,
- SITE_TITLE,
- SITE_DESC,
+ home.body_html,
post_list,
)
- return render_chrome(SITE_TITLE, body)
+ return render_chrome(site_title, body)
}
-render_posts_html :: proc(pages: []Page) -> string {
+render_posts_html :: proc(pages: []Page, site_title: string) -> string {
parts: [dynamic]string
defer delete(parts)
@@ -123,7 +135,7 @@ render_posts_html :: proc(pages: []Page) -> string {
append(&parts, "\n")
body := strings.join(parts[:], "")
- title := fmt.tprintf("Posts | %s", SITE_TITLE)
+ title := fmt.tprintf("Posts | %s", site_title)
return render_chrome(title, body)
}
@@ -153,6 +165,18 @@ render_chrome :: proc(page_title: string, body: string) -> string {
)
}
+HEADER :: `
+
+`
+
render_post_item :: proc(page: Page) -> string {
date_html := ""
if page.date != "" {