Compare commits
3 Commits
4f0546cfd5
...
9f4b04da7d
Author | SHA1 | Date | |
---|---|---|---|
9f4b04da7d | |||
cc24785756 | |||
20ae7df57d |
18
Makefile
18
Makefile
@ -1,11 +1,21 @@
|
|||||||
.PHONY: tailwind build publish
|
.PHONY: watch clean tailwind static build publish
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build/*
|
||||||
|
|
||||||
tailwind:
|
tailwind:
|
||||||
npx @tailwindcss/cli -i ./resources/input.css -o ./resources/static/output.css
|
npx @tailwindcss/cli -i ./resources/input.css -o ./resources/static/output.css
|
||||||
|
|
||||||
build: tailwind
|
static:
|
||||||
rsync -av --delete resources/static/ build/static/
|
rsync -av resources/static/ build/static/
|
||||||
|
|
||||||
|
build:
|
||||||
clj -M:build
|
clj -M:build
|
||||||
|
|
||||||
publish: build
|
org: ./org/*
|
||||||
|
emacs -Q --script build-org.el
|
||||||
|
|
||||||
|
watch: tailwind static build
|
||||||
|
|
||||||
|
publish: build-org tailwind static build
|
||||||
rsync -av --delete build/ joe-vps:/home/josephferano/websites/ferano.io/
|
rsync -av --delete build/ joe-vps:/home/josephferano/websites/ferano.io/
|
||||||
|
34
build-org.el
Normal file
34
build-org.el
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
;; Load the publishing system
|
||||||
|
(require 'ox-publish)
|
||||||
|
|
||||||
|
(setq org-publish-project-alist
|
||||||
|
'(("org-files"
|
||||||
|
:recursive t
|
||||||
|
:base-directory "./org"
|
||||||
|
:publishing-directory "./build/blog/"
|
||||||
|
:publishing-function org-html-publish-to-html
|
||||||
|
:body-only t
|
||||||
|
: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
|
||||||
|
:title "Joseph Ferano - Blog"
|
||||||
|
: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!")
|
4
deps.edn
4
deps.edn
@ -1,7 +1,7 @@
|
|||||||
{:deps {org.clojure/clojure {:mvn/version "1.11.1"}
|
{:deps {org.clojure/clojure {:mvn/version "1.11.1"}
|
||||||
hiccup/hiccup {:mvn/version "1.0.5"}}
|
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"}
|
:dev {:extra-deps {nrepl/nrepl {:mvn/version "1.0.0"}
|
||||||
cider/cider-nrepl {:mvn/version "0.28.5"}}
|
cider/cider-nrepl {:mvn/version "0.28.5"}}
|
||||||
:jvm-opts ["-Ddev=true"]
|
: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"]}}}
|
||||||
|
@ -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
org/2025-07-19-first-entry.org
Normal file
8
org/2025-07-19-first-entry.org
Normal 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.
|
@ -9,13 +9,13 @@
|
|||||||
:bio "Brief description of yourself and what you do."}
|
:bio "Brief description of yourself and what you do."}
|
||||||
|
|
||||||
:projects [{:name "pmme"
|
: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"
|
:url "https://git.ferano.io/JosephFerano/pmme"
|
||||||
:technologies ["Rust" "ESP32" "ClojureScript" "Tauri" "Rocket" "PostgreSQL"]
|
:technologies ["Rust" "ESP32" "ClojureScript" "Tauri" "Rocket" "PostgreSQL"]
|
||||||
:status "Active"
|
:status "Active"
|
||||||
:year 2025}
|
:year 2025}
|
||||||
{:name "8086 CPU Simulator"
|
{: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"
|
:url "https://git.ferano.io/JosephFerano/performance-aware"
|
||||||
:technologies ["Odin" "Assembly" "x86"]
|
:technologies ["Odin" "Assembly" "x86"]
|
||||||
:status "Done"
|
:status "Done"
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
(ns ferano-io.core
|
(ns ferano-io.main
|
||||||
(:require [hiccup.core :as h]
|
(:require [clojure.string :as str]
|
||||||
|
[clojure.java.io :as io]
|
||||||
|
[hiccup.core :as h]
|
||||||
[hiccup.page :as page]
|
[hiccup.page :as page]
|
||||||
[clojure.edn :as edn]))
|
[clojure.edn :as edn]))
|
||||||
|
|
||||||
@ -33,12 +35,34 @@
|
|||||||
[:p {:class "text-base md:text-lg opacity-90"}
|
[: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."]]]]])
|
"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]
|
(defn game-card [game]
|
||||||
[:div {:class "bg-white border border-gray-200 p-5 hover:shadow-md transition-shadow"}
|
[:div {:class "bg-white border border-gray-200 p-5 hover:shadow-md transition-shadow"}
|
||||||
[:div {:class "w-full mb-4"}
|
[:div {:class "w-full mb-4"}
|
||||||
[:img {:src (:image game)
|
[:img {:src (:image game)
|
||||||
:alt (:name 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"}
|
[:div {:class "flex justify-between items-start mb-3"}
|
||||||
[:h3 {:class "font-medium text-lg"} (:name game)]]
|
[:h3 {:class "font-medium text-lg"} (:name game)]]
|
||||||
[:p {:class "text-sm text-gray-600 mb-4 line-clamp-3"} (:description game)]
|
[:p {:class "text-sm text-gray-600 mb-4 line-clamp-3"} (:description game)]
|
||||||
@ -58,25 +82,23 @@
|
|||||||
[:div {:class "max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-6"}
|
[:div {:class "max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-6"}
|
||||||
(map game-card (:games data))]])
|
(map game-card (:games data))]])
|
||||||
|
|
||||||
(defn projects-grid [data]
|
(defn experiment [expt]
|
||||||
[:section {:id "projects" :class "py-16"}
|
[:div {:class "bg-white border border-gray-200 p-5 hover:shadow-md transition-shadow"}
|
||||||
[:h2 {:class "text-lg tracking-wide mb-8 text-center"} "PROJECTS"]
|
[: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"}
|
[: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)]
|
(map experiment (:experiments 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)]]])]])
|
|
||||||
|
|
||||||
(defn contact [data]
|
(defn contact [data]
|
||||||
[:section {:id "contact" :class "py-16 bg-gray-100"}
|
[:section {:id "contact" :class "py-16 bg-gray-100"}
|
||||||
@ -101,7 +123,13 @@
|
|||||||
:class "flex items-center gap-2 text-gray-700 hover:text-blue-600 transition-colors"}
|
:class "flex items-center gap-2 text-gray-700 hover:text-blue-600 transition-colors"}
|
||||||
[:span "🐙"] [:span "GitHub"]]]]])
|
[: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
|
(page/html5
|
||||||
[:head
|
[:head
|
||||||
[:meta {:charset "utf-8"}]
|
[:meta {:charset "utf-8"}]
|
||||||
@ -114,15 +142,29 @@
|
|||||||
[:style ".menu-toggle:checked + label + .mobile-menu { opacity: 1; visibility: visible; transform: translateY(0); } .menu-toggle { display: none; } .mobile-menu { opacity: 0; visibility: hidden; transform: translateY(-10px); transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease; } .mobile-menu a { display: block; }"]]
|
[:style ".menu-toggle:checked + label + .mobile-menu { opacity: 1; visibility: visible; transform: translateY(0); } .menu-toggle { display: none; } .mobile-menu { opacity: 0; visibility: hidden; transform: translateY(-10px); transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease; } .mobile-menu a { display: block; }"]]
|
||||||
[:body
|
[:body
|
||||||
[:div {:class "min-h-screen bg-gray-50"}
|
[:div {:class "min-h-screen bg-gray-50"}
|
||||||
(header data)
|
body
|
||||||
(hero data)
|
|
||||||
(projects-grid data)
|
|
||||||
(games data)
|
|
||||||
(contact data)]]))
|
(contact data)]]))
|
||||||
|
|
||||||
(defn -main [& args]
|
(defn main-page [data]
|
||||||
(let [data (-> "resources/data.edn"
|
(page-template
|
||||||
slurp
|
data
|
||||||
edn/read-string)]
|
(let [fns [header hero projects-grid games blog-summary]]
|
||||||
|
(map #(% data) fns))))
|
||||||
|
|
||||||
|
(defn blog-page [data blog-entry-path]
|
||||||
|
(let [path (str "org/" blog-entry-path)]
|
||||||
|
(println path)
|
||||||
|
(page-template data (slurp path))))
|
||||||
|
|
||||||
|
(defn build-website []
|
||||||
|
(let [data (edn/read-string (slurp "resources/data.edn"))
|
||||||
|
blog-entries (.list (io/file "org/"))
|
||||||
|
data (assoc data :blog-entries blog-entries)]
|
||||||
(spit "build/index.html" (main-page data))
|
(spit "build/index.html" (main-page data))
|
||||||
(println "Generated build/index.html")))
|
(doseq [entry blog-entries]
|
||||||
|
(spit (str "build/blog/" (str/replace entry #"\.org$" ".html"))
|
||||||
|
(blog-page data entry)))))
|
||||||
|
|
||||||
|
(defn -main [& args]
|
||||||
|
(build-website)
|
||||||
|
(println "Generated website!"))
|
39
watch.sh
39
watch.sh
@ -1,34 +1,43 @@
|
|||||||
#!/bin/sh
|
#!/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=$!
|
PID_HTTP=$!
|
||||||
|
|
||||||
sleep 2
|
sleep 1
|
||||||
if ! kill -0 $PID_HTTP 2>/dev/null; then
|
if ! kill -0 $PID_HTTP 2>/dev/null; then
|
||||||
echo "Failed to start HTTP server"
|
echo "Failed to start HTTP server"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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() {
|
cleanup() {
|
||||||
echo "Shutting down..."
|
echo "Shutting down..."
|
||||||
kill $PID_HTTP 2>/dev/null
|
kill $PID_HTTP 2>/dev/null
|
||||||
kill $PID_WATCH_STATIC 2>/dev/null
|
kill $BRAVE_PID 2>/dev/null
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
trap cleanup SIGINT SIGTERM
|
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
|
cleanup
|
||||||
|
Loading…
x
Reference in New Issue
Block a user