Skip to content

Instantly share code, notes, and snippets.

@martinklepsch
Last active February 21, 2026 04:10
Show Gist options
  • Select an option

  • Save martinklepsch/b534f6be88cd48bf9aad4076dc2ccbfa to your computer and use it in GitHub Desktop.

Select an option

Save martinklepsch/b534f6be88cd48bf9aad4076dc2ccbfa to your computer and use it in GitHub Desktop.
#!/usr/bin/env bb
(require '[babashka.cli :as cli]
'[babashka.fs :as fs]
'[clojure.string :as str]
'[clj-yaml.core :as yaml])
(def cli-opts
{:spec {:files {:desc "Individual markdown files to process"
:coerce []}
:pattern {:desc "Regex pattern to match markdown files (e.g. \"posts/*.md\")"
:alias :p}
:dry-run {:desc "Print what would be changed without making changes"
:coerce :boolean}
:verbose {:desc "Print additional information during processing"
:coerce :boolean}}})
(defn extract-frontmatter
"Extracts YAML frontmatter from markdown content.
Returns [frontmatter remaining-content] or nil if no frontmatter found."
[content]
(when (str/starts-with? content "---\n")
(when-let [end-idx (str/index-of content "\n---\n" 4)]
(let [frontmatter (subs content 4 end-idx)
remaining (subs content (+ end-idx 5))]
[frontmatter remaining]))))
(defn update-frontmatter
"Updates the frontmatter by adding type: post if not present"
[markdown-str]
(if-let [[frontmatter content] (extract-frontmatter markdown-str)]
(let [data (yaml/parse-string frontmatter)
updated-data (cond-> data
(not (:type data)) (assoc :type "post"))
new-frontmatter (yaml/generate-string updated-data :dumper-options {:flow-style :block})]
(str "---\n" new-frontmatter "---\n" content))
markdown-str))
(defn process-file
"Process a single markdown file, updating its frontmatter"
[file {:keys [dry-run verbose]}]
(let [content (slurp file)
updated-content (update-frontmatter content)]
(when verbose
(println "πŸ“ Processing" (str file)))
(if (= content updated-content)
(when verbose
(println "⏭️ No changes needed for" (str file)))
(do
(when verbose
(println "πŸ”„ Updating frontmatter in" (str file)))
(when-not dry-run
(spit file updated-content))))))
(defn process-files
"Process multiple markdown files based on CLI options"
[{:keys [files pattern] :as opts}]
(let [pattern-files (when pattern
(->> (fs/glob "." pattern)
(map fs/file)
(filter #(str/ends-with? (str %) ".md"))))
all-files (concat (map fs/file files) pattern-files)]
(if (seq all-files)
(do
(when (:verbose opts)
(println "πŸ” Found" (count all-files) "files to process"))
(doseq [file all-files]
(process-file file opts))
(println "✨ Processing complete!"))
(println "⚠️ No markdown files found to process"))))
(defn -main [& args]
(let [opts (cli/parse-opts args cli-opts)]
(if (:help opts)
(println (cli/format-opts cli-opts))
(process-files opts))))
(when (= *file* (System/getProperty "babashka.file"))
(apply -main *command-line-args*))
@martinklepsch

martinklepsch commented Feb 5, 2025

Copy link
Copy Markdown
Author

Another example that downloads files based on frontmatter, straight out of Claude:

#!/usr/bin/env bb

(require '[babashka.fs :as fs]
         '[clj-yaml.core :as yaml]
         '[babashka.http-client :as http]
         '[clojure.string :as str])

(def base-url "https://martinklepsch.org/")

(defn extract-frontmatter
  "Extracts YAML frontmatter from markdown content.
   Returns [frontmatter remaining-content] or nil if no frontmatter found."
  [content]
  (when (str/starts-with? content "---\n")
    (when-let [end-idx (str/index-of content "\n---\n" 4)]
      (let [frontmatter (subs content 4 end-idx)
            remaining (subs content (+ end-idx 5))]
        [frontmatter remaining]))))

(defn backup-post
  "Downloads a post and saves it to the backup directory"
  [permalink backup-dir]
  (let [url (str base-url (str/replace-first permalink #"^/" ""))
        filename (str backup-dir "/" (str/replace permalink #"/" "_") ".html")]
    (println "πŸ“₯ Downloading:" url)
    (try
      (let [response (http/get url)]
        (if (= 200 (:status response))
          (do
            (fs/create-dirs (fs/parent filename))
            (spit filename (:body response))
            (println "βœ… Saved to:" filename))
          (println "⚠️ Failed to download - Status:" (:status response))))
      (catch Exception e
        (println "❌ Error downloading:" (.getMessage e))))))

(defn process-markdown-file
  "Process a single markdown file and backup its content"
  [file backup-dir]
  (println "\nProcessing:" (str file))
  (let [content (slurp file)
        [frontmatter _] (extract-frontmatter content)]
    (if-let [data (yaml/parse-string frontmatter)]
      (if-let [permalink (:permalink data)]
        (backup-post permalink backup-dir)
        (println "⚠️ No permalink found in:" (str file)))
      (println "⚠️ No valid frontmatter found in:" (str file)))))

(defn -main [& args]
  (let [posts-dir (or (first args) "content/posts")
        backup-dir (or (second args) "backup")]
    
    (println "πŸ” Looking for markdown files in:" posts-dir)
    
    (if (fs/exists? posts-dir)
      (let [markdown-files (->> (fs/glob posts-dir "**.md")
                               (map fs/file))]
        (if (seq markdown-files)
          (do
            (println "πŸ“ Creating backup directory:" backup-dir)
            (fs/create-dirs backup-dir)
            (doseq [file markdown-files]
              (process-markdown-file file backup-dir))
            (println "\nβœ… Backup complete!"))
          (println "⚠️ No markdown files found in:" posts-dir)))
      (println "❌ Posts directory not found:" posts-dir))))

(when (= *file* (System/getProperty "babashka.file"))
  (-main *command-line-args*))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment