Compare commits

...

10 Commits

Author SHA1 Message Date
Keith Solomon
97526cf71f 🖥️ wip: Feed work 2025-04-22 07:15:22 -05:00
Keith Solomon
6dd7e8874e 🖥️ wip: Set up RSS feed 2025-04-21 15:49:57 -05:00
Keith Solomon
06c6523b5d 🐞 fix: Remove debug print statements 2025-04-21 15:47:03 -05:00
Keith Solomon
77797231d3 feature: Set nav order via front matter 2025-04-21 15:45:45 -05:00
Keith Solomon
29b398921b feature: Wire up contact and thank you pages, add showInNav meta functionality 2025-04-21 15:10:28 -05:00
Keith Solomon
1a9b58d7d7 Add Contact Page and Update Navigation Links
- Updated navigation links across multiple blog pages to replace "Index" with "Home" and added "Contact" link.
- Created a new contact page with a form for user inquiries.
- Added a new contact template to render the contact page content.
- Updated blog index page to enhance structure and styling.
- Added a contact markdown file for content management.
2025-04-21 07:15:03 -05:00
Keith Solomon
9957a5c116 feature: Set up Tailwind and automate dev process to watch all files for changes and rebuild as needed 2025-04-20 17:01:14 -05:00
Keith Solomon
3fe75e9ad5 feature: Set up templated views and blog support 2025-04-20 15:39:22 -05:00
Keith Solomon
463250eb7b feature: Wire up blog section 2025-04-19 15:48:56 -05:00
Keith Solomon
472aad6bc6 feature: Add YAML dependency to go.mod and go.sum 2025-04-19 14:58:16 -05:00
42 changed files with 4063 additions and 149 deletions

34
.air.toml Normal file
View File

@@ -0,0 +1,34 @@
root = "."
[build]
cmd = "go run main.go"
bin = ""
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "bak", "node_modules", "public"]
include_ext = ["go", "md", "html"]
kill_delay = "0s"
log = "build-errors.log"
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
bak/
tmp/
*.log

View File

@@ -1,3 +1,9 @@
# About
---
title: Go SSG - About
navTitle: About
description: More About My Go SSG Site.
date: 2025-04-19
navPosition: 2
---
Experimenting with Go. Seems pretty cool so far!

View File

@@ -0,0 +1,20 @@
---
title: Go SSG - Another Blog Post
description: A longer summary of the post. Lorem ipsum dolor set amit.
date: 2025-04-19
categories:
- go
- anger
---
## Why isn't this working?! 😭
This is my first Go SSG blog post.
This project is a simple static site generator written in Go. It is designed to be easy to use and extend, allowing you to create a static website quickly and efficiently.
It supports features like:
- Markdown content
- Customizable templates
- Simple configuration

View File

@@ -0,0 +1,20 @@
---
title: Go SSG - My First Blog Post
description: A short summary of the post.
date: 2025-04-19
categories:
- progrmming
- go
---
## *Tap Tap Tap* Is this thing on?
<p class="text-primary">This is my first Go SSG blog post.</p>
This project is a simple static site generator written in Go. It is designed to be easy to use and extend, allowing you to create a static website quickly and efficiently.
It supports features like:
- Markdown content
- Customizable templates
- Simple configuration

5
content/contact.md Normal file
View File

@@ -0,0 +1,5 @@
---
title: Contact
navTitle: Contact
navPosition: 4
---

View File

@@ -1,3 +1,10 @@
# Welcome
---
title: Go SSG - Home
navTitle: Home
showInNav: true
navPosition: 1
description: My Go SSG Site.
date: 2025-04-19
---
This is your first static site built with Go!
Welcome to my static site built with Go!

5
content/thanks.md Normal file
View File

@@ -0,0 +1,5 @@
---
title: Thank You
navTitle: ""
showInNav: false
---

32
css/base.css Normal file
View File

@@ -0,0 +1,32 @@
@import "tailwindcss";
@import './global.css';
@import './colors.css';
@import './typography.css';
@import './prose.css';
/* Import Tailwind typography plugin */
@plugin "@tailwindcss/typography";
body {
@apply bg-background text-text font-sans;
}
header#site_head {
@apply bg-primary text-white flex items-center justify-between;
@apply py-4 px-60;
nav ul {
@apply flex space-x-4 justify-end-safe prose-a:text-white;
}
}
main {
article, div.main {
@apply prose container mx-auto px-4 py-8 max-w-5xl;
}
}
footer#site_foot {
@apply bg-secondary text-white text-center m-0 p-0;
}

38
css/colors.css Normal file
View File

