diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | README.md | 81 | ||||
| -rw-r--r-- | files/base.html | 15 | ||||
| -rw-r--r-- | files/config.yaml | 16 | ||||
| -rw-r--r-- | files/first.md | 15 | ||||
| -rw-r--r-- | files/index.html | 14 | ||||
| -rw-r--r-- | files/index.xml | 21 | ||||
| -rw-r--r-- | files/post.html | 14 | ||||
| -rw-r--r-- | main.go | 100 |
9 files changed, 252 insertions, 26 deletions
| @@ -1,2 +1,4 @@ | |||
| 1 | public/ | 1 | public/ |
| 2 | example/ | 2 | example/ |
| 3 | junk/ | ||
| 4 | jbmafp \ No newline at end of file | ||
| @@ -9,3 +9,84 @@ This generator is not for people who need something more complicated. Use Hugo | |||
| 9 | instead. But if you need a simple blog page that needs to spit out an RSS feed | 9 | instead. But if you need a simple blog page that needs to spit out an RSS feed |
| 10 | or two and have the option to define different templates for different posts, | 10 | or two and have the option to define different templates for different posts, |
| 11 | well then this might be useful to you. | 11 | well then this might be useful to you. |
| 12 | |||
| 13 | The only thing hard about this project is the spelling of its name. | ||
| 14 | |||
| 15 | Some facts (will be more clear when you read the whole readme): | ||
| 16 | |||
| 17 | - You cannot nest your markdown file under `content` folder. All files must be | ||
| 18 | in the root of `content` folder. | ||
| 19 | - `public` folder gets automatically created on `jbmafp --build`. | ||
| 20 | - All files in `static` folder will be moved to the root of `public` folder. | ||
| 21 | - When you provide `url` in your markdown files, this will create these files in | ||
| 22 | the root of `public` folder. No nesting allowed. | ||
| 23 | |||
| 24 | ## Install | ||
| 25 | |||
| 26 | ```sh | ||
| 27 | git clone git@github.com:mitjafelicijan/jbmafp.git | ||
| 28 | cd jbmafp | ||
| 29 | go install . | ||
| 30 | ``` | ||
| 31 | |||
| 32 | ## Generate first site | ||
| 33 | |||
| 34 | - Go to your projects folder or wherever you want to place the site. | ||
| 35 | |||
| 36 | ```sh | ||
| 37 | mkdir my-ugly-website | ||
| 38 | cd my-ugly-website | ||
| 39 | jbmafp --init | ||
| 40 | jbmafp --build | ||
| 41 | ``` | ||
| 42 | |||
| 43 | - Check out `public` folder and you will see a website. That is about it. | ||
| 44 | - You can also do `jbmafp --help` to see all the option. | ||
| 45 | |||
| 46 | ## Understanding all this bullshit | ||
| 47 | |||
| 48 | - Posts go into `content` folder. | ||
| 49 | - You can change them in `templates` folder. | ||
| 50 | - Each post must have. All of the fields are required. If you have ever used | ||
| 51 | Hugo, this is the same thing. Below is example `content/first.md`. | ||
| 52 | |||
| 53 | ```md | ||
| 54 | --- | ||
| 55 | title: "My first post" | ||
| 56 | url: first.html | ||
| 57 | date: 2023-06-29T14:51:39+02:00 | ||
| 58 | type: post | ||
| 59 | draft: false | ||
| 60 | --- | ||
| 61 | |||
| 62 | This is my first post. It ain't much but it's an honest post. | ||
| 63 | ``` | ||
| 64 | |||
| 65 | - `type` is used all over the place. It is used to define a template file of the | ||
| 66 | page that will be generated. If type is `post` then the program will load | ||
| 67 | `templates/post.html` to handle generation of the page. | ||
| 68 | - You can use whatever name you want. I use `note`, `post` as types to separate | ||
| 69 | all the pages into categories. | ||
| 70 | - `type` is also used inside templates like: | ||
| 71 | ```html | ||
| 72 | <ul> | ||
| 73 | {{ range .Pages }} | ||
| 74 | {{ if eq .Type "post" }} | ||
| 75 | <li><a href="/{{ .RelPermalink }}">{{ .Title }}</a></li> | ||
| 76 | {{ end }} | ||
| 77 | {{ end }} | ||
| 78 | </ul> | ||
| 79 | ``` | ||
| 80 | - This is also use for generating RSS feed. Check `templates/index.xml` to see | ||
| 81 | the example. | ||
| 82 | - This opens door to quite versatile build option. | ||
| 83 | - You can trigger additional generation of content under `extras` field in | ||
| 84 | `config.yaml` file. RSS feed gets generated this way. `template` field tells | ||
| 85 | generator which file in `templates` folder to use and `url` tells generator | ||
| 86 | what the file should be called when its saved. | ||
| 87 | |||
| 88 | ## License | ||
| 89 | |||
| 90 | [jbmafp](https://github.com/mitjafelicijan/jbmafp) was written by [Mitja | ||
| 91 | Felicijan](https://mitjafelicijan.com) and is released under the BSD two-clause | ||
| 92 | license, see the LICENSE file for more information. | ||
diff --git a/files/base.html b/files/base.html new file mode 100644 index 0000000..d965c25 --- /dev/null +++ b/files/base.html | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html lang="{{ .Config.Language }}"> | ||
| 3 | <head> | ||
| 4 | <meta charset="utf-8"> | ||
| 5 | <meta name="viewport" content="width=device-width,initial-scale=1"> | ||
| 6 | <title>{{ block "title" . }}{{ .Config.Title }}{{ end }}</title> | ||
| 7 | <meta name="description" content="{{ block "description" . }}{{ .Config.Description }}{{ end }}"> | ||
| 8 | <link rel="alternate" type="application/rss+xml" href="{{ .Config.BaseURL }}/index.xml"> | ||
| 9 | </head> | ||
| 10 | <body> | ||
| 11 | <main> | ||
| 12 | {{ block "content" . }}{{ end }} | ||
| 13 | </main> | ||
| 14 | </body> | ||
| 15 | </html> | ||
diff --git a/files/config.yaml b/files/config.yaml new file mode 100644 index 0000000..a89795f --- /dev/null +++ b/files/config.yaml | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | title: "Title of your website" | ||
| 2 | baseurl: "https://example.com" | ||
| 3 | description: "My new shiny website" | ||
| 4 | language: "en-us" | ||
| 5 | |||
| 6 | # Code highlighting. | ||
| 7 | # https://swapoff.org/chroma/playground/ | ||
| 8 | highlighting: "vs" | ||
| 9 | |||
| 10 | # Minifies output HTML (including inline CSS, JS). | ||
| 11 | minify: true | ||
| 12 | |||
| 13 | # Other generaters, in this case RSS generator. | ||
| 14 | extras: | ||
| 15 | - template: index.xml | ||
| 16 | url: index.xml | ||
diff --git a/files/first.md b/files/first.md new file mode 100644 index 0000000..9a4b97f --- /dev/null +++ b/files/first.md | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | --- | ||
| 2 | title: "My first post" | ||
| 3 | url: first.html | ||
| 4 | date: 2023-06-29T14:51:39+02:00 | ||
| 5 | type: post | ||
| 6 | draft: false | ||
| 7 | --- | ||
| 8 | |||
| 9 | This is my first post. It ain't much but it's an honest post. | ||
| 10 | |||
| 11 | ```lua | ||
| 12 | for k, v in pairs(arr) do | ||
| 13 | print(k, v[1], v[2], v[3]) | ||
| 14 | end | ||
| 15 | ``` | ||
diff --git a/files/index.html b/files/index.html new file mode 100644 index 0000000..eeb2641 --- /dev/null +++ b/files/index.html | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | {{ template "base.html" . }} | ||
| 2 | |||
| 3 | {{ define "content" }} | ||
| 4 | <div> | ||
| 5 | <h2>Posts</h2> | ||
| 6 | <ul> | ||
| 7 | {{ range .Pages }} | ||
| 8 | {{ if eq .Type "post" }} | ||
| 9 | <li><a href="/{{ .RelPermalink }}">{{ .Title }}</a></li> | ||
| 10 | {{ end }} | ||
| 11 | {{ end }} | ||
| 12 | </ul> | ||
| 13 | </div> | ||
| 14 | {{ end }} | ||
diff --git a/files/index.xml b/files/index.xml new file mode 100644 index 0000000..830dd90 --- /dev/null +++ b/files/index.xml | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | <rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"> | ||
| 2 | <channel> | ||
| 3 | <title>{{ .Config.Title }}'s posts</title> | ||
| 4 | <link>{{ .Config.BaseURL }}</link> | ||
| 5 | <description>{{ .Config.Description }}</description> | ||
| 6 | <language>{{ .Config.Language }}</language> | ||
| 7 | |||
| 8 | {{ range $idx, $page := .Pages }} | ||
| 9 | {{ if eq $page.Type "post" }} | ||
| 10 | <item> | ||
| 11 | <title>{{ $page.Title }}</title> | ||
| 12 | <link>{{ $.Config.BaseURL }}/{{ $page.RelPermalink }}</link> | ||
| 13 | <pubDate>{{ $page.Created.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</pubDate> | ||
| 14 | <guid>{{ $.Config.BaseURL }}/{{ $page.RelPermalink }}</guid> | ||
| 15 | <description>{{ $page.Summary }}</description> | ||
| 16 | <content:encoded>{{ $page.Raw }}</content:encoded> | ||
| 17 | </item> | ||
| 18 | {{ end }} | ||
| 19 | {{ end }} | ||
| 20 | </channel> | ||
| 21 | </rss> | ||
diff --git a/files/post.html b/files/post.html new file mode 100644 index 0000000..28ffad2 --- /dev/null +++ b/files/post.html | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | {{ template "base.html" . }} | ||
| 2 | |||
| 3 | {{ define "title" }}{{ .Page.Title }}{{ end }} | ||
| 4 | {{ define "description" }}{{ .Page.Summary }}{{ end }} | ||
| 5 | |||
| 6 | {{ define "content" }} | ||
| 7 | <div> | ||
| 8 | <h1>{{ .Page.Title }}</h1> | ||
| 9 | <p>{{ .Page.Created.Format "Jan 2, 2006" }}</p> | ||
| 10 | <div> | ||
| 11 | {{ .Page.HTML }} | ||
| 12 | </div> | ||
| 13 | </div> | ||
| 14 | {{ end }} | ||
| @@ -32,6 +32,8 @@ import ( | |||
| 32 | highlighting "github.com/yuin/goldmark-highlighting/v2" | 32 | highlighting "github.com/yuin/goldmark-highlighting/v2" |
| 33 | 33 | ||
| 34 | cp "github.com/otiai10/copy" | 34 | cp "github.com/otiai10/copy" |
| 35 | |||
| 36 | _ "embed" | ||
| 35 | ) | 37 | ) |
| 36 | 38 | ||
| 37 | type ConfigExtrasItem struct { | 39 | type ConfigExtrasItem struct { |
| @@ -64,6 +66,24 @@ type Page struct { | |||
| 64 | Draft bool | 66 | Draft bool |
| 65 | } | 67 | } |
| 66 | 68 | ||
| 69 | //go:embed "files/config.yaml" | ||
| 70 | var EmbedConfig string | ||
| 71 | |||
| 72 | //go:embed "files/first.md" | ||
| 73 | var EmbedPost string | ||
| 74 | |||
| 75 | //go:embed "files/base.html" | ||
| 76 | var EmbedTemplateBase string | ||
| 77 | |||
| 78 | //go:embed "files/index.html" | ||
| 79 | var EmbedTemplateIndex string | ||
| 80 | |||
| 81 | //go:embed "files/post.html" | ||
| 82 | var EmbedTemplatePost string | ||
| 83 | |||
| 84 | //go:embed "files/index.xml" | ||
| 85 | var EmbedTemplateFeed string | ||
| 86 | |||
| 67 | // Function to clean HTML tags using bluemonday. | 87 | // Function to clean HTML tags using bluemonday. |
| 68 | func cleanHTMLTags(htmlString string) string { | 88 | func cleanHTMLTags(htmlString string) string { |
| 69 | p := bluemonday.StrictPolicy() | 89 | p := bluemonday.StrictPolicy() |
| @@ -72,38 +92,36 @@ func cleanHTMLTags(htmlString string) string { | |||
| 72 | } | 92 | } |
| 73 | 93 | ||
| 74 | func initializeProject(projectRoot string) { | 94 | func initializeProject(projectRoot string) { |
| 75 | fmt.Println("Initializing new project") | 95 | log.Println("Initializing new project") |
| 76 | } | ||
| 77 | 96 | ||
| 78 | func main() { | 97 | if err := os.Mkdir(path.Join(projectRoot, "templates"), 0755); err != nil && !os.IsExist(err) { |
| 79 | projectRoot := os.Getenv("PROJECT_ROOT") | 98 | log.Println("Error creating directory:", err) |
| 80 | if projectRoot == "" { | 99 | return |
| 81 | projectRoot = "./" | ||
| 82 | } | 100 | } |
| 83 | 101 | ||
| 84 | fmt.Println("Come back later! Still WIP!") | 102 | if err := os.Mkdir(path.Join(projectRoot, "content"), 0755); err != nil && !os.IsExist(err) { |
| 85 | os.Exit(0) | 103 | log.Println("Error creating directory:", err) |
| 86 | 104 | return | |
| 87 | // Parsing arguments. | ||
| 88 | var args struct { | ||
| 89 | Init bool `arg:"-i,--init" help:"initialize new project"` | ||
| 90 | Build bool `arg:"-b,--build" help:"build the website"` | ||
| 91 | } | 105 | } |
| 92 | 106 | ||
| 93 | arg.MustParse(&args) | 107 | if err := os.Mkdir(path.Join(projectRoot, "static"), 0755); err != nil && !os.IsExist(err) { |
| 94 | 108 | log.Println("Error creating directory:", err) | |
| 95 | if !args.Init && !args.Build { | 109 | return |
| 96 | fmt.Println("No arguments provided. Try using `jbmafp --help`") | ||
| 97 | os.Exit(0) | ||
| 98 | } | 110 | } |
| 99 | 111 | ||
| 100 | if args.Init { | 112 | os.WriteFile(path.Join(projectRoot, "templates", ".gitkeep"), []byte{}, 0755) |
| 101 | initializeProject(projectRoot) | 113 | os.WriteFile(path.Join(projectRoot, "content", ".gitkeep"), []byte{}, 0755) |
| 102 | os.Exit(0) | 114 | os.WriteFile(path.Join(projectRoot, "static", ".gitkeep"), []byte{}, 0755) |
| 103 | } | ||
| 104 | 115 | ||
| 105 | os.Exit(0) | 116 | os.WriteFile(path.Join(projectRoot, "config.yaml"), []byte(EmbedConfig), 0755) |
| 117 | os.WriteFile(path.Join(projectRoot, "content", "first.md"), []byte(EmbedPost), 0755) | ||
| 118 | os.WriteFile(path.Join(projectRoot, "templates", "base.html"), []byte(EmbedTemplateBase), 0755) | ||
| 119 | os.WriteFile(path.Join(projectRoot, "templates", "index.html"), []byte(EmbedTemplateIndex), 0755) | ||
| 120 | os.WriteFile(path.Join(projectRoot, "templates", "post.html"), []byte(EmbedTemplatePost), 0755) | ||
| 121 | os.WriteFile(path.Join(projectRoot, "templates", "index.xml"), []byte(EmbedTemplateFeed), 0755) | ||
| 122 | } | ||
| 106 | 123 | ||
| 124 | func buildProject(projectRoot string) { | ||
| 107 | // Read config file. | 125 | // Read config file. |
| 108 | configFilepath := path.Join(projectRoot, "config.yaml") | 126 | configFilepath := path.Join(projectRoot, "config.yaml") |
| 109 | configFile, err := os.ReadFile(configFilepath) | 127 | configFile, err := os.ReadFile(configFilepath) |
| @@ -195,6 +213,12 @@ func main() { | |||
| 195 | return pages[i].Created.After(pages[j].Created) | 213 | return pages[i].Created.After(pages[j].Created) |
| 196 | }) | 214 | }) |
| 197 | 215 | ||
| 216 | // Creates public folder if it doesn't exist yet. | ||
| 217 | if err := os.Mkdir(path.Join(projectRoot, "public"), 0755); err != nil && !os.IsExist(err) { | ||
| 218 | log.Println("Error creating directory:", err) | ||
| 219 | return | ||
| 220 | } | ||
| 221 | |||
| 198 | // Generate HTML files for all pages. | 222 | // Generate HTML files for all pages. |
| 199 | for _, page := range pages { | 223 | for _, page := range pages { |
| 200 | outFilepath := path.Join(projectRoot, "public", page.Meta["url"].(string)) | 224 | outFilepath := path.Join(projectRoot, "public", page.Meta["url"].(string)) |
| @@ -237,9 +261,7 @@ func main() { | |||
| 237 | log.Println("Wrote", outFilepath) | 261 | log.Println("Wrote", outFilepath) |
| 238 | } else { | 262 | } else { |
| 239 | log.Println("Skipped", outFilepath) | 263 | log.Println("Skipped", outFilepath) |
| 240 | |||
| 241 | } | 264 | } |
| 242 | |||
| 243 | } | 265 | } |
| 244 | 266 | ||
| 245 | // Generates index page. | 267 | // Generates index page. |
| @@ -317,10 +339,36 @@ func main() { | |||
| 317 | 339 | ||
| 318 | outFilepath := path.Join(projectRoot, "public", extra.URL) | 340 | outFilepath := path.Join(projectRoot, "public", extra.URL) |
| 319 | os.WriteFile(outFilepath, []byte(buf.String()), 0755) | 341 | os.WriteFile(outFilepath, []byte(buf.String()), 0755) |
| 320 | |||
| 321 | } | 342 | } |
| 322 | } | 343 | } |
| 323 | 344 | ||
| 324 | // Guess we are done! | 345 | // Guess we are done! |
| 325 | log.Println("Done & done...") | 346 | log.Println("Done & done...") |
| 326 | } | 347 | } |
| 348 | |||
| 349 | func main() { | ||
| 350 | projectRoot := os.Getenv("PROJECT_ROOT") | ||
| 351 | if projectRoot == "" { | ||
| 352 | projectRoot = "./" | ||
| 353 | } | ||
| 354 | |||
| 355 | var args struct { | ||
| 356 | Init bool `arg:"-i,--init" help:"initialize new project"` | ||
| 357 | Build bool `arg:"-b,--build" help:"build the website"` | ||
| 358 | } | ||
| 359 | |||
| 360 | arg.MustParse(&args) | ||
| 361 | |||
| 362 | if !args.Init && !args.Build { | ||
| 363 | fmt.Println("No arguments provided. Try using `jbmafp --help`") | ||
| 364 | os.Exit(0) | ||
| 365 | } | ||
| 366 | |||
| 367 | if args.Init { | ||
| 368 | initializeProject(projectRoot) | ||
| 369 | } | ||
| 370 | |||
| 371 | if args.Build { | ||
| 372 | buildProject(projectRoot) | ||
| 373 | } | ||
| 374 | } | ||
