Skip to content

Commit

Permalink
* Implement built-in content management (admin) interface
Browse files Browse the repository at this point in the history
* Implement no-longer-referenced tags cleanup functionality
* Other minor tweaks/improvements
  • Loading branch information
kion committed Jan 5, 2025
1 parent e46a913 commit faf51ca
Show file tree
Hide file tree
Showing 16 changed files with 661 additions and 174 deletions.
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ embracing the convention-over-configuration philosophy.

## Features

* Built-in content management (admin) interface
* As an alternative: watch & hot reload mode to monitor content source file (`.md`) changes,
generate corresponding output (`.html`) files on the fly,
and dynamically update the corresponding view in browser
* Built-in search engine
* Archive generation
* Tag Index (aka Tag Cloud) generation
* Thumbnail generation
* Watch & Hot Reload mode to preview changes in browser in real-time
* Simple and intuitive to use image and video embedding
* Customizable configuration (pagination, thumbnails, etc.)

Expand Down Expand Up @@ -84,9 +87,15 @@ Then open the following address in a browser to preview your site:

_(the default host and port values can be modified in the `config.yml`)_

You can also use the `--watch-reload` flag to automatically regenerate the site
and see the changes being reflected in the browser in real-time
when you change any of the `.md` files in the `pages` or `posts` dirs:
Use the `--admin` flag to render content management UI components to create/edit/delete pages and posts:

```shell
$ mbgen serve --admin
```

Alternatively, you can use the `--watch-reload` flag
to monitor any changes to the source content (`.md`) files in the `pages` and `posts` dirs,
automatically regenerate the site on the fly, and see the changes dynamically reflected in browser.