@@ -0,0 +1,38 @@
/* Theme color definitions */
@theme {
--color-black: oklch(0% 0 0);
--color-white: oklch(100% 0 0);
--color-background: oklch(89.75% 0 0);
--color-text: oklch(0% 0 0);
--color-primary: oklch(60.48% 0.2166 257.2);
--color-primary-100: color-mix(in oklch, var(--color-primary) 10%, white);
--color-primary-200: color-mix(in oklch, var(--color-primary) 20%, white);
--color-primary-300: color-mix(in oklch, var(--color-primary) 30%, white);
--color-primary-400: color-mix(in oklch, var(--color-primary) 40%, white);
--color-primary-500: color-mix(in oklch, var(--color-primary) 50%, white);
--color-primary-600: color-mix(in oklch, var(--color-primary) 60%, white);
--color-primary-700: color-mix(in oklch, var(--color-primary) 70%, white);
--color-primary-800: color-mix(in oklch, var(--color-primary) 80%, white);
--color-primary-900: color-mix(in oklch, var(--color-primary) 90%, white);
--color-secondary: oklch(55.75% 0.0165 244.9);
--color-secondary-100: color-mix(in oklch, var(--color-secondary) 10%, white);
--color-secondary-200: color-mix(in oklch, var(--color-secondary) 20%, white);
--color-secondary-300: color-mix(in oklch, var(--color-secondary) 30%, white);
--color-secondary-400: color-mix(in oklch, var(--color-secondary) 40%, white);
--color-secondary-500: color-mix(in oklch, var(--color-secondary) 50%, white);
--color-secondary-600: color-mix(in oklch, var(--color-secondary) 60%, white);
--color-secondary-700: color-mix(in oklch, var(--color-secondary) 70%, white);
--color-secondary-800: color-mix(in oklch, var(--color-secondary) 80%, white);
--color-secondary-900: color-mix(in oklch, var(--color-secondary) 90%, white);
--color-success: oklch(64.01% 0.1751 146.7);
--color-info: oklch(65.52% 0.1105 212.2);
--color-warning: oklch(84.42% 0.1722 84.93);
--color-danger: oklch(59.15% 0.202 21.24);
--color-light: oklch(98.16% 0.0017 247.8);
--color-dark: oklch(34.51% 0.0133 248.2);
}

84
css/global.css Normal file
View File

@@ -0,0 +1,84 @@
/* Miscellaneous custom styles */
@theme {
--spacing-menu-top: calc(100% + .9375rem);
--spacing-section: 2rem;
--shadow-menu-shadow: 0 .25rem .375rem rgba(0,0,0,0.1);
/** Breakpoints
* The breakpoints are set to match the default Tailwind breakpoints.
* You can override them here if you want to use different breakpoints.
*
* @see https://tailwindcss.com/docs/breakpoints
*/
--breakpoint-*: initial;
--breakpoint-xxs: 22.5rem; /* 360px */
--breakpoint-xs: 29.6875rem; /* 475px */
--breakpoint-sm: 40rem; /* 640px */
--breakpoint-md: 48rem; /* 768px */
--breakpoint-lg: 64rem; /* 1024px */
--breakpoint-xl: 80rem; /* 1280px */
--breakpoint-2xl: 96rem; /* 1536px */
}
/* Basic layout styles */
body, html {
background-color: var(--color-background);
color: var(--color-text);
margin: 0;
padding: 0;
}
.container {
margin: 0 auto;
width: 100%;
}
.section {
@apply relative my-section px-section;
&:first-child {
@apply mt-0;
}
&:last-child, p:last-child {
@apply mb-0;
}
&.has-background {
@apply py-section bg-cover bg-no-repeat;
}
}
/** Allows containers inside containers
*
* .container .wp-block-section {
* @apply mx-break-out;
* }
*/
.content-wrapper {
.alignfull {
@apply max-w-full;
}
.alignwide {
@apply max-w-full;
}
.alignleft {
@apply ml-0 mr-auto float-none;
}
.alignright {
@apply ml-auto mr-0 float-none;
}
.aligncenter {
@apply mx-auto;
}
}
/* Responsive embeds */
.embed { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; }
.embed iframe, .embed object, .embed embed, .embed video { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }

36
css/prose.css Normal file
View File

@@ -0,0 +1,36 @@
/* Theme prose styles */
@theme {
--tw-prose-body: var(--color-primary);
--tw-prose-headings: var(--color-primary);
--tw-prose-lead: var(--color-primary);
--tw-prose-links: var(--color-info);
--tw-prose-bold: var(--color-primary);
--tw-prose-counters: var(--color-primary);
--tw-prose-bullets: var(--color-secondary);
--tw-prose-hr: var(--color-secondary);
--tw-prose-quotes: var(--color-primary);
--tw-prose-quote-borders: var(--color-primary);
--tw-prose-captions: var(--color-secondary);
--tw-prose-code: var(--color-primary);
--tw-prose-pre-code: var(--color-primary);
--tw-prose-pre-bg: var(--color-secondary);
--tw-prose-th-borders: var(--color-secondary);
--tw-prose-td-borders: var(--color-secondary);
--tw-prose-invert-body: var(--color-primary);
--tw-prose-invert-headings: var(--color-primary);
--tw-prose-invert-lead: var(--color-primary);
--tw-prose-invert-links: var(--color-secondary);
--tw-prose-invert-bold: var(--color-primary);
--tw-prose-invert-counters: var(--color-primary);
--tw-prose-invert-bullets: var(--color-primary);
--tw-prose-invert-hr: var(--color-secondary);
--tw-prose-invert-quotes: var(--color-primary);
--tw-prose-invert-quote-borders: var(--color-primary);
--tw-prose-invert-captions: var(--color-primary);
--tw-prose-invert-code: var(--color-secondary);
--tw-prose-invert-pre-code: var(--color-primary);
--tw-prose-invert-pre-bg: oklch(0% 0 0 / 50%);
--tw-prose-invert-th-borders: var(--color-primary);
--tw-prose-invert-td-borders: var(--color-primary);
}

158
css/typography.css Normal file
View File

