blob: 94be9dd51f71b8e8a9717f67af55b9d179d7223b (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
|
;;; hugo-config.el --- Hugo Blog Configuration -*- lexical-binding: t; coding: utf-8; -*-
;; author: Craig Jennings <c@cjennings.net>
;;; Commentary:
;;
;; Layer: 3 (Domain Workflow).
;; Category: D/P.
;; Load shape: eager.
;; Eager reason: none; blog publishing is a command-loaded deferral candidate
;; for Phase 4.
;; Top-level side effects: package configuration via use-package.
;; Runtime requires: user-constants, host-environment.
;; Direct test load: yes.
;;
;; Integrates ox-hugo for publishing Org files to a Hugo website.
;;
;; One-file-per-post workflow:
;; - Each blog post is a standalone Org file in content-org/log/
;; - File-level keywords control Hugo front matter
;; - Publish by committing and pushing the website repo; the server-side
;; post-receive hook on cjennings.net rebuilds Hugo and deploys
;;
;; Keybindings live under the C-; h prefix. See the bottom of this file
;; or the which-key panel for the full listing.
;;; Code:
(require 'user-constants)
(require 'host-environment)
;; --------------------------------- Constants ---------------------------------
(defconst cj/hugo-content-org-dir
(expand-file-name "content-org/log/" website-dir)
"Directory containing Org source files for Hugo blog posts.")
;; ---------------------------------- ox-hugo ----------------------------------
(use-package ox-hugo
:after ox)
;; ------------------------------- Post Creation -------------------------------
(defun cj/hugo--post-file-path (title)
"Return the file path for a Hugo post with TITLE.
Generates a slug from TITLE using `org-hugo-slug' and returns
the full path under `cj/hugo-content-org-dir'.
Assumes ox-hugo is already loaded (via use-package declaration above)."
(let ((slug (org-hugo-slug title)))
(expand-file-name (concat slug ".org") cj/hugo-content-org-dir)))
(defun cj/hugo--post-template (title date)
"Return the Org front matter template for a Hugo post.
TITLE is the post title, DATE is the date string (YYYY-MM-DD)."
(format "#+hugo_base_dir: ../../
#+hugo_section: log
#+hugo_auto_set_lastmod: t
#+title: %s
#+date: %s
#+hugo_tags:
#+hugo_draft: true
#+hugo_custom_front_matter: :description \"\"
" title date))
(defun cj/hugo-new-post ()
"Create a new Hugo blog post as a standalone Org file.
Prompts for title, generates the slug filename, and opens the
new file with Hugo front matter keywords pre-filled."
(interactive)
(let* ((title (read-from-minibuffer "Post Title: "))
(file (cj/hugo--post-file-path title))
(date (format-time-string "%Y-%m-%d")))
(when (file-exists-p file)
(user-error "Post already exists: %s" file))
(unless (file-directory-p cj/hugo-content-org-dir)
(make-directory cj/hugo-content-org-dir t))
(find-file file)
(insert (cj/hugo--post-template title date))
(save-buffer)
(message "New post: %s" file)))
;; -------------------------------- Post Export --------------------------------
(defun cj/hugo-export-post ()
"Export the current Org file to Hugo-compatible Markdown."
(interactive)
(require 'ox-hugo)
(unless (derived-mode-p 'org-mode)
(user-error "Not in an Org buffer"))
(org-hugo-export-to-md)
(message "Exported: %s" (buffer-name)))
;; ---------------------------- Directory Navigation ---------------------------
(defun cj/hugo-open-blog-dir ()
"Open the blog source directory in dirvish/dired."
(interactive)
(unless (file-directory-p cj/hugo-content-org-dir)
(make-directory cj/hugo-content-org-dir t))
(dired cj/hugo-content-org-dir))
(defun cj/hugo-open-blog-dir-external ()
"Open the blog source directory in the system file manager."
(interactive)
(unless (file-directory-p cj/hugo-content-org-dir)
(make-directory cj/hugo-content-org-dir t))
(let ((cmd (cond
((env-macos-p) "open")
((env-windows-p) "explorer.exe")
(t "xdg-open"))))
(start-process "hugo-file-manager" nil cmd cj/hugo-content-org-dir)))
;; ----------------------------- Draft Management ------------------------------
(defun cj/hugo--post-metadata (file)
"Return minimal front-matter metadata for Hugo post FILE, or nil if not one.
A file counts as a Hugo post only if it contains `#+hugo_draft: true' or
`#+hugo_draft: false' in its front matter region.
Returns a plist (:title TITLE :draft BOOL). TITLE falls back to the file
basename when `#+title:' is absent. Reads only the first 2048 bytes."
(with-temp-buffer
(insert-file-contents file nil 0 2048)
(let (title draft is-hugo)
(goto-char (point-min))
(when (re-search-forward "^#\\+title: *\\(.+\\)$" nil t)
(setq title (match-string 1)))
(goto-char (point-min))
(when (re-search-forward "^#\\+hugo_draft: *\\(true\\|false\\)" nil t)
(setq draft (string= (match-string 1) "true")
is-hugo t))
(when is-hugo
(list :title (or title (file-name-base file)) :draft draft)))))
(defun cj/hugo--collect-drafts (dir)
"Return alist of (TITLE . FILEPATH) for draft Hugo posts under DIR.
Walks non-recursively through DIR for .org files and keeps only those
whose `cj/hugo--post-metadata' returns a :draft-t plist."
(let (drafts)
(dolist (f (directory-files dir t "\\.org\\'"))
(let ((meta (cj/hugo--post-metadata f)))
(when (and meta (plist-get meta :draft))
(push (cons (plist-get meta :title) f) drafts))))
drafts))
(defun cj/hugo-toggle-draft ()
"Toggle the draft status of the current Hugo post.
Switches #+hugo_draft between true and false."
(interactive)
(save-excursion
(goto-char (point-min))
(if (re-search-forward "^#\\+hugo_draft: *\\(true\\|false\\)" nil t)
(let ((current (match-string 1)))
(replace-match (if (string= current "true") "false" "true") t t nil 1)
(save-buffer)
(message "Draft: %s → %s" current
(if (string= current "true") "false" "true")))
(user-error "No #+hugo_draft keyword found in this file"))))
(defun cj/hugo-open-draft ()
"Pick a draft post via completing-read and open it."
(interactive)
(let ((drafts (cj/hugo--collect-drafts cj/hugo-content-org-dir)))
(if (null drafts)
(message "No drafts found in %s" cj/hugo-content-org-dir)
(let ((choice (completing-read "Open draft: "
(mapcar #'car drafts) nil t)))
(find-file (cdr (assoc choice drafts)))))))
;; ---------------------------- Preview and Publish ----------------------------
(defvar cj/hugo--preview-process nil
"Handle to the running hugo preview server, or nil.")
(defun cj/hugo--preview-filter (proc output)
"Process filter for the hugo preview server.
Appends OUTPUT to PROC's buffer and opens the browser the first time
Hugo reports the server is ready. Hugo prints a line containing
\"Web Server is available at\" once it has bound the port, so waiting
for that string is more reliable than a fixed delay."
(when (buffer-live-p (process-buffer proc))
(with-current-buffer (process-buffer proc)
(let ((moving (= (point) (process-mark proc))))
(save-excursion
(goto-char (process-mark proc))
(insert output)
(set-marker (process-mark proc) (point)))
(when moving (goto-char (process-mark proc))))))
(when (and (process-live-p proc)
(string-match-p "Web Server is available at" output))
(browse-url "http://localhost:1313/")
(set-process-filter proc nil)))
(defun cj/hugo--preview-sentinel (proc _event)
"Sentinel for the hugo preview server.
Clears `cj/hugo--preview-process' on any exit and announces crashes.
User-initiated stops arrive with status `signal' and are silent here
because `cj/hugo-preview' already prints its own stop message."
(when (memq (process-status proc) '(exit signal))
(setq cj/hugo--preview-process nil)
(when (and (eq (process-status proc) 'exit)
(not (zerop (process-exit-status proc))))
(message "hugo server crashed (exit %d) — see *hugo-server* buffer"
(process-exit-status proc)))))
(defun cj/hugo-preview ()
"Toggle the `hugo server' preview.
Start the server and open the browser if stopped; stop it if running.
The browser opens only once Hugo has finished its initial build and is
actually listening on the port. If Hugo exits on its own (for example
a template error), the sentinel reports the failure."
(interactive)
(if (process-live-p cj/hugo--preview-process)
(progn
(kill-process cj/hugo--preview-process)
(setq cj/hugo--preview-process nil)
(message "hugo server stopped"))
(let ((default-directory website-dir))
(setq cj/hugo--preview-process
(start-process "hugo-server" "*hugo-server*"
"hugo" "server" "-D"
"--noHTTPCache" "--disableFastRender"))
(set-process-filter cj/hugo--preview-process
#'cj/hugo--preview-filter)
(set-process-sentinel cj/hugo--preview-process
#'cj/hugo--preview-sentinel)
(message "hugo server starting — browser will open when ready"))))
(declare-function magit-status-setup-buffer "magit-status")
(defun cj/hugo-publish ()
"Open magit-status on the website repo so a push triggers server-side deploy.
The cjennings.net bare repo's post-receive hook rebuilds Hugo and writes
to /var/www/cjennings/, so a successful push is the deploy."
(interactive)
(magit-status-setup-buffer website-dir))
;; -------------------------------- Keybindings --------------------------------
(global-set-key (kbd "C-; h n") #'cj/hugo-new-post)
(global-set-key (kbd "C-; h e") #'cj/hugo-export-post)
(global-set-key (kbd "C-; h o") #'cj/hugo-open-blog-dir)
(global-set-key (kbd "C-; h O") #'cj/hugo-open-blog-dir-external)
(global-set-key (kbd "C-; h d") #'cj/hugo-open-draft)
(global-set-key (kbd "C-; h D") #'cj/hugo-toggle-draft)
(global-set-key (kbd "C-; h p") #'cj/hugo-preview)
(global-set-key (kbd "C-; h P") #'cj/hugo-publish)
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-; h" "hugo blog menu"
"C-; h n" "new post"
"C-; h e" "export post"
"C-; h o" "open in dirvish"
"C-; h O" "open in file manager"
"C-; h d" "open draft"
"C-; h D" "toggle draft"
"C-; h p" "preview (toggle)"
"C-; h P" "publish (magit push)"))
(provide 'hugo-config)
;;; hugo-config.el ends here
|