```shell
$ mbgen serve --watch-reload
Expand Down
171 changes: 111 additions & 60 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
)

Expand Down Expand Up @@ -40,14 +42,17 @@ var (
" - " + commandCleanupTargetContent + ": deletes all previously generated content (" + contentFileExtension + ") files\n" +
" for which markdown (" + markdownFileExtension + ") content files no longer exist\n\n" +
" - " + commandCleanupTargetThumbs + ": deletes all previously generated thumbnail files\n\n" +
" - " + commandCleanupTargetArchive + ": deletes the previously generated archive files\n\n" +
" - " + commandCleanupTargetTags + ": deletes all previously generated tag files\n" +
" that are no longer referenced by any markdown (" + markdownFileExtension + ") content files\n\n" +
" - " + commandCleanupTargetTagIndex + ": deletes the previously generated tag index file\n\n" +
" - " + commandCleanupTargetArchive + ": deletes the previously generated archive files\n\n" +
" - " + commandCleanupTargetSearch + ": deletes all previously generated search files\n\n" +
" - if no <target> is specified, each target is performed based on the following conditions:\n\n" +
" - " + commandCleanupTargetContent + ": always\n\n" +
" - " + commandCleanupTargetTags + ": always\n\n" +
" - " + commandCleanupTargetThumbs + ": if `useThumbs` config option is disabled\n\n" +
" - " + commandCleanupTargetArchive + ": if `generateArchive` config option is disabled\n\n" +
" - " + commandCleanupTargetTagIndex + ": if `generateTagIndex` config option is disabled\n\n" +
" - " + commandCleanupTargetArchive + ": if `generateArchive` config option is disabled\n\n" +
" - " + commandCleanupTargetSearch + ": if `enableSearch` config option is disabled\n\n",
reqConfig: true,
optArgCnt: 1,
Expand All @@ -71,7 +76,9 @@ var (
description: "start a web server to serve the site",
usage: "mbgen serve [" + commandServeOptionWatchReload + "]\n\n" +
"must be run from a working dir containing " + configFileName + " file and " + deployDirName + " directory with generated assets\n\n" +
" - the optional " + commandServeOptionWatchReload + " flag can be used to automatically regenerate the site and see the changes being reflected in the browser in real-time when you change any of the markdown content (.md) files in the " + markdownPagesDirName + " or " + markdownPostsDirName + " dirs\n",
" ONE of the following flags can be specified:\n" +
" " + commandServeOptionAdmin + " - to render content admin links\n" +
" " + commandServeOptionWatchReload + " - to automatically regenerate the site and see the changes being reflected in the browser in real-time when you change any of the markdown content (.md) files in the " + markdownPagesDirName + " or " + markdownPostsDirName + " dirs\n",
reqConfig: true,
optArgCnt: 1,
}
Expand Down Expand Up @@ -182,11 +189,13 @@ func _init(config appConfig, commandArgs ...string) {
func _cleanup(config appConfig, commandArgs ...string) {
cleanupContent := false
cleanupThumbs := false
cleanupArchive := false
cleanupTags := false
cleanupTagIndex := false
cleanupArchive := false
cleanupSearch := false
if commandArgs == nil || len(commandArgs) == 0 {
cleanupContent = true
cleanupTags = true
cleanupThumbs = !config.useThumbs
cleanupArchive = !config.generateArchive
cleanupTagIndex = !config.generateTagIndex
Expand All @@ -198,10 +207,12 @@ func _cleanup(config appConfig, commandArgs ...string) {
cleanupContent = true
case commandCleanupTargetThumbs:
cleanupThumbs = true
case commandCleanupTargetArchive:
cleanupArchive = true
case commandCleanupTargetTags:
cleanupTags = true
case commandCleanupTargetTagIndex:
cleanupTagIndex = true
case commandCleanupTargetArchive:
cleanupArchive = true
case commandCleanupTargetSearch:
cleanupSearch = true
default:
Expand Down Expand Up @@ -257,10 +268,34 @@ func _cleanup(config appConfig, commandArgs ...string) {
parsePages(config, resLoader, deleteImgThumbnails, false)
parsePosts(config, resLoader, deleteImgThumbnails, false)
}
if cleanupArchive {
deployArchivePath := fmt.Sprintf("%s%c%s", deployDirName, os.PathSeparator, deployArchiveDirName)
if deleteIfExists(deployArchivePath) {
sprintln(" - deleted archive dir: " + deployArchivePath)
if cleanupTags {
deployTagsDirPath := fmt.Sprintf("%s%c%s", deployDirName, os.PathSeparator, deployTagsDirName)
deployTagsDirEntries, err := os.ReadDir(deployTagsDirPath)
check(err)
if len(deployTagsDirEntries) > 0 {
posts := parsePosts(config, getResourceLoader(config), nil, false)
var tags []string
for _, post := range posts {
for _, tag := range post.Tags {
t := strings.ToLower(tag)
if !slices.Contains(tags, t) {
tags = append(tags, t)
}
}
}
for _, deployTagDirEntry := range deployTagsDirEntries {
deployTagDirEntryInfo, err := deployTagDirEntry.Info()
check(err)
if deployTagDirEntryInfo.IsDir() {
deployTagDirName := deployTagDirEntryInfo.Name()
if !slices.Contains(tags, deployTagDirName) {
sprintln(" - tag no longer referenced: " + deployTagDirName)
deployTagDirPath := fmt.Sprintf("%s%c%s%c%s", deployDirName, os.PathSeparator, deployTagsDirName, os.PathSeparator, deployTagDirName)
deleteIfExists(deployTagDirPath)
sprintln(" - deleted tag dir: " + deployTagDirPath)
}
}
}
}
}
if cleanupTagIndex {
Expand All @@ -269,6 +304,12 @@ func _cleanup(config appConfig, commandArgs ...string) {
sprintln(" - deleted tag index file: " + deployTagIndexPath)
}
}
if cleanupArchive {
deployArchivePath := fmt.Sprintf("%s%c%s", deployDirName, os.PathSeparator, deployArchiveDirName)
if deleteIfExists(deployArchivePath) {
sprintln(" - deleted archive dir: " + deployArchivePath)
}
}
if cleanupSearch {
deploySearchIndexPath := fmt.Sprintf("%s%c%s", deployDirName, os.PathSeparator, searchIndexFileName)
if deleteIfExists(deploySearchIndexPath) {
Expand Down Expand Up @@ -312,62 +353,72 @@ func _stats(config appConfig, commandArgs ...string) {
}

func _serve(config appConfig, commandArgs ...string) {
resLoader := getResourceLoader(config)
var wChan chan watchReloadData
var admin bool
if commandArgs != nil && len(commandArgs) > 0 {
if commandArgs[0] != commandServeOptionWatchReload {
sprintln("error: invalid serve command argument: " + commandArgs[0])
usageHelp := "usage:\n\n" + commandServe.usage
usage(usageHelp, 1)
} else {
wChan = make(chan watchReloadData)
resLoader := getResourceLoader(config)
go watchDirForChanges(markdownPagesDirName, markdownFileExtension, func(changedFilePath string, deleted bool) {
filePath := strings.Split(changedFilePath, string(os.PathSeparator))
fileName := filePath[len(filePath)-1]
pageId := fileName[:len(fileName)-len(filepath.Ext(fileName))]
pageDeleted := deleted || !fileExists(changedFilePath)
if !pageDeleted {
println(" - [watch] page markdown file added/updated: " + changedFilePath)
processAndHandleStats(config, resLoader, true)
} else {
println(" - [watch] page markdown file deleted: " + changedFilePath)
deployPageFilePath := fmt.Sprintf("%s%c%s%c%s", deployDirName, os.PathSeparator, deployPageDirName, os.PathSeparator, pageId+contentFileExtension)
if deleteIfExists(deployPageFilePath) {
sprintln(" - deleted page content file: " + deployPageFilePath)
if len(commandArgs) == 1 {
arg := commandArgs[0]
if arg == commandServeOptionAdmin {
admin = true
} else if arg == commandServeOptionWatchReload {
wChan = make(chan watchReloadData)
go watchDirForChanges(markdownPagesDirName, markdownFileExtension, func(changedFilePath string, deleted bool) {
filePath := strings.Split(changedFilePath, string(os.PathSeparator))
fileName := filePath[len(filePath)-1]
pageId := fileName[:len(fileName)-len(filepath.Ext(fileName))]
pageDeleted := deleted || !fileExists(changedFilePath)
if !pageDeleted {
println(" - [watch] page markdown file added/updated: " + changedFilePath)
processAndHandleStats(config, resLoader, true)
} else {
println(" - [watch] page markdown file deleted: " + changedFilePath)
deployPageFilePath := fmt.Sprintf("%s%c%s%c%s", deployDirName, os.PathSeparator, deployPageDirName, os.PathSeparator, pageId+contentFileExtension)
if deleteIfExists(deployPageFilePath) {
sprintln(" - deleted page content file: " + deployPageFilePath)
}
processAndHandleStats(config, resLoader, true)
}
processAndHandleStats(config, resLoader, true)
}
wChan <- watchReloadData{
Type: Page,
Id: pageId,
Deleted: pageDeleted,
}
})
go watchDirForChanges(markdownPostsDirName, markdownFileExtension, func(changedFilePath string, deleted bool) {
filePath := strings.Split(changedFilePath, string(os.PathSeparator))
fileName := filePath[len(filePath)-1]
postId := fileName[:len(fileName)-len(filepath.Ext(fileName))]
postDeleted := deleted || !fileExists(changedFilePath)
if !postDeleted {
println(" - [watch] post markdown file added/updated: " + changedFilePath)
processAndHandleStats(config, resLoader, true)
} else {
println(" - [watch] post markdown file deleted: " + changedFilePath)
deployPostFilePath := fmt.Sprintf("%s%c%s%c%s", deployDirName, os.PathSeparator, deployPostDirName, os.PathSeparator, postId+contentFileExtension)
if deleteIfExists(deployPostFilePath) {
sprintln(" - deleted post content file: " + deployPostFilePath)
wChan <- watchReloadData{
Type: Page,
Id: pageId,
Deleted: pageDeleted,
}
processAndHandleStats(config, resLoader, true)
}
wChan <- watchReloadData{
Type: Post,
Id: postId,
Deleted: postDeleted,
}
})
})
go watchDirForChanges(markdownPostsDirName, markdownFileExtension, func(changedFilePath string, deleted bool) {
filePath := strings.Split(changedFilePath, string(os.PathSeparator))
fileName := filePath[len(filePath)-1]
postId := fileName[:len(fileName)-len(filepath.Ext(fileName))]
postDeleted := deleted || !fileExists(changedFilePath)
if !postDeleted {
println(" - [watch] post markdown file added/updated: " + changedFilePath)
processAndHandleStats(config, resLoader, true)
} else {
println(" - [watch] post markdown file deleted: " + changedFilePath)
deployPostFilePath := fmt.Sprintf("%s%c%s%c%s", deployDirName, os.PathSeparator, deployPostDirName, os.PathSeparator, postId+contentFileExtension)
if deleteIfExists(deployPostFilePath) {
sprintln(" - deleted post content file: " + deployPostFilePath)
}
processAndHandleStats(config, resLoader, true)
}
wChan <- watchReloadData{
Type: Post,
Id: postId,
Deleted: postDeleted,
}
})
} else {
sprintln("error: invalid serve command argument: " + commandArgs[0])
usageHelp := "usage:\n\n" + commandServe.usage
usage(usageHelp, 1)
}
} else {
sprintln("error: invalid number of serve command arguments (max allowed: " + strconv.Itoa(commandServe.optArgCnt) + ")")
usageHelp := "usage:\n\n" + commandServe.usage
usage(usageHelp, 1)
}
}
listenAndServe(fmt.Sprintf("%s:%d", config.serveHost, config.servePort), wChan)
listenAndServe(fmt.Sprintf("%s:%d", config.serveHost, config.servePort), admin, wChan, config, resLoader)
}

func _theme(config appConfig, commandArgs ...string) {
Expand Down
15 changes: 12 additions & 3 deletions const.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
)

const (
appVersion = "1.3.0"
appVersion = "1.5.0"
defaultGitHubRepoUrl = "github.com/kion/mbgen"
defaultGitHubRepoThemesUrl = defaultGitHubRepoUrl + "/themes"
defaultGitHubRepoPageContentSamplesUrl = defaultGitHubRepoUrl + "/content-samples/pages"
Expand Down Expand Up @@ -69,9 +69,11 @@ const (
subTemplatePlaceholder = "{{@ sub-template @}}"
commandCleanupTargetContent = "content"
commandCleanupTargetThumbs = "thumbs"
commandCleanupTargetArchive = "archive"
commandCleanupTargetTags = "tags"
commandCleanupTargetTagIndex = "tag-index"
commandCleanupTargetArchive = "archive"
commandCleanupTargetSearch = "search"
commandServeOptionAdmin = "--admin"
commandServeOptionWatchReload = "--watch-reload"
commandThemeActionActivate = "activate"
commandThemeActionInstall = "install"
Expand All @@ -83,7 +85,14 @@ const (
websocketProtocol = "ws://"
websocketPath = "/--ws--"
websocketPingPeriod = 60 * time.Second
contentClosingTag = "</body>"
jsOpeningTag = "<script type='text/javascript'>"
jsClosingTag = "</script>"
styleOpeningTag = "<style>"
styleClosingTag = "</style>"
headClosingTag = "</head>"
bodyClosingTag = "</body>"
mainOpeningTag = "<main>"
mainClosingTag = "</main>"
)

var (
Expand Down
6 changes: 6 additions & 0 deletions fsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ func closeFile(file *os.File) {
check(err)
}

func readDataFromFile(filePath string) []byte {
content, err := os.ReadFile(filePath)
check(err)
return content
}

func writeDataToFile(outputFilePath string, data []byte) {
outputFile, err := os.Create(outputFilePath)
check(err)
Expand Down
Loading

0 comments on commit faf51ca

Please sign in to comment.