@@ -0,0 +1,158 @@
/* Basic typographical styles */
/**
* All font sizes are based on 16px base font size and 1920px wide screen
* Default size is expressed as percentage of screen width.
* text-14px: 12px-27px, default: 14px
* text-16px: 14px-28px, default: 16px
* text-18px: 14px-30px, default: 18px
* text-20px: 16px-32px, default: 20px
* text-22px: 17px-33px, default: 22px
* text-25px: 18px-35px, default: 25px
* text-30px: 19px-37px, default: 30px
* text-35px: 20px-40px, default: 35px
* text-38px: 22px-48px, default: 38px
* text-40px: 24px-56px, default: 40px
* text-45px: 25px-64px, default: 45px
* text-50px: 27px-72px, default: 50px
* text-55px: 28px-76px, default: 55px
* text-60px: 30px-80px, default: 60px
* text-70px: 30px-76px, default: 70px
* text-75px: 32px-80px, default: 75px
*/
@theme {
--font-sans: "Raleway", sans-serif;
--line-height: 1.6;
--text-base: 1rem;
--text-14px: clamp(0.75rem, 0.7292vw, 1.7rem);
--text-16px: clamp(0.875rem, 0.8333vw, 1.8rem);
--text-18px: clamp(0.875rem, 0.9375vw, 1.9rem);
--text-20px: clamp(1rem, 1.0417vw, 2rem);
--text-22px: clamp(1.1rem, 1.15vw, 2.1rem);
--text-25px: clamp(1.125rem, 1.3021vw, 2.2rem);
--text-30px: clamp(1.185rem, 1.5625vw, 2.35rem);
--text-35px: clamp(1.25rem, 1.8229vw, 2.5rem);
--text-38px: clamp(1.4rem, 1.9791vw, 3rem);
--text-40px: clamp(1.5rem, 2.0834vw, 3.5rem);
--text-45px: clamp(1.6rem, 2.3438vw, 4rem);
--text-50px: clamp(1.7rem, 2.6042vw, 4.5rem);
--text-70px: clamp(1.9rem, 3.6458vw, 4.8rem);
--text-75px: clamp(2rem, 3.9063vw, 5rem);
--h1: calc(var(--text-base) * 2.25);
--h2: calc(var(--text-base) * 1.75);
--h3: calc(var(--text-base) * 1.5);
--h4: calc(var(--text-base) * 1.25);
--h5: calc(var(--text-base) * 1.125);
--h6: calc(var(--text-base) * 1.05);
}
body {
background-color: white;
color: black;
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--line-height);
}
::selection { background: var(--color-warning); }
@layer components {
h1, h2, h3,
h4, h5, h6 {
font-weight: 700;
margin: 0 0 1rem;
}
h1, .h1 {
font-size: var(--h1);
line-height: 1.2;
}
h2, .h2 {
font-size: var(--h2);
line-height: 1.3;
}
h3, .h3 {
font-size: var(--h3);
line-height: 1.4;
}
h4, .h4 {
font-size: var(--h4);
line-height: 1.5;
}
h5, .h5 { font-size: var(--h5); }
h6, .h6 { font-size: var(--h6); }
}
a, .link {
color: var(--color-info);
text-decoration: none;
transition: color 200ms;
cursor: pointer;
&:hover { color: var(--color-primary); }
}
h1 a, .h1 a,
h2 a, .h2 a,
h3 a, .h3 a {
color: inherit;
text-decoration: underline;
}
#site_head h1 a {
@apply text-white text-40px hover:text-primary-200;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
li ul, li ol { margin: 0 1rem; }
ul { list-style-type: disc; }
ol { list-style-type: decimal; }
ol ol { list-style: lower-alpha; }
ol ol ol { list-style: lower-roman; }
ol ol ol ol { list-style: lower-alpha; }
pre, code,
samp, style { font-family: monospace; }
pre {
font-size: 0.875rem;
overflow: auto;
padding: 1.5rem;
}
pre code {
background-color: inherit;
border-radius: 0;
color: inherit;
padding: 0;
}
code {
@apply bg-black/40 px-[3px] py-[2px] font-mono text-light text-xs rounded-sm;
}
hr {
background-color: black;
border: none;
display: block;
height: 1px;
margin: 1rem 0;
width: 100%;
}

5
go.mod
View File

@@ -2,4 +2,7 @@ module go-ssg
go 1.24.2
require github.com/russross/blackfriday/v2 v2.1.0 // indirect
require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

3
go.sum
View File

@@ -1,2 +1,5 @@
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

376
main.go
View File

@@ -1,89 +1,167 @@
package main
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"sort"
"io/fs"
"github.com/russross/blackfriday/v2"
"gopkg.in/yaml.v3"
)
// NavItem represents a link in the navigation menu
type NavItem struct {
Title string
URL string
Title string
URL string
Position int
}
type PageMeta struct {
Title string `yaml:"title"`
NavTitle string `yaml:"navTitle"`
ShowInNav *bool `yaml:"showInNav"`
NavPosition *int `yaml:"navPosition"`
Description string `yaml:"description"`
Date string `yaml:"date"`
FormattedDate string `yaml:"-"`
Categories []string `yaml:"categories"`
Slug string
}
var slugRegex = regexp.MustCompile(`[^a-z0-9]+`)
func main() {
contentDir := "./content"
outputDir := "./public"
templateFile := "./templates/layout.html"
tpl, err := parseTemplate(templateFile)
tpl, err := loadTemplates()
if err != nil {
fmt.Printf("Template parsing error: %v\n", err)
return
}
files, err := readContentFiles(contentDir)
files, err := ioutil.ReadDir(contentDir)
if err != nil {
fmt.Printf("Failed to read content directory: %v\n", err)
return
}
nav := buildNavigation(files)
for _, file := range files {
if filepath.Ext(file.Name()) == ".md" {
err := generateHTML(file, contentDir, outputDir, tpl, nav)
if err != nil {
fmt.Printf("Error generating HTML for %s: %v\n", file.Name(), err)
}
}
}
entries, _ := os.ReadDir(contentDir)
nav := buildNav(entries, contentDir)
renderStaticPages(files, contentDir, outputDir, tpl, nav)
blogPosts, categoryMap := processBlogPosts(contentDir, outputDir, tpl, nav)
renderContactPage(contentDir, outputDir, tpl, nav)
renderThanksPage(contentDir, outputDir, tpl, nav)
buildBlogIndex(blogPosts, tpl, outputDir, nav)
buildCategoryPages(categoryMap, tpl, outputDir, nav)
fmt.Println("✅ Site generation complete.")
}
func parseTemplate(templateFile string) (*template.Template, error) {
return template.ParseFiles(templateFile)
func loadTemplates() (*template.Template, error) {
return template.New("").Funcs(templateFuncs()).ParseGlob("templates/*.html")
}
func readContentFiles(contentDir string) ([]os.FileInfo, error) {
return ioutil.ReadDir(contentDir)
}
func buildNavigation(files []os.FileInfo) []NavItem {
func buildNav(entries []fs.DirEntry, contentDir string) []NavItem {
var nav []NavItem
for _, file := range files {
if filepath.Ext(file.Name()) == ".md" {
name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
title := strings.Title(name)
url := "/"
if name != "index" {
url = "/" + name + "/"
}
nav = append(nav, NavItem{Title: title, URL: url})
for _, entry := range entries {
if entry.Type().IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
continue
}
name := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
path := filepath.Join(contentDir, entry.Name())
rawContent, err := os.ReadFile(path)
if err != nil {
fmt.Printf("Failed to read %s: %v\n", entry.Name(), err)
continue
}
meta, _ := parseFrontMatter(rawContent)
// Skip pages with showInNav: false
if meta.ShowInNav != nil && !*meta.ShowInNav {
continue
}
// Pull title info
title := strings.TrimSpace(meta.NavTitle)
if title == "" {
title = meta.Title
}
if title == "" && name == "index" {
title = "Home"
} else if title == "" {
title = strings.Title(name)
}
// Build URL
url := "/"
if name != "index" {
url = "/" + name + "/"
}
// Pull nav position regardless of title logic
position := 999
if meta.NavPosition != nil {
position = *meta.NavPosition
}
nav = append(nav, NavItem{
Title: title,
URL: url,
Position: position,
})
}
// Optional: add Blog manually (or skip if you handle it as a page)
nav = append(nav, NavItem{
Title: "Blog",
URL: "/blog/",
Position: 3,
})
// Sort by position
sort.SliceStable(nav, func(i, j int) bool {
return nav[i].Position < nav[j].Position
})
return nav
}
func generateHTML(file os.FileInfo, contentDir, outputDir string, tpl *template.Template, nav []NavItem) error {
func renderStaticPages(files []os.FileInfo, contentDir, outputDir string, tpl *template.Template, nav []NavItem) {
for _, file := range files {
if filepath.Ext(file.Name()) == ".md" {
renderStaticPage(file, contentDir, outputDir, tpl, nav)
}
}
}
func renderStaticPage(file os.FileInfo, contentDir, outputDir string, tpl *template.Template, nav []NavItem) {
name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
title := strings.Title(name)
mdPath := filepath.Join(contentDir, file.Name())
mdContent, err := ioutil.ReadFile(mdPath)
rawContent, err := ioutil.ReadFile(mdPath)
if err != nil {
return fmt.Errorf("failed to read %s: %w", file.Name(), err)
fmt.Printf("Failed to read %s: %v\n", file.Name(), err)
return
}
htmlContent := blackfriday.Run(mdContent)
meta, content := parseFrontMatter(rawContent)
htmlContent := blackfriday.Run(content)
if meta.Title == "" {
meta.Title = title
}
var outPath string
if name == "index" {
@@ -96,17 +174,219 @@ func generateHTML(file os.FileInfo, contentDir, outputDir string, tpl *template.
outFile, err := os.Create(outPath)
if err != nil {
return fmt.Errorf("failed to create %s: %w", outPath, err)
}
defer outFile.Close()
data := map[string]interface{}{
"Content": template.HTML(htmlContent),
"Title": title,
"Nav": nav,
"Year": time.Now().Year(),
"Date": time.Now().Format("January 2, 2006"),
fmt.Printf("Failed to create %s: %v\n", outPath, err)
return
}
return tpl.Execute(outFile, data)
tpl.ExecuteTemplate(outFile, "static_page", map[string]interface{}{
"Title": meta.Title,
"Description": meta.Description,
"Date": meta.Date,
"Categories": meta.Categories,
"Content": template.HTML(htmlContent),
"Nav": nav,
"Year": time.Now().Year(),
"PageTemplate": "static",
})
fmt.Printf("Generated: %s\n", outPath)
}
func renderContactPage(contentDir, outputDir string, tpl *template.Template, nav []NavItem) {
contactPath := filepath.Join(contentDir, "contact.md")
rawContent, err := os.ReadFile(contactPath)
if err != nil {
fmt.Printf("Contact page not found: %v\n", err)
return
}
meta, _ := parseFrontMatter(rawContent)
outPath := filepath.Join(outputDir, "contact", "index.html")
os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
outFile, err := os.Create(outPath)
if err != nil {
fmt.Printf("Failed to create contact page: %v\n", err)
return
}
tpl.ExecuteTemplate(outFile, "contact_page", map[string]interface{}{
"Title": meta.Title,
"Description": meta.Description,
"Nav": nav,
"Year": time.Now().Year(),
"PageTemplate": "contact_page",
})
fmt.Println("Generated: /contact/")
}
func renderThanksPage(contentDir, outputDir string, tpl *template.Template, nav []NavItem) {
path := filepath.Join(contentDir, "thanks.md")
rawContent, err := os.ReadFile(path)
if err != nil {
fmt.Printf("Thanks page not found: %v\n", err)
return
}
meta, _ := parseFrontMatter(rawContent)
outPath := filepath.Join(outputDir, "thanks", "index.html")
os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
outFile, err := os.Create(outPath)
if err != nil {
fmt.Printf("Failed to create thanks page: %v\n", err)
return
}
tpl.ExecuteTemplate(outFile, "thanks_page", map[string]interface{}{
"Title": meta.Title,
"Description": meta.Description,
"Nav": nav,
"Year": time.Now().Year(),
"PageTemplate": "thanks_page",
})
fmt.Println("Generated: /thanks/")
}
func processBlogPosts(contentDir, outputDir string, tpl *template.Template, nav []NavItem) ([]PageMeta, map[string][]PageMeta) {
var blogPosts []PageMeta
categoryMap := make(map[string][]PageMeta)
blogDir := filepath.Join(contentDir, "blog")
blogFiles, _ := ioutil.ReadDir(blogDir)
for _, file := range blogFiles {
if filepath.Ext(file.Name()) != ".md" {
continue
}
processBlogPost(file, blogDir, outputDir, tpl, nav, &blogPosts, categoryMap)
}
return blogPosts, categoryMap
}
func processBlogPost(file os.FileInfo, blogDir, outputDir string, tpl *template.Template, nav []NavItem, blogPosts *[]PageMeta, categoryMap map[string][]PageMeta) {
slug := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
blogPath := filepath.Join(blogDir, file.Name())
rawContent, err := ioutil.ReadFile(blogPath)
if err != nil {
fmt.Printf("Failed to read blog post %s: %v\n", file.Name(), err)
return
}
meta, content := parseFrontMatter(rawContent)
html := blackfriday.Run(content)
meta.Slug = slug
if meta.Title == "" {
meta.Title = strings.Title(slug)
}
for _, cat := range meta.Categories {
slug := slugify(cat)
categoryMap[slug] = append(categoryMap[slug], meta)
}
outPath := filepath.Join(outputDir, "blog", slug, "index.html")
os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
outFile, err := os.Create(outPath)
if err != nil {
fmt.Printf("Failed to create blog file %s: %v\n", outPath, err)
return
}
tpl.ExecuteTemplate(outFile, "blog_post_page", map[string]interface{}{
"Title": meta.Title,
"Description": meta.Description,
"Date": meta.Date,
"Categories": meta.Categories,
"Content": template.HTML(html),
"Nav": nav,
"Year": time.Now().Year(),
"PageTemplate": "blog_post",
})
meta.FormattedDate = meta.Date.Format(time.RFC1123Z)
*blogPosts = append(*blogPosts, meta)
}
func buildCategoryPages(categoryMap map[string][]PageMeta, tpl *template.Template, outputDir string, nav []NavItem) {
for catSlug, posts := range categoryMap {
outDir := filepath.Join(outputDir, "blog", "category", catSlug)
os.MkdirAll(outDir, os.ModePerm)
outPath := filepath.Join(outDir, "index.html")
outFile, err := os.Create(outPath)
if err != nil {
fmt.Printf("Failed to create category page for %s: %v\n", catSlug, err)
continue
}
tpl.ExecuteTemplate(outFile, "category_page", map[string]interface{}{
"Title": "Category: " + strings.Title(strings.ReplaceAll(catSlug, "-", " ")),
"Posts": posts,
"Nav": nav,
"Year": time.Now().Year(),
"PageTemplate": "category_page",
})
fmt.Printf("Generated category: /blog/category/%s/\n", catSlug)
}
}
func parseFrontMatter(raw []byte) (PageMeta, []byte) {
var meta PageMeta
raw = bytes.ReplaceAll(raw, []byte("\r\n"), []byte("\n"))
if bytes.HasPrefix(raw, []byte("---\n")) {
parts := bytes.SplitN(raw, []byte("---\n"), 3)
if len(parts) == 3 {
err := yaml.Unmarshal(parts[1], &meta)
if err != nil {
fmt.Printf("⚠️ Failed to parse front matter: %v\n", err)
}
return meta, parts[2]
}
}
return meta, raw
}
func buildBlogIndex(posts []PageMeta, tpl *template.Template, outputDir string, nav []NavItem) {
outPath := filepath.Join(outputDir, "blog", "index.html")
os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
outFile, err := os.Create(outPath)
if err != nil {
fmt.Println("Failed to create blog index:", err)
return
}
tpl.ExecuteTemplate(outFile, "blog_index_page", map[string]interface{}{
"Title": "Blog Index",
"Posts": posts,
"Nav": nav,
"Year": time.Now().Year(),
"PageTemplate": "blog_index",
})
}
func slugify(s string) string {
s = strings.ToLower(s)
s = slugRegex.ReplaceAllString(s, "-")
return strings.Trim(s, "-")
}
func templateFuncs() template.FuncMap {
return template.FuncMap{
"lower": strings.ToLower,
"slugify": slugify,
"urlquery": template.URLQueryEscaper,
}
}

1427
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "ssg",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "concurrently -k -n TAILWIND,GO,SERVE -c blue,green,magenta \"npm run tailwind\" \"npm run go\" \"npm run serve\"",
"tailwind": "npx @tailwindcss/cli -i ./css/base.css -o ./public/assets/style.css --watch",
"go": "air",
"serve": "live-server public"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@tailwindcss/cli": "^4.1.4",
"@tailwindcss/typography": "^0.5.16",
"concurrently": "^9.1.2",
"tailwindcss": "^4.1.4"
}
}

View File

@@ -1,35 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>About</title>
</head>
<body>
<header>
<nav>
<ul>
<li><a href="/about/">About</a></li>
<li><a href="/">Index</a></li>
</ul>
</nav>
</header>
<main>
<h1>About
<p>
Experimenting with Go. Seems pretty cool so far!
</p>
</main>
<footer>
<p>&copy; 2025 Keith Solomon</p>
</footer>
</body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go SSG - About</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
<header id="site_head">
<h1><a href="/">Go SSG</a></h1>
<nav>
<ul class="list-none">
<li class="list-none"><a class="text-white hover:text-primary-200" href="/">Home</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/about/">About</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/blog/">Blog</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/contact/">Contact</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Go SSG - About</h1>
<p>Experimenting with Go. Seems pretty cool so far!</p>
</article>
</main>
<footer id="site_foot">
<p class="p-0 py-4 m-0 leading-none">&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

41
public/assets/contact.js Normal file
View File

@@ -0,0 +1,41 @@
document.getElementById("contactForm").addEventListener("submit", function (e) {
const name = document.getElementById("name");
const email = document.getElementById("email");
const message = document.getElementById("message");
let valid = true;
// Clear errors
document.querySelectorAll("[data-error]").forEach(el => {
el.classList.add("hidden");
el.textContent = "";
});
// Validate name
if (name.value.trim() === "") {
showError("name", "Name is required");
valid = false;
}
// Validate email
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.value)) {
showError("email", "Enter a valid email address");
valid = false;
}
// Validate message
if (message.value.trim().length < 10) {
showError("message", "Message must be at least 10 characters");
valid = false;
}
if (!valid) {
e.preventDefault();
}
function showError(field, message) {
const el = document.querySelector(`[data-error="\${field}"]`);
el.textContent = message;
el.classList.remove("hidden");
}
});

BIN
public/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

1021
public/assets/style.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go SSG - Another Blog Post</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
<header id="site_head">
<h1><a href="/">Go SSG</a></h1>
<nav>
<ul class="list-none">
<li class="list-none"><a class="text-white hover:text-primary-200" href="/">Home</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/about/">About</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/blog/">Blog</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/contact/">Contact</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Go SSG - Another Blog Post</h1>
<p><small>2025-04-19</small></p>
<p>
Categories:
<a href="/blog/category/go/">go</a>
,
<a href="/blog/category/anger/">anger</a>
</p>
<h2>Why isn&rsquo;t this working?! 😭</h2>
<p>This is my first Go SSG blog post.</p>
<p>This project is a simple static site generator written in Go. It is designed to be easy to use and extend, allowing you to create a static website quickly and efficiently.</p>
<p>It supports features like:</p>
<ul>
<li>Markdown content</li>
<li>Customizable templates</li>
<li>Simple configuration</li>
</ul>
</article>
</main>
<footer id="site_foot">
<p class="p-0 py-4 m-0 leading-none">&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Category: Anger</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
<header id="site_head">
<h1><a href="/">Go SSG</a></h1>
<nav>
<ul class="list-none">
<li class="list-none"><a class="text-white hover:text-primary-200" href="/">Home</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/about/">About</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/blog/">Blog</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/contact/">Contact</a></li>
</ul>
</nav>
</header>
<main>
<h1>Category: Anger</h1>
<ul>
<li><a href="/blog/another-blog-post/">Go SSG - Another Blog Post</a><br>A longer summary of the post. Lorem ipsum dolor set amit.</li>
</ul>
</main>
<footer id="site_foot">
<p class="p-0 py-4 m-0 leading-none">&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Category: Go</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
<header id="site_head">
<h1><a href="/">Go SSG</a></h1>
<nav>
<ul class="list-none">
<li class="list-none"><a class="text-white hover:text-primary-200" href="/">Home</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/about/">About</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/blog/">Blog</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/contact/">Contact</a></li>
</ul>
</nav>
</header>
<main>
<h1>Category: Go</h1>
<ul>
<li><a href="/blog/another-blog-post/">Go SSG - Another Blog Post</a><br>A longer summary of the post. Lorem ipsum dolor set amit.</li>
<li><a href="/blog/my-first-blog-post/">Go SSG - My First Blog Post</a><br>A short summary of the post.</li>
</ul>
</main>
<footer id="site_foot">
<p class="p-0 py-4 m-0 leading-none">&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Category: Progrmming</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
<header id="site_head">
<h1><a href="/">Go SSG</a></h1>
<nav>
<ul class="list-none">
<li class="list-none"><a class="text-white hover:text-primary-200" href="/">Home</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/about/">About</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/blog/">Blog</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/contact/">Contact</a></li>
</ul>
</nav>
</header>
<main>
<h1>Category: Progrmming</h1>
<ul>
<li><a href="/blog/my-first-blog-post/">Go SSG - My First Blog Post</a><br>A short summary of the post.</li>
</ul>
</main>
<footer id="site_foot">
<p class="p-0 py-4 m-0 leading-none">&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

54
public/blog/index.html Normal file
View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Blog Index</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
<header id="site_head">
<h1><a href="/">Go SSG</a></h1>
<nav>
<ul class="list-none">
<li class="list-none"><a class="text-white hover:text-primary-200" href="/">Home</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/about/">About</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/blog/">Blog</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/contact/">Contact</a></li>
</ul>
</nav>
</header>
<main>
<div class="main">
<h1>Go SSG - Blog Index</h1>
<ul>
<li><a href="/blog/another-blog-post/">Go SSG - Another Blog Post</a><br>A longer summary of the post. Lorem ipsum dolor set amit.</li>
<li><a href="/blog/my-first-blog-post/">Go SSG - My First Blog Post</a><br>A short summary of the post.</li>
</ul>
</div>
</main>
<footer id="site_foot">
<p class="p-0 py-4 m-0 leading-none">&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go SSG - My First Blog Post</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
<header id="site_head">
<h1><a href="/">Go SSG</a></h1>
<nav>
<ul class="list-none">
<li class="list-none"><a class="text-white hover:text-primary-200" href="/">Home</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/about/">About</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/blog/">Blog</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/contact/">Contact</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Go SSG - My First Blog Post</h1>
<p><small>2025-04-19</small></p>
<p>
Categories:
<a href="/blog/category/progrmming/">progrmming</a>
,
<a href="/blog/category/go/">go</a>
</p>
<h2><em>Tap Tap Tap</em> Is this thing on?</h2>
<p class="text-primary">This is my first Go SSG blog post.</p>
<p>This project is a simple static site generator written in Go. It is designed to be easy to use and extend, allowing you to create a static website quickly and efficiently.</p>
<p>It supports features like:</p>
<ul>
<li>Markdown content</li>
<li>Customizable templates</li>
<li>Simple configuration</li>
</ul>
</article>
</main>
<footer id="site_foot">
<p class="p-0 py-4 m-0 leading-none">&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

79
public/contact/index.html Normal file
View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Contact</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
<header id="site_head">
<h1><a href="/">Go SSG</a></h1>
<nav>
<ul class="list-none">
<li class="list-none"><a class="text-white hover:text-primary-200" href="/">Home</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/about/">About</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/blog/">Blog</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/contact/">Contact</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1 class="text-2xl font-bold mb-4">Contact Me</h1>
<form action="https://api.staticforms.xyz/submit" method="POST" class="space-y-4 max-w-xl">
<input type="hidden" name="accessKey" value="sf_5m86ek16hmele1jlkl62ghml" />
<input type="hidden" name="redirectTo" value="/thanks/" />
<label>
<span>Name</span>
<input id="name" type="text" name="name" required class="w-full border p-2 rounded" />
<span class="text-red-600 text-sm hidden" data-error="name"></span>
</label>
<label>
<span>Email</span>
<input id="email" type="email" name="email" required class="w-full border p-2 rounded" />
<span class="text-red-600 text-sm hidden" data-error="email"></span>
</label>
<label>
<span>Message</span>
<textarea id="message" name="message" rows="5" required class="w-full border p-2 rounded"></textarea>
<span class="text-red-600 text-sm hidden" data-error="message"></span>
</label>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Send Message
</button>
</form>
</article>
</main>
<footer id="site_foot">
<p class="p-0 py-4 m-0 leading-none">&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
<script src="/assets/contact.js" defer></script></body>
</html>

View File

@@ -1,35 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
<header>
<nav>
<ul>
<li><a href="/about/">About</a></li>
<li><a href="/">Index</a></li>
</ul>
</nav>
</header>
<main>
<h1>Welcome
<p>
This is your first static site built with Go!
</p>
</main>
<footer>
<p>&copy; 2025 Keith Solomon</p>
</footer>
</body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go SSG - Home</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
<header id="site_head">
<h1><a href="/">Go SSG</a></h1>
<nav>
<ul class="list-none">
<li class="list-none"><a class="text-white hover:text-primary-200" href="/">Home</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/about/">About</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/blog/">Blog</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/contact/">Contact</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Go SSG - Home</h1>
<p>Welcome to my static site built with Go!</p>
</article>
</main>
<footer id="site_foot">
<p class="p-0 py-4 m-0 leading-none">&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

47
public/thanks/index.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Thank You</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
<header id="site_head">
<h1><a href="/">Go SSG</a></h1>
<nav>
<ul class="list-none">
<li class="list-none"><a class="text-white hover:text-primary-200" href="/">Home</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/about/">About</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/blog/">Blog</a></li>
<li class="list-none"><a class="text-white hover:text-primary-200" href="/contact/">Contact</a></li>
</ul>
</nav>
</header>
<main>
<div class="max-w-2xl mx-auto mt-10 text-center">
<h1 class="text-3xl font-bold text-green-700 mb-4">Thanks for reaching out!</h1>
<p class="text-lg">Your message has been received. Ill get back to you as soon as I can.</p>
</div>
</main>
<footer id="site_foot">
<p class="p-0 py-4 m-0 leading-none">&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

40
templates/base.html Normal file
View File

@@ -0,0 +1,40 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ .Title }}</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
{{ template "header" . }}
<main>
{{- if eq .PageTemplate "blog_index" -}}
{{ template "blog_index_content" . }}
{{- else if eq .PageTemplate "static" -}}
{{ template "static_content" . }}
{{- else if eq .PageTemplate "blog_post" -}}
{{ template "blog_post_content" . }}
{{- else if eq .PageTemplate "category_page" -}}
{{ template "category_content" . }}
{{- else if eq .PageTemplate "contact_page" -}}
{{ template "contact_content" . }}
{{- else if eq .PageTemplate "thanks_page" -}}
{{ template "thanks_content" . }}
{{- else -}}
<p>Unknown PageTemplate: {{ .PageTemplate }}</p>
{{- end -}}
</main>
{{ template "footer" . }}
{{- if eq .PageTemplate "contact_page" -}}
<script src="/assets/contact.js" defer></script>
{{- end -}}
</body>
</html>
{{ end }}

View File

@@ -0,0 +1,17 @@
{{ define "blog_index_page" }}
{{ template "base" . }}
{{ end }}
{{ define "blog_index_content" }}
<div class="main">
<h1>Go SSG - Blog Index</h1>
<ul>
{{ range .Posts }}
<li><a href="/blog/{{ .Slug }}/">{{ .Title }}</a><br>{{ .Description }}</li>
{{ else }}
<li>No posts found.</li>
{{ end }}
</ul>
</div>
{{ end }}

View File

@@ -0,0 +1,22 @@
{{ define "blog_post_page" }}
{{ template "base" . }}
{{ end }}
{{ define "blog_post_content" }}
<article>
<h1>{{ .Title }}</h1>
{{ if .Date }}<p><small>{{ .Date }}</small></p>{{ end }}
{{ if .Categories }}
<p>
Categories:
{{ range $i, $cat := .Categories }}
{{ if $i }}, {{ end }}
<a href="/blog/category/{{ $cat | slugify }}/">{{ $cat }}</a>
{{ end }}
</p>
{{ end }}
{{ .Content }}
</article>
{{ end }}

View File

@@ -0,0 +1,12 @@
{{ define "category_page" }}
{{ template "base" . }}
{{ end }}
{{ define "category_content" }}
<h1>{{ .Title }}</h1>
<ul>
{{ range .Posts }}
<li><a href="/blog/{{ .Slug }}/">{{ .Title }}</a><br>{{ .Description }}</li>
{{ end }}
</ul>
{{ end }}

