Compare commits

...
5 Commits
10 changed files with 206 additions and 77 deletions

No files matched your search

+1
View File
@@ -5,3 +5,4 @@ build/*
\*Claude*
/resources/static/output.css
/node_modules/
/org/output/
+14 -4
View File
@@ -1,11 +1,21 @@
.PHONY: tailwind build publish
.PHONY: watch clean tailwind static build publish
clean:
rm -rf build/*
tailwind:
npx @tailwindcss/cli -i ./resources/input.css -o ./resources/static/output.css
build: tailwind
rsync -av --delete resources/static/ build/static/
static:
rsync -av resources/static/ build/static/
build:
clj -M:build
publish: build
org: ./org/*
emacs -Q --script build-org.el 2>&1
watch: tailwind static build
publish: build-org tailwind static build
rsync -av --delete build/ joe-vps:/home/josephferano/websites/ferano.io/
+45
View File
@@ -0,0 +1,45 @@
#+title: ferano.io
#+options: toc:nil
Static portfolio site. No framework — just Clojure generating HTML.
* Stack
- *Clojure + Hiccup* — generates all HTML in ~src/ferano_io/main.clj~
- *EDN* — all site data (projects, games, experiments, links) lives in ~resources/data.edn~
- *Tailwind CSS* — compiled from ~resources/input.css~ to ~build/static/output.css~
- *Org-mode* — blog posts written in ~org/~, compiled to HTML fragments by Emacs via ~build-org.el~
* Build
#+begin_src sh
make watch # tailwind + static assets + clj build (local dev)
make publish # full build + rsync to VPS
#+end_src
Individual targets:
| ~make build~ | ~clj -M:build~ — generates ~build/index.html~ and ~build/blog/*.html~ |
| ~make org~ | runs Emacs headless to compile ~org/*.org~ → ~org/output/*.html~ |
| ~make tailwind~ | compiles Tailwind CSS |
| ~make static~ | rsyncs ~resources/static/~ → ~build/static/~ |
* Dev (nREPL)
#+begin_src sh
clj -M:dev
#+end_src
Starts a CIDER-compatible nREPL. Sets ~-Ddev=true~ so the page loads Tailwind from CDN instead of the compiled file.
* Blog
Write ~org/YYYY-MM-DD-slug.org~ files. ~make org~ (or ~make build~, which calls it) converts them to body-only HTML fragments, then ~build-website~ wraps each one in the site template and writes it to ~build/blog/~.
* Publish
#+begin_src sh
make publish
#+end_src
Rsyncs ~build/~ to ~joe-vps:/home/josephferano/websites/ferano.io/~.
+35
View File
@@ -0,0 +1,35 @@
;; Load the publishing system
(require 'ox-publish)
(setq org-publish-project-alist
'(("org-files"
:recursive t
:base-directory "./org"
:publishing-directory "./org/output/"
:publishing-function org-html-publish-to-html
:body-only t
:title "Joseph Ferano - Blog"
;; :with-author nil ;; Don't include author name
;; :with-creator nil ;; Include Emacs and Org versions in footer
;; :with-toc nil ;; Include a table of contents
;; :with-title nil
;; :with-validation nil
;; :section-numbers nil ;; Don't include section numbers
;; :time-stamp-file nil
)
("static-files"
:base-directory "./content/"
:base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|svg"
:publishing-directory "./public/"
:recursive t
:publishing-function org-publish-attachment)
("site" :components ("org-files" "static-files"))))
(setq org-html-validation-link nil
org-html-head-include-default-style nil)
;; org-html-head "<link rel=\"stylesheet\" href=\"css/normalize.css\" /><link rel=\"stylesheet\" href=\"css/main.css\" />")
;; (org-publish-project "site" t)
(org-publish-project "org-files" t)
(message "Build complete!")
+2 -2
View File
@@ -1,7 +1,7 @@
{:deps {org.clojure/clojure {:mvn/version "1.11.1"}
hiccup/hiccup {:mvn/version "1.0.5"}}
:aliases {:build {:main-opts ["-m" "ferano-io.core"]}
:aliases {:build {:main-opts ["-m" "ferano-io.main"]}
:dev {:extra-deps {nrepl/nrepl {:mvn/version "1.0.0"}
cider/cider-nrepl {:mvn/version "0.28.5"}}
:jvm-opts ["-Ddev=true"]
:main-opts ["-e" "(require 'ferano-io.core) (in-ns 'ferano-io.core)" "-r"]}}}
:main-opts ["-e" "(require 'ferano-io.main) (in-ns 'ferano-io.main)" "-r"]}}}
-24
View File
@@ -1,24 +0,0 @@
#!/bin/sh
# Wait for DevTools to start listening on websocket
sleep 1
BRAVE_PORT=9222
brave-browser --new-window --user-data-dir=/tmp/brave-dev --remote-debugging-port=${BRAVE_PORT} &
BRAVE_PID=$!
echo "Wait 2 seconds, then go..."
sleep 2
TAB_ID=$(curl -s http://localhost:${BRAVE_PORT}/json | jq -r '.[] | .id')
echo "Got Tab ID: $TAB_ID"
HTTP_PORT=$(grep "python -m http.server 3000" watch.sh | grep -o -e "[[:digit:]]\+")
echo "Found Jetty Port: $HTTP_PORT"
JS_NAV=$(cat <<EOF
{"id":1,"method":"Page.navigate","params":{"url":"http://localhost:${HTTP_PORT}"}}
EOF
)
echo $TAB_ID > .tab-id
echo "Open http://localhost:${HTTP_PORT}"
echo $JS_NAV | websocat ws://localhost:9222/devtools/page/${TAB_ID} > /dev/null
+8
View File
@@ -0,0 +1,8 @@
#+TITLE: First Post
#+AUTHOR: Joseph Ferano
#+DATE: <2025-07-19 Sat>
#+OPTIONS:
This is my first blog post. What is the purpose of my blog. Well, I want to
discuss technical things, for sure. Richard Feynman said things that are cool.
Something about how writing is learning.
+2 -2
View File
@@ -9,13 +9,13 @@
:bio "Brief description of yourself and what you do."}
:projects [{:name "pmme"
:description "Done IoT air quality monitoring ecosystem with ESP32 sensors, cloud backend, mobile app, and web dashboard for real-time PM2.5 tracking"
:description "IoT air quality monitoring ecosystem with ESP32 sensors, cloud backend, mobile app, and web dashboard for real-time PM2.5 tracking"
:url "https://git.ferano.io/JosephFerano/pmme"
:technologies ["Rust" "ESP32" "ClojureScript" "Tauri" "Rocket" "PostgreSQL"]
:status "Active"
:year 2025}
{:name "8086 CPU Simulator"
:description "Done 8086 processor emulator with instruction decoding, cycle-accurate execution, and comprehensive testing suite"
:description "8086 processor emulator with instruction decoding, cycle-accurate execution, and comprehensive testing suite"
:url "https://git.ferano.io/JosephFerano/performance-aware"
:technologies ["Odin" "Assembly" "x86"]
:status "Done"
@@ -1,5 +1,8 @@
(ns ferano-io.core
(:require [hiccup.core :as h]
(ns ferano-io.main
(:require [clojure.string :as str]
[clojure.java.io :as io]
[clojure.java.shell :refer [sh]]
[hiccup.core :as h]
[hiccup.page :as page]
[clojure.edn :as edn]))
@@ -33,12 +36,34 @@
[:p {:class "text-base md:text-lg opacity-90"}
"Building engaging experiences and robust systems across gaming, graphics, blockchain, and web technologies. Multilingual programmer with a diverse technical toolkit for solving complex problems."]]]]])
(defn project-card [proj]
[:div {:class "bg-white border border-gray-200 p-5 hover:shadow-md transition-shadow"}
[:div {:class "flex justify-between items-start mb-3"}
[:h3 {:class "font-medium text-lg"} (:name proj)]
[:span {:class "text-xs text-gray-500"} (:year proj)]]
[:p {:class "text-sm text-gray-600 mb-4 line-clamp-3"} (:description proj)]
[:div {:class "flex flex-wrap gap-1 mb-4"}
(for [tech (:technologies proj)]
[:span {:class "text-xs px-2 py-1 bg-gray-100 rounded"} tech])]
[:div {:class "flex justify-between items-center"}
[:div {:class "flex gap-3"}
[:a {:href (:url proj) :target "_blank" :class "text-xs text-blue-600 hover:underline"} "Source"]
(when (:demo proj)
[:a {:href (:demo proj) :target "_blank" :class "text-xs text-blue-600 hover:underline"} "Demo"])]
[:span {:class "text-xs px-2 py-1 bg-gray-100 rounded"} (:status proj)]]])
(defn projects-grid [data]
[:section {:id "projects" :class "py-16"}
[:h2 {:class "text-lg tracking-wide mb-8 text-center"} "PROJECTS"]
[:div {:class "max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"}
(map project-card (:projects data))]])
(defn game-card [game]
[:div {:class "bg-white border border-gray-200 p-5 hover:shadow-md transition-shadow"}
[:div {:class "w-full mb-4"}
[:img {:src (:image game)
:alt (:name game)
:class "w-full h-48 object-cover"}]]
:class "w-full h-60 object-cover"}]]
[:div {:class "flex justify-between items-start mb-3"}
[:h3 {:class "font-medium text-lg"} (:name game)]]
[:p {:class "text-sm text-gray-600 mb-4 line-clamp-3"} (:description game)]
@@ -58,25 +83,23 @@
[:div {:class "max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-6"}
(map game-card (:games data))]])
(defn projects-grid [data]
[:section {:id "projects" :class "py-16"}
[:h2 {:class "text-lg tracking-wide mb-8 text-center"} "PROJECTS"]
(defn experiment [expt]
[:div {:class "bg-white border border-gray-200 p-5 hover:shadow-md transition-shadow"}
[:div {:class "flex justify-between items-start mb-3"}
[:h3 {:class "font-medium text-lg"} (:name expt)]
[:img {:src "/static/images/slide-03_resized.jpg" :alt "Testing"}]]
[:p {:class "text-sm text-gray-600 mb-4 line-clamp-3"} (:description expt)]
[:div {:class "flex justify-between items-center"}
[:div {:class "flex gap-3"}
[:a {:href (:url expt) :target "_blank" :class "text-xs text-blue-600 hover:underline"} "Source"]
(when (:demo expt)
[:a {:href (:demo expt) :target "_blank" :class "text-xs text-blue-600 hover:underline"} "Demo"])]]])
(defn experiments [data]
[:section {:id "experiments" :class "py-16"}
[:h2 {:class "text-lg tracking-wide mb-8 text-center"} "EXPERIMENTS"]
[:div {:class "max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"}
(for [project (:projects data)]
[:div {:class "bg-white border border-gray-200 p-5 hover:shadow-md transition-shadow"}
[:div {:class "flex justify-between items-start mb-3"}
[:h3 {:class "font-medium text-lg"} (:name project)]
[:span {:class "text-xs text-gray-500"} (:year project)]]
[:p {:class "text-sm text-gray-600 mb-4 line-clamp-3"} (:description project)]
[:div {:class "flex flex-wrap gap-1 mb-4"}
(for [tech (:technologies project)]
[:span {:class "text-xs px-2 py-1 bg-gray-100 rounded"} tech])]
[:div {:class "flex justify-between items-center"}
[:div {:class "flex gap-3"}
[:a {:href (:url project) :target "_blank" :class "text-xs text-blue-600 hover:underline"} "Source"]
(when (:demo project)
[:a {:href (:demo project) :target "_blank" :class "text-xs text-blue-600 hover:underline"} "Demo"])]
[:span {:class "text-xs px-2 py-1 bg-gray-100 rounded"} (:status project)]]])]])
(map experiment (:experiments data))]])
(defn contact [data]
[:section {:id "contact" :class "py-16 bg-gray-100"}
@@ -101,7 +124,13 @@
:class "flex items-center gap-2 text-gray-700 hover:text-blue-600 transition-colors"}
[:span "🐙"] [:span "GitHub"]]]]])
(defn main-page [data]
(defn blog-summary [data]
[:section {:id "experiments" :class "py-16"}
[:h2 {:class "text-lg tracking-wide mb-8 text-center"} "EXPERIMENTS"]
[:div {:class "max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"}
(map experiment (:experiments data))]])
(defn page-template [data & body]
(page/html5
[:head
[:meta {:charset "utf-8"}]
@@ -115,14 +144,30 @@
[:body
[:div {:class "min-h-screen bg-gray-50"}
(header data)
(hero data)
(projects-grid data)
(games data)
body
(contact data)]]))
(defn -main [& args]
(let [data (-> "resources/data.edn"
slurp
edn/read-string)]
(defn main-page [data]
(page-template
data
(let [fns [hero projects-grid games blog-summary]]
(map #(% data) fns))))
(defn blog-page [data blog-entry-path]
(let [path (str "org/output/" blog-entry-path)]
(page-template data (slurp path))))
(defn build-website []
(println (:out (sh "make" "org")))
(let [data (edn/read-string (slurp "resources/data.edn"))
blog-entries (.list (io/file "org/output"))
data (assoc data :blog-entries blog-entries)]
(spit "build/index.html" (main-page data))
(println "Generated build/index.html")))
(println "Generated index.html")
(doseq [entry blog-entries]
(spit (str "build/blog/" entry) (blog-page data entry)))))
(defn -main [& args]
(build-website)
(println "Generated website!")
(shutdown-agents))
+24 -15
View File
@@ -1,34 +1,43 @@
#!/bin/sh
python -m http.server 3000 --directory build &
BRAVE_PORT=9222
HTTP_PORT=${1:-3000}
brave-browser --new-window --user-data-dir=/tmp/brave-dev --remote-debugging-port=${BRAVE_PORT} &
BRAVE_PID=$!
echo "Wait 1 seconds, then go..."
sleep 1
TAB_ID=$(curl -s http://localhost:${BRAVE_PORT}/json | jq -r '.[] | .id')
echo "Got Tab ID: $TAB_ID"
JS_NAV=$(cat <<EOF
{"id":1,"method":"Page.navigate","params":{"url":"http://localhost:${HTTP_PORT}"}}
EOF
)
echo $TAB_ID > .tab-id
echo "Open http://localhost:${HTTP_PORT}"
echo $JS_NAV | websocat ws://localhost:${BRAVE_PORT}/devtools/page/${TAB_ID} > /dev/null
python -m http.server $HTTP_PORT --directory build &
PID_HTTP=$!
sleep 2
sleep 1
if ! kill -0 $PID_HTTP 2>/dev/null; then
echo "Failed to start HTTP server"
exit 1
fi
# When using the -d flag, entr will exist, so just keep looping with a function to get it to relaunch
watch_static() {
while true; do
echo resources/static/ | entr -dp rsync -av --delete resources/static/ build/static/
sleep 0.5
done
}
watch_static &
PID_WATCH_STATIC=$!
cleanup() {
echo "Shutting down..."
kill $PID_HTTP 2>/dev/null
kill $PID_WATCH_STATIC 2>/dev/null
kill $BRAVE_PID 2>/dev/null
exit 0
}
trap cleanup SIGINT SIGTERM
find src/ resources/ -name "*.clj" -o -name "*.edn" | entr -r clj -M:build
find src/ resources/ -name "*.clj" -o -name "*.edn" | entr -r make -j3 watch
cleanup