View File

@@ -0,0 +1,42 @@
{{ define "contact_page" }}
{{ template "base" . }}
{{ end }}
{{ define "contact_content" }}
<article>
<h1 class="text-2xl font-bold mb-4">Contact Me</h1>
<form action="https://api.staticforms.xyz/submit" method="POST" class="space-y-4 max-w-xl">
<!-- StaticForms API Key -->
<input type="hidden" name="accessKey" value="sf_5m86ek16hmele1jlkl62ghml" />
<!-- Optional redirect after success -->
<input type="hidden" name="redirectTo" value="/thanks/" />
<label>
<span>Name</span>
<input id="name" type="text" name="name" required class="w-full border p-2 rounded" />
<span class="text-red-600 text-sm hidden" data-error="name"></span>
</label>
<label>
<span>Email</span>
<input id="email" type="email" name="email" required class="w-full border p-2 rounded" />
<span class="text-red-600 text-sm hidden" data-error="email"></span>
</label>
<label>
<span>Message</span>
<textarea id="message" name="message" rows="5" required class="w-full border p-2 rounded"></textarea>
<span class="text-red-600 text-sm hidden" data-error="message"></span>
</label>
<!-- reCAPTCHA if configured in dashboard -->
<!-- <div class="g-recaptcha" data-sitekey="your-recaptcha-site-key"></div> -->
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Send Message
</button>
</form>
</article>
{{ end }}

22
templates/feed.xml Normal file
View File

@@ -0,0 +1,22 @@
{{- define "rss_feed" -}}
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>{{ .Title }}</title>
<link>{{ .SiteURL }}</link>
<description>{{ .Description }}</description>
<language>en-us</language>
<lastBuildDate>{{ .BuildDate }}</lastBuildDate>
{{ range .Posts }}
<item>
<title>{{ .Title }}</title>
<link>{{ $.SiteURL }}/blog/{{ .Slug }}/</link>
<guid>{{ $.SiteURL }}/blog/{{ .Slug }}/</guid>
<pubDate>{{ .DateFormatted }}</pubDate>
<description><![CDATA[{{ .Description }}]]></description>
</item>
{{ end }}
</channel>
</rss>
{{- end -}}

5
templates/footer.html Normal file
View File

@@ -0,0 +1,5 @@
{{ define "footer" }}
<footer id="site_foot">
<p class="p-0 py-4 m-0 leading-none">&copy; {{ .Year }} Keith Solomon - Go SSG</p>
</footer>
{{ end }}

13
templates/header.html Normal file
View File

@@ -0,0 +1,13 @@
{{ define "header" }}
<header id="site_head">
<h1><a href="/">Go SSG</a></h1>
<nav>
<ul class="list-none">
{{ range .Nav }}
<li class="list-none"><a class="text-white hover:text-primary-200" href="{{ .URL }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</nav>
</header>
{{ end }}

View File

@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ .Title }}</title>
</head>
<body>
<header>
<nav>
<ul>
{{ range .Nav }}
<li><a href="{{ .URL }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</nav>
</header>
<main>
{{ .Content }}
</main>
<footer>
<p>&copy; {{ .Year }} Keith Solomon</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,10 @@
{{ define "static_page" }}
{{ template "base" . }}
{{ end }}
{{ define "static_content" }}
<article>
<h1>{{ .Title }}</h1>
{{ .Content }}
</article>
{{ end }}

View File

@@ -0,0 +1,10 @@
{{ define "thanks_page" }}
{{ template "base" . }}
{{ end }}
{{ define "thanks_content" }}
<div class="max-w-2xl mx-auto mt-10 text-center">
<h1 class="text-3xl font-bold text-green-700 mb-4">Thanks for reaching out!</h1>
<p class="text-lg">Your message has been received. Ill get back to you as soon as I can.</p>
</div>
{{ end }}