From 37343cc01833d960576595efb7bf06134a652e1c Mon Sep 17 00:00:00 2001 From: skydread1 Date: Sun, 12 Oct 2025 14:56:24 +0800 Subject: [PATCH 01/11] Update readme --- README.md | 210 +++++++++++++++++++++++++----------------------------- 1 file changed, 99 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index cab8937..bad9491 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@
- Clojure Version + Clojure Version CI πŸ”· Portfolio SPA in ClojureScript πŸ”· -## πŸ”· Rational +## πŸ”· Rationale This portfolio was implemented with **ClojureScript**. It is a Single Page Application that leverages [reagent](https://github.com/reagent-project/reagent), a minimalistic interface between ClojureScript and **React**. @@ -26,7 +26,11 @@ The markdown content is converted into [hiccup](https://github.com/weavejester/h At compile time, a RSS feed is generated for the clojure blog posts using [clj-rss](https://github.com/yogthos/clj-rss). -The app is deployed on **Netlify** every time a branch is merged to master. +The app is deployed on **Netlify** via GitHub Actions every time a branch is merged to master. Build artifacts (JS bundles, RSS feeds) are generated during CI/CD and are not committed to the repository. + +## πŸ”· Prerequisites + +- [babashka](https://github.com/babashka/babashka) to run the different project tasks. ## πŸ”· Features @@ -37,7 +41,7 @@ The UI supports light/dark mode and is responsive. At the top of each markdown file, you can provide an EDN config map to add additional properties to the post such as an illustrative image, the order on the page, the title in the URL, github and article links etc... When you create a pull request (PR) to master, **Netlify** provides a preview of what the new app version would look like. -For development, starting a clj/cljs REPL with `fighweel` allows you to have the local changes reflected on port 9500 on file save. +For development, starting a clj/cljs REPL with `figwheel` allows you to have the local changes reflected on port 9500 on file save. ## πŸ”· Content @@ -50,18 +54,14 @@ For instance: . β”œβ”€β”€ about β”‚ └── aboutme.md -└── portfolio - β”œβ”€β”€ blog-django.md - β”œβ”€β”€ flybot-website.md - └── props-recommender.md β”œβ”€β”€ blog -β”‚ └── just-tick-lib.md +β”‚ └── some-article.md ``` ### Config Clojure map A markdown file for a post is divided into three parts, separated by +++ demarcations: -```md +``` some config map +++ post summary @@ -83,40 +83,17 @@ Following is an example of clojure map for a post config: This map goes at the top of a markdown file like so: -```md +``` post map here +++ -My project consists in... +My article summary goes here +++ -## Rational - -The goal of my project is... - -## Stack - -To achieve this... +My article content goes here ... ``` I refer to the post summary part as the `vignette` because it can conveniently be used on a home page for instance to just display a recap of the projects. -### Config validation - -To know what params can be provided to the config map, you can have a look at the malli schema in [validation](./src/loicb/common/validation.cljc). You can take inspiration from my own posts in [content](./src/loicb/server/content/). - -## πŸ”· Compile - -At CLJ compile time, the following steps happen: -1. Read all markdown files -2. Validate the post configs against a `Malli` schema -3. Assoc the post markdown content to the configs -4. A macro stores a vector of the posts to be loaded in the re-frame DB - -At CLJS compile time, the following steps happen: -1. A re-frame event initializes the re-frame DB, loading all the posts from the clojure macro and the theme from local storage. -2. The `reitit` route table is created -3. The post markdown are converted to hiccup via `markdown-to-hiccup`. - ## πŸ”· Getting Started ### Clone the repo @@ -125,131 +102,142 @@ Feel free to clone or fork the repository and modify both the code and post cont ### Dev -You can perform ClojureScript jack-in to open the webpage in a browser on port `9500`, alongside an interactive REPL in your IDE. - -You can then edit and save source files to trigger hot reloading in the browser. - -#### Prerequisites +```sh +bb dev +``` -- Delete any `main.js` in the resources folder -- Check if `cljs-out/dev-main.js` is the script source in [index.html](./resources/public/index.html): that is where figwheel will recompile the cljs when you save a file. +This starts a REPL server with the necessary middleware for ClojureScript development. -#### VS Code +Then connect to the REPL server with project type `deps.edn + Figwheel Main` and tick the `:dev` alias. -If you use VS Code, the jack-in is done in 2 steps to be able to start the REPL in VS Code instead of terminal: +### Hot reloading -1. Choose the aliases for the deps and press enter -2. Choose the ClojureScript REPL you want to launch and press enter +Once the REPL started, saving a cljs file will automatically update the js in your localhost. -For 2. since our app is only a frontend app, we don't need to load some clj backend deps. +However, if you change the content of a markdown file, you will need to save the file [md.clj](./src/loicb/server/md.clj) which is where the macro that reads all the markdown files resides. Refresh your browser and the new markdown content will appear. -Jack-in `deps+figwheel`: +## πŸ”· Tests -- Deps: no alias to add so just press enter -- REPL: tick `:dev` and press enter +### Local development tests -This should start a cljs REPL in VS Code and open your browser localhost in port 9500. +```sh +bb test +``` -#### Emacs +This task runs both clj and cljs tests: +- clj tests ensure that all your markdown files respect the malli schema. +- cljs tests ensure that the state (in our re-frame DB) is as expected after user actions (navigation, theme, post interaction etc). -If you use Emacs (or Doom Emacs, or Spacemacs) with CIDER, the CIDER jack-in is done in 3 steps: +### Code formatting -1. `C-u M-x cider-jack-in-clj&cljs` or `C-u M-x cider-jack-in-cljs` -2. By default, emacs use the `cider/nrepl` alias such as in `-M:cider/nrepl`. You need to keep this alias at the end such as `-M:dev:cider/nrepl` -3. Select ClojureScript REPL type: `figwheel-main` -4. Select figwheel-main build: `dev` +Check code formatting: +```sh +bb fmt-check +``` -#### Hot reloading +Fix code formatting: +```sh +bb fmt-fix +``` -Once the REPL started, saving a cljs file will automatically update the js in your localhost. +### Regression cljs tests on save -However, if you change the content of a markdown file, you will need to save the file [md.clj](./src/loicb/server/md.clj) which is where the macro that read all the markdown files resides. Refresh your browser and the new markdown content will appear. +The cljs tests are also run on every save and the results are displayed at http://localhost:9500/figwheel-extra-main/auto-testing -### Prod +### CI tests (headless) -#### Js bundle +In the CI, there is no browser, so we use headless mode: -The GitHub action is triggered when code is pushed. +```sh +bb test-ci +``` -I use [clojure/tools.build](https://github.com/clojure/tools.build) to create tasks related to the build. +## πŸ”· Build Tasks -It runs the `js-bundle` task in the build.clj file as follows: +### Clean build artifacts -``` -clojure -T:build js-bundle +```sh +bb clean ``` -This command compiles the cljs to the optimized js bundle that Netlify will use to generate the preview in the PR. +Removes `target` and `dist` directories. -Note: be sure to not forget to use `main.js` as script source in [index.html](./resources/public/index.html): that is where figwheel will produce the optimize js from your cljs file when you push your update to your online repo. +### JS bundle -#### RSS feed - -A RSS feed of the Clojure blog posts is also generated when code is pushed: - -``` -clojure -T:build rss-feed +```sh +bb build ``` -The RSS **clojure** feed is accessible at the url [blog/rss/clojure-feed.xml](https://www.loicb.dev/blog/rss/clojure-feed.xml) +Builds the optimized ClojureScript bundle and generates `index.html` in `target/public`. -## πŸ”· Tests - -### Clj tests +### RSS feed -The clj macro that loads the markdown files is tested and the test can be run like so: - -```clojure -clj -M:server/test +```sh +bb rss ``` -This test ensure that all your markdown files respect the malli schema. +Generates RSS feed for blog posts tagged with `clojure` in `resources/public/blog/rss/`. -### Cljs tests +The RSS **clojure** feed is accessible at [blog/rss/clojure-feed.xml](https://www.loicb.dev/blog/rss/clojure-feed.xml). -The cljs state management tests can be run like so: +### Production release -```clojure -clj -M:test +```sh +bb dist ``` -These frontend cljs tests ensure that the state (in our re-frame DB) is as expected after user actions (navigation, theme, post interaction etc). +This task runs `bb build` and `bb rss`, then gathers all necessary static files into a `dist/` folder ready for deployment. -### Regression tests on save +### Dependencies -The tests mentioned above are also run on every save and the results are displayed at http://localhost:9500/figwheel-extra-main/auto-testing +Check for outdated dependencies: +```sh +bb outdated +``` -### Cljs tests (headless) - for CI +## πŸ”· CI/CD -In the CI, there is no browser, so we need a specific alias to run the test in headless mode: +The project uses GitHub Actions for continuous integration and deployment. -```clojure -clj -M::test-headless -``` +### For Pull Requests +1. Run tests with `bb test-ci` +2. Run code formatting checks with `bb fmt-check` +3. Build static files with `bb dist` +4. Deploy preview to Netlify with unique URL (`pr-{number}`) -## πŸ”· Continuous Integration +### For Main Branch (Production) +1. Run tests with `bb test-ci` +2. Run code formatting checks with `bb fmt-check` +3. Build static files with `bb dist` +4. Deploy to production Netlify site -Every time you push some code, the following happens: -- run the clj tests -- run the cljs tests in headless mode -- compile the cljs file into the js bundle `main.js` and commit it to the repo. +## πŸ”· Hosting with Netlify -## πŸ”· Continuous Deployment +The website is hosted on Netlify (US servers by default). -**Opening a pull request (PR)** to merge your changes to master, makes `Netlify` create a preview for you to see how the new version of the website would look like. +To enable deployments, configure these secrets in your GitHub repository settings: -**Merging to master** automatically publishes the last version of the website via Netlify. +- `NETLIFY_AUTH_TOKEN`: Personal access token from Netlify +- `NETLIFY_SITE_ID`: Site ID from your Netlify site settings -Note: The markdown files are converted to hiccup via Clojure macros, so they are converted at compile time. +## πŸ”· Implementation Details -## πŸ”· Hosting with Netlify +### Config validation -I use **Netlify** for hosting platform because it is easy to setup and the previews of the new website version on GitHub MR is convenient. +To know what params can be provided to the config map, you can have a look at the malli schema in [validation](./src/loicb/common/validation.cljc). You can take inspiration from my own posts in [content](./src/loicb/server/content/). + +### Compile process -Connecting your repo to Netlify is straight forward and the only configuration we need to update is the `Publish directory`. It should be set to -`resources/public` because that is where our static files reside. +At CLJ compile time, the following steps happen: +1. Read all markdown files +2. Validate the post configs against a `Malli` schema +3. Assoc the post markdown content to the configs +4. A macro stores a vector of the posts to be loaded in the re-frame DB -Note that the server will be hosted in the US by default, and you cannot change the location with the free version. +At CLJS compile time, the following steps happen: +1. A re-frame event initializes the re-frame DB, loading all the posts from the clojure macro and the theme from local storage. +2. The `reitit` route table is created +3. The post markdown are converted to hiccup via `markdown-to-hiccup`. ## πŸ”· Contributing From cd269deceaad0685c31d04c7aa9e9194a47df8f1 Mon Sep 17 00:00:00 2001 From: skydread1 Date: Mon, 13 Oct 2025 12:58:58 +0800 Subject: [PATCH 02/11] Add hibou platform article --- .../server/content/blog/flybot-stats-rama.md | 40 +++- .../server/content/blog/hibou-platform.md | 180 ++++++++++++++++++ 2 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 src/loicb/server/content/blog/hibou-platform.md diff --git a/src/loicb/server/content/blog/flybot-stats-rama.md b/src/loicb/server/content/blog/flybot-stats-rama.md index a18684c..948a6ed 100644 --- a/src/loicb/server/content/blog/flybot-stats-rama.md +++ b/src/loicb/server/content/blog/flybot-stats-rama.md @@ -18,7 +18,7 @@ As a **Clojure**-focused company, we decided to experiment with a new architectu +++ ## Rationale -I am currently working on a project that aims at moving our gaming platform stat aggregation and visualization project to a new stack. +I worked on a POC on a project that aims at moving our gaming platform stat aggregation and visualization project to a new stack. We have an event-driven system that produces lots of events in multiple kafka topics. Our current solution to aggregate stats and visualize them in dashboards is the setup [Apache Druid](https://druid.apache.org/) + [Imply](https://imply.io/). @@ -32,7 +32,7 @@ Rama being in Beta, it was quickly evolving as I was experimenting with it. When I started experimenting with Rama, Rama version was *0.18.0*. -Nathan Marz guided me throughout the different releases and was always very prompt to help me understand Rama. +[Nathan Marz](http://nathanmarz.com/about/) guided me throughout the different releases and was always very prompt to help me understand Rama. A very experienced colleague took care of the deployments in AWS as he is responsible for our servers and we worked together with Nathan to understand the AWS setup that could suit our needs the best. @@ -69,7 +69,7 @@ Therefore, I uberjar the rama module and deployed it on a single-node cluster on ## test a Rama module on a AWS multi-node Cluster -My colleague set up a multi-node Rama cluster on our AWS. We then deployed the telemetry module provided my Rama (to get the cluster UI metrics) amd our own module with success. +My colleague set up a multi-node Rama cluster on our AWS. We then deployed the telemetry module provided my Rama (to get the cluster metrics showing in the UI) and our own module with success. ## Designing an API @@ -92,7 +92,7 @@ We do not want the dashboard app (client) to require any knowledge about Rama de :group-by-keys [:reward-type :__time]} ``` -Actually, we even use the [lasagna-pull](https://github.com/flybot-sg/lasagna-pull) to represent our API as pure clojure data (you can read more about it in [here](http://localhost:9500/blog/lasagna-pull-applied-to-flybot)) +Actually, we even use the [lasagna-pull](https://github.com/flybot-sg/lasagna-pull) to represent our API as pure clojure data (you can read more about it in [here](http://loicb.dev/blog/lasagna-pull-applied-to-flybot)) So to sum up, we use our own API instead of built-in Rama REST API. Our API only uses pull request and represents the API endpoints as one single pullable (via a pattern) EDN data structure. The client only hits the rama-api server so the rama logic is never exposed. The server also takes care of authorization. @@ -114,7 +114,35 @@ We have many kafka topics and we have lots of different kind of metrics we want As Red Planet Labs release new RAMA versions and with the feedback from Nathan Marz, we are able to improve the implementation of our rama app and also improve our AWS setup to have an acceptable performance. It is still work in progress but we are getting there. -## Full-stack web app for the client +## SPA web app for the client + +Instead of having the client being just a local clerk notebook, we switched to a proper SPA using [Replicant](https://github.com/cjohansen/replicant) as rendering library. + +## Dashboards as Rama module + +At first, I was going to use Datomic to store our user dashboards by force of habits and because we already use Datomic for other apps in our EKS cluster. + +However, [Luo Tian](https://github.com/robertluo) mentioned to me: "why not store them with Rama since we are using it to store our analytics anyway, let's commit to it fully". It made total sense and writing the a simple Dashboard Rama module took my colleague just a few days because of how intuitive gets with time. + +## POC complete + +At this point, we were able to show our client some working dashboard builders with only rolled-up data per granularity. + +So we had 2 Rama modules: one for analytics and one for dashboards + +We had a pullable API server with authentication and that encapsulates the rama queries. + +The POC was complete in the sense that we were able to prove ourselves and our clients that we can use Rama for analytics. + +## Limitations + +The main limitations are the following: +- only one big module for `analytics` +- the `analytics` module implementation works for our current client but it is not generic and easy to maintain +- the API pullable data implementation is too coupled with the Rama module: they do not share configs yet + +## Next goal + +The next step is to have a generic `analytics` module builder with clear configs. And we want these configs to be easily reusable for the API/frontend validation schema for the different queries. -Instead of having the client being just a local clerk notebook, I am switching to a full-stack Clojure app so game operators can save their dashboards and also share dashboards with others etc (similar to what they could do with Imply). I already designed full-stack web-app before so it should not take us too long to have something ready. It is work in progress as well. diff --git a/src/loicb/server/content/blog/hibou-platform.md b/src/loicb/server/content/blog/hibou-platform.md new file mode 100644 index 0000000..1bb2f31 --- /dev/null +++ b/src/loicb/server/content/blog/hibou-platform.md @@ -0,0 +1,180 @@ +#:post{:id "hibou-platform" + :page :blog + :home-page? true + :employer "Flybot Pte Ltd" + :date ["2025-10-13"] + :title "Building a Generic Analytics Platform with Hibou" + :css-class "hibou-platform" + :tags ["Clojure" "Rama" "Analytics" "Mono Repo" "Full-Stack"] + :image #:image{:src "https://www.flybot.sg/assets/flybot-logo.png" + :src-dark "https://www.flybot.sg/assets/flybot-logo.png" + :alt "Flybot Logo"}} ++++ +Following our successful POC with Rama for analytics, we evolved the stack into a generic, reusable platform called **Hibou** (French for "owl", known for their excellent eyesight). + +The goal was to transform our client-specific implementation into a **business-agnostic** analytics suite that can serve multiple clients with different data requirements. ++++ +## Context + +This project builds upon the foundation established in our [Gaming Stats Aggregation with Rama](http://loicb.dev/blog/game-stats-rama) POC. + +While the POC proved that Rama could handle our analytics needs, it had limitations: +- One monolithic analytics module +- Implementation tightly coupled to a specific client's needs +- Difficult to maintain and extend +- API and module configurations were not aligned + +The next logical step was to create a **generic analytics platform** that could be configured for different use cases without rewriting core logic. + +## Architecture + +The `hibou` stack is organized as a **mono-repo** composed of multiple components: + +- `hibou-analytics` : A generic Rama module builder for analytics +- `hibou-dashboards` : A pre-built Rama module for dashboard storage +- `hibou-api`: Server providing authentication and Rama interactions via pullable API +- `hibou-ui` : Replicant-based web app for the UI + +Each component is designed to be business-agnostic, allowing different clients to leverage the platform with minimal customization. + +## hibou-analytics: Module Builder + +The core innovation is treating the analytics module as a **configuration-driven** system. + +Instead of writing Rama code for each new client, we provide a `defclient-module` function that generates an entire Rama module from an EDN config file validated by Malli schema. + +The module builder supports: +- **Multiple depot types**: Rama native depots and Kafka external depots +- **Flexible PState storage**: Nested maps for dimensions with arbitrary leaf metrics +- **Microbatch topologies**: Configurable partitioning strategies +- **Query topologies**: Predefined queries that always aggregate by granularity/bucket for efficiency +- **Cross-module joins**: Mirror queries to aggregate data across different modules + +This approach makes the analytics layer truly generic. The same codebase works for any event-driven data source. + +## Configuration as the Source of Truth + +One of the key design decisions was to make **annotated EDN configs** the single source of truth for the entire stack. + +A client repo only needs to provide EDN config files for its modules. These configs are then reused across: + +1. **Module Generation**: The config defines the Rama module structure +2. **API Validation**: The API derives its Malli schemas directly from the configs +3. **UI Form Building**: The frontend generates query forms using the same configs + +This eliminates the need to maintain separate schemas in three different places. When you update your module config, the API validation and UI forms automatically adapt. + +To make this work, we use **annotations**, which are namespaced keywords that provide metadata for the API and UI layers without affecting Rama's interpretation of the config. + +## Dashboards as Data + +Initially, I considered using Datomic to store user dashboards. However, a colleague suggested: "why not store them with Rama since we're already using it for analytics?" + +This made total sense. Commit fully to the platform. + +The `hibou-dashboards` component is a simple Rama module that handles: +- Storing dashboards in a PState +- Processing commands via a depot (save, edit, delete) +- Querying user-specific and public dashboards + +Having dashboards in Rama keeps everything in one place and leverages the same infrastructure. + +## API: Encapsulation Layer + +Like in our POC, we avoid using Rama's built-in REST API directly. Instead, `hibou-api` provides a **pullable** interface using [lasagna-pull](https://github.com/flybot-sg/lasagna-pull). + +The API: +- Validates all queries using Malli schemas before hitting the Rama cluster +- Provides authentication/authorization via SSO tokens +- Encapsulates all Rama complexity from the frontend +- Represents the entire API as pure EDN data structures + +This approach means the frontend never needs to know about Rama's internals. It just sends EDN queries and receives EDN responses. + +## UI: Query Builder and Visualizations + +The `hibou-ui` component uses [Replicant](https://replicant.fun/) for rendering and ECharts for visualizations. + +The query editor is particularly interesting. It's a form builder that generates itself from the Malli schemas derived from the module configs. Users build queries visually with proper validation, but the underlying representation is always pure EDN. + +Supported visualizations include: +- Overall aggregated summaries +- Tables with change indicators +- Bar charts and pie charts +- Time series line charts +- Record tables for detailed views + +## Mono Repo Structure + +Following our experience with [Clojure Mono Repos](http://loicb.dev/blog/clojure-mono-repo), we organized `hibou` as a mono-repo with clear component boundaries. + +Each component has its own: +- Source directories +- Test suites +- README documentation +- CI/CD tasks + +We use [Babashka tasks](https://book.babashka.org/#tasks) as our task runner across all components, making it easy to build, test, and deploy each part independently or together. + +## Client Integration + +A client repo integrates the hibou stack by: + +1. Defining annotated EDN configs for their analytics modules +2. Using `hibou-analytics` to build and deploy Rama modules +3. Using `hibou-api` to create an authenticated web server +4. Embedding `hibou-ui` for the frontend + +The client repo is minimal. It's mostly just configuration. All the heavy lifting is done by the reusable hibou components. + +## Testing Strategy + +We use multiple testing approaches: + +- [rich-comment-tests](https://github.com/matthewdowney/rich-comment-tests) for quick REPL-driven tests +- [Kaocha](https://github.com/lambdaisland/kaocha) for module-level and integration tests +- [tools.build](https://github.com/clojure/tools.build) to build uberjars for testing +- Rama's In-Process Cluster (IPC) for unit testing modules + +The configuration-driven approach makes testing easier. We can generate test modules with different configs and verify behavior systematically. + +## Deployment + +We use several tools for deployment: + +- [Jibbit](https://github.com/atomisthq/jibbit) to build container images without Docker +- Custom Rama CI tools for deploying modules from deployment files +- EKS deployment configs for the API server pods + +The separation between the Rama cluster (hosting analytics and dashboard modules) and the EKS cluster (hosting the API/UI) allows us to scale each independently. + +## Results + +The hibou platform successfully transformed our POC into a production-ready system. + +We now have: +- A generic analytics platform that works with any event-driven data +- Reduced development time for new clients (just write configs) +- A single source of truth via annotated EDN files +- Full-stack Clojure from data ingestion to UI +- Clean separation of concerns across components + +## Limitations and Future Work + +While the platform is functional, there are areas for improvement: + +- Query logic can be optimized further +- Only outer joins are fully supported (inner/left joins are WIP) +- Dashboard features are still basic (no admin operations, duplication, ownership transfer) +- Config format could be more user-friendly +- The Rama analytics PStates are only storing **rolled-up** data as for now + +## Conclusion + +Building hibou taught us valuable lessons about designing for reusability from the start. + +By treating everything as data (from module definitions to API endpoints to UI forms), we created a flexible system that adapts to different requirements without code changes. + +The combination of Rama for distributed stream processing, lasagna-pull for pure data APIs, and Replicant for reactive UIs created a coherent full-stack platform that leverages Clojure's strengths at every layer. + +The key was resisting the temptation to optimize for the first client and instead investing time upfront to build generic, configurable components. From d1282064b046885790ca29477589b9bbe084f450 Mon Sep 17 00:00:00 2001 From: skydread1 Date: Mon, 13 Oct 2025 12:59:05 +0800 Subject: [PATCH 03/11] Fix some typos --- .../server/content/blog/portfolio-website.md | 68 ++++++++++++------- .../server/content/blog/pwd-manager+2fa.md | 18 ++--- 2 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/loicb/server/content/blog/portfolio-website.md b/src/loicb/server/content/blog/portfolio-website.md index c1062e6..b0258ba 100644 --- a/src/loicb/server/content/blog/portfolio-website.md +++ b/src/loicb/server/content/blog/portfolio-website.md @@ -5,7 +5,7 @@ :repos [["Portfolio" "https://github.com/skydread1/portfolio"]] :title "Portfolio Website" :css-class "portfolio" - :tags ["ClojureScript" "Figwheel" "Re-Frame" "Reagent" "Lasagna-pull" "Reitit"] + :tags ["ClojureScript" "Figwheel" "Re-Frame" "Reagent" "Lasagna-pull" "Reitit" "Babashka"] :image #:image{:src "/assets/loic-logo.png" :src-dark "/assets/loic-logo.png" :alt "Logo referencing Aperture Science"}} @@ -20,7 +20,7 @@ The markdown content is converted into [hiccup](https://github.com/weavejester/h Light/dark mode and code block syntax highlighted are supported. -The app is deployed on **Netlify** every time a branch is merged to master. +The app is deployed on **Netlify** via GitHub Actions every time a branch is merged to master. Build artifacts are generated during CI/CD and are not committed to the repository. +++ ## Rational @@ -34,11 +34,12 @@ The markdown content is converted into [hiccup](https://github.com/weavejester/h Light/dark mode and code block syntax highlighted are supported. -The app is deployed on **Netlify** every time a branch is merged to master. +The app is deployed on **Netlify** via GitHub Actions every time a branch is merged to master. Build artifacts are generated during CI/CD and are not committed to the repository. ## Stack I have the following stack: +- [babashka](https://github.com/babashka/babashka) for task automation and build scripts - [figwheel-main](https://figwheel.org/) for live code reloading - [reagent](https://github.com/reagent-project/reagent) for react components - [hiccup](https://github.com/weavejester/hiccup) for DOM representation @@ -47,6 +48,7 @@ I have the following stack: - [markdown-to-hiccup](https://github.com/mpcarolin/markdown-to-hiccup) to allow me to write the page content in markdown. - [re-frame](https://github.com/day8/re-frame) a framework for building user interfaces, leveraging [reagent](https://github.com/reagent-project/reagent) - [lasagna-pull](https://github.com/flybot-sg/lasagna-pull) to precisely select from deep data structure +- [clj-rss](https://github.com/yogthos/clj-rss) to generate RSS feeds for blog posts ## Features @@ -119,42 +121,62 @@ At **CLJS** compile time, the following steps happen: 2. The `reitit` router is created 3. The post markdowns are converted to hiccup via `markdown-to-hiccup`. -## Build +## Build Tasks with Babashka -### Dev +I use [babashka](https://github.com/babashka/babashka) to manage all build tasks through a `bb.edn` configuration file. -I use clj/cljs REPL of `figwheel` for hot reloading on file save. +### Development -### Prod - -The github action is triggered when the code is pushed. +```bash +bb dev +``` -I use [clojure/tools.build](https://github.com/clojure/tools.build) to create tasks related to the build. +Starts a REPL server with the necessary middleware for ClojureScript development and figwheel hot reloading. -It runs the build.clj task to generate the main.js bundle: +### Testing +```bash +bb test # Local development tests +bb test-ci # CI tests (headless mode) +bb fmt-check # Check code formatting +bb fmt-fix # Fix code formatting ``` -clojure -T:build js-bundle + +### Production Build + +```bash +bb build # Build optimized ClojureScript bundle +bb rss # Generate RSS feeds for blog posts +bb dist # Create complete distribution in dist/ folder +bb clean # Clean build artifacts ``` -This command compiles the cljs to the optimized js bundle that Netlify will use to generate the preview in the PR. +The `bb dist` command creates a complete static site in the `dist/` directory ready for deployment. -## Continuous Integration +## Continuous Integration & Deployment -The CI does the following: -- Run the clj tests -- Run the cljs tests in headless mode -- Compile the cljs file into the js bundle `main.js` and commit it to the repo. +The project uses GitHub Actions for CI/CD with separate workflows for pull requests and production deployments. -## Continuous Deployment +### Pull Request Workflow +1. Run tests with `bb test-ci` +2. Check code formatting with `bb fmt-check` +3. Build static files with `bb dist` +4. Deploy preview to Netlify with unique URL (`pr-{number}`) -**Opening a pull request (PR)** to merge changes to master makes `Netlify` create a preview of how the new version of the website would look like once deployed. +### Production Deployment (main branch) +1. Run tests with `bb test-ci` +2. Check code formatting with `bb fmt-check` +3. Build static files with `bb dist` +4. Deploy to production Netlify site -**Merging to master** automatically publishes the last version of the website on Netlify. +## Hosting with Netlify -## Hosted with Netlify +The website is hosted on Netlify (US servers) and deployed via GitHub Actions using the Netlify CLI. This approach provides: -I use **Netlify** for hosting platform because it is easy to setup and the previews of the new website version on GitHub PR are convenient. +- **Predictable deployments**: Build happens in controlled CI environment +- **Preview deployments**: Each pull request gets its own preview URL +- **No committed artifacts**: Clean git history without build files +- **Environment separation**: Different deploy targets for previews vs production ## Learn More diff --git a/src/loicb/server/content/blog/pwd-manager+2fa.md b/src/loicb/server/content/blog/pwd-manager+2fa.md index e0c022c..c622130 100644 --- a/src/loicb/server/content/blog/pwd-manager+2fa.md +++ b/src/loicb/server/content/blog/pwd-manager+2fa.md @@ -10,7 +10,7 @@ +++ Using a password manager with strong passwords and enabling 2FA for critical accounts are simple yet powerful steps to protect your online security. +++ -## Rational +## Rationale One of the best ways to store and organize your online credentials is to use a password manager. @@ -60,9 +60,9 @@ Furthermore, note that Bitwarden has also filled a `url` field with `https://www ![Bitwarden extension icon](/assets/pwd-man-2fa/extension.png) -This way, you know that you are on the real website and not on a fake website like `redit.com` or a more subtle `ΙΎeddit.com`. Always verify that the little `1` in the extension is showing when you visit the website again. +This way, you know that you are on the real website and not on a phishing site like `redit.com` or the more subtle `ΙΎeddit.com` (note the different 'r' character). Always verify that the little `1` in the extension is showing when you visit the website again. -Tou can also navigate to the website by clicking on the url field in you password manager which is convenient. +You can also navigate to the website by clicking on the URL field in your password manager, which is convenient. Finally, you can add custom fields. @@ -90,7 +90,7 @@ Basically, you need another device to approve the login. Most websites dealing w > - **Knowledge Factor**: This is usually your password, something you know. > - **Possession Factor**: This is something you have, such as a code sent to your phone via SMS, a code generated by an authenticator app, or a security key. -It is advisable to use an **authenticator app**. There are multiple authenticator apps, such as Google Authenticator, Authy, Ante, etc. +It is advisable to use an **authenticator app** rather than SMS-based codes. I personally use [ente](https://ente.io/auth/). ### When to Use 2FA @@ -125,7 +125,7 @@ To store your recovery codes, I think either of these two options is acceptable: ### Forced 2FA -Some organizations, like your government or your bank, might β€œforce” you to set up 2FA. So, you might already be using it in some places. You most likely have 2FA with OTP on your phone. +Some organizations, like your government or your bank, might "force" you to set up 2FA. So, you might already be using it in some places. Some organizations have their own 2FA systems, often using their app. For instance, Steam uses Steam Guard, their own 2FA system. So, don’t be surprised if enabling 2FA differs from website to website. @@ -133,7 +133,7 @@ Some organizations have their own 2FA systems, often using their app. For instan I recommend using 2FA for your emails. You can do that for Gmail, ProtonMail, etc. -Also, store your Gmail address passwords in your vault with randomly generated passwords, as usual. +Also, store your email passwords in your vault with randomly generated passwords, as usual. ### Different Email Addresses @@ -186,14 +186,14 @@ That does not take long to do and gives you peace of mind. ## Step-by-Step Recap -You want to create an account for a new bank. +You want to create an account for a new website. 1. Click the Bitwarden extension once on the website. 2. Add your email that you use for important stuff in the `username`. 3. Generate a random `password`. 4. Save and `auto-fill` the registration form for the bank. 5. Once the account is created, go to the settings, and you’ll see that you can enable `2FA` and click on that. -6. Open your authentication app (most likely Google Authenticator). +6. Open your authentication app. 7. Scan the QR code shown by the bank to add the code to your app. 8. The website will expect you to enter the generated code to finalize the 2FA setup. -9. The bank will show the recovery codes (print them, label them, and hide them in your home). +9. The website will show the recovery codes (print them, label them, and hide them in your home). From f8d9634f35f6f739626fbdbf2856b220c895e715 Mon Sep 17 00:00:00 2001 From: skydread1 Date: Mon, 13 Oct 2025 13:49:02 +0800 Subject: [PATCH 04/11] Md files now uses yaml metadata --- src/loicb/server/content/about/aboutme.md | 41 ++++++++---- src/loicb/server/content/blog/blog-django.md | 39 +++++++---- .../server/content/blog/cd-sl-p-method.md | 30 ++++++--- src/loicb/server/content/blog/clj-in-unity.md | 37 +++++++---- .../server/content/blog/clj-mono-repo.md | 30 ++++++--- .../server/content/blog/datomic-setup.md | 30 ++++++--- .../content/blog/deploy-clj-app-to-aws.md | 32 ++++++---- .../content/blog/deploy-django-beanstalk.md | 33 ++++++---- .../server/content/blog/flybot-card-games.md | 41 ++++++++---- .../server/content/blog/flybot-mobile-app.md | 41 ++++++++---- .../server/content/blog/flybot-stats-rama.md | 37 +++++++---- .../server/content/blog/flybot-website.md | 52 ++++++++++----- .../server/content/blog/fun-map-flybot.md | 33 ++++++---- src/loicb/server/content/blog/fun-map.md | 30 ++++++--- .../server/content/blog/git-workflows.md | 30 ++++++--- .../server/content/blog/hibou-platform.md | 35 ++++++---- .../server/content/blog/juxt-tick-lib.md | 30 ++++++--- .../content/blog/lasagna-pull-flybot.md | 31 +++++---- src/loicb/server/content/blog/lasagna-pull.md | 29 ++++++--- .../server/content/blog/magic-nostrand.md | 53 +++++++++------ .../server/content/blog/mcts-card-games.md | 28 +++++--- src/loicb/server/content/blog/mcts.md | 36 +++++++---- .../content/blog/port-clj-lib-to-clr.md | 35 ++++++---- .../server/content/blog/portfolio-website.md | 39 +++++++---- .../server/content/blog/props-recommender.md | 35 ++++++---- .../server/content/blog/pwd-manager+2fa.md | 30 ++++++--- .../server/content/blog/reagent-native-app.md | 33 ++++++---- .../content/blog/redirect-domain-subdomain.md | 30 ++++++--- .../server/content/blog/testing-in-clojure.md | 33 ++++++---- test/loicb/server/md_test.clj | 64 +++++++++++++++++-- 30 files changed, 730 insertions(+), 347 deletions(-) diff --git a/src/loicb/server/content/about/aboutme.md b/src/loicb/server/content/about/aboutme.md index 28e6d91..1e1955e 100644 --- a/src/loicb/server/content/about/aboutme.md +++ b/src/loicb/server/content/about/aboutme.md @@ -1,17 +1,32 @@ -#:post{:id "about-me" - :page :about - :date ["2024-11-12"] - :employer "My journey so far" - :repos [["My GitHub" "https://github.com/skydread1"]] - :articles [["My Tech Blog" "../blog"]] - :title "About Me" - :tags ["πŸ› οΈ Staff SE" "πŸ“ Singapore" "🎫 Pass: EP" "πŸ’» 5+ years of experience" "πŸŽ“ Master in CS" "πŸ‡«πŸ‡· French Citizen"] - :css-class "about-me" - :image #:image{:src "/assets/loic-logo.png" - :src-dark "/assets/loic-logo.png" - :alt "Loic Logo"}} -+++ +--- +id: about-me +page: about +date: + - 2024-11-12 +title: About Me +employer: My journey so far +css-class: about-me +tags: + - πŸ› οΈ Staff SE + - πŸ“ Singapore + - "🎫 Pass: EP" + - πŸ’» 5+ years of experience + - πŸŽ“ Master in CS + - πŸ‡«πŸ‡· French Citizen +repos: + - ["My GitHub", "https://github.com/skydread1"] +articles: + - ["My Tech Blog", "../blog"] +image: + src: /assets/loic-logo.png + src-dark: /assets/loic-logo.png + alt: Loic Logo +--- + + + +++ + ## In short πŸ‘‹πŸ» Hi, I'm Loic Blanchard, a Staff Software Engineer based in Singapore with over 5 years of experience in building scalable software solutions. My expertise lies in designing and leading the development of high-quality software systems, primarily using the Functional Programming language **Clojure** and its ecosystem. I am currently leading the engineering team at [Flybot Pte Ltd](https://www.flybot.sg/). diff --git a/src/loicb/server/content/blog/blog-django.md b/src/loicb/server/content/blog/blog-django.md index f107818..5adb20c 100644 --- a/src/loicb/server/content/blog/blog-django.md +++ b/src/loicb/server/content/blog/blog-django.md @@ -1,15 +1,28 @@ -#:post{:id "blog-django" - :page :blog - :date ["2023-05-27" "2023-11-12"] - :repos [["Blog" "https://github.com/skydread1/blog"]] - :articles [["Deploy Django Blog in AWS Beanstalk" "../blog/deploy-django-aws-beanstalk"]] - :title "Tech Blog with Django" - :css-class "blog-django" - :tags ["Python" "Django" "Elastic Beanstalk" "RDS PostgreSQL" "S3" "AWS"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: blog-django +page: blog +date: + - 2023-05-27 + - 2023-11-12 +title: Tech Blog with Django +css-class: blog-django +tags: + - Python + - Django + - Elastic Beanstalk + - RDS PostgreSQL + - S3 + - AWS +repos: + - ["Blog", "https://github.com/skydread1/blog"] +articles: + - ["Deploy Django Blog in AWS Beanstalk", "../blog/deploy-django-aws-beanstalk"] +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + I developed a tech blog in **python** using the `Django` framework. Thus, it is Server-Side Rendered. The blog was deployed on AWS `Beanstalk`, the static files were served from an AWS `S3` bucket, and the production data was stored in an AWS `RDS Postgres` database. @@ -17,7 +30,9 @@ The blog was deployed on AWS `Beanstalk`, the static files were served from an A It used `HTMX` for the search bar. After the AWS free tier expired, I moved the tech blog to the ClojureScript SPA. + +++ + ## Rational I developed a tech blog in **python** using the `Django` framework. Thus, it is Server-Side Rendered. diff --git a/src/loicb/server/content/blog/cd-sl-p-method.md b/src/loicb/server/content/blog/cd-sl-p-method.md index 0ff61da..e588529 100644 --- a/src/loicb/server/content/blog/cd-sl-p-method.md +++ b/src/loicb/server/content/blog/cd-sl-p-method.md @@ -1,15 +1,25 @@ -#:post{:id "cd-sl-p-method" - :page :blog - :date ["2025-06-23"] - :title "Custom Domain + SimpleLogin + Proton" - :css-class "cd-sl-p" - :tags ["ProtonMail" "SimpleLogin" "Custom Domain" "Privacy"] - :image #:image{:src "/assets/cd-sl-p/proton-mail-icon.jpg" - :src-dark "/assets/cd-sl-p/proton-mail-icon.jpg" - :alt "ProtonMail Logo"}} -+++ +--- +id: cd-sl-p-method +page: blog +date: + - 2025-06-23 +title: Custom Domain + SimpleLogin + Proton +css-class: cd-sl-p +tags: + - ProtonMail + - SimpleLogin + - Custom Domain + - Privacy +image: + src: /assets/cd-sl-p/proton-mail-icon.jpg + src-dark: /assets/cd-sl-p/proton-mail-icon.jpg + alt: ProtonMail Logo +--- + CD-SL-P (Custom Domain + SimpleLogin + Proton) method to organize your email flow. + +++ + ## πŸ‘οΈ Rationale In this article, I will show you the CD-SL-P (Custom Domain + SimpleLogin + Proton) method that is good to balance privacy and convenience. diff --git a/src/loicb/server/content/blog/clj-in-unity.md b/src/loicb/server/content/blog/clj-in-unity.md index 17bd78b..7b94c7f 100644 --- a/src/loicb/server/content/blog/clj-in-unity.md +++ b/src/loicb/server/content/blog/clj-in-unity.md @@ -1,18 +1,29 @@ -#:post{:id "clojure-in-unity" - :page :blog - :date ["2022-04-22"] - :title "Pack, Push and Import Clojure to Unity" - :repos [["Magic" "https://github.com/nasser/magic"] - ["Nostrand" "https://github.com/nasser/nostrand"] - ["Magic.Unity" "https://github.com/nasser/Magic.Unity"]] - :css-class "blog-clj-in-unity" - :tags ["Clojure" "Compiler" "CLR" "Unity"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: clojure-in-unity +page: blog +date: + - 2022-04-22 +title: Pack, Push and Import Clojure to Unity +css-class: blog-clj-in-unity +tags: + - Clojure + - Compiler + - CLR + - Unity +repos: + - ["Magic", "https://github.com/nasser/magic"] + - ["Nostrand", "https://github.com/nasser/nostrand"] + - ["Magic.Unity", "https://github.com/nasser/Magic.Unity"] +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + How to package a Compiled Clojure lib (With MAGIC compiler) to NuGet, push it to remote and import it to Unity. + +++ + ## Prerequisites Your Clojure library is assumed to be already compiled to dotnet. diff --git a/src/loicb/server/content/blog/clj-mono-repo.md b/src/loicb/server/content/blog/clj-mono-repo.md index b8bfaf3..66d72cf 100644 --- a/src/loicb/server/content/blog/clj-mono-repo.md +++ b/src/loicb/server/content/blog/clj-mono-repo.md @@ -1,15 +1,25 @@ -#:post{:id "clojure-mono-repo" - :page :blog - :date[ "2023-02-16"] - :title "Clojure Mono Repo example : server + 2 clients" - :css-class "blog-mono-repo" - :tags ["Clojure" "Reagent" "Figwheel" "Mono Repo"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: clojure-mono-repo +page: blog +date: + - 2023-02-16 +title: "Clojure Mono Repo example : server + 2 clients" +css-class: blog-mono-repo +tags: + - Clojure + - Reagent + - Figwheel + - Mono Repo +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + Example of a Clojure mono-repo structure for a web server and 2 clients (web and mobile). + +++ + ## Context Our app [skydread1/flybot.sg](https://github.com/skydread1/flybot.sg) is a full-stack Clojure **web** and **mobile** app. diff --git a/src/loicb/server/content/blog/datomic-setup.md b/src/loicb/server/content/blog/datomic-setup.md index 3012131..a720c82 100644 --- a/src/loicb/server/content/blog/datomic-setup.md +++ b/src/loicb/server/content/blog/datomic-setup.md @@ -1,15 +1,25 @@ -#:post{:id "datomic-setup-examples" - :page :blog - :date ["2022-12-02"] - :title "Datomic Setup examples: embedded, cassandra, docker." - :css-class "blog-datomic-setup" - :tags ["Clojure" "Datomic" "Cassandra" "Docker"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: datomic-setup-examples +page: blog +date: + - 2022-12-02 +title: "Datomic Setup examples: embedded, cassandra, docker." +css-class: blog-datomic-setup +tags: + - Clojure + - Datomic + - Cassandra + - Docker +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + How to run an embedded version of Datomic. How to run Datomic with a Cassandra cluster. How to run it in Docker. + +++ + ## Introduction While working on [flybot.sg](http://flybot.sg) , I experimented with `datomic-free`, datomic `starter-pro` with Cassandra and datomic starter-pro with embedded storage. diff --git a/src/loicb/server/content/blog/deploy-clj-app-to-aws.md b/src/loicb/server/content/blog/deploy-clj-app-to-aws.md index 35eaf95..1098c13 100644 --- a/src/loicb/server/content/blog/deploy-clj-app-to-aws.md +++ b/src/loicb/server/content/blog/deploy-clj-app-to-aws.md @@ -1,16 +1,26 @@ -#:post{:id "deploy-clj-app-to-aws" - :page :blog - :date ["2023-01-20"] - :title "Deploy full stack Clojure website to AWS" - :repos [["Flybot" "https://github.com/skydread1/flybot.sg"]] - :css-class "blog-deploy-clj-aws" - :tags ["Clojure" "AWS" "Full-Stack"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: deploy-clj-app-to-aws +page: blog +date: + - 2023-01-20 +title: Deploy full stack Clojure website to AWS +css-class: blog-deploy-clj-aws +tags: + - Clojure + - AWS + - Full-Stack +repos: + - ["Flybot", "https://github.com/skydread1/flybot.sg"] +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + Example of how to deploy a containerized full-stack Clojure app in AWS EC2. + +++ + This is an example of how to deploy a containerized full-stack Clojure app in AWS EC2. I will use the [flybot.sg website](https://github.com/skydread1/flybot.sg) as example of app to deploy. diff --git a/src/loicb/server/content/blog/deploy-django-beanstalk.md b/src/loicb/server/content/blog/deploy-django-beanstalk.md index fd1d192..32f0157 100644 --- a/src/loicb/server/content/blog/deploy-django-beanstalk.md +++ b/src/loicb/server/content/blog/deploy-django-beanstalk.md @@ -1,16 +1,27 @@ -#:post{:id "deploy-django-aws-beanstalk" - :page :blog - :date ["2023-08-08"] - :repos [["Blog" "https://github.com/skydread1/blog"]] - :title "Deploy Django Blog in AWS Beanstalk" - :css-class "blog-django-aws" - :tags ["Python" "Django" "AWS" "Elastic Beanstalk"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: deploy-django-aws-beanstalk +page: blog +date: + - 2023-08-08 +title: Deploy Django Blog in AWS Beanstalk +css-class: blog-django-aws +tags: + - Python + - Django + - AWS + - Elastic Beanstalk +repos: + - ["Blog", "https://github.com/skydread1/blog"] +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + What libraries/settings I used to develop a Django app and how to deploy it on AWS Elastic Beanstalk. + +++ + ## Context My open-source tech blog used to be deployed on AWS (during my AWS free tier period). diff --git a/src/loicb/server/content/blog/flybot-card-games.md b/src/loicb/server/content/blog/flybot-card-games.md index 3341d17..571ef5f 100644 --- a/src/loicb/server/content/blog/flybot-card-games.md +++ b/src/loicb/server/content/blog/flybot-card-games.md @@ -1,23 +1,36 @@ -#:post{:id "card-games-api" - :page :blog - :home-page? true - :employer "Flybot Pte Ltd" - :date ["2020-01-06" "2021-10-29"] - :repos [["Magic" "https://github.com/nasser/magic"]] - :articles [["Port your Clojure lib to the CLR with MAGIC" "../blog/port-clj-lib-to-clr"]] - :title "Clojure Card Games Backend APIs" - :css-class "card-games" - :tags ["Clojure" "Clojure Spec" "Magic Compiler" "Interop CLR"] - :image #:image{:src "https://www.flybot.sg/assets/flybot-logo.png" - :src-dark "https://www.flybot.sg/assets/flybot-logo.png" - :alt "Flybot Logo"}} -+++ +--- +id: card-games-api +page: blog +date: + - 2020-01-06 + - 2021-10-29 +title: Clojure Card Games Backend APIs +home-page?: true +employer: Flybot Pte Ltd +css-class: card-games +tags: + - Clojure + - Clojure Spec + - Magic Compiler + - Interop CLR +repos: + - ["Magic", "https://github.com/nasser/magic"] +articles: + - ["Port your Clojure lib to the CLR with MAGIC", "../blog/port-clj-lib-to-clr"] +image: + src: https://www.flybot.sg/assets/flybot-logo.png + src-dark: https://www.flybot.sg/assets/flybot-logo.png + alt: Flybot Logo +--- + At Flybot, I had the opportunity to create popular Asian Card Games APIs in **Clojure**. I developed the backend of games such as *Pǎo DΓ© KuΓ i* (θ·‘εΎ—εΏ«) and *Big two* (ι”„ε€§εœ°) which are climbing card games. I also worked on a Library we called **MetaGame** that allows us to compose several `Pǎo DΓ© KuΓ i` or `Big two` games (or a mix of both). By composing games, I mean to play a few rounds of them up to a certain score for instance. This composition is generic enough to even allow us to make tournaments out of the underlying games. + +++ + ## Rational At Flybot, I had the opportunity to create popular Asian Card Games APIs in **Clojure**. diff --git a/src/loicb/server/content/blog/flybot-mobile-app.md b/src/loicb/server/content/blog/flybot-mobile-app.md index aeaa318..a84431e 100644 --- a/src/loicb/server/content/blog/flybot-mobile-app.md +++ b/src/loicb/server/content/blog/flybot-mobile-app.md @@ -1,23 +1,36 @@ -#:post{:id "clojure-mobile-app" - :page :blog - :employer "Flybot Pte Ltd" - :date ["2023-02-13" "2023-08-08"] - :repos [["Flybot" "https://github.com/skydread1/flybot.sg"]] - :articles [["Reagent React Native Mobile App" "../blog/reagent-native-app"] - ["Clojure Mono Repo example : server + 2 clients" "../blog/clojure-mono-repo"]] - :title "Flybot Mobile App" - :tags ["ClojureScript" "Figwheel" "Re-Frame" "Reagent React Native"] - :css-class "flybot-mobile-app" - :image #:image{:src "https://www.flybot.sg/assets/flybot-logo.png" - :src-dark "https://www.flybot.sg/assets/flybot-logo.png" - :alt "Flybot Logo"}} -+++ +--- +id: clojure-mobile-app +page: blog +date: + - 2023-02-13 + - 2023-08-08 +title: Flybot Mobile App +employer: Flybot Pte Ltd +css-class: flybot-mobile-app +tags: + - ClojureScript + - Figwheel + - Re-Frame + - Reagent React Native +repos: + - ["Flybot", "https://github.com/skydread1/flybot.sg"] +articles: + - ["Reagent React Native Mobile App", "../blog/reagent-native-app"] + - ["Clojure Mono Repo example : server + 2 clients", "../blog/clojure-mono-repo"] +image: + src: https://www.flybot.sg/assets/flybot-logo.png + src-dark: https://www.flybot.sg/assets/flybot-logo.png + alt: Flybot Logo +--- + At Flybot, I developed a mobile app using **ClojureScript** with `React Native` using `Figwheel` and `Reagent React Native` (to interact with reagent in ClojureScript). The code for the mobile app resides in the same repo as the server and the web client of the [flybot.sg](https://www.flybot.sg/) website. The goal of the mobile app was to allow employees to write blog posts using an app instead of the web UI and also to evaluate if our ClojureScript frontend stack could reuse most of the `re-frame` logic in both the mobile and web UIs. + +++ + ## Rational The goal is to have a mobile app targeting both iOS and Android, written in **ClojureScript**, which can reuse most of our web frontend logic. diff --git a/src/loicb/server/content/blog/flybot-stats-rama.md b/src/loicb/server/content/blog/flybot-stats-rama.md index 948a6ed..f72233d 100644 --- a/src/loicb/server/content/blog/flybot-stats-rama.md +++ b/src/loicb/server/content/blog/flybot-stats-rama.md @@ -1,21 +1,32 @@ -#:post{:id "game-stats-rama" - :page :blog - :home-page? true - :employer "Flybot Pte Ltd" - :date ["2024-08-12"] - :title "Gaming Stats Aggregation with Rama" - :css-class "stats-rama" - :tags ["Clojure" "Rama" "AWS" "EKS" "Kafka"] - :image #:image{:src "https://www.flybot.sg/assets/flybot-logo.png" - :src-dark "https://www.flybot.sg/assets/flybot-logo.png" - :alt "Flybot Logo"}} -+++ +--- +id: game-stats-rama +page: blog +date: + - 2024-08-12 +title: Gaming Stats Aggregation with Rama +home-page?: true +employer: Flybot Pte Ltd +css-class: stats-rama +tags: + - Clojure + - Rama + - AWS + - EKS + - Kafka +image: + src: https://www.flybot.sg/assets/flybot-logo.png + src-dark: https://www.flybot.sg/assets/flybot-logo.png + alt: Flybot Logo +--- + I am currently working on a project that aims at moving our gaming platform stat aggregation and visualization project to a new stack. We have an event-driven system that produces lots of events in multiple kafka topics. Our current solution to aggregate stats and visualize them in dashboards is the setup [Apache Druid](https://druid.apache.org/) + [Imply](https://imply.io/). As a **Clojure**-focused company, we decided to experiment with a new architecture: [Rama](https://redplanetlabs.com/learn-rama) + in-house full-stack Clojure Visualization tool. + +++ + ## Rationale I worked on a POC on a project that aims at moving our gaming platform stat aggregation and visualization project to a new stack. @@ -144,5 +155,3 @@ The main limitations are the following: ## Next goal The next step is to have a generic `analytics` module builder with clear configs. And we want these configs to be easily reusable for the API/frontend validation schema for the different queries. - - diff --git a/src/loicb/server/content/blog/flybot-website.md b/src/loicb/server/content/blog/flybot-website.md index 183f1f0..85fd259 100644 --- a/src/loicb/server/content/blog/flybot-website.md +++ b/src/loicb/server/content/blog/flybot-website.md @@ -1,24 +1,42 @@ -#:post{:id "clojure-full-stack-webapp" - :page :blog - :home-page? true - :employer "Flybot Pte Ltd" - :date ["2022-05-20" "2023-08-04"] - :repos [["Flybot" "https://github.com/skydread1/flybot.sg"]] - :articles [["Deploy full stack Clojure website to AWS" "../blog/deploy-clj-app-to-aws"] - ["Lasagna-pull applied to flybot.sg" "../blog/lasagna-pull-applied-to-flybot"] - ["Fun-Map applied to flybot.sg" "../blog/fun-map-applied-to-flybot"] - ["Clojure Mono Repo example : server + 2 clients" "../blog/clojure-mono-repo"]] - :title "Flybot Website" - :tags ["Clojure" "ClojureScript" "Figwheel" "Re-Frame" "Malli" "Lasagna-pull" "Fun-map" "Datalevin" "Reitit"] - :css-class "flybot-website" - :image #:image{:src "https://www.flybot.sg/assets/flybot-logo.png" - :src-dark "https://www.flybot.sg/assets/flybot-logo.png" - :alt "Flybot Logo"}} -+++ +--- +id: clojure-full-stack-webapp +page: blog +date: + - 2022-05-20 + - 2023-08-04 +title: Flybot Website +home-page?: true +employer: Flybot Pte Ltd +css-class: flybot-website +tags: + - Clojure + - ClojureScript + - Figwheel + - Re-Frame + - Malli + - Lasagna-pull + - Fun-map + - Datalevin + - Reitit +repos: + - ["Flybot", "https://github.com/skydread1/flybot.sg"] +articles: + - ["Deploy full stack Clojure website to AWS", "../blog/deploy-clj-app-to-aws"] + - ["Lasagna-pull applied to flybot.sg", "../blog/lasagna-pull-applied-to-flybot"] + - ["Fun-Map applied to flybot.sg", "../blog/fun-map-applied-to-flybot"] + - ["Clojure Mono Repo example : server + 2 clients", "../blog/clojure-mono-repo"] +image: + src: https://www.flybot.sg/assets/flybot-logo.png + src-dark: https://www.flybot.sg/assets/flybot-logo.png + alt: Flybot Logo +--- + [flybot.sg](https://www.flybot.sg/) is an open-source full-stack **Clojure** web-app that allows company’s employees to write posts to showcase their open-source libraries, their contributions and all technical knowledge that could interest the functional programming community. HRs can also post job offers. Admins can edit any piece of content in any pages as the whole content can be written in Markdown. The purpose of this project was to demonstrate how the `lasagna stack` ([flybot-sg/lasagna-pull](https://github.com/flybot-sg/lasagna-pull) and [robertluo/fun-map](https://github.com/robertluo/fun-map)) could ease the web development experience for any Clojure developers. + +++ + ## Rational [flybot.sg](https://www.flybot.sg/) is an open-source full-stack **Clojure** web-app that allows company’s employees to write posts to showcase their open-source libraries, their contributions and all technical knowledge that could interest the functional programming community. HRs can also post job offers. Admins can edit any piece of content in any pages as the whole content can be written in Markdown. diff --git a/src/loicb/server/content/blog/fun-map-flybot.md b/src/loicb/server/content/blog/fun-map-flybot.md index 7c0cfef..c92aaf7 100644 --- a/src/loicb/server/content/blog/fun-map-flybot.md +++ b/src/loicb/server/content/blog/fun-map-flybot.md @@ -1,16 +1,27 @@ -#:post{:id "fun-map-applied-to-flybot" - :page :blog - :date ["2023-05-08"] - :title "Fun-Map applied to flybot.sg" - :repos [["Flybot" "https://github.com/skydread1/flybot.sg"]] - :css-class "blog-fun-map-flybot" - :tags ["Clojure" "System" "Component" "Deps Injection"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: fun-map-applied-to-flybot +page: blog +date: + - 2023-05-08 +title: Fun-Map applied to flybot.sg +css-class: blog-fun-map-flybot +tags: + - Clojure + - System + - Component + - Deps Injection +repos: + - ["Flybot", "https://github.com/skydread1/flybot.sg"] +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + how we leverage `fun-map` to create different systems in the website flybot.sg: `prod-system`, `dev-system`, `test-system` and `figwheel-system`. + +++ + ## Prerequisites If you are not familiar with [fun-map](https://github.com/robertluo/fun-map), please refer to the doc [Fun-Map: a solution to deps injection in Clojure](https://www.loicb.dev/blog/fun-map). diff --git a/src/loicb/server/content/blog/fun-map.md b/src/loicb/server/content/blog/fun-map.md index 9dda09a..8f69502 100644 --- a/src/loicb/server/content/blog/fun-map.md +++ b/src/loicb/server/content/blog/fun-map.md @@ -1,15 +1,25 @@ -#:post{:id "fun-map" - :page :blog - :date ["2022-04-12"] - :title "Fun-Map: a solution to deps injection in Clojure" - :css-class "blog-fun-map" - :tags ["Clojure" "System" "Component" "Deps Injection"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: fun-map +page: blog +date: + - 2022-04-12 +title: "Fun-Map: a solution to deps injection in Clojure" +css-class: blog-fun-map +tags: + - Clojure + - System + - Component + - Deps Injection +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + Rational of `fun-map`, including the benefit of the `life-cycle-map` as dependency injection system. + +++ + ## Context The Lasagna stack library [fun-map](https://github.com/robertluo/fun-map) by [@robertluo](https://github.com/robertluo) blurs the line between identity, state and function. As a results, it is a very convenient tool to define `system` in your applications by providing an elegant way to perform associative dependency injections. diff --git a/src/loicb/server/content/blog/git-workflows.md b/src/loicb/server/content/blog/git-workflows.md index 8cf5da0..f3086e6 100644 --- a/src/loicb/server/content/blog/git-workflows.md +++ b/src/loicb/server/content/blog/git-workflows.md @@ -1,15 +1,25 @@ -#:post{:id "git-workflows" - :page :blog - :date ["2024-05-12"] - :title "What Git workflow is suitable for your project" - :css-class "git-workflows" - :tags ["Git" "Workflows" "Branching" "CI/CD"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: git-workflows +page: blog +date: + - 2024-05-12 +title: What Git workflow is suitable for your project +css-class: git-workflows +tags: + - Git + - Workflows + - Branching + - CI/CD +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + What Git workflow is suitable for your project: trunk-based, feature branching, forking, release branching, release candidate workflow, Feature branching to develop, GitFlow + +++ + ## Introduction Depending on the size of the projects and its CI/CD requirements, one might choose one of the popular [Git Workflows](https://www.atlassian.com/git/tutorials/comparing-workflows). Some are good for some scenarios, some are never good and some are questionable to say the least. diff --git a/src/loicb/server/content/blog/hibou-platform.md b/src/loicb/server/content/blog/hibou-platform.md index 1bb2f31..7fd8645 100644 --- a/src/loicb/server/content/blog/hibou-platform.md +++ b/src/loicb/server/content/blog/hibou-platform.md @@ -1,19 +1,30 @@ -#:post{:id "hibou-platform" - :page :blog - :home-page? true - :employer "Flybot Pte Ltd" - :date ["2025-10-13"] - :title "Building a Generic Analytics Platform with Hibou" - :css-class "hibou-platform" - :tags ["Clojure" "Rama" "Analytics" "Mono Repo" "Full-Stack"] - :image #:image{:src "https://www.flybot.sg/assets/flybot-logo.png" - :src-dark "https://www.flybot.sg/assets/flybot-logo.png" - :alt "Flybot Logo"}} -+++ +--- +id: hibou-platform +page: blog +date: + - 2025-10-13 +title: Building a Generic Analytics Platform with Hibou +home-page?: true +employer: Flybot Pte Ltd +css-class: hibou-platform +tags: + - Clojure + - Rama + - Analytics + - Mono Repo + - Full-Stack +image: + src: https://www.flybot.sg/assets/flybot-logo.png + src-dark: https://www.flybot.sg/assets/flybot-logo.png + alt: Flybot Logo +--- + Following our successful POC with Rama for analytics, we evolved the stack into a generic, reusable platform called **Hibou** (French for "owl", known for their excellent eyesight). The goal was to transform our client-specific implementation into a **business-agnostic** analytics suite that can serve multiple clients with different data requirements. + +++ + ## Context This project builds upon the foundation established in our [Gaming Stats Aggregation with Rama](http://loicb.dev/blog/game-stats-rama) POC. diff --git a/src/loicb/server/content/blog/juxt-tick-lib.md b/src/loicb/server/content/blog/juxt-tick-lib.md index a3a951b..a330ff1 100644 --- a/src/loicb/server/content/blog/juxt-tick-lib.md +++ b/src/loicb/server/content/blog/juxt-tick-lib.md @@ -1,15 +1,25 @@ -#:post{:id "tick" - :page :blog - :date ["2024-04-20"] - :title "Time as a value with Tick" - :css-class "juxt-tick" - :tags ["Clojure" "Java 8 time" "Tick" "Duration/Period"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: tick +page: blog +date: + - 2024-04-20 +title: Time as a value with Tick +css-class: juxt-tick +tags: + - Clojure + - Java 8 time + - Tick + - Duration/Period +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + Illustrate date and time concepts in programming using the Clojure Tick library: timestamp, date-time, offset, zone, instant, inst, UTC, DST, duration, period etc. + +++ + ## Introduction It is always very confusing to deal with time in programming. In fact there are so many time representations, for legacy reasons, that sticking to one is not possible as our dependencies, databases or even programming languages might use different ways of representing time! diff --git a/src/loicb/server/content/blog/lasagna-pull-flybot.md b/src/loicb/server/content/blog/lasagna-pull-flybot.md index 904e225..0864234 100644 --- a/src/loicb/server/content/blog/lasagna-pull-flybot.md +++ b/src/loicb/server/content/blog/lasagna-pull-flybot.md @@ -1,16 +1,25 @@ -#:post{:id "lasagna-pull-applied-to-flybot" - :page :blog - :date ["2023-04-08"] - :title "Lasagna-pull applied to flybot.sg" - :repos [["Flybot" "https://github.com/skydread1/flybot.sg"]] - :css-class "blog-lasagna-pull-flybot" - :tags ["Clojure" "Pull Pattern"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: lasagna-pull-applied-to-flybot +page: blog +date: + - 2023-04-08 +title: Lasagna-pull applied to flybot.sg +css-class: blog-lasagna-pull-flybot +tags: + - Clojure + - Pull Pattern +repos: + - ["Flybot", "https://github.com/skydread1/flybot.sg"] +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + How we leverage `lasagna-pull` in the flybot.sg Clojure web app to define a pure data API. + +++ + ## Prerequisites If you are not familiar with [lasagna-pull](https://github.com/flybot-sg/lasagna-pull), please refer to the doc [Lasagna Pull: Precisely select from deep nested data](https://www.loicb.dev/blog/lasagna-pull) diff --git a/src/loicb/server/content/blog/lasagna-pull.md b/src/loicb/server/content/blog/lasagna-pull.md index 945ad05..8b14f4d 100644 --- a/src/loicb/server/content/blog/lasagna-pull.md +++ b/src/loicb/server/content/blog/lasagna-pull.md @@ -1,15 +1,24 @@ -#:post{:id "lasagna-pull" - :page :blog - :date ["2022-04-12"] - :title "Lasagna Pull: Precisely select from deep nested data" - :css-class "blog-lasagna-pull" - :tags ["Clojure" "Pull Pattern" "Malli"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: lasagna-pull +page: blog +date: + - 2022-04-12 +title: "Lasagna Pull: Precisely select from deep nested data" +css-class: blog-lasagna-pull +tags: + - Clojure + - Pull Pattern + - Malli +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + Rational of flybot-sg/lasagna-pull library: precisely select from deep data structure in Clojure. + +++ + ## Context [flybot-sg/lasagna-pull](https://github.com/flybot-sg/lasagna-pull) by [@robertluo](https://github.com/robertluo) aims at precisely select from deep data structure in Clojure. diff --git a/src/loicb/server/content/blog/magic-nostrand.md b/src/loicb/server/content/blog/magic-nostrand.md index f27d85b..6cf7097 100644 --- a/src/loicb/server/content/blog/magic-nostrand.md +++ b/src/loicb/server/content/blog/magic-nostrand.md @@ -1,27 +1,44 @@ -#:post{:id "magic-clojure-compiler-to-clr" - :page :blog - :home-page? true - :employer "Flybot Pte Ltd" - :date ["2021-02-01" "2022-12-09"] - :repos [["Magic" "https://github.com/nasser/magic"] - ["Nostrand" "https://github.com/nasser/nostrand"] - ["Magic.Unity" "https://github.com/nasser/Magic.Unity"] - ["magic-pipeline" "https://github.com/magic-clojure/magic"]] - :articles [["Port your Clojure lib to the CLR with MAGIC" "../blog/port-clj-lib-to-clr"] - ["Pack, Push and Import Clojure to Unity" "../blog/clojure-in-unity"]] - :title "Magic Compiler and Nostrand" - :css-class "magic-nostrand" - :tags ["Clojure" "C#" "JVM" "CLR" "Compiler" "Interop" "NuGet" "Unity"] - :image #:image{:src "/assets/magic-hat.png" - :src-dark "/assets/magic-hat.png" - :alt "Data Light Wallpaper"}} -+++ +--- +id: magic-clojure-compiler-to-clr +page: blog +date: + - 2021-02-01 + - 2022-12-09 +title: Magic Compiler and Nostrand +home-page?: true +employer: Flybot Pte Ltd +css-class: magic-nostrand +tags: + - Clojure + - C# + - JVM + - CLR + - Compiler + - Interop + - NuGet + - Unity +repos: + - ["Magic", "https://github.com/nasser/magic"] + - ["Nostrand", "https://github.com/nasser/nostrand"] + - ["Magic.Unity", "https://github.com/nasser/Magic.Unity"] + - ["magic-pipeline", "https://github.com/magic-clojure/magic"] +articles: + - ["Port your Clojure lib to the CLR with MAGIC", "../blog/port-clj-lib-to-clr"] + - ["Pack, Push and Import Clojure to Unity", "../blog/clojure-in-unity"] +image: + src: /assets/magic-hat.png + src-dark: /assets/magic-hat.png + alt: Data Light Wallpaper +--- + I worked on integrating the Magic Compiler and its tooling to Flybot's development workflow. [Magic](https://github.com/nasser/magic) is an open-source bootstrapped compiler written in **Clojure** that takes Clojure code as input and produces dotnet assemblies (.dll) as output. The dlls produced by Magic can be run in the Game engine `Unity` which Flybot uses for their card game UIs. The goal was to be able to compile our backend Clojure APIs to dlls so we can used that logic in Unity directly. Working closely with the author of the Magic compiler [Ramsey Nasser](https://github.com/nasser), I helped improving the tooling around the Magic compiler so it integrates well with our workflow Clojure Backend/Unity Frontend. I notably simplified the way a clojure project and its dependencies are compiled to dlls, packed to NuGet, deployed to online repos and finally imported to Unity. + +++ + ## Rational I worked on integrating the Magic Compiler and its tooling to Flybot's development workflow. diff --git a/src/loicb/server/content/blog/mcts-card-games.md b/src/loicb/server/content/blog/mcts-card-games.md index 3114601..3c81a08 100644 --- a/src/loicb/server/content/blog/mcts-card-games.md +++ b/src/loicb/server/content/blog/mcts-card-games.md @@ -1,15 +1,23 @@ -#:post{:id "article-mcts" - :page :blog - :date ["2021-08-13"] - :title "MCTS applied to card games" - :css-class "blog-post-mcts" - :tags ["Clojure" "MCTS"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: article-mcts +page: blog +date: + - 2021-08-13 +title: MCTS applied to card games +css-class: blog-post-mcts +tags: + - Clojure + - MCTS +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + General principle of MCTS applied to a Clojure use case. + +++ + ## Objective At [Flybot Pte Ltd](https://www.flybot.sg/), we wanted to have a robot-player that can play several rounds of some of our card games (such as `big-two`) at a decent level. diff --git a/src/loicb/server/content/blog/mcts.md b/src/loicb/server/content/blog/mcts.md index 3ccbec6..4bf4a8e 100644 --- a/src/loicb/server/content/blog/mcts.md +++ b/src/loicb/server/content/blog/mcts.md @@ -1,21 +1,33 @@ -#:post{:id "mcts" - :page :blog - :employer "Flybot Pte Ltd" - :date ["2021-07-19" "2021-08-13"] - :articles [["MCTS applied to card games" "../blog/article-mcts"]] - :title "MCTS applied to card games" - :css-class "mcts" - :tags ["MCTS" "Monte Carlo Tree Search" "Clojure" "Card Game"] - :image #:image{:src "https://www.flybot.sg/assets/flybot-logo.png" - :src-dark "https://www.flybot.sg/assets/flybot-logo.png" - :alt "Flybot Logo"}} -+++ +--- +id: mcts +page: blog +date: + - 2021-07-19 + - 2021-08-13 +title: MCTS applied to card games +employer: Flybot Pte Ltd +css-class: mcts +tags: + - MCTS + - Monte Carlo Tree Search + - Clojure + - Card Game +articles: + - ["MCTS applied to card games", "../blog/article-mcts"] +image: + src: https://www.flybot.sg/assets/flybot-logo.png + src-dark: https://www.flybot.sg/assets/flybot-logo.png + alt: Flybot Logo +--- + At Flybot, I had the opportunity to work on a Monte Carlo Tree Search (MCTS) **Clojure** implementation for our card games. I combined `MCTS` with `domain knowledge` of our games to balance quality and performance of the plays. A few of our games implement the same `Game` protocol so the MCTS implementation is independent of the underlying game. + +++ + ## Rational At Flybot Pte Ltd, one of my projects was to develop a robot-player capable of playing multiple rounds of card games, such as the popular Chinese card game `big-two`. diff --git a/src/loicb/server/content/blog/port-clj-lib-to-clr.md b/src/loicb/server/content/blog/port-clj-lib-to-clr.md index ec82419..b497e2a 100644 --- a/src/loicb/server/content/blog/port-clj-lib-to-clr.md +++ b/src/loicb/server/content/blog/port-clj-lib-to-clr.md @@ -1,17 +1,28 @@ -#:post{:id "port-clj-lib-to-clr" - :page :blog - :date ["2022-04-08"] - :title "Port your Clojure lib to the CLR with MAGIC" - :repos [["Magic" "https://github.com/nasser/magic"] - ["Nostrand" "https://github.com/nasser/nostrand"]] - :css-class "blog-port-clj-lib" - :tags ["Clojure" "Compiler" "CLR" "Unity"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: port-clj-lib-to-clr +page: blog +date: + - 2022-04-08 +title: Port your Clojure lib to the CLR with MAGIC +css-class: blog-port-clj-lib +tags: + - Clojure + - Compiler + - CLR + - Unity +repos: + - ["Magic", "https://github.com/nasser/magic"] + - ["Nostrand", "https://github.com/nasser/nostrand"] +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + How to port your Clojure lib to the CLR. Then how to be build with the MAGIC compiler allowing you to obtain DLLs compatible with Unity (no DLR used by MAGIC). + +++ + In this article, I will show you: 1. how to handle CLR interop to prepare your Clojure code for the CLR diff --git a/src/loicb/server/content/blog/portfolio-website.md b/src/loicb/server/content/blog/portfolio-website.md index b0258ba..769f8f3 100644 --- a/src/loicb/server/content/blog/portfolio-website.md +++ b/src/loicb/server/content/blog/portfolio-website.md @@ -1,15 +1,28 @@ -#:post{:id "portfolio-clojurescript-spa" - :page :blog - :date ["2023-04-07" "2024-04-21"] - :home-page? true - :repos [["Portfolio" "https://github.com/skydread1/portfolio"]] - :title "Portfolio Website" - :css-class "portfolio" - :tags ["ClojureScript" "Figwheel" "Re-Frame" "Reagent" "Lasagna-pull" "Reitit" "Babashka"] - :image #:image{:src "/assets/loic-logo.png" - :src-dark "/assets/loic-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: portfolio-clojurescript-spa +page: blog +date: + - 2023-04-07 + - 2024-04-21 +title: Portfolio Website +home-page?: true +css-class: portfolio +tags: + - ClojureScript + - Figwheel + - Re-Frame + - Reagent + - Lasagna-pull + - Reitit + - Babashka +repos: + - ["Portfolio", "https://github.com/skydread1/portfolio"] +image: + src: /assets/loic-logo.png + src-dark: /assets/loic-logo.png + alt: Logo referencing Aperture Science +--- + This portfolio was implemented with **ClojureScript**. It is a Single Page Application that leverages [reagent](https://github.com/reagent-project/reagent), a minimalistic interface between ClojureScript and **React**. For frontend management, [re-frame](https://github.com/day8/re-frame) is used. To compile the cljs code and perform hot reloading, I used [figwheel-main](https://figwheel.org/). @@ -21,7 +34,9 @@ The markdown content is converted into [hiccup](https://github.com/weavejester/h Light/dark mode and code block syntax highlighted are supported. The app is deployed on **Netlify** via GitHub Actions every time a branch is merged to master. Build artifacts are generated during CI/CD and are not committed to the repository. + +++ + ## Rational This portfolio was implemented with **ClojureScript**. It is a Single Page Application that leverages [reagent](https://github.com/reagent-project/reagent), a minimalistic interface between ClojureScript and **React**. diff --git a/src/loicb/server/content/blog/props-recommender.md b/src/loicb/server/content/blog/props-recommender.md index ea2d5a7..c7bb370 100644 --- a/src/loicb/server/content/blog/props-recommender.md +++ b/src/loicb/server/content/blog/props-recommender.md @@ -1,15 +1,24 @@ -#:post{:id "game-challenge-recommender" - :page :blog - :home-page? true - :employer "Flybot Pte Ltd" - :date ["2023-08-08"] - :title "Challenge Recommender" - :tags ["Clojure" "Kafka" "Datomic" "Kubernetes" "AWS EKS"] - :css-class "props-recommender" - :image #:image{:src "https://www.flybot.sg/assets/flybot-logo.png" - :src-dark "https://www.flybot.sg/assets/flybot-logo.png" - :alt "Flybot Logo"}} -+++ +--- +id: game-challenge-recommender +page: blog +date: + - 2023-08-08 +title: Challenge Recommender +home-page?: true +employer: Flybot Pte Ltd +css-class: props-recommender +tags: + - Clojure + - Kafka + - Datomic + - Kubernetes + - AWS EKS +image: + src: https://www.flybot.sg/assets/flybot-logo.png + src-dark: https://www.flybot.sg/assets/flybot-logo.png + alt: Flybot Logo +--- + I worked on a recommender system stack that suggests user-based challenges to [Golden Island](https://www.80166.com/)'s players. `Golden Island` is a card/board game platforms that offers a dozen of games including *Pǎo DΓ© KuΓ i* (θ·‘εΎ—εΏ«), *Dou dizhu* (ι¬₯地主), *Mahjong* etc. Some game operators are already creating props (challenges) that are personalized in the sense that they apply only to a subset of users (game, levels, coin balance etc) in response to a subset of events (game, bet size etc). @@ -17,7 +26,9 @@ Some game operators are already creating props (challenges) that are personalize The end goal was to ease the job of game operators by taking care of `when` to recommend challenges and to `whom`, so the game operators can focus on the `what` the challenges could be and have constant performance feedback. The recommender is made of **Clojure** applications deployed in PODs in `AWS EKS` that consume events from `kafka` topics and produces props (personalized challenges) to a dedicated kafka topic. It uses `Datomic` as storage solution. The app can query multiple recommender engines to perform A/B testing, meaning that we can run 2 recommenders at once and compare their performance. + +++ + ## Rational I worked on a recommender system stack that suggests user-based challenges to [Golden Island](https://www.80166.com/)'s players. `Golden Island` is a card/board game platforms that offers a dozen of games including *Pǎo DΓ© KuΓ i* (θ·‘εΎ—εΏ«), *Dou dizhu* (ι¬₯地主), *Mahjong* etc. diff --git a/src/loicb/server/content/blog/pwd-manager+2fa.md b/src/loicb/server/content/blog/pwd-manager+2fa.md index c622130..de10be8 100644 --- a/src/loicb/server/content/blog/pwd-manager+2fa.md +++ b/src/loicb/server/content/blog/pwd-manager+2fa.md @@ -1,15 +1,25 @@ -#:post{:id "pwd-manager-2FA" - :page :blog - :date ["2025-06-27"] - :title "Password manager + 2FA" - :css-class "pwd-man-2fa" - :tags ["Bitwarden" "Password Manager" "2FA" "Security"] - :image #:image{:src "/assets/pwd-man-2fa/blue-shield.png" - :src-dark "/assets/pwd-man-2fa/white-shield.png" - :alt "Bitwarden Logo"}} -+++ +--- +id: pwd-manager-2FA +page: blog +date: + - 2025-06-27 +title: Password manager + 2FA +css-class: pwd-man-2fa +tags: + - Bitwarden + - Password Manager + - 2FA + - Security +image: + src: /assets/pwd-man-2fa/blue-shield.png + src-dark: /assets/pwd-man-2fa/white-shield.png + alt: Bitwarden Logo +--- + Using a password manager with strong passwords and enabling 2FA for critical accounts are simple yet powerful steps to protect your online security. + +++ + ## Rationale One of the best ways to store and organize your online credentials is to use a password manager. diff --git a/src/loicb/server/content/blog/reagent-native-app.md b/src/loicb/server/content/blog/reagent-native-app.md index 914a6da..c048e90 100644 --- a/src/loicb/server/content/blog/reagent-native-app.md +++ b/src/loicb/server/content/blog/reagent-native-app.md @@ -1,16 +1,27 @@ -#:post{:id "reagent-native-app" - :page :blog - :date ["2023-02-03"] - :title "Reagent React Native Mobile App" - :repos [["Flybot" "https://github.com/skydread1/flybot.sg"]] - :css-class "blog-reagent-native" - :tags ["Clojure" "React Native" "Reagent" "Figwheel"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: reagent-native-app +page: blog +date: + - 2023-02-03 +title: Reagent React Native Mobile App +css-class: blog-reagent-native +tags: + - Clojure + - React Native + - Reagent + - Figwheel +repos: + - ["Flybot", "https://github.com/skydread1/flybot.sg"] +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + Reagent React Native mobile app reusing re-frame logic from exiting web client. + +++ + ## Prerequisites This project is stored alongside the backend and the web frontend in the mono-repo: [skydread1/flybot.sg](https://github.com/skydread1/flybot.sg) diff --git a/src/loicb/server/content/blog/redirect-domain-subdomain.md b/src/loicb/server/content/blog/redirect-domain-subdomain.md index 06919a1..8a4f740 100644 --- a/src/loicb/server/content/blog/redirect-domain-subdomain.md +++ b/src/loicb/server/content/blog/redirect-domain-subdomain.md @@ -1,15 +1,25 @@ -#:post{:id "redirect-domain-to-subdomain" - :page :blog - :date ["2023-11-09"] - :title "Redirecting Domain to Subdomain using AWS ALB+NLB" - :css-class "blog-redirect-domain" - :tags ["AWS" "Load Balancers" "DNS" "GoDaddy"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: redirect-domain-to-subdomain +page: blog +date: + - 2023-11-09 +title: Redirecting Domain to Subdomain using AWS ALB+NLB +css-class: blog-redirect-domain +tags: + - AWS + - Load Balancers + - DNS + - GoDaddy +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + Domain to subdomain forwarding using AWS ALB+NLB setup. + +++ + ## Context My goal was to redirect the domain `flybot.sg` to the subdomain `www.flybot.sg`. diff --git a/src/loicb/server/content/blog/testing-in-clojure.md b/src/loicb/server/content/blog/testing-in-clojure.md index 9f0394f..1bdb81c 100644 --- a/src/loicb/server/content/blog/testing-in-clojure.md +++ b/src/loicb/server/content/blog/testing-in-clojure.md @@ -1,15 +1,27 @@ -#:post{:id "testing-in-clojure" - :page :blog - :date ["2024-08-10"] - :title "Testing in Clojure" - :css-class "testing-in-clojure" - :tags ["Clojure" "Kaocha" "Malli" "Rich Comment Tests" "Instrumentation" "Data validation/generation"] - :image #:image{:src "/assets/loic-blog-logo.png" - :src-dark "/assets/loic-blog-logo.png" - :alt "Logo referencing Aperture Science"}} -+++ +--- +id: testing-in-clojure +page: blog +date: + - 2024-08-10 +title: Testing in Clojure +css-class: testing-in-clojure +tags: + - Clojure + - Kaocha + - Malli + - Rich Comment Tests + - Instrumentation + - Data validation/generation +image: + src: /assets/loic-blog-logo.png + src-dark: /assets/loic-blog-logo.png + alt: Logo referencing Aperture Science +--- + Introducing some popular testing tools to developers new to Clojure. Highlight solutions for how to do unit testing with Rich Comment Tests, data validation and generative testing with Malli, running test suites and metrics with Kaocha and how to do integration testing using external containerized services. + +++ + ## Introduction This article introduces effective testing libraries and methods for those new to Clojure. @@ -620,4 +632,3 @@ I highlighted some common testing tools and methods that the Clojure community u If you are starting your journey as a Clojure developer, I hope you can appreciate the quality of open-source testing libraries we have access to. Also, please remember that keeping things pure is the key to easy testing and debugging; a luxury not so common in the programming world. Inevitably, you will need to deal with side effects but isolate them as much as you can to make your code robust and your tests straight forward. Finally, there are some tools I didn’t mention to keep things short so feel free to explore what the Clojure community has to offer. The last advice I would give is to not try to use too many tools or only the shiny new ones you might find. Keep things simple and evaluate if a library is worth being added to your deps. - diff --git a/test/loicb/server/md_test.clj b/test/loicb/server/md_test.clj index c2e3aa3..e2ebadd 100644 --- a/test/loicb/server/md_test.clj +++ b/test/loicb/server/md_test.clj @@ -1,8 +1,60 @@ (ns loicb.server.md-test - (:require [clojure.test :refer [deftest testing is]] - [loicb.server.md :as sut])) + (:require + [clojure.string :as s] + [clojure.test :refer [deftest is testing]] + [loicb.server.md :as sut] + [markdown.core :as md])) -(deftest load-posts-macro - (testing "All marldown files are loaded successfully" - (is (->> (sut/load-posts-macro) - (not-any? :errors))))) +;; Test data +(def yaml-frontmatter-md + "--- +title: Test Post +date: 2024-05-12 +page: blog +id: test-post +tags: + - Clojure + - Test +css-class: test-class +image: + src: /test.png + src-dark: /test-dark.png + alt: Test image +--- + +This is the short description. + ++++ + +# Main Content + +This is the main content of the post.") + +(deftest markdown-clj-yaml-support + (testing "markdown-clj can extract YAML metadata" + (let [metadata (md/md-to-meta yaml-frontmatter-md)] + (is (map? metadata)) + (is (= "Test Post" (:title metadata))) + (is (inst? (:date metadata))))) + + (testing "markdown-clj separates metadata from content" + (let [result (md/md-to-html-string-with-meta yaml-frontmatter-md) + html (:html result)] + (is (string? html)) + (is (s/includes? html "

Main Content

"))))) + +(deftest load-post-yaml-format + (testing "load-post can parse YAML frontmatter format" + (let [temp-file (java.io.File/createTempFile "test-yaml" ".md") + _ (spit temp-file yaml-frontmatter-md) + result (sut/load-post (.getPath temp-file))] + (.delete temp-file) + (is (map? result)) + (is (nil? (:error result))) + (is (= "Test Post" (:post/title result))) + (is (= :blog (:post/page result))) + (is (vector? (:post/date result))) + (is (= ["Clojure" "Test"] (:post/tags result))) + (is (map? (:post/image result))) + (is (string? (:post/md-content result))) + (is (string? (:post/md-content-short result)))))) From 3e6cf529b61892b7126e308319c510a2af84b476 Mon Sep 17 00:00:00 2001 From: skydread1 Date: Mon, 13 Oct 2025 13:57:31 +0800 Subject: [PATCH 05/11] Add rss-feed key --- src/loicb/server/content/blog/blog-django.md | 4 +++- src/loicb/server/content/blog/clj-in-unity.md | 4 +++- src/loicb/server/content/blog/clj-mono-repo.md | 4 +++- src/loicb/server/content/blog/datomic-setup.md | 4 +++- src/loicb/server/content/blog/deploy-clj-app-to-aws.md | 4 +++- src/loicb/server/content/blog/flybot-card-games.md | 4 +++- src/loicb/server/content/blog/flybot-mobile-app.md | 4 +++- src/loicb/server/content/blog/flybot-stats-rama.md | 4 +++- src/loicb/server/content/blog/flybot-website.md | 4 +++- src/loicb/server/content/blog/fun-map-flybot.md | 4 +++- src/loicb/server/content/blog/fun-map.md | 4 +++- src/loicb/server/content/blog/hibou-platform.md | 4 +++- src/loicb/server/content/blog/juxt-tick-lib.md | 4 +++- src/loicb/server/content/blog/lasagna-pull-flybot.md | 4 +++- src/loicb/server/content/blog/lasagna-pull.md | 4 +++- src/loicb/server/content/blog/magic-nostrand.md | 4 +++- src/loicb/server/content/blog/mcts-card-games.md | 4 +++- src/loicb/server/content/blog/mcts.md | 4 +++- src/loicb/server/content/blog/port-clj-lib-to-clr.md | 4 +++- src/loicb/server/content/blog/portfolio-website.md | 4 +++- src/loicb/server/content/blog/props-recommender.md | 4 +++- src/loicb/server/content/blog/reagent-native-app.md | 4 +++- src/loicb/server/content/blog/testing-in-clojure.md | 4 +++- 23 files changed, 69 insertions(+), 23 deletions(-) diff --git a/src/loicb/server/content/blog/blog-django.md b/src/loicb/server/content/blog/blog-django.md index 5adb20c..05adc7d 100644 --- a/src/loicb/server/content/blog/blog-django.md +++ b/src/loicb/server/content/blog/blog-django.md @@ -13,6 +13,8 @@ tags: - RDS PostgreSQL - S3 - AWS +rss-feeds: + - clojure repos: - ["Blog", "https://github.com/skydread1/blog"] articles: @@ -98,4 +100,4 @@ However I moved the blog content to my clojure SPA instead because after my AWS I wrote an article on how I deployed the Django app in AWS (link at the top of the page). -Have a look at the repo [README](https://github.com/skydread1/blog/blob/master/README.md) for more information. +Have a look at the repo [README](https://github.com/skydread1/blog/blob/master/README.md) for more information. \ No newline at end of file diff --git a/src/loicb/server/content/blog/clj-in-unity.md b/src/loicb/server/content/blog/clj-in-unity.md index 7b94c7f..dc7c626 100644 --- a/src/loicb/server/content/blog/clj-in-unity.md +++ b/src/loicb/server/content/blog/clj-in-unity.md @@ -10,6 +10,8 @@ tags: - Compiler - CLR - Unity +rss-feeds: + - clojure repos: - ["Magic", "https://github.com/nasser/magic"] - ["Nostrand", "https://github.com/nasser/nostrand"] @@ -334,4 +336,4 @@ Import your packages in Unity: nuget restore ``` -`Magic.Unity` is the Magic runtime for Unity and is already nuget packaged on its public repo +`Magic.Unity` is the Magic runtime for Unity and is already nuget packaged on its public repo \ No newline at end of file diff --git a/src/loicb/server/content/blog/clj-mono-repo.md b/src/loicb/server/content/blog/clj-mono-repo.md index 66d72cf..80256cb 100644 --- a/src/loicb/server/content/blog/clj-mono-repo.md +++ b/src/loicb/server/content/blog/clj-mono-repo.md @@ -10,6 +10,8 @@ tags: - Reagent - Figwheel - Mono Repo +rss-feeds: + - clojure image: src: /assets/loic-blog-logo.png src-dark: /assets/loic-blog-logo.png @@ -200,4 +202,4 @@ Using `:extra-paths` and `:extra-deps` in deps.edn is important because it preve Adding namespace to the aliases make the distinction between backend, common and client (web and mobile) clearer. -Using `deps` jack-in for server only work and `deps+figwheel` for frontend work is made easy using `calva` in VSCode (work in other editors as well). +Using `deps` jack-in for server only work and `deps+figwheel` for frontend work is made easy using `calva` in VSCode (work in other editors as well). \ No newline at end of file diff --git a/src/loicb/server/content/blog/datomic-setup.md b/src/loicb/server/content/blog/datomic-setup.md index a720c82..575e958 100644 --- a/src/loicb/server/content/blog/datomic-setup.md +++ b/src/loicb/server/content/blog/datomic-setup.md @@ -10,6 +10,8 @@ tags: - Datomic - Cassandra - Docker +rss-feeds: + - clojure image: src: /assets/loic-blog-logo.png src-dark: /assets/loic-blog-logo.png @@ -293,4 +295,4 @@ alt-host=datomicdb storage-access=remote ``` -After updating the transactor properties, you should be able to see the app running on port 8123 and be able to perform transactions as expected. +After updating the transactor properties, you should be able to see the app running on port 8123 and be able to perform transactions as expected. \ No newline at end of file diff --git a/src/loicb/server/content/blog/deploy-clj-app-to-aws.md b/src/loicb/server/content/blog/deploy-clj-app-to-aws.md index 1098c13..e727637 100644 --- a/src/loicb/server/content/blog/deploy-clj-app-to-aws.md +++ b/src/loicb/server/content/blog/deploy-clj-app-to-aws.md @@ -9,6 +9,8 @@ tags: - Clojure - AWS - Full-Stack +rss-feeds: + - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] image: @@ -253,4 +255,4 @@ For the root domain `flybot.sg`, we use a `A` record for `@` resolving to the st ## Learn More -You can have a look at the open-source repo: [skydread1/flybot.sg](https://github.com/skydread1/flybot.sg) +You can have a look at the open-source repo: [skydread1/flybot.sg](https://github.com/skydread1/flybot.sg) \ No newline at end of file diff --git a/src/loicb/server/content/blog/flybot-card-games.md b/src/loicb/server/content/blog/flybot-card-games.md index 571ef5f..829edb0 100644 --- a/src/loicb/server/content/blog/flybot-card-games.md +++ b/src/loicb/server/content/blog/flybot-card-games.md @@ -13,6 +13,8 @@ tags: - Clojure Spec - Magic Compiler - Interop CLR +rss-feeds: + - clojure repos: - ["Magic", "https://github.com/nasser/magic"] articles: @@ -104,4 +106,4 @@ Flybot Ptd Ltd All Right Reserved ### Magic compiler -The magic compiler is open source and you can read more about my contribution in the dedicated section. +The magic compiler is open source and you can read more about my contribution in the dedicated section. \ No newline at end of file diff --git a/src/loicb/server/content/blog/flybot-mobile-app.md b/src/loicb/server/content/blog/flybot-mobile-app.md index a84431e..8a09f51 100644 --- a/src/loicb/server/content/blog/flybot-mobile-app.md +++ b/src/loicb/server/content/blog/flybot-mobile-app.md @@ -12,6 +12,8 @@ tags: - Figwheel - Re-Frame - Reagent React Native +rss-feeds: + - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] articles: @@ -112,4 +114,4 @@ The app has not been deployed on any Store yet. You can learn more about how the mobile app was setup in the article linked at the top of the page. -Also, feel free to visit [flybot.sg](https://www.flybot.sg/) and especially the [blog](https://www.flybot.sg/blog). +Also, feel free to visit [flybot.sg](https://www.flybot.sg/) and especially the [blog](https://www.flybot.sg/blog). \ No newline at end of file diff --git a/src/loicb/server/content/blog/flybot-stats-rama.md b/src/loicb/server/content/blog/flybot-stats-rama.md index f72233d..ffa1405 100644 --- a/src/loicb/server/content/blog/flybot-stats-rama.md +++ b/src/loicb/server/content/blog/flybot-stats-rama.md @@ -13,6 +13,8 @@ tags: - AWS - EKS - Kafka +rss-feeds: + - clojure image: src: https://www.flybot.sg/assets/flybot-logo.png src-dark: https://www.flybot.sg/assets/flybot-logo.png @@ -154,4 +156,4 @@ The main limitations are the following: ## Next goal -The next step is to have a generic `analytics` module builder with clear configs. And we want these configs to be easily reusable for the API/frontend validation schema for the different queries. +The next step is to have a generic `analytics` module builder with clear configs. And we want these configs to be easily reusable for the API/frontend validation schema for the different queries. \ No newline at end of file diff --git a/src/loicb/server/content/blog/flybot-website.md b/src/loicb/server/content/blog/flybot-website.md index 85fd259..245b2d9 100644 --- a/src/loicb/server/content/blog/flybot-website.md +++ b/src/loicb/server/content/blog/flybot-website.md @@ -18,6 +18,8 @@ tags: - Fun-map - Datalevin - Reitit +rss-feeds: + - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] articles: @@ -173,4 +175,4 @@ You can read more about how I deployed the app to AWS in this article: [How to d ## Learn more -Feel free to visit [flybot.sg](https://www.flybot.sg/) and especially the [blog](https://www.flybot.sg/blog). +Feel free to visit [flybot.sg](https://www.flybot.sg/) and especially the [blog](https://www.flybot.sg/blog). \ No newline at end of file diff --git a/src/loicb/server/content/blog/fun-map-flybot.md b/src/loicb/server/content/blog/fun-map-flybot.md index c92aaf7..18da120 100644 --- a/src/loicb/server/content/blog/fun-map-flybot.md +++ b/src/loicb/server/content/blog/fun-map-flybot.md @@ -10,6 +10,8 @@ tags: - System - Component - Deps Injection +rss-feeds: + - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] image: @@ -269,4 +271,4 @@ So here is the `figwheel-system` : The `figheel-handler` is the value of the key `:reitit-router` of our running system. -So the system is started first via `touch` and its handler is provided to the servers figwheel starts that will be running while we work on our frontend. +So the system is started first via `touch` and its handler is provided to the servers figwheel starts that will be running while we work on our frontend. \ No newline at end of file diff --git a/src/loicb/server/content/blog/fun-map.md b/src/loicb/server/content/blog/fun-map.md index 8f69502..a024485 100644 --- a/src/loicb/server/content/blog/fun-map.md +++ b/src/loicb/server/content/blog/fun-map.md @@ -10,6 +10,8 @@ tags: - System - Component - Deps Injection +rss-feeds: + - clojure image: src: /assets/loic-blog-logo.png src-dark: /assets/loic-blog-logo.png @@ -254,4 +256,4 @@ fun-map also support other features such as function call tracing, value caching ## Fun-Map applied to flybot.sg -To see Fun Map in action, refer to the doc [Fun-Map applied to flybot.sg](https://www.loicb.dev/blog/fun-map-applied-to-flybot). +To see Fun Map in action, refer to the doc [Fun-Map applied to flybot.sg](https://www.loicb.dev/blog/fun-map-applied-to-flybot). \ No newline at end of file diff --git a/src/loicb/server/content/blog/hibou-platform.md b/src/loicb/server/content/blog/hibou-platform.md index 7fd8645..9109600 100644 --- a/src/loicb/server/content/blog/hibou-platform.md +++ b/src/loicb/server/content/blog/hibou-platform.md @@ -13,6 +13,8 @@ tags: - Analytics - Mono Repo - Full-Stack +rss-feeds: + - clojure image: src: https://www.flybot.sg/assets/flybot-logo.png src-dark: https://www.flybot.sg/assets/flybot-logo.png @@ -188,4 +190,4 @@ By treating everything as data (from module definitions to API endpoints to UI f The combination of Rama for distributed stream processing, lasagna-pull for pure data APIs, and Replicant for reactive UIs created a coherent full-stack platform that leverages Clojure's strengths at every layer. -The key was resisting the temptation to optimize for the first client and instead investing time upfront to build generic, configurable components. +The key was resisting the temptation to optimize for the first client and instead investing time upfront to build generic, configurable components. \ No newline at end of file diff --git a/src/loicb/server/content/blog/juxt-tick-lib.md b/src/loicb/server/content/blog/juxt-tick-lib.md index a330ff1..6eb33e1 100644 --- a/src/loicb/server/content/blog/juxt-tick-lib.md +++ b/src/loicb/server/content/blog/juxt-tick-lib.md @@ -10,6 +10,8 @@ tags: - Java 8 time - Tick - Duration/Period +rss-feeds: + - clojure image: src: /assets/loic-blog-logo.png src-dark: /assets/loic-blog-logo.png @@ -320,4 +322,4 @@ The **time since epoch** is the universal *computer-friendly* of representing ti A `duration` measures an amount of time using time-based values whereas a `period` uses date-based (calendar) values. -Finally, for Clojure developers, I highly recommend using `juxt/tick` as it allows us to handle time efficiently (conversion, operations) and elegantly (readable, as values) and I use it in several of my projects. It is also of course possible to do interop with the `java.time.Instant` class directly if you prefer. +Finally, for Clojure developers, I highly recommend using `juxt/tick` as it allows us to handle time efficiently (conversion, operations) and elegantly (readable, as values) and I use it in several of my projects. It is also of course possible to do interop with the `java.time.Instant` class directly if you prefer. \ No newline at end of file diff --git a/src/loicb/server/content/blog/lasagna-pull-flybot.md b/src/loicb/server/content/blog/lasagna-pull-flybot.md index 0864234..ea118fa 100644 --- a/src/loicb/server/content/blog/lasagna-pull-flybot.md +++ b/src/loicb/server/content/blog/lasagna-pull-flybot.md @@ -8,6 +8,8 @@ css-class: blog-lasagna-pull-flybot tags: - Clojure - Pull Pattern +rss-feeds: + - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] image: @@ -419,4 +421,4 @@ Having no side-effects at all makes it way easier to tests and debug and it is m Finally, the `ring-handler` will be the component responsible to **execute** all the side effects at once. -So the `saturn-handler` purpose was to be sure the data is being pulled properly, validated using malli, and that the side effects descriptions are gathered in one place to be executed later on. +So the `saturn-handler` purpose was to be sure the data is being pulled properly, validated using malli, and that the side effects descriptions are gathered in one place to be executed later on. \ No newline at end of file diff --git a/src/loicb/server/content/blog/lasagna-pull.md b/src/loicb/server/content/blog/lasagna-pull.md index 8b14f4d..a05bb62 100644 --- a/src/loicb/server/content/blog/lasagna-pull.md +++ b/src/loicb/server/content/blog/lasagna-pull.md @@ -9,6 +9,8 @@ tags: - Clojure - Pull Pattern - Malli +rss-feeds: + - clojure image: src: /assets/loic-blog-logo.png src-dark: /assets/loic-blog-logo.png @@ -211,4 +213,4 @@ This context can help you gathering information from the query and apply a funct ## Lasagna Pull applied to flybot.sg -To see Lasagna Pull in action, refer to the doc [Lasagna Pull applied to flybot.sg](https://www.loicb.dev/blog/lasagna-pull-applied-to-flybot). +To see Lasagna Pull in action, refer to the doc [Lasagna Pull applied to flybot.sg](https://www.loicb.dev/blog/lasagna-pull-applied-to-flybot). \ No newline at end of file diff --git a/src/loicb/server/content/blog/magic-nostrand.md b/src/loicb/server/content/blog/magic-nostrand.md index 6cf7097..281d54f 100644 --- a/src/loicb/server/content/blog/magic-nostrand.md +++ b/src/loicb/server/content/blog/magic-nostrand.md @@ -17,6 +17,8 @@ tags: - Interop - NuGet - Unity +rss-feeds: + - clojure repos: - ["Magic", "https://github.com/nasser/magic"] - ["Nostrand", "https://github.com/nasser/nostrand"] @@ -145,4 +147,4 @@ Since all the Clojure libraries and the Magic.Unity were packaged via nugget and ## Learn more -You can learn more about `Magic` in my blog articles listed at the top of the page. +You can learn more about `Magic` in my blog articles listed at the top of the page. \ No newline at end of file diff --git a/src/loicb/server/content/blog/mcts-card-games.md b/src/loicb/server/content/blog/mcts-card-games.md index 3c81a08..02a0bb7 100644 --- a/src/loicb/server/content/blog/mcts-card-games.md +++ b/src/loicb/server/content/blog/mcts-card-games.md @@ -8,6 +8,8 @@ css-class: blog-post-mcts tags: - Clojure - MCTS +rss-feeds: + - clojure image: src: /assets/loic-blog-logo.png src-dark: /assets/loic-blog-logo.png @@ -201,4 +203,4 @@ The details of this `game-plan` are confidential for obvious reasons. Having an hybrid approach, meaning using a `game-plan` for the first moves of the game when the possible plays are too numerous, and then use MCTS at the end of the game allowed us to have a decent AI we can use. -As of the time I write this article, the implementation is being tested (as part of a bigger system) and not yet in production. +As of the time I write this article, the implementation is being tested (as part of a bigger system) and not yet in production. \ No newline at end of file diff --git a/src/loicb/server/content/blog/mcts.md b/src/loicb/server/content/blog/mcts.md index 4bf4a8e..f9077d0 100644 --- a/src/loicb/server/content/blog/mcts.md +++ b/src/loicb/server/content/blog/mcts.md @@ -12,6 +12,8 @@ tags: - Monte Carlo Tree Search - Clojure - Card Game +rss-feeds: + - clojure articles: - ["MCTS applied to card games", "../blog/article-mcts"] image: @@ -85,4 +87,4 @@ One limitation of MCTS is its tendency to explore non-promising branches. To ove In conclusion, the fusion of MCTS and domain knowledge has enabled us to create a functional AI for big-two that can replace human players and offer different levels of difficulty. As of the time of writing this article, the implementation is currently undergoing testing as part of a larger system and is not yet in production. This hybrid approach represents a promising solution for developing robust AI players in complex card games. -You can learn more about the implementation strategy in my blog article listed at the top of the page. +You can learn more about the implementation strategy in my blog article listed at the top of the page. \ No newline at end of file diff --git a/src/loicb/server/content/blog/port-clj-lib-to-clr.md b/src/loicb/server/content/blog/port-clj-lib-to-clr.md index b497e2a..6ff7d6a 100644 --- a/src/loicb/server/content/blog/port-clj-lib-to-clr.md +++ b/src/loicb/server/content/blog/port-clj-lib-to-clr.md @@ -10,6 +10,8 @@ tags: - Compiler - CLR - Unity +rss-feeds: + - clojure repos: - ["Magic", "https://github.com/nasser/magic"] - ["Nostrand", "https://github.com/nasser/nostrand"] @@ -349,4 +351,4 @@ My fork uses reader conditionals so it can be run and tested in both JVM and CLR ## Learn more Now that your library is compiled to dotnet, you can learn how to package it to nuget, push it in to your host repo and import in Unity in this article: -- [Pack, Push and Import Clojure to Unity](https://www.loicb.dev/blog/clojure-in-unity). +- [Pack, Push and Import Clojure to Unity](https://www.loicb.dev/blog/clojure-in-unity). \ No newline at end of file diff --git a/src/loicb/server/content/blog/portfolio-website.md b/src/loicb/server/content/blog/portfolio-website.md index 769f8f3..b2bc15f 100644 --- a/src/loicb/server/content/blog/portfolio-website.md +++ b/src/loicb/server/content/blog/portfolio-website.md @@ -15,6 +15,8 @@ tags: - Lasagna-pull - Reitit - Babashka +rss-feeds: + - clojure repos: - ["Portfolio", "https://github.com/skydread1/portfolio"] image: @@ -195,4 +197,4 @@ The website is hosted on Netlify (US servers) and deployed via GitHub Actions us ## Learn More -Have a look at the repo [README](https://github.com/skydread1/portfolio/blob/master/README.md) for more information. +Have a look at the repo [README](https://github.com/skydread1/portfolio/blob/master/README.md) for more information. \ No newline at end of file diff --git a/src/loicb/server/content/blog/props-recommender.md b/src/loicb/server/content/blog/props-recommender.md index c7bb370..a5daade 100644 --- a/src/loicb/server/content/blog/props-recommender.md +++ b/src/loicb/server/content/blog/props-recommender.md @@ -13,6 +13,8 @@ tags: - Datomic - Kubernetes - AWS EKS +rss-feeds: + - clojure image: src: https://www.flybot.sg/assets/flybot-logo.png src-dark: https://www.flybot.sg/assets/flybot-logo.png @@ -138,4 +140,4 @@ We could recap the steps I took like this: - Extra the model training and queue computations from the kafka logic 5. Performance - Once the app running fine, start improving recommender algo - - Make the kafka app suitable to use multiple recommender engines and improve data ingestion + - Make the kafka app suitable to use multiple recommender engines and improve data ingestion \ No newline at end of file diff --git a/src/loicb/server/content/blog/reagent-native-app.md b/src/loicb/server/content/blog/reagent-native-app.md index c048e90..2e9efcd 100644 --- a/src/loicb/server/content/blog/reagent-native-app.md +++ b/src/loicb/server/content/blog/reagent-native-app.md @@ -10,6 +10,8 @@ tags: - React Native - Reagent - Figwheel +rss-feeds: + - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] image: @@ -446,4 +448,4 @@ I hope that this unusual mobile app stack made you want to consider `ClojureScri It is important to note that the state management logic (re-frame) is the same at 90% for both the web app and the mobile app which is very convenient. -Finally, the web app is deployed but not the mobile app. All the codebase is open-source so feel free to take inspiration. +Finally, the web app is deployed but not the mobile app. All the codebase is open-source so feel free to take inspiration. \ No newline at end of file diff --git a/src/loicb/server/content/blog/testing-in-clojure.md b/src/loicb/server/content/blog/testing-in-clojure.md index 1bdb81c..4bfe194 100644 --- a/src/loicb/server/content/blog/testing-in-clojure.md +++ b/src/loicb/server/content/blog/testing-in-clojure.md @@ -12,6 +12,8 @@ tags: - Rich Comment Tests - Instrumentation - Data validation/generation +rss-feeds: + - clojure image: src: /assets/loic-blog-logo.png src-dark: /assets/loic-blog-logo.png @@ -631,4 +633,4 @@ I highlighted some common testing tools and methods that the Clojure community u If you are starting your journey as a Clojure developer, I hope you can appreciate the quality of open-source testing libraries we have access to. Also, please remember that keeping things pure is the key to easy testing and debugging; a luxury not so common in the programming world. Inevitably, you will need to deal with side effects but isolate them as much as you can to make your code robust and your tests straight forward. -Finally, there are some tools I didn’t mention to keep things short so feel free to explore what the Clojure community has to offer. The last advice I would give is to not try to use too many tools or only the shiny new ones you might find. Keep things simple and evaluate if a library is worth being added to your deps. +Finally, there are some tools I didn’t mention to keep things short so feel free to explore what the Clojure community has to offer. The last advice I would give is to not try to use too many tools or only the shiny new ones you might find. Keep things simple and evaluate if a library is worth being added to your deps. \ No newline at end of file From f114b98db64b62b94077be3dff57388c3f905aa6 Mon Sep 17 00:00:00 2001 From: skydread1 Date: Mon, 13 Oct 2025 14:09:59 +0800 Subject: [PATCH 06/11] Md files now have a TLDR h2 for vignette --- src/loicb/server/content/about/aboutme.md | 5 +++-- src/loicb/server/content/blog/blog-django.md | 7 +++++-- src/loicb/server/content/blog/cd-sl-p-method.md | 5 ++++- src/loicb/server/content/blog/clj-in-unity.md | 7 +++++-- src/loicb/server/content/blog/clj-mono-repo.md | 7 +++++-- src/loicb/server/content/blog/datomic-setup.md | 7 +++++-- src/loicb/server/content/blog/deploy-clj-app-to-aws.md | 7 +++++-- src/loicb/server/content/blog/deploy-django-beanstalk.md | 5 ++++- src/loicb/server/content/blog/flybot-card-games.md | 7 +++++-- src/loicb/server/content/blog/flybot-mobile-app.md | 7 +++++-- src/loicb/server/content/blog/flybot-stats-rama.md | 7 +++++-- src/loicb/server/content/blog/flybot-website.md | 7 +++++-- src/loicb/server/content/blog/fun-map-flybot.md | 7 +++++-- src/loicb/server/content/blog/fun-map.md | 7 +++++-- src/loicb/server/content/blog/git-workflows.md | 5 ++++- src/loicb/server/content/blog/hibou-platform.md | 7 +++++-- src/loicb/server/content/blog/juxt-tick-lib.md | 7 +++++-- src/loicb/server/content/blog/lasagna-pull-flybot.md | 7 +++++-- src/loicb/server/content/blog/lasagna-pull.md | 7 +++++-- src/loicb/server/content/blog/magic-nostrand.md | 7 +++++-- src/loicb/server/content/blog/mcts-card-games.md | 7 +++++-- src/loicb/server/content/blog/mcts.md | 7 +++++-- src/loicb/server/content/blog/port-clj-lib-to-clr.md | 7 +++++-- src/loicb/server/content/blog/portfolio-website.md | 7 +++++-- src/loicb/server/content/blog/props-recommender.md | 7 +++++-- src/loicb/server/content/blog/pwd-manager+2fa.md | 5 ++++- src/loicb/server/content/blog/reagent-native-app.md | 7 +++++-- src/loicb/server/content/blog/redirect-domain-subdomain.md | 5 ++++- src/loicb/server/content/blog/testing-in-clojure.md | 7 +++++-- 29 files changed, 138 insertions(+), 53 deletions(-) diff --git a/src/loicb/server/content/about/aboutme.md b/src/loicb/server/content/about/aboutme.md index 1e1955e..7b8ce2a 100644 --- a/src/loicb/server/content/about/aboutme.md +++ b/src/loicb/server/content/about/aboutme.md @@ -23,10 +23,9 @@ image: alt: Loic Logo --- +## TLDR -+++ - ## In short πŸ‘‹πŸ» Hi, I'm Loic Blanchard, a Staff Software Engineer based in Singapore with over 5 years of experience in building scalable software solutions. My expertise lies in designing and leading the development of high-quality software systems, primarily using the Functional Programming language **Clojure** and its ecosystem. I am currently leading the engineering team at [Flybot Pte Ltd](https://www.flybot.sg/). @@ -65,3 +64,5 @@ Finally, I am currently working on a data aggregator using Red Planet Labs's [RA ### Certifications - **AWS**: Solutions Architect - Associate + + diff --git a/src/loicb/server/content/blog/blog-django.md b/src/loicb/server/content/blog/blog-django.md index 05adc7d..e59d79d 100644 --- a/src/loicb/server/content/blog/blog-django.md +++ b/src/loicb/server/content/blog/blog-django.md @@ -25,6 +25,8 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + I developed a tech blog in **python** using the `Django` framework. Thus, it is Server-Side Rendered. The blog was deployed on AWS `Beanstalk`, the static files were served from an AWS `S3` bucket, and the production data was stored in an AWS `RDS Postgres` database. @@ -33,7 +35,6 @@ It used `HTMX` for the search bar. After the AWS free tier expired, I moved the tech blog to the ClojureScript SPA. -+++ ## Rational @@ -100,4 +101,6 @@ However I moved the blog content to my clojure SPA instead because after my AWS I wrote an article on how I deployed the Django app in AWS (link at the top of the page). -Have a look at the repo [README](https://github.com/skydread1/blog/blob/master/README.md) for more information. \ No newline at end of file +Have a look at the repo [README](https://github.com/skydread1/blog/blob/master/README.md) for more information. + + diff --git a/src/loicb/server/content/blog/cd-sl-p-method.md b/src/loicb/server/content/blog/cd-sl-p-method.md index e588529..1472094 100644 --- a/src/loicb/server/content/blog/cd-sl-p-method.md +++ b/src/loicb/server/content/blog/cd-sl-p-method.md @@ -16,9 +16,10 @@ image: alt: ProtonMail Logo --- +## TLDR + CD-SL-P (Custom Domain + SimpleLogin + Proton) method to organize your email flow. -+++ ## πŸ‘οΈ Rationale @@ -227,3 +228,5 @@ Finally, you might want to keep a few Gmail addresses for the accounts that coul This setup is working fine for me and the transition was smooth. Some websites don’t make it easy to change email addresses, but ultimately, only a handful of websites didn’t allow me to change my email address. This setup obviously comes at a cost (Custom domain cost + Proton Unlimited cost), but the services justify it in my opinion (VPN, SL Premium, Drive with 500GB of storage). + + diff --git a/src/loicb/server/content/blog/clj-in-unity.md b/src/loicb/server/content/blog/clj-in-unity.md index dc7c626..9aa9b24 100644 --- a/src/loicb/server/content/blog/clj-in-unity.md +++ b/src/loicb/server/content/blog/clj-in-unity.md @@ -22,9 +22,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + How to package a Compiled Clojure lib (With MAGIC compiler) to NuGet, push it to remote and import it to Unity. -+++ ## Prerequisites @@ -336,4 +337,6 @@ Import your packages in Unity: nuget restore ``` -`Magic.Unity` is the Magic runtime for Unity and is already nuget packaged on its public repo \ No newline at end of file +`Magic.Unity` is the Magic runtime for Unity and is already nuget packaged on its public repo + + diff --git a/src/loicb/server/content/blog/clj-mono-repo.md b/src/loicb/server/content/blog/clj-mono-repo.md index 80256cb..7165083 100644 --- a/src/loicb/server/content/blog/clj-mono-repo.md +++ b/src/loicb/server/content/blog/clj-mono-repo.md @@ -18,9 +18,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + Example of a Clojure mono-repo structure for a web server and 2 clients (web and mobile). -+++ ## Context @@ -202,4 +203,6 @@ Using `:extra-paths` and `:extra-deps` in deps.edn is important because it preve Adding namespace to the aliases make the distinction between backend, common and client (web and mobile) clearer. -Using `deps` jack-in for server only work and `deps+figwheel` for frontend work is made easy using `calva` in VSCode (work in other editors as well). \ No newline at end of file +Using `deps` jack-in for server only work and `deps+figwheel` for frontend work is made easy using `calva` in VSCode (work in other editors as well). + + diff --git a/src/loicb/server/content/blog/datomic-setup.md b/src/loicb/server/content/blog/datomic-setup.md index 575e958..25e03f8 100644 --- a/src/loicb/server/content/blog/datomic-setup.md +++ b/src/loicb/server/content/blog/datomic-setup.md @@ -18,9 +18,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + How to run an embedded version of Datomic. How to run Datomic with a Cassandra cluster. How to run it in Docker. -+++ ## Introduction @@ -295,4 +296,6 @@ alt-host=datomicdb storage-access=remote ``` -After updating the transactor properties, you should be able to see the app running on port 8123 and be able to perform transactions as expected. \ No newline at end of file +After updating the transactor properties, you should be able to see the app running on port 8123 and be able to perform transactions as expected. + + diff --git a/src/loicb/server/content/blog/deploy-clj-app-to-aws.md b/src/loicb/server/content/blog/deploy-clj-app-to-aws.md index e727637..e80d9e0 100644 --- a/src/loicb/server/content/blog/deploy-clj-app-to-aws.md +++ b/src/loicb/server/content/blog/deploy-clj-app-to-aws.md @@ -19,9 +19,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + Example of how to deploy a containerized full-stack Clojure app in AWS EC2. -+++ This is an example of how to deploy a containerized full-stack Clojure app in AWS EC2. @@ -255,4 +256,6 @@ For the root domain `flybot.sg`, we use a `A` record for `@` resolving to the st ## Learn More -You can have a look at the open-source repo: [skydread1/flybot.sg](https://github.com/skydread1/flybot.sg) \ No newline at end of file +You can have a look at the open-source repo: [skydread1/flybot.sg](https://github.com/skydread1/flybot.sg) + + diff --git a/src/loicb/server/content/blog/deploy-django-beanstalk.md b/src/loicb/server/content/blog/deploy-django-beanstalk.md index 32f0157..9112e43 100644 --- a/src/loicb/server/content/blog/deploy-django-beanstalk.md +++ b/src/loicb/server/content/blog/deploy-django-beanstalk.md @@ -18,9 +18,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + What libraries/settings I used to develop a Django app and how to deploy it on AWS Elastic Beanstalk. -+++ ## Context @@ -446,3 +447,5 @@ Be sure to keep your env variables safe and split dev and prod settings to avoid Since EB comes with ALB, you can easily use a CNAME record to map your own personal subdomain to the ALB. Be aware of the hosting costs. I moved the blog content to my clojure SPA instead because after my AWS free tier expired, the monthly cost for hosting the blog was around $50 which was too much for a simple blog like that. + + diff --git a/src/loicb/server/content/blog/flybot-card-games.md b/src/loicb/server/content/blog/flybot-card-games.md index 829edb0..00b7f17 100644 --- a/src/loicb/server/content/blog/flybot-card-games.md +++ b/src/loicb/server/content/blog/flybot-card-games.md @@ -25,13 +25,14 @@ image: alt: Flybot Logo --- +## TLDR + At Flybot, I had the opportunity to create popular Asian Card Games APIs in **Clojure**. I developed the backend of games such as *Pǎo DΓ© KuΓ i* (θ·‘εΎ—εΏ«) and *Big two* (ι”„ε€§εœ°) which are climbing card games. I also worked on a Library we called **MetaGame** that allows us to compose several `Pǎo DΓ© KuΓ i` or `Big two` games (or a mix of both). By composing games, I mean to play a few rounds of them up to a certain score for instance. This composition is generic enough to even allow us to make tournaments out of the underlying games. -+++ ## Rational @@ -106,4 +107,6 @@ Flybot Ptd Ltd All Right Reserved ### Magic compiler -The magic compiler is open source and you can read more about my contribution in the dedicated section. \ No newline at end of file +The magic compiler is open source and you can read more about my contribution in the dedicated section. + + diff --git a/src/loicb/server/content/blog/flybot-mobile-app.md b/src/loicb/server/content/blog/flybot-mobile-app.md index 8a09f51..d879ab0 100644 --- a/src/loicb/server/content/blog/flybot-mobile-app.md +++ b/src/loicb/server/content/blog/flybot-mobile-app.md @@ -25,13 +25,14 @@ image: alt: Flybot Logo --- +## TLDR + At Flybot, I developed a mobile app using **ClojureScript** with `React Native` using `Figwheel` and `Reagent React Native` (to interact with reagent in ClojureScript). The code for the mobile app resides in the same repo as the server and the web client of the [flybot.sg](https://www.flybot.sg/) website. The goal of the mobile app was to allow employees to write blog posts using an app instead of the web UI and also to evaluate if our ClojureScript frontend stack could reuse most of the `re-frame` logic in both the mobile and web UIs. -+++ ## Rational @@ -114,4 +115,6 @@ The app has not been deployed on any Store yet. You can learn more about how the mobile app was setup in the article linked at the top of the page. -Also, feel free to visit [flybot.sg](https://www.flybot.sg/) and especially the [blog](https://www.flybot.sg/blog). \ No newline at end of file +Also, feel free to visit [flybot.sg](https://www.flybot.sg/) and especially the [blog](https://www.flybot.sg/blog). + + diff --git a/src/loicb/server/content/blog/flybot-stats-rama.md b/src/loicb/server/content/blog/flybot-stats-rama.md index ffa1405..8081ea0 100644 --- a/src/loicb/server/content/blog/flybot-stats-rama.md +++ b/src/loicb/server/content/blog/flybot-stats-rama.md @@ -21,13 +21,14 @@ image: alt: Flybot Logo --- +## TLDR + I am currently working on a project that aims at moving our gaming platform stat aggregation and visualization project to a new stack. We have an event-driven system that produces lots of events in multiple kafka topics. Our current solution to aggregate stats and visualize them in dashboards is the setup [Apache Druid](https://druid.apache.org/) + [Imply](https://imply.io/). As a **Clojure**-focused company, we decided to experiment with a new architecture: [Rama](https://redplanetlabs.com/learn-rama) + in-house full-stack Clojure Visualization tool. -+++ ## Rationale @@ -156,4 +157,6 @@ The main limitations are the following: ## Next goal -The next step is to have a generic `analytics` module builder with clear configs. And we want these configs to be easily reusable for the API/frontend validation schema for the different queries. \ No newline at end of file +The next step is to have a generic `analytics` module builder with clear configs. And we want these configs to be easily reusable for the API/frontend validation schema for the different queries. + + diff --git a/src/loicb/server/content/blog/flybot-website.md b/src/loicb/server/content/blog/flybot-website.md index 245b2d9..4bb1e87 100644 --- a/src/loicb/server/content/blog/flybot-website.md +++ b/src/loicb/server/content/blog/flybot-website.md @@ -33,11 +33,12 @@ image: alt: Flybot Logo --- +## TLDR + [flybot.sg](https://www.flybot.sg/) is an open-source full-stack **Clojure** web-app that allows company’s employees to write posts to showcase their open-source libraries, their contributions and all technical knowledge that could interest the functional programming community. HRs can also post job offers. Admins can edit any piece of content in any pages as the whole content can be written in Markdown. The purpose of this project was to demonstrate how the `lasagna stack` ([flybot-sg/lasagna-pull](https://github.com/flybot-sg/lasagna-pull) and [robertluo/fun-map](https://github.com/robertluo/fun-map)) could ease the web development experience for any Clojure developers. -+++ ## Rational @@ -175,4 +176,6 @@ You can read more about how I deployed the app to AWS in this article: [How to d ## Learn more -Feel free to visit [flybot.sg](https://www.flybot.sg/) and especially the [blog](https://www.flybot.sg/blog). \ No newline at end of file +Feel free to visit [flybot.sg](https://www.flybot.sg/) and especially the [blog](https://www.flybot.sg/blog). + + diff --git a/src/loicb/server/content/blog/fun-map-flybot.md b/src/loicb/server/content/blog/fun-map-flybot.md index 18da120..99e3374 100644 --- a/src/loicb/server/content/blog/fun-map-flybot.md +++ b/src/loicb/server/content/blog/fun-map-flybot.md @@ -20,9 +20,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + how we leverage `fun-map` to create different systems in the website flybot.sg: `prod-system`, `dev-system`, `test-system` and `figwheel-system`. -+++ ## Prerequisites @@ -271,4 +272,6 @@ So here is the `figwheel-system` : The `figheel-handler` is the value of the key `:reitit-router` of our running system. -So the system is started first via `touch` and its handler is provided to the servers figwheel starts that will be running while we work on our frontend. \ No newline at end of file +So the system is started first via `touch` and its handler is provided to the servers figwheel starts that will be running while we work on our frontend. + + diff --git a/src/loicb/server/content/blog/fun-map.md b/src/loicb/server/content/blog/fun-map.md index a024485..69f89ef 100644 --- a/src/loicb/server/content/blog/fun-map.md +++ b/src/loicb/server/content/blog/fun-map.md @@ -18,9 +18,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + Rational of `fun-map`, including the benefit of the `life-cycle-map` as dependency injection system. -+++ ## Context @@ -256,4 +257,6 @@ fun-map also support other features such as function call tracing, value caching ## Fun-Map applied to flybot.sg -To see Fun Map in action, refer to the doc [Fun-Map applied to flybot.sg](https://www.loicb.dev/blog/fun-map-applied-to-flybot). \ No newline at end of file +To see Fun Map in action, refer to the doc [Fun-Map applied to flybot.sg](https://www.loicb.dev/blog/fun-map-applied-to-flybot). + + diff --git a/src/loicb/server/content/blog/git-workflows.md b/src/loicb/server/content/blog/git-workflows.md index f3086e6..9a3b352 100644 --- a/src/loicb/server/content/blog/git-workflows.md +++ b/src/loicb/server/content/blog/git-workflows.md @@ -16,9 +16,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + What Git workflow is suitable for your project: trunk-based, feature branching, forking, release branching, release candidate workflow, Feature branching to develop, GitFlow -+++ ## Introduction @@ -243,3 +244,5 @@ If you have a test/staging env that needs to go through integration testing befo I think people tend to refer to CI as the test jobs done on PRs from `feature` to `develop` and CD to refer to the build jobs happening on merge to `main`. Others refer to CI as the philosophy of merging short/partial (but working) features as quickly as possible. This can be applied in **Feature branching to develop** in my opinion. Taking the time to have the simplest branching strategy possible for your project can really make the development experience a bliss for all developers of your team. People should focus on implementing quality features and not doing some botanic (lots of branches… anybody?). + + diff --git a/src/loicb/server/content/blog/hibou-platform.md b/src/loicb/server/content/blog/hibou-platform.md index 9109600..36f15c6 100644 --- a/src/loicb/server/content/blog/hibou-platform.md +++ b/src/loicb/server/content/blog/hibou-platform.md @@ -21,11 +21,12 @@ image: alt: Flybot Logo --- +## TLDR + Following our successful POC with Rama for analytics, we evolved the stack into a generic, reusable platform called **Hibou** (French for "owl", known for their excellent eyesight). The goal was to transform our client-specific implementation into a **business-agnostic** analytics suite that can serve multiple clients with different data requirements. -+++ ## Context @@ -190,4 +191,6 @@ By treating everything as data (from module definitions to API endpoints to UI f The combination of Rama for distributed stream processing, lasagna-pull for pure data APIs, and Replicant for reactive UIs created a coherent full-stack platform that leverages Clojure's strengths at every layer. -The key was resisting the temptation to optimize for the first client and instead investing time upfront to build generic, configurable components. \ No newline at end of file +The key was resisting the temptation to optimize for the first client and instead investing time upfront to build generic, configurable components. + + diff --git a/src/loicb/server/content/blog/juxt-tick-lib.md b/src/loicb/server/content/blog/juxt-tick-lib.md index 6eb33e1..1f8e3f1 100644 --- a/src/loicb/server/content/blog/juxt-tick-lib.md +++ b/src/loicb/server/content/blog/juxt-tick-lib.md @@ -18,9 +18,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + Illustrate date and time concepts in programming using the Clojure Tick library: timestamp, date-time, offset, zone, instant, inst, UTC, DST, duration, period etc. -+++ ## Introduction @@ -322,4 +323,6 @@ The **time since epoch** is the universal *computer-friendly* of representing ti A `duration` measures an amount of time using time-based values whereas a `period` uses date-based (calendar) values. -Finally, for Clojure developers, I highly recommend using `juxt/tick` as it allows us to handle time efficiently (conversion, operations) and elegantly (readable, as values) and I use it in several of my projects. It is also of course possible to do interop with the `java.time.Instant` class directly if you prefer. \ No newline at end of file +Finally, for Clojure developers, I highly recommend using `juxt/tick` as it allows us to handle time efficiently (conversion, operations) and elegantly (readable, as values) and I use it in several of my projects. It is also of course possible to do interop with the `java.time.Instant` class directly if you prefer. + + diff --git a/src/loicb/server/content/blog/lasagna-pull-flybot.md b/src/loicb/server/content/blog/lasagna-pull-flybot.md index ea118fa..fd464bf 100644 --- a/src/loicb/server/content/blog/lasagna-pull-flybot.md +++ b/src/loicb/server/content/blog/lasagna-pull-flybot.md @@ -18,9 +18,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + How we leverage `lasagna-pull` in the flybot.sg Clojure web app to define a pure data API. -+++ ## Prerequisites @@ -421,4 +422,6 @@ Having no side-effects at all makes it way easier to tests and debug and it is m Finally, the `ring-handler` will be the component responsible to **execute** all the side effects at once. -So the `saturn-handler` purpose was to be sure the data is being pulled properly, validated using malli, and that the side effects descriptions are gathered in one place to be executed later on. \ No newline at end of file +So the `saturn-handler` purpose was to be sure the data is being pulled properly, validated using malli, and that the side effects descriptions are gathered in one place to be executed later on. + + diff --git a/src/loicb/server/content/blog/lasagna-pull.md b/src/loicb/server/content/blog/lasagna-pull.md index a05bb62..d2f4881 100644 --- a/src/loicb/server/content/blog/lasagna-pull.md +++ b/src/loicb/server/content/blog/lasagna-pull.md @@ -17,9 +17,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + Rational of flybot-sg/lasagna-pull library: precisely select from deep data structure in Clojure. -+++ ## Context @@ -213,4 +214,6 @@ This context can help you gathering information from the query and apply a funct ## Lasagna Pull applied to flybot.sg -To see Lasagna Pull in action, refer to the doc [Lasagna Pull applied to flybot.sg](https://www.loicb.dev/blog/lasagna-pull-applied-to-flybot). \ No newline at end of file +To see Lasagna Pull in action, refer to the doc [Lasagna Pull applied to flybot.sg](https://www.loicb.dev/blog/lasagna-pull-applied-to-flybot). + + diff --git a/src/loicb/server/content/blog/magic-nostrand.md b/src/loicb/server/content/blog/magic-nostrand.md index 281d54f..081c8ea 100644 --- a/src/loicb/server/content/blog/magic-nostrand.md +++ b/src/loicb/server/content/blog/magic-nostrand.md @@ -33,13 +33,14 @@ image: alt: Data Light Wallpaper --- +## TLDR + I worked on integrating the Magic Compiler and its tooling to Flybot's development workflow. [Magic](https://github.com/nasser/magic) is an open-source bootstrapped compiler written in **Clojure** that takes Clojure code as input and produces dotnet assemblies (.dll) as output. The dlls produced by Magic can be run in the Game engine `Unity` which Flybot uses for their card game UIs. The goal was to be able to compile our backend Clojure APIs to dlls so we can used that logic in Unity directly. Working closely with the author of the Magic compiler [Ramsey Nasser](https://github.com/nasser), I helped improving the tooling around the Magic compiler so it integrates well with our workflow Clojure Backend/Unity Frontend. I notably simplified the way a clojure project and its dependencies are compiled to dlls, packed to NuGet, deployed to online repos and finally imported to Unity. -+++ ## Rational @@ -147,4 +148,6 @@ Since all the Clojure libraries and the Magic.Unity were packaged via nugget and ## Learn more -You can learn more about `Magic` in my blog articles listed at the top of the page. \ No newline at end of file +You can learn more about `Magic` in my blog articles listed at the top of the page. + + diff --git a/src/loicb/server/content/blog/mcts-card-games.md b/src/loicb/server/content/blog/mcts-card-games.md index 02a0bb7..3144345 100644 --- a/src/loicb/server/content/blog/mcts-card-games.md +++ b/src/loicb/server/content/blog/mcts-card-games.md @@ -16,9 +16,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + General principle of MCTS applied to a Clojure use case. -+++ ## Objective @@ -203,4 +204,6 @@ The details of this `game-plan` are confidential for obvious reasons. Having an hybrid approach, meaning using a `game-plan` for the first moves of the game when the possible plays are too numerous, and then use MCTS at the end of the game allowed us to have a decent AI we can use. -As of the time I write this article, the implementation is being tested (as part of a bigger system) and not yet in production. \ No newline at end of file +As of the time I write this article, the implementation is being tested (as part of a bigger system) and not yet in production. + + diff --git a/src/loicb/server/content/blog/mcts.md b/src/loicb/server/content/blog/mcts.md index f9077d0..1af1fcb 100644 --- a/src/loicb/server/content/blog/mcts.md +++ b/src/loicb/server/content/blog/mcts.md @@ -22,13 +22,14 @@ image: alt: Flybot Logo --- +## TLDR + At Flybot, I had the opportunity to work on a Monte Carlo Tree Search (MCTS) **Clojure** implementation for our card games. I combined `MCTS` with `domain knowledge` of our games to balance quality and performance of the plays. A few of our games implement the same `Game` protocol so the MCTS implementation is independent of the underlying game. -+++ ## Rational @@ -87,4 +88,6 @@ One limitation of MCTS is its tendency to explore non-promising branches. To ove In conclusion, the fusion of MCTS and domain knowledge has enabled us to create a functional AI for big-two that can replace human players and offer different levels of difficulty. As of the time of writing this article, the implementation is currently undergoing testing as part of a larger system and is not yet in production. This hybrid approach represents a promising solution for developing robust AI players in complex card games. -You can learn more about the implementation strategy in my blog article listed at the top of the page. \ No newline at end of file +You can learn more about the implementation strategy in my blog article listed at the top of the page. + + diff --git a/src/loicb/server/content/blog/port-clj-lib-to-clr.md b/src/loicb/server/content/blog/port-clj-lib-to-clr.md index 6ff7d6a..18f20e1 100644 --- a/src/loicb/server/content/blog/port-clj-lib-to-clr.md +++ b/src/loicb/server/content/blog/port-clj-lib-to-clr.md @@ -21,9 +21,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + How to port your Clojure lib to the CLR. Then how to be build with the MAGIC compiler allowing you to obtain DLLs compatible with Unity (no DLR used by MAGIC). -+++ In this article, I will show you: @@ -351,4 +352,6 @@ My fork uses reader conditionals so it can be run and tested in both JVM and CLR ## Learn more Now that your library is compiled to dotnet, you can learn how to package it to nuget, push it in to your host repo and import in Unity in this article: -- [Pack, Push and Import Clojure to Unity](https://www.loicb.dev/blog/clojure-in-unity). \ No newline at end of file +- [Pack, Push and Import Clojure to Unity](https://www.loicb.dev/blog/clojure-in-unity). + + diff --git a/src/loicb/server/content/blog/portfolio-website.md b/src/loicb/server/content/blog/portfolio-website.md index b2bc15f..e86919c 100644 --- a/src/loicb/server/content/blog/portfolio-website.md +++ b/src/loicb/server/content/blog/portfolio-website.md @@ -25,6 +25,8 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + This portfolio was implemented with **ClojureScript**. It is a Single Page Application that leverages [reagent](https://github.com/reagent-project/reagent), a minimalistic interface between ClojureScript and **React**. For frontend management, [re-frame](https://github.com/day8/re-frame) is used. To compile the cljs code and perform hot reloading, I used [figwheel-main](https://figwheel.org/). @@ -37,7 +39,6 @@ Light/dark mode and code block syntax highlighted are supported. The app is deployed on **Netlify** via GitHub Actions every time a branch is merged to master. Build artifacts are generated during CI/CD and are not committed to the repository. -+++ ## Rational @@ -197,4 +198,6 @@ The website is hosted on Netlify (US servers) and deployed via GitHub Actions us ## Learn More -Have a look at the repo [README](https://github.com/skydread1/portfolio/blob/master/README.md) for more information. \ No newline at end of file +Have a look at the repo [README](https://github.com/skydread1/portfolio/blob/master/README.md) for more information. + + diff --git a/src/loicb/server/content/blog/props-recommender.md b/src/loicb/server/content/blog/props-recommender.md index a5daade..c3f03c1 100644 --- a/src/loicb/server/content/blog/props-recommender.md +++ b/src/loicb/server/content/blog/props-recommender.md @@ -21,6 +21,8 @@ image: alt: Flybot Logo --- +## TLDR + I worked on a recommender system stack that suggests user-based challenges to [Golden Island](https://www.80166.com/)'s players. `Golden Island` is a card/board game platforms that offers a dozen of games including *Pǎo DΓ© KuΓ i* (θ·‘εΎ—εΏ«), *Dou dizhu* (ι¬₯地主), *Mahjong* etc. Some game operators are already creating props (challenges) that are personalized in the sense that they apply only to a subset of users (game, levels, coin balance etc) in response to a subset of events (game, bet size etc). @@ -29,7 +31,6 @@ The end goal was to ease the job of game operators by taking care of `when` to r The recommender is made of **Clojure** applications deployed in PODs in `AWS EKS` that consume events from `kafka` topics and produces props (personalized challenges) to a dedicated kafka topic. It uses `Datomic` as storage solution. The app can query multiple recommender engines to perform A/B testing, meaning that we can run 2 recommenders at once and compare their performance. -+++ ## Rational @@ -140,4 +141,6 @@ We could recap the steps I took like this: - Extra the model training and queue computations from the kafka logic 5. Performance - Once the app running fine, start improving recommender algo - - Make the kafka app suitable to use multiple recommender engines and improve data ingestion \ No newline at end of file + - Make the kafka app suitable to use multiple recommender engines and improve data ingestion + + diff --git a/src/loicb/server/content/blog/pwd-manager+2fa.md b/src/loicb/server/content/blog/pwd-manager+2fa.md index de10be8..fa3c593 100644 --- a/src/loicb/server/content/blog/pwd-manager+2fa.md +++ b/src/loicb/server/content/blog/pwd-manager+2fa.md @@ -16,9 +16,10 @@ image: alt: Bitwarden Logo --- +## TLDR + Using a password manager with strong passwords and enabling 2FA for critical accounts are simple yet powerful steps to protect your online security. -+++ ## Rationale @@ -207,3 +208,5 @@ You want to create an account for a new website. 7. Scan the QR code shown by the bank to add the code to your app. 8. The website will expect you to enter the generated code to finalize the 2FA setup. 9. The website will show the recovery codes (print them, label them, and hide them in your home). + + diff --git a/src/loicb/server/content/blog/reagent-native-app.md b/src/loicb/server/content/blog/reagent-native-app.md index 2e9efcd..bc230b5 100644 --- a/src/loicb/server/content/blog/reagent-native-app.md +++ b/src/loicb/server/content/blog/reagent-native-app.md @@ -20,9 +20,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + Reagent React Native mobile app reusing re-frame logic from exiting web client. -+++ ## Prerequisites @@ -448,4 +449,6 @@ I hope that this unusual mobile app stack made you want to consider `ClojureScri It is important to note that the state management logic (re-frame) is the same at 90% for both the web app and the mobile app which is very convenient. -Finally, the web app is deployed but not the mobile app. All the codebase is open-source so feel free to take inspiration. \ No newline at end of file +Finally, the web app is deployed but not the mobile app. All the codebase is open-source so feel free to take inspiration. + + diff --git a/src/loicb/server/content/blog/redirect-domain-subdomain.md b/src/loicb/server/content/blog/redirect-domain-subdomain.md index 8a4f740..1b6b3f6 100644 --- a/src/loicb/server/content/blog/redirect-domain-subdomain.md +++ b/src/loicb/server/content/blog/redirect-domain-subdomain.md @@ -16,9 +16,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + Domain to subdomain forwarding using AWS ALB+NLB setup. -+++ ## Context @@ -108,3 +109,5 @@ GoDaddy domain to subdomain forwarding does not support path so it is not viable Using AWS route53 to enjoy the feature of mapping a domain to the ALB DNS name via the special record ALIAS comes at a cost: all subdomains would need to be resolved using AWS Name Servers. ALB+NLB is the setup that worked well for me. Having the internal ALB handling redirect to https and to the subdomain is very convenient. Using an internet-facing NLB solves the static IP problem to resolve the domain record at an extra cost of minimum $18 per month. + + diff --git a/src/loicb/server/content/blog/testing-in-clojure.md b/src/loicb/server/content/blog/testing-in-clojure.md index 4bfe194..bccfe1b 100644 --- a/src/loicb/server/content/blog/testing-in-clojure.md +++ b/src/loicb/server/content/blog/testing-in-clojure.md @@ -20,9 +20,10 @@ image: alt: Logo referencing Aperture Science --- +## TLDR + Introducing some popular testing tools to developers new to Clojure. Highlight solutions for how to do unit testing with Rich Comment Tests, data validation and generative testing with Malli, running test suites and metrics with Kaocha and how to do integration testing using external containerized services. -+++ ## Introduction @@ -633,4 +634,6 @@ I highlighted some common testing tools and methods that the Clojure community u If you are starting your journey as a Clojure developer, I hope you can appreciate the quality of open-source testing libraries we have access to. Also, please remember that keeping things pure is the key to easy testing and debugging; a luxury not so common in the programming world. Inevitably, you will need to deal with side effects but isolate them as much as you can to make your code robust and your tests straight forward. -Finally, there are some tools I didn’t mention to keep things short so feel free to explore what the Clojure community has to offer. The last advice I would give is to not try to use too many tools or only the shiny new ones you might find. Keep things simple and evaluate if a library is worth being added to your deps. \ No newline at end of file +Finally, there are some tools I didn’t mention to keep things short so feel free to explore what the Clojure community has to offer. The last advice I would give is to not try to use too many tools or only the shiny new ones you might find. Keep things simple and evaluate if a library is worth being added to your deps. + + From fd79eb738d59e42cc7e74e8bab528881d91dcfda Mon Sep 17 00:00:00 2001 From: skydread1 Date: Mon, 13 Oct 2025 14:24:10 +0800 Subject: [PATCH 07/11] Add frontmatter support and TLDR section support --- src/loicb/common/validation.cljc | 1 + src/loicb/server/md.clj | 148 ++++++++++++++++++++++++++----- test/loicb/server/md_test.clj | 70 ++++++++++++++- 3 files changed, 193 insertions(+), 26 deletions(-) diff --git a/src/loicb/common/validation.cljc b/src/loicb/common/validation.cljc index 4bef134..961fb69 100644 --- a/src/loicb/common/validation.cljc +++ b/src/loicb/common/validation.cljc @@ -30,6 +30,7 @@ [:post/md-content :string] [:post/md-content-short :string] [:post/tags {:optional true} [:vector :string]] + [:post/rss-feeds {:optional true} [:vector :string]] [:post/repos {:optional true} [:vector [:vector :string]]] [:post/articles {:optional true} [:vector [:vector :string]]] [:post/image diff --git a/src/loicb/server/md.clj b/src/loicb/server/md.clj index c514b61..4652f09 100644 --- a/src/loicb/server/md.clj +++ b/src/loicb/server/md.clj @@ -1,6 +1,5 @@ (ns loicb.server.md - (:require [clojure.edn :as edn] - [clojure.java.io :as io] + (:require [clojure.java.io :as io] [clojure.string :as str] [loicb.common.validation :as v] [markdown.core :as md] @@ -31,28 +30,133 @@ ;; ---------- Markdown to Hiccup ---------- +;; ---------- Helper Functions ---------- + +(defn- inst->date-string + "Convert an inst object to YYYY-MM-DD string." + [inst-obj] + (.format (java.text.SimpleDateFormat. "yyyy-MM-dd") inst-obj)) + +(defn- ->vector + "Convert a collection to a vector. Returns as-is if already a vector." + [coll] + (if (vector? coll) coll (vec coll))) + +(defn- namespace-image-keys + "Convert flat image map keys to :image/ namespaced keys." + [image-map] + (reduce-kv + (fn [acc k v] + (assoc acc (keyword "image" (name k)) v)) + {} + image-map)) + +(defn normalize-metadata + "Normalize YAML metadata to use :post/ namespaced keys and proper types. + + Conversions: + - All keys β†’ :post/ namespace + - date (inst or coll of inst) β†’ vector of YYYY-MM-DD strings + - page (string) β†’ keyword + - tags, rss-feeds (list) β†’ vector + - repos, articles (list of lists) β†’ vector of vectors + - image map β†’ :image/ namespaced keys" + [metadata] + (reduce-kv + (fn [acc k v] + (let [namespaced-key (if (qualified-keyword? k) + k + (keyword "post" (name k)))] + (case namespaced-key + ;; Image: namespace the keys + :post/image + (assoc acc namespaced-key (namespace-image-keys v)) + + ;; Date: convert inst to vector of date strings + :post/date + (assoc acc namespaced-key + (cond + (inst? v) [(inst->date-string v)] + (and (coll? v) (not (map? v))) (mapv #(if (inst? %) (inst->date-string %) %) v) + :else [v])) + + ;; Page: ensure keyword + :post/page + (assoc acc namespaced-key (if (keyword? v) v (keyword v))) + + ;; Simple vectors: tags, rss-feeds + :post/tags + (assoc acc namespaced-key (->vector v)) + + :post/rss-feeds + (assoc acc namespaced-key (->vector v)) + + ;; Nested vectors: repos, articles + :post/repos + (assoc acc namespaced-key (mapv ->vector v)) + + :post/articles + (assoc acc namespaced-key (mapv ->vector v)) + + ;; Default: keep as-is + (assoc acc namespaced-key v)))) + {} + metadata)) + +(defn extract-tldr + "Extract TLDR/Summary section from markdown content. + + Looks for ## TLDR or ## Summary heading (case insensitive). + Returns a map with :tldr and :full-content keys. + The TLDR section is removed from full-content to avoid duplication." + [markdown-content] + (let [tldr-pattern #"(?i)##\s+(TLDR|Summary)\s*\n([\s\S]*?)(?=\n##|\z)" + matches (re-find tldr-pattern markdown-content)] + (if matches + {:tldr (str/trim (nth matches 2)) + :full-content (str/trim (str/replace markdown-content tldr-pattern ""))} + {:tldr "" + :full-content (str/trim markdown-content)}))) + (defn load-post - "Slurp the file and extract the config and the markdown. - Code above `+++` is a clojure map of config. - Code under `+++` is the post content as md - Returns a map with the post info" + "Load a markdown file with YAML frontmatter. + + Format: + --- + title: Post Title + date: YYYY-MM-DD + ... + --- + + ## TLDR + + Quick summary here. + + ## Introduction + + Full article content... + + Returns a map with :post/ namespaced keys." [file-path] - (let [raw (slurp file-path) - [post-full post-short config] (->> (str/split raw #"\+\+\+") - (take 3) - reverse) - post (-> config - edn/read-string - (assoc :post/md-content post-full - :post/md-content-short post-short))] - (try + (try + (let [raw (slurp file-path) + ;; Parse YAML frontmatter using markdown-clj + result (md/md-to-html-string-with-meta raw) + metadata (or (:metadata result) {}) + ;; Extract markdown content (without YAML frontmatter) + markdown-content (str/replace-first raw #"(?s)^---.*?---\s*" "") + ;; Extract TLDR section and full content + {:keys [tldr full-content]} (extract-tldr markdown-content) + ;; Build the post map with normalized metadata + post (-> metadata + normalize-metadata + (assoc :post/md-content full-content + :post/md-content-short tldr))] + ;; Validate returns the post if valid, or explain-data if invalid (-> post - (v/validate v/post-schema)) - (catch Exception ex - {:error (str (ex-data ex))})))) - -(comment - (load-post "./src/loicb/server/content/about/cpe.md")) + (v/validate v/post-schema))) + (catch Exception ex + {:error (str (ex-data ex))}))) (def load-posts (mapv load-post all-files)) @@ -73,7 +177,7 @@ [] (let [clojure-blog-posts (->> (files-of "blog") (mapv load-post) - (filter #(some #{"Clojure"} (:post/tags %))) + (filter #(some #{"clojure"} (:post/rss-feeds %))) (sort-by #(first (:post/date %))) reverse) base-url "https://www.loicb.dev" diff --git a/test/loicb/server/md_test.clj b/test/loicb/server/md_test.clj index e2ebadd..6f4e068 100644 --- a/test/loicb/server/md_test.clj +++ b/test/loicb/server/md_test.clj @@ -22,11 +22,11 @@ image: alt: Test image --- -This is the short description. +## TLDR -+++ +This is the short description. -# Main Content +## Main Content This is the main content of the post.") @@ -41,8 +41,70 @@ This is the main content of the post.") (let [result (md/md-to-html-string-with-meta yaml-frontmatter-md) html (:html result)] (is (string? html)) - (is (s/includes? html "

Main Content

"))))) + (is (clojure.string/includes? html "

Main Content

"))))) + +;; Tests for normalize-metadata +(deftest normalize-metadata-test + (testing "namespaces all keys with :post/" + (let [metadata {:title "Test" :id "test-id"} + result (sut/normalize-metadata metadata)] + (is (= "Test" (:post/title result))) + (is (= "test-id" (:post/id result))))) + + (testing "converts inst date to vector of strings" + (let [inst-date #inst "2024-05-12T00:00:00.000-00:00" + result (sut/normalize-metadata {:date inst-date})] + (is (= ["2024-05-12"] (:post/date result))))) + + (testing "converts collection of inst dates to vector of strings" + (let [dates (list #inst "2024-05-12" #inst "2024-06-15") + result (sut/normalize-metadata {:date dates})] + (is (= ["2024-05-12" "2024-06-15"] (:post/date result))))) + + (testing "converts page string to keyword" + (let [result (sut/normalize-metadata {:page "blog"})] + (is (= :blog (:post/page result))))) + + (testing "converts tags list to vector" + (let [tags (list "Clojure" "Web") + result (sut/normalize-metadata {:tags tags})] + (is (vector? (:post/tags result))) + (is (= ["Clojure" "Web"] (:post/tags result))))) + + (testing "converts repos nested lists to vectors" + (let [repos (list (list "Repo1" "url1") (list "Repo2" "url2")) + result (sut/normalize-metadata {:repos repos})] + (is (= [["Repo1" "url1"] ["Repo2" "url2"]] (:post/repos result))))) + + (testing "namespaces image keys" + (let [image {:src "/img.png" :src-dark "/img-dark.png" :alt "Image"} + result (sut/normalize-metadata {:image image})] + (is (= "/img.png" (:image/src (:post/image result)))) + (is (= "/img-dark.png" (:image/src-dark (:post/image result)))) + (is (= "Image" (:image/alt (:post/image result))))))) + +;; Tests for extract-tldr +(deftest extract-tldr-test + (testing "extracts ## TLDR section" + (let [md "## TLDR\n\nShort summary.\n\n## Introduction\n\nFull content." + result (sut/extract-tldr md)] + (is (= "Short summary." (:tldr result))) + (is (clojure.string/includes? (:full-content result) "Introduction")) + (is (not (clojure.string/includes? (:full-content result) "TLDR"))))) + + (testing "extracts ## Summary section (case insensitive)" + (let [md "## summary\n\nQuick overview.\n\n## Content\n\nMain text." + result (sut/extract-tldr md)] + (is (= "Quick overview." (:tldr result))) + (is (clojure.string/includes? (:full-content result) "Content")))) + + (testing "handles missing TLDR section" + (let [md "## Introduction\n\nNo TLDR here." + result (sut/extract-tldr md)] + (is (= "" (:tldr result))) + (is (= "## Introduction\n\nNo TLDR here." (:full-content result)))))) +;; Tests for load-post (deftest load-post-yaml-format (testing "load-post can parse YAML frontmatter format" (let [temp-file (java.io.File/createTempFile "test-yaml" ".md") From 8336a423c1bb27bcaedb77d4b8ec2f59ebbea0d0 Mon Sep 17 00:00:00 2001 From: skydread1 Date: Mon, 13 Oct 2025 14:33:08 +0800 Subject: [PATCH 08/11] Add all rss feed --- src/loicb/server/content/about/aboutme.md | 2 + src/loicb/server/content/blog/blog-django.md | 1 + .../server/content/blog/cd-sl-p-method.md | 2 + src/loicb/server/content/blog/clj-in-unity.md | 1 + .../server/content/blog/clj-mono-repo.md | 1 + .../server/content/blog/datomic-setup.md | 1 + .../content/blog/deploy-clj-app-to-aws.md | 1 + .../content/blog/deploy-django-beanstalk.md | 2 + .../server/content/blog/flybot-card-games.md | 1 + .../server/content/blog/flybot-mobile-app.md | 1 + .../server/content/blog/flybot-stats-rama.md | 1 + .../server/content/blog/flybot-website.md | 1 + .../server/content/blog/fun-map-flybot.md | 1 + src/loicb/server/content/blog/fun-map.md | 1 + .../server/content/blog/git-workflows.md | 2 + .../server/content/blog/hibou-platform.md | 1 + .../server/content/blog/juxt-tick-lib.md | 1 + .../content/blog/lasagna-pull-flybot.md | 1 + src/loicb/server/content/blog/lasagna-pull.md | 1 + .../server/content/blog/magic-nostrand.md | 1 + .../server/content/blog/mcts-card-games.md | 1 + src/loicb/server/content/blog/mcts.md | 1 + .../content/blog/port-clj-lib-to-clr.md | 1 + .../server/content/blog/portfolio-website.md | 76 ++++++++++++------- .../server/content/blog/props-recommender.md | 1 + .../server/content/blog/pwd-manager+2fa.md | 2 + .../server/content/blog/reagent-native-app.md | 1 + .../content/blog/redirect-domain-subdomain.md | 2 + .../server/content/blog/testing-in-clojure.md | 1 + 29 files changed, 83 insertions(+), 27 deletions(-) diff --git a/src/loicb/server/content/about/aboutme.md b/src/loicb/server/content/about/aboutme.md index 7b8ce2a..50db659 100644 --- a/src/loicb/server/content/about/aboutme.md +++ b/src/loicb/server/content/about/aboutme.md @@ -13,6 +13,8 @@ tags: - πŸ’» 5+ years of experience - πŸŽ“ Master in CS - πŸ‡«πŸ‡· French Citizen +rss-feeds: + - all repos: - ["My GitHub", "https://github.com/skydread1"] articles: diff --git a/src/loicb/server/content/blog/blog-django.md b/src/loicb/server/content/blog/blog-django.md index e59d79d..f675977 100644 --- a/src/loicb/server/content/blog/blog-django.md +++ b/src/loicb/server/content/blog/blog-django.md @@ -14,6 +14,7 @@ tags: - S3 - AWS rss-feeds: + - all - clojure repos: - ["Blog", "https://github.com/skydread1/blog"] diff --git a/src/loicb/server/content/blog/cd-sl-p-method.md b/src/loicb/server/content/blog/cd-sl-p-method.md index 1472094..592d82f 100644 --- a/src/loicb/server/content/blog/cd-sl-p-method.md +++ b/src/loicb/server/content/blog/cd-sl-p-method.md @@ -10,6 +10,8 @@ tags: - SimpleLogin - Custom Domain - Privacy +rss-feeds: + - all image: src: /assets/cd-sl-p/proton-mail-icon.jpg src-dark: /assets/cd-sl-p/proton-mail-icon.jpg diff --git a/src/loicb/server/content/blog/clj-in-unity.md b/src/loicb/server/content/blog/clj-in-unity.md index 9aa9b24..61a59e1 100644 --- a/src/loicb/server/content/blog/clj-in-unity.md +++ b/src/loicb/server/content/blog/clj-in-unity.md @@ -11,6 +11,7 @@ tags: - CLR - Unity rss-feeds: + - all - clojure repos: - ["Magic", "https://github.com/nasser/magic"] diff --git a/src/loicb/server/content/blog/clj-mono-repo.md b/src/loicb/server/content/blog/clj-mono-repo.md index 7165083..979cbf5 100644 --- a/src/loicb/server/content/blog/clj-mono-repo.md +++ b/src/loicb/server/content/blog/clj-mono-repo.md @@ -11,6 +11,7 @@ tags: - Figwheel - Mono Repo rss-feeds: + - all - clojure image: src: /assets/loic-blog-logo.png diff --git a/src/loicb/server/content/blog/datomic-setup.md b/src/loicb/server/content/blog/datomic-setup.md index 25e03f8..36b3f5c 100644 --- a/src/loicb/server/content/blog/datomic-setup.md +++ b/src/loicb/server/content/blog/datomic-setup.md @@ -11,6 +11,7 @@ tags: - Cassandra - Docker rss-feeds: + - all - clojure image: src: /assets/loic-blog-logo.png diff --git a/src/loicb/server/content/blog/deploy-clj-app-to-aws.md b/src/loicb/server/content/blog/deploy-clj-app-to-aws.md index e80d9e0..151edbc 100644 --- a/src/loicb/server/content/blog/deploy-clj-app-to-aws.md +++ b/src/loicb/server/content/blog/deploy-clj-app-to-aws.md @@ -10,6 +10,7 @@ tags: - AWS - Full-Stack rss-feeds: + - all - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] diff --git a/src/loicb/server/content/blog/deploy-django-beanstalk.md b/src/loicb/server/content/blog/deploy-django-beanstalk.md index 9112e43..63d73b4 100644 --- a/src/loicb/server/content/blog/deploy-django-beanstalk.md +++ b/src/loicb/server/content/blog/deploy-django-beanstalk.md @@ -10,6 +10,8 @@ tags: - Django - AWS - Elastic Beanstalk +rss-feeds: + - all repos: - ["Blog", "https://github.com/skydread1/blog"] image: diff --git a/src/loicb/server/content/blog/flybot-card-games.md b/src/loicb/server/content/blog/flybot-card-games.md index 00b7f17..88a9b6b 100644 --- a/src/loicb/server/content/blog/flybot-card-games.md +++ b/src/loicb/server/content/blog/flybot-card-games.md @@ -14,6 +14,7 @@ tags: - Magic Compiler - Interop CLR rss-feeds: + - all - clojure repos: - ["Magic", "https://github.com/nasser/magic"] diff --git a/src/loicb/server/content/blog/flybot-mobile-app.md b/src/loicb/server/content/blog/flybot-mobile-app.md index d879ab0..143dc36 100644 --- a/src/loicb/server/content/blog/flybot-mobile-app.md +++ b/src/loicb/server/content/blog/flybot-mobile-app.md @@ -13,6 +13,7 @@ tags: - Re-Frame - Reagent React Native rss-feeds: + - all - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] diff --git a/src/loicb/server/content/blog/flybot-stats-rama.md b/src/loicb/server/content/blog/flybot-stats-rama.md index 8081ea0..a848cb5 100644 --- a/src/loicb/server/content/blog/flybot-stats-rama.md +++ b/src/loicb/server/content/blog/flybot-stats-rama.md @@ -14,6 +14,7 @@ tags: - EKS - Kafka rss-feeds: + - all - clojure image: src: https://www.flybot.sg/assets/flybot-logo.png diff --git a/src/loicb/server/content/blog/flybot-website.md b/src/loicb/server/content/blog/flybot-website.md index 4bb1e87..5c875bd 100644 --- a/src/loicb/server/content/blog/flybot-website.md +++ b/src/loicb/server/content/blog/flybot-website.md @@ -19,6 +19,7 @@ tags: - Datalevin - Reitit rss-feeds: + - all - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] diff --git a/src/loicb/server/content/blog/fun-map-flybot.md b/src/loicb/server/content/blog/fun-map-flybot.md index 99e3374..9325587 100644 --- a/src/loicb/server/content/blog/fun-map-flybot.md +++ b/src/loicb/server/content/blog/fun-map-flybot.md @@ -11,6 +11,7 @@ tags: - Component - Deps Injection rss-feeds: + - all - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] diff --git a/src/loicb/server/content/blog/fun-map.md b/src/loicb/server/content/blog/fun-map.md index 69f89ef..7537950 100644 --- a/src/loicb/server/content/blog/fun-map.md +++ b/src/loicb/server/content/blog/fun-map.md @@ -11,6 +11,7 @@ tags: - Component - Deps Injection rss-feeds: + - all - clojure image: src: /assets/loic-blog-logo.png diff --git a/src/loicb/server/content/blog/git-workflows.md b/src/loicb/server/content/blog/git-workflows.md index 9a3b352..bec2715 100644 --- a/src/loicb/server/content/blog/git-workflows.md +++ b/src/loicb/server/content/blog/git-workflows.md @@ -10,6 +10,8 @@ tags: - Workflows - Branching - CI/CD +rss-feeds: + - all image: src: /assets/loic-blog-logo.png src-dark: /assets/loic-blog-logo.png diff --git a/src/loicb/server/content/blog/hibou-platform.md b/src/loicb/server/content/blog/hibou-platform.md index 36f15c6..13012bd 100644 --- a/src/loicb/server/content/blog/hibou-platform.md +++ b/src/loicb/server/content/blog/hibou-platform.md @@ -14,6 +14,7 @@ tags: - Mono Repo - Full-Stack rss-feeds: + - all - clojure image: src: https://www.flybot.sg/assets/flybot-logo.png diff --git a/src/loicb/server/content/blog/juxt-tick-lib.md b/src/loicb/server/content/blog/juxt-tick-lib.md index 1f8e3f1..f657f20 100644 --- a/src/loicb/server/content/blog/juxt-tick-lib.md +++ b/src/loicb/server/content/blog/juxt-tick-lib.md @@ -11,6 +11,7 @@ tags: - Tick - Duration/Period rss-feeds: + - all - clojure image: src: /assets/loic-blog-logo.png diff --git a/src/loicb/server/content/blog/lasagna-pull-flybot.md b/src/loicb/server/content/blog/lasagna-pull-flybot.md index fd464bf..2ffd3fc 100644 --- a/src/loicb/server/content/blog/lasagna-pull-flybot.md +++ b/src/loicb/server/content/blog/lasagna-pull-flybot.md @@ -9,6 +9,7 @@ tags: - Clojure - Pull Pattern rss-feeds: + - all - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] diff --git a/src/loicb/server/content/blog/lasagna-pull.md b/src/loicb/server/content/blog/lasagna-pull.md index d2f4881..ebf4978 100644 --- a/src/loicb/server/content/blog/lasagna-pull.md +++ b/src/loicb/server/content/blog/lasagna-pull.md @@ -10,6 +10,7 @@ tags: - Pull Pattern - Malli rss-feeds: + - all - clojure image: src: /assets/loic-blog-logo.png diff --git a/src/loicb/server/content/blog/magic-nostrand.md b/src/loicb/server/content/blog/magic-nostrand.md index 081c8ea..15c1f70 100644 --- a/src/loicb/server/content/blog/magic-nostrand.md +++ b/src/loicb/server/content/blog/magic-nostrand.md @@ -18,6 +18,7 @@ tags: - NuGet - Unity rss-feeds: + - all - clojure repos: - ["Magic", "https://github.com/nasser/magic"] diff --git a/src/loicb/server/content/blog/mcts-card-games.md b/src/loicb/server/content/blog/mcts-card-games.md index 3144345..bb6e316 100644 --- a/src/loicb/server/content/blog/mcts-card-games.md +++ b/src/loicb/server/content/blog/mcts-card-games.md @@ -9,6 +9,7 @@ tags: - Clojure - MCTS rss-feeds: + - all - clojure image: src: /assets/loic-blog-logo.png diff --git a/src/loicb/server/content/blog/mcts.md b/src/loicb/server/content/blog/mcts.md index 1af1fcb..58739c6 100644 --- a/src/loicb/server/content/blog/mcts.md +++ b/src/loicb/server/content/blog/mcts.md @@ -13,6 +13,7 @@ tags: - Clojure - Card Game rss-feeds: + - all - clojure articles: - ["MCTS applied to card games", "../blog/article-mcts"] diff --git a/src/loicb/server/content/blog/port-clj-lib-to-clr.md b/src/loicb/server/content/blog/port-clj-lib-to-clr.md index 18f20e1..2c19dfd 100644 --- a/src/loicb/server/content/blog/port-clj-lib-to-clr.md +++ b/src/loicb/server/content/blog/port-clj-lib-to-clr.md @@ -11,6 +11,7 @@ tags: - CLR - Unity rss-feeds: + - all - clojure repos: - ["Magic", "https://github.com/nasser/magic"] diff --git a/src/loicb/server/content/blog/portfolio-website.md b/src/loicb/server/content/blog/portfolio-website.md index e86919c..1f877d8 100644 --- a/src/loicb/server/content/blog/portfolio-website.md +++ b/src/loicb/server/content/blog/portfolio-website.md @@ -16,6 +16,7 @@ tags: - Reitit - Babashka rss-feeds: + - all - clojure repos: - ["Portfolio", "https://github.com/skydread1/portfolio"] @@ -33,7 +34,7 @@ For frontend management, [re-frame](https://github.com/day8/re-frame) is used. T The routing is done with [reitit](https://github.com/metosin/reitit). -The markdown content is converted into [hiccup](https://github.com/weavejester/hiccup) (a clojure-friendly markup) and the post/vignette configurations are made in EDN which is validated at compile time with a [malli](https://github.com/metosin/malli) schema. +The markdown content is converted into [hiccup](https://github.com/weavejester/hiccup) (a clojure-friendly markup) and the post/vignette configurations use standard YAML frontmatter which is validated at compile time with a [malli](https://github.com/metosin/malli) schema. Light/dark mode and code block syntax highlighted are supported. @@ -48,7 +49,7 @@ For frontend management, [re-frame](https://github.com/day8/re-frame) is used. T The routing is done with [reitit](https://github.com/metosin/reitit). -The markdown content is converted into [hiccup](https://github.com/weavejester/hiccup) (a clojure-friendly markup) and the post/vignette configurations are made in EDN which is validated at compile time with a [malli](https://github.com/metosin/malli) schema. +The markdown content is converted into [hiccup](https://github.com/weavejester/hiccup) (a clojure-friendly markup) and the post/vignette configurations use standard YAML frontmatter which is validated at compile time with a [malli](https://github.com/metosin/malli) schema. Light/dark mode and code block syntax highlighted are supported. @@ -99,33 +100,54 @@ Some `blog` posts have a flag to display the post in the home page via what I ca When the user clicks on a vignette, he goes to a new route with the full post content. -### Config Clojure map - -A markdown file of a post is divided into 3 parts: -- above the demarcation `+ + +` is a clojure map of configs (title, page, order etc.) -- below the first demarcation `+ + +` is the post short description as markdown. -- below the second demarcation `+ + +` is the post content as markdown. - -Here is an example of clojure config map for a post: - -```clojure -#:post{:id "clojure-full-stack-webapp" - :page :blog - :home-page? true ;; display vignette in home page - :employer "Flybot Pte Ltd" - :date "2022" - :repos [["Flybot" "https://github.com/skydread1/flybot.sg"]] - :articles [["How to deploy full stack Clojure website to AWS" "../blog/deploy-clj-app-to-aws"] - ["Lasagna-pull Pattern applied to flybot.sg backend" "../blog/lasagna-pull-applied-to-flybot"] - ["Clojure Mono Repo example : server + 2 clients" "../blog/clojure-mono-repo"]] - :title "Flybot Website" - :tags ["Clojure" "ClojureScript" "Figwheel" "Re-Frame" "Malli" "Lasagna-pull" "Fun-map" "Datalevin" "Reitit"] - :css-class "flybot-website" - :image #:image{:src "https://www.flybot.sg/assets/flybot-logo.png" - :src-dark "https://www.flybot.sg/assets/flybot-logo.png" - :alt "Flybot Logo"}} +### Markdown Format + +Each markdown file uses standard **YAML frontmatter** for metadata, followed by an optional **`## TLDR`** section for the summary: + +```markdown +--- +id: clojure-full-stack-webapp +page: blog +home-page?: true +employer: Flybot Pte Ltd +date: + - 2022-01-01 +title: Flybot Website +tags: + - Clojure + - ClojureScript + - Re-Frame +rss-feeds: + - all + - clojure +css-class: flybot-website +image: + src: https://www.flybot.sg/assets/flybot-logo.png + src-dark: https://www.flybot.sg/assets/flybot-logo.png + alt: Flybot Logo +repos: + - ["Flybot", "https://github.com/skydread1/flybot.sg"] +articles: + - ["Deploy Clojure app to AWS", "../blog/deploy-clj-app-to-aws"] + - ["Lasagna-pull Pattern", "../blog/lasagna-pull-applied-to-flybot"] +--- + +## TLDR + +A brief summary of the Flybot project. + +## Introduction + +Full article content starts here... ``` +**Key points:** +- **YAML frontmatter** (between `---` markers) configures post metadata +- **`## TLDR` section** (optional) provides a summary for vignettes +- **Standard markdown** for the rest of the content +- Posts with `home-page?: true` will display their vignette on the home page +- Posts with `rss-feeds: ["clojure"]` will be included in the RSS feed + ## Compile At **CLJ** compile time, the following steps happen: diff --git a/src/loicb/server/content/blog/props-recommender.md b/src/loicb/server/content/blog/props-recommender.md index c3f03c1..ed891e3 100644 --- a/src/loicb/server/content/blog/props-recommender.md +++ b/src/loicb/server/content/blog/props-recommender.md @@ -14,6 +14,7 @@ tags: - Kubernetes - AWS EKS rss-feeds: + - all - clojure image: src: https://www.flybot.sg/assets/flybot-logo.png diff --git a/src/loicb/server/content/blog/pwd-manager+2fa.md b/src/loicb/server/content/blog/pwd-manager+2fa.md index fa3c593..ec5ec10 100644 --- a/src/loicb/server/content/blog/pwd-manager+2fa.md +++ b/src/loicb/server/content/blog/pwd-manager+2fa.md @@ -10,6 +10,8 @@ tags: - Password Manager - 2FA - Security +rss-feeds: + - all image: src: /assets/pwd-man-2fa/blue-shield.png src-dark: /assets/pwd-man-2fa/white-shield.png diff --git a/src/loicb/server/content/blog/reagent-native-app.md b/src/loicb/server/content/blog/reagent-native-app.md index bc230b5..701a5a6 100644 --- a/src/loicb/server/content/blog/reagent-native-app.md +++ b/src/loicb/server/content/blog/reagent-native-app.md @@ -11,6 +11,7 @@ tags: - Reagent - Figwheel rss-feeds: + - all - clojure repos: - ["Flybot", "https://github.com/skydread1/flybot.sg"] diff --git a/src/loicb/server/content/blog/redirect-domain-subdomain.md b/src/loicb/server/content/blog/redirect-domain-subdomain.md index 1b6b3f6..801faf1 100644 --- a/src/loicb/server/content/blog/redirect-domain-subdomain.md +++ b/src/loicb/server/content/blog/redirect-domain-subdomain.md @@ -10,6 +10,8 @@ tags: - Load Balancers - DNS - GoDaddy +rss-feeds: + - all image: src: /assets/loic-blog-logo.png src-dark: /assets/loic-blog-logo.png diff --git a/src/loicb/server/content/blog/testing-in-clojure.md b/src/loicb/server/content/blog/testing-in-clojure.md index bccfe1b..d6c9f90 100644 --- a/src/loicb/server/content/blog/testing-in-clojure.md +++ b/src/loicb/server/content/blog/testing-in-clojure.md @@ -13,6 +13,7 @@ tags: - Instrumentation - Data validation/generation rss-feeds: + - all - clojure image: src: /assets/loic-blog-logo.png From 028a3d68a281b522c5afad32c78f2765d4443d85 Mon Sep 17 00:00:00 2001 From: skydread1 Date: Mon, 13 Oct 2025 14:58:49 +0800 Subject: [PATCH 09/11] Refactoring --- .gitignore | 96 ++------------- README.md | 115 +++++++++++------- deps.edn | 2 +- figwheel-main.edn | 4 +- .../content/about/aboutme.md | 0 .../content/blog/blog-django.md | 0 .../content/blog/cd-sl-p-method.md | 0 .../content/blog/clj-in-unity.md | 0 .../content/blog/clj-mono-repo.md | 0 .../content/blog/datomic-setup.md | 0 .../content/blog/deploy-clj-app-to-aws.md | 0 .../content/blog/deploy-django-beanstalk.md | 0 .../content/blog/flybot-card-games.md | 0 .../content/blog/flybot-mobile-app.md | 0 .../content/blog/flybot-stats-rama.md | 0 .../content/blog/flybot-website.md | 0 .../content/blog/fun-map-flybot.md | 0 .../{server => build}/content/blog/fun-map.md | 0 .../content/blog/git-workflows.md | 0 .../content/blog/hibou-platform.md | 0 .../content/blog/juxt-tick-lib.md | 0 .../content/blog/lasagna-pull-flybot.md | 0 .../content/blog/lasagna-pull.md | 0 .../content/blog/magic-nostrand.md | 0 .../content/blog/mcts-card-games.md | 0 .../{server => build}/content/blog/mcts.md | 0 .../content/blog/port-clj-lib-to-clr.md | 0 .../content/blog/portfolio-website.md | 60 ++++++++- .../content/blog/props-recommender.md | 0 .../content/blog/pwd-manager+2fa.md | 0 .../content/blog/reagent-native-app.md | 0 .../content/blog/redirect-domain-subdomain.md | 0 .../content/blog/testing-in-clojure.md | 0 src/loicb/{server => build}/md.clj | 43 +------ src/loicb/build/rss.clj | 79 ++++++++++++ src/loicb/client/core/md.cljs | 2 +- src/loicb/{server => dev}/core.clj | 4 +- src/loicb/server/rss.clj | 6 - test/loicb/{server => build}/md_test.clj | 4 +- 39 files changed, 227 insertions(+), 188 deletions(-) rename src/loicb/{server => build}/content/about/aboutme.md (100%) rename src/loicb/{server => build}/content/blog/blog-django.md (100%) rename src/loicb/{server => build}/content/blog/cd-sl-p-method.md (100%) rename src/loicb/{server => build}/content/blog/clj-in-unity.md (100%) rename src/loicb/{server => build}/content/blog/clj-mono-repo.md (100%) rename src/loicb/{server => build}/content/blog/datomic-setup.md (100%) rename src/loicb/{server => build}/content/blog/deploy-clj-app-to-aws.md (100%) rename src/loicb/{server => build}/content/blog/deploy-django-beanstalk.md (100%) rename src/loicb/{server => build}/content/blog/flybot-card-games.md (100%) rename src/loicb/{server => build}/content/blog/flybot-mobile-app.md (100%) rename src/loicb/{server => build}/content/blog/flybot-stats-rama.md (100%) rename src/loicb/{server => build}/content/blog/flybot-website.md (100%) rename src/loicb/{server => build}/content/blog/fun-map-flybot.md (100%) rename src/loicb/{server => build}/content/blog/fun-map.md (100%) rename src/loicb/{server => build}/content/blog/git-workflows.md (100%) rename src/loicb/{server => build}/content/blog/hibou-platform.md (100%) rename src/loicb/{server => build}/content/blog/juxt-tick-lib.md (100%) rename src/loicb/{server => build}/content/blog/lasagna-pull-flybot.md (100%) rename src/loicb/{server => build}/content/blog/lasagna-pull.md (100%) rename src/loicb/{server => build}/content/blog/magic-nostrand.md (100%) rename src/loicb/{server => build}/content/blog/mcts-card-games.md (100%) rename src/loicb/{server => build}/content/blog/mcts.md (100%) rename src/loicb/{server => build}/content/blog/port-clj-lib-to-clr.md (100%) rename src/loicb/{server => build}/content/blog/portfolio-website.md (80%) rename src/loicb/{server => build}/content/blog/props-recommender.md (100%) rename src/loicb/{server => build}/content/blog/pwd-manager+2fa.md (100%) rename src/loicb/{server => build}/content/blog/reagent-native-app.md (100%) rename src/loicb/{server => build}/content/blog/redirect-domain-subdomain.md (100%) rename src/loicb/{server => build}/content/blog/testing-in-clojure.md (100%) rename src/loicb/{server => build}/md.clj (71%) create mode 100644 src/loicb/build/rss.clj rename src/loicb/{server => dev}/core.clj (68%) delete mode 100644 src/loicb/server/rss.clj rename test/loicb/{server => build}/md_test.clj (98%) diff --git a/.gitignore b/.gitignore index ef59170..63e6113 100644 --- a/.gitignore +++ b/.gitignore @@ -1,95 +1,21 @@ -##========== -## Clojure +# Clojure .cpcache .lsp -.nrepl* -.target +.nrepl-port .rebel* -*.swp -deps/ -build -.idea -*.iml + +# IDE / Editors .calva .vscode .clj-kondo -.nrepl-port - -## config -OAUTH2.edn -admin.edn - -## build -target -dist -resources/public/blog/rss - -## datalevin -datalevin - -##========== -## React Native - -# OSX -# -.DS_Store - -# Xcode -# -build/ -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata -*.xccheckout -*.moved-aside -DerivedData -*.hmap -*.ipa -*.xcuserstate -ios/.xcode.env.local - -# Android/IntelliJ -# -build/ .idea -.gradle -local.properties *.iml -*.hprof -.cxx/ -*.keystore -!debug.keystore - -# node.js -# -node_modules/ -npm-debug.log -yarn-error.log - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the -# screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/ - -**/fastlane/report.xml -**/fastlane/Preview.html -**/fastlane/screenshots -**/fastlane/test_output - -# Bundle artifact -*.jsbundle +*.swp -# Ruby / CocoaPods -/ios/Pods/ -/vendor/bundle/ +# Build artifacts +target/ +dist/ +resources/public/blog/rss/ -# Temporary files created by Metro to check the health of the file watcher -.metro-health-check* +# OS +.DS_Store diff --git a/README.md b/README.md index bad9491..a7e458a 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ For frontend management, [re-frame](https://github.com/day8/re-frame) is used. T The routing is done with [reitit](https://github.com/metosin/reitit). -The markdown content is converted into [hiccup](https://github.com/weavejester/hiccup) (a clojure-friendly markup) and the post/vignette configurations are made in EDN which is validated at compile time with a [malli](https://github.com/metosin/malli) schema. +The markdown content is converted into [hiccup](https://github.com/weavejester/hiccup) (a clojure-friendly markup) and the post/vignette configurations use standard YAML frontmatter which is validated at compile time with a [malli](https://github.com/metosin/malli) schema. -At compile time, a RSS feed is generated for the clojure blog posts using [clj-rss](https://github.com/yogthos/clj-rss). +At compile time, RSS feeds are generated for blog posts using [clj-rss](https://github.com/yogthos/clj-rss). Multiple feeds can be configured (e.g., "clojure" for Clojure-specific posts, "all" for all blog posts). The app is deployed on **Netlify** via GitHub Actions every time a branch is merged to master. Build artifacts (JS bundles, RSS feeds) are generated during CI/CD and are not committed to the repository. @@ -34,11 +34,12 @@ The app is deployed on **Netlify** via GitHub Actions every time a branch is mer ## πŸ”· Features -All the posts/vignettes are written in markdown files in [content](./src/loicb/server/content/) folder. +All the posts/vignettes are written in standard markdown files in [content](./src/loicb/build/content/) folder. The UI supports light/dark mode and is responsive. -At the top of each markdown file, you can provide an EDN config map to add additional properties to the post such as an illustrative image, the order on the page, the title in the URL, github and article links etc... +At the top of each markdown file, you can provide YAML frontmatter to configure post metadata such as title, date, tags, image, github repos, article links, and RSS feed inclusion. A `## TLDR` section can be added to provide a summary that appears in post vignettes. + When you create a pull request (PR) to master, **Netlify** provides a preview of what the new app version would look like. For development, starting a clj/cljs REPL with `figwheel` allows you to have the local changes reflected on port 9500 on file save. @@ -58,41 +59,44 @@ For instance: β”‚ └── some-article.md ``` -### Config Clojure map - -A markdown file for a post is divided into three parts, separated by +++ demarcations: -``` -some config map -+++ -post summary -+++ -full post content -``` - -Following is an example of clojure map for a post config: - -```clojure -#:post{:date ["2024-02-01" "2024-04-01"] - :page :portfolio - :title "My New Project" - :css-class "my-project" - :image-beside #:image{:src "/assets/some-illustrative-img.png" - :src-dark "/assets/some-illustrative-img-dark.png" - :alt "A logo"}} -``` - -This map goes at the top of a markdown file like so: - -``` -post map here -+++ -My article summary goes here -+++ -My article content goes here -... +### Markdown Format + +Each markdown file uses standard **YAML frontmatter** for metadata, followed by an optional **`## TLDR`** section for the summary, and then the full article content: + +```markdown +--- +title: My New Project +date: 2024-02-01 +page: blog +id: my-new-project +tags: + - Clojure + - Web +rss-feeds: + - clojure +css-class: my-project +image: + src: /assets/some-illustrative-img.png + src-dark: /assets/some-illustrative-img-dark.png + alt: A logo +repos: + - ["Project Name", "https://github.com/user/repo"] +--- + +## TLDR + +A brief summary of the project. This will appear in the vignette/preview. + +## Introduction + +The full article content starts here... ``` -I refer to the post summary part as the `vignette` because it can conveniently be used on a home page for instance to just display a recap of the projects. +**Key points:** +- **YAML frontmatter** (between `---` markers) configures post metadata +- **`## TLDR` section** (optional) provides a summary shown in vignettes +- **Standard markdown** for the rest of the content +- The TLDR section is automatically extracted and removed from the full article view to avoid duplication ## πŸ”· Getting Started @@ -114,7 +118,7 @@ Then connect to the REPL server with project type `deps.edn + Figwheel Main` and Once the REPL started, saving a cljs file will automatically update the js in your localhost. -However, if you change the content of a markdown file, you will need to save the file [md.clj](./src/loicb/server/md.clj) which is where the macro that reads all the markdown files resides. Refresh your browser and the new markdown content will appear. +However, if you change the content of a markdown file, you will need to save the file [md.clj](./src/loicb/build/md.clj) which is where the macro that reads all the markdown files resides. Refresh your browser and the new markdown content will appear. ## πŸ”· Tests @@ -170,15 +174,30 @@ bb build Builds the optimized ClojureScript bundle and generates `index.html` in `target/public`. -### RSS feed +### RSS feeds ```sh bb rss ``` -Generates RSS feed for blog posts tagged with `clojure` in `resources/public/blog/rss/`. +Generates all configured RSS feeds in `resources/public/blog/rss/`. + +Currently available feeds: +- **clojure**: Posts tagged with Clojure content +- **all**: All blog posts + +To include a post in specific RSS feeds, add to your YAML frontmatter: +```yaml +rss-feeds: + - clojure # Include in Clojure feed + - all # Include in all posts feed +``` + +Available RSS feeds: +- [blog/rss/clojure-feed.xml](https://www.loicb.dev/blog/rss/clojure-feed.xml) - Clojure-related articles +- [blog/rss/all-feed.xml](https://www.loicb.dev/blog/rss/all-feed.xml) - All blog articles -The RSS **clojure** feed is accessible at [blog/rss/clojure-feed.xml](https://www.loicb.dev/blog/rss/clojure-feed.xml). +New feeds can be added by updating the `feed-configs` in `src/loicb/build/rss.clj`. ### Production release @@ -224,20 +243,22 @@ To enable deployments, configure these secrets in your GitHub repository setting ### Config validation -To know what params can be provided to the config map, you can have a look at the malli schema in [validation](./src/loicb/common/validation.cljc). You can take inspiration from my own posts in [content](./src/loicb/server/content/). +To know what params can be provided to the config map, you can have a look at the malli schema in [validation](./src/loicb/common/validation.cljc). You can take inspiration from my own posts in [content](./src/loicb/build/content/). ### Compile process At CLJ compile time, the following steps happen: -1. Read all markdown files -2. Validate the post configs against a `Malli` schema -3. Assoc the post markdown content to the configs -4. A macro stores a vector of the posts to be loaded in the re-frame DB +1. Read all markdown files with YAML frontmatter +2. Parse YAML metadata using `markdown-clj` +3. Extract `## TLDR` section as post summary +4. Normalize metadata (convert YAML types to Clojure types) +5. Validate the post configs against a `Malli` schema +6. A macro stores a vector of the posts to be loaded in the re-frame DB At CLJS compile time, the following steps happen: 1. A re-frame event initializes the re-frame DB, loading all the posts from the clojure macro and the theme from local storage. 2. The `reitit` route table is created -3. The post markdown are converted to hiccup via `markdown-to-hiccup`. +3. The post markdown content is converted to hiccup via `markdown-to-hiccup`. ## πŸ”· Contributing diff --git a/deps.edn b/deps.edn index 30e23f6..000131a 100644 --- a/deps.edn +++ b/deps.edn @@ -46,7 +46,7 @@ "-co" "tests.cljs.edn" "-m" "loicb.client.test-runner"]} ;; RSS - :rss {:main-opts ["-m" "loicb.server.rss"]} + :rss {:main-opts ["-m" "loicb.build.rss"]} ;;---------- BUILD ---------- :build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.10" :git/sha "deedd62"}} diff --git a/figwheel-main.edn b/figwheel-main.edn index 94862fb..29d46d4 100644 --- a/figwheel-main.edn +++ b/figwheel-main.edn @@ -1,2 +1,2 @@ -{:ring-handler loicb.server.core/handler - :auto-testing true} \ No newline at end of file +{:ring-handler loicb.dev.core/handler + :auto-testing true} diff --git a/src/loicb/server/content/about/aboutme.md b/src/loicb/build/content/about/aboutme.md similarity index 100% rename from src/loicb/server/content/about/aboutme.md rename to src/loicb/build/content/about/aboutme.md diff --git a/src/loicb/server/content/blog/blog-django.md b/src/loicb/build/content/blog/blog-django.md similarity index 100% rename from src/loicb/server/content/blog/blog-django.md rename to src/loicb/build/content/blog/blog-django.md diff --git a/src/loicb/server/content/blog/cd-sl-p-method.md b/src/loicb/build/content/blog/cd-sl-p-method.md similarity index 100% rename from src/loicb/server/content/blog/cd-sl-p-method.md rename to src/loicb/build/content/blog/cd-sl-p-method.md diff --git a/src/loicb/server/content/blog/clj-in-unity.md b/src/loicb/build/content/blog/clj-in-unity.md similarity index 100% rename from src/loicb/server/content/blog/clj-in-unity.md rename to src/loicb/build/content/blog/clj-in-unity.md diff --git a/src/loicb/server/content/blog/clj-mono-repo.md b/src/loicb/build/content/blog/clj-mono-repo.md similarity index 100% rename from src/loicb/server/content/blog/clj-mono-repo.md rename to src/loicb/build/content/blog/clj-mono-repo.md diff --git a/src/loicb/server/content/blog/datomic-setup.md b/src/loicb/build/content/blog/datomic-setup.md similarity index 100% rename from src/loicb/server/content/blog/datomic-setup.md rename to src/loicb/build/content/blog/datomic-setup.md diff --git a/src/loicb/server/content/blog/deploy-clj-app-to-aws.md b/src/loicb/build/content/blog/deploy-clj-app-to-aws.md similarity index 100% rename from src/loicb/server/content/blog/deploy-clj-app-to-aws.md rename to src/loicb/build/content/blog/deploy-clj-app-to-aws.md diff --git a/src/loicb/server/content/blog/deploy-django-beanstalk.md b/src/loicb/build/content/blog/deploy-django-beanstalk.md similarity index 100% rename from src/loicb/server/content/blog/deploy-django-beanstalk.md rename to src/loicb/build/content/blog/deploy-django-beanstalk.md diff --git a/src/loicb/server/content/blog/flybot-card-games.md b/src/loicb/build/content/blog/flybot-card-games.md similarity index 100% rename from src/loicb/server/content/blog/flybot-card-games.md rename to src/loicb/build/content/blog/flybot-card-games.md diff --git a/src/loicb/server/content/blog/flybot-mobile-app.md b/src/loicb/build/content/blog/flybot-mobile-app.md similarity index 100% rename from src/loicb/server/content/blog/flybot-mobile-app.md rename to src/loicb/build/content/blog/flybot-mobile-app.md diff --git a/src/loicb/server/content/blog/flybot-stats-rama.md b/src/loicb/build/content/blog/flybot-stats-rama.md similarity index 100% rename from src/loicb/server/content/blog/flybot-stats-rama.md rename to src/loicb/build/content/blog/flybot-stats-rama.md diff --git a/src/loicb/server/content/blog/flybot-website.md b/src/loicb/build/content/blog/flybot-website.md similarity index 100% rename from src/loicb/server/content/blog/flybot-website.md rename to src/loicb/build/content/blog/flybot-website.md diff --git a/src/loicb/server/content/blog/fun-map-flybot.md b/src/loicb/build/content/blog/fun-map-flybot.md similarity index 100% rename from src/loicb/server/content/blog/fun-map-flybot.md rename to src/loicb/build/content/blog/fun-map-flybot.md diff --git a/src/loicb/server/content/blog/fun-map.md b/src/loicb/build/content/blog/fun-map.md similarity index 100% rename from src/loicb/server/content/blog/fun-map.md rename to src/loicb/build/content/blog/fun-map.md diff --git a/src/loicb/server/content/blog/git-workflows.md b/src/loicb/build/content/blog/git-workflows.md similarity index 100% rename from src/loicb/server/content/blog/git-workflows.md rename to src/loicb/build/content/blog/git-workflows.md diff --git a/src/loicb/server/content/blog/hibou-platform.md b/src/loicb/build/content/blog/hibou-platform.md similarity index 100% rename from src/loicb/server/content/blog/hibou-platform.md rename to src/loicb/build/content/blog/hibou-platform.md diff --git a/src/loicb/server/content/blog/juxt-tick-lib.md b/src/loicb/build/content/blog/juxt-tick-lib.md similarity index 100% rename from src/loicb/server/content/blog/juxt-tick-lib.md rename to src/loicb/build/content/blog/juxt-tick-lib.md diff --git a/src/loicb/server/content/blog/lasagna-pull-flybot.md b/src/loicb/build/content/blog/lasagna-pull-flybot.md similarity index 100% rename from src/loicb/server/content/blog/lasagna-pull-flybot.md rename to src/loicb/build/content/blog/lasagna-pull-flybot.md diff --git a/src/loicb/server/content/blog/lasagna-pull.md b/src/loicb/build/content/blog/lasagna-pull.md similarity index 100% rename from src/loicb/server/content/blog/lasagna-pull.md rename to src/loicb/build/content/blog/lasagna-pull.md diff --git a/src/loicb/server/content/blog/magic-nostrand.md b/src/loicb/build/content/blog/magic-nostrand.md similarity index 100% rename from src/loicb/server/content/blog/magic-nostrand.md rename to src/loicb/build/content/blog/magic-nostrand.md diff --git a/src/loicb/server/content/blog/mcts-card-games.md b/src/loicb/build/content/blog/mcts-card-games.md similarity index 100% rename from src/loicb/server/content/blog/mcts-card-games.md rename to src/loicb/build/content/blog/mcts-card-games.md diff --git a/src/loicb/server/content/blog/mcts.md b/src/loicb/build/content/blog/mcts.md similarity index 100% rename from src/loicb/server/content/blog/mcts.md rename to src/loicb/build/content/blog/mcts.md diff --git a/src/loicb/server/content/blog/port-clj-lib-to-clr.md b/src/loicb/build/content/blog/port-clj-lib-to-clr.md similarity index 100% rename from src/loicb/server/content/blog/port-clj-lib-to-clr.md rename to src/loicb/build/content/blog/port-clj-lib-to-clr.md diff --git a/src/loicb/server/content/blog/portfolio-website.md b/src/loicb/build/content/blog/portfolio-website.md similarity index 80% rename from src/loicb/server/content/blog/portfolio-website.md rename to src/loicb/build/content/blog/portfolio-website.md index 1f877d8..0fde83c 100644 --- a/src/loicb/server/content/blog/portfolio-website.md +++ b/src/loicb/build/content/blog/portfolio-website.md @@ -146,7 +146,7 @@ Full article content starts here... - **`## TLDR` section** (optional) provides a summary for vignettes - **Standard markdown** for the rest of the content - Posts with `home-page?: true` will display their vignette on the home page -- Posts with `rss-feeds: ["clojure"]` will be included in the RSS feed +- Posts with `rss-feeds: ["all", "clojure"]` will be included in the respective RSS feeds ## Compile @@ -186,13 +186,69 @@ bb fmt-fix # Fix code formatting ```bash bb build # Build optimized ClojureScript bundle -bb rss # Generate RSS feeds for blog posts +bb rss # Generate all configured RSS feeds (clojure, all, etc.) bb dist # Create complete distribution in dist/ folder bb clean # Clean build artifacts ``` The `bb dist` command creates a complete static site in the `dist/` directory ready for deployment. +## RSS Feeds + +The portfolio generates multiple RSS feeds using [clj-rss](https://github.com/yogthos/clj-rss) to allow readers to subscribe to blog updates. + +### Available Feeds + +Currently, two RSS feeds are available: + +1. **All Blog Posts**: [/blog/rss/all-feed.xml](https://www.loicb.dev/blog/rss/all-feed.xml) + - Contains all blog articles + - Updated whenever any blog post is published + +2. **Clojure Feed**: [/blog/rss/clojure-feed.xml](https://www.loicb.dev/blog/rss/clojure-feed.xml) + - Contains only Clojure-related articles + - Useful for readers specifically interested in Clojure content + +### How It Works + +Posts control which feeds they appear in via the `rss-feeds` field in their YAML frontmatter: + +```yaml +rss-feeds: + - all # Include in "all" feed + - clojure # Include in "clojure" feed +``` + +The RSS generation is handled by a generic `rss-feed` function in `src/loicb/build/rss.clj` that: +1. Loads all blog posts from `md.clj` +2. Filters posts by the specified feed name +3. Sorts by date (newest first) +4. Generates the RSS XML with full content +5. Writes to `resources/public/blog/rss/{feed-name}-feed.xml` + +A `rss-feeds` function generates all configured feeds at once during the build process. + +### Adding New Feeds + +To add a new feed (e.g., for Python content): + +1. Add the feed configuration to `feed-configs` in `src/loicb/build/rss.clj`: + ```clojure + "python" {:title "Loic Blanchard - Python Blog Feed" + :description "Articles related to Python"} + ``` + +2. Add the feed identifier to relevant posts: + ```yaml + rss-feeds: + - all + - python + ``` + +3. Run `bb rss` to generate the new feed + +The generated feeds include full article content (not just summaries), making them suitable for offline reading in RSS readers. + ## Continuous Integration & Deployment The project uses GitHub Actions for CI/CD with separate workflows for pull requests and production deployments. diff --git a/src/loicb/server/content/blog/props-recommender.md b/src/loicb/build/content/blog/props-recommender.md similarity index 100% rename from src/loicb/server/content/blog/props-recommender.md rename to src/loicb/build/content/blog/props-recommender.md diff --git a/src/loicb/server/content/blog/pwd-manager+2fa.md b/src/loicb/build/content/blog/pwd-manager+2fa.md similarity index 100% rename from src/loicb/server/content/blog/pwd-manager+2fa.md rename to src/loicb/build/content/blog/pwd-manager+2fa.md diff --git a/src/loicb/server/content/blog/reagent-native-app.md b/src/loicb/build/content/blog/reagent-native-app.md similarity index 100% rename from src/loicb/server/content/blog/reagent-native-app.md rename to src/loicb/build/content/blog/reagent-native-app.md diff --git a/src/loicb/server/content/blog/redirect-domain-subdomain.md b/src/loicb/build/content/blog/redirect-domain-subdomain.md similarity index 100% rename from src/loicb/server/content/blog/redirect-domain-subdomain.md rename to src/loicb/build/content/blog/redirect-domain-subdomain.md diff --git a/src/loicb/server/content/blog/testing-in-clojure.md b/src/loicb/build/content/blog/testing-in-clojure.md similarity index 100% rename from src/loicb/server/content/blog/testing-in-clojure.md rename to src/loicb/build/content/blog/testing-in-clojure.md diff --git a/src/loicb/server/md.clj b/src/loicb/build/md.clj similarity index 71% rename from src/loicb/server/md.clj rename to src/loicb/build/md.clj index 4652f09..7c13452 100644 --- a/src/loicb/server/md.clj +++ b/src/loicb/build/md.clj @@ -1,14 +1,12 @@ -(ns loicb.server.md +(ns loicb.build.md (:require [clojure.java.io :as io] [clojure.string :as str] [loicb.common.validation :as v] - [markdown.core :as md] - [clj-rss.core :as rss] - [tick.core :as t])) + [markdown.core :as md])) ;; ---------- IO ---------- -(def directory "./src/loicb/server/content/") +(def directory "./src/loicb/build/content/") (def sub-dirs "pages to be published." ["about" "blog"]) @@ -166,38 +164,3 @@ (defmacro load-posts-macro [] `~load-posts) - -;; ---------- Markdown to RSS feed ---------- - -(defn link-relative->absolute - [text state] - [(str/replace text #"src=\"/" "src=\"https://www.loicb.dev/") state]) - -(defn blog-rss-clojure-feed - [] - (let [clojure-blog-posts (->> (files-of "blog") - (mapv load-post) - (filter #(some #{"clojure"} (:post/rss-feeds %))) - (sort-by #(first (:post/date %))) - reverse) - base-url "https://www.loicb.dev" - blog-url (str base-url "/blog") - channel {:title "Loic Blanchard - Clojure Blog Feed" - :link "https://www.loicb.dev" - :feed-url (str blog-url "/rss/clojure-feed.xml") - :description "Articles related to Clojure" - :language "en-us" - :lastBuildDate (t/now)} - items (for [{:post/keys [id date md-content md-content-short title]} clojure-blog-posts] - {:title title - :link (str blog-url "/" id) - :guid (str blog-url "/" id) - :pubDate (-> (t/time "18:00") (t/on (first date)) (t/in "Asia/Singapore") t/instant) - :description md-content-short - "content:encoded" (str "absolute]) - "]]>")})] - (->> (apply rss/channel-xml (conj items channel)) - (spit "resources/public/blog/rss/clojure-feed.xml")))) diff --git a/src/loicb/build/rss.clj b/src/loicb/build/rss.clj new file mode 100644 index 0000000..aaf376c --- /dev/null +++ b/src/loicb/build/rss.clj @@ -0,0 +1,79 @@ +(ns loicb.build.rss + (:require [clojure.string :as str] + [loicb.build.md :as md] + [clj-rss.core :as rss] + [markdown.core :as markdown] + [tick.core :as t])) + +;; ---------- RSS Feed Generation ---------- + +(defn link-relative->absolute + "Transform markdown image links from relative to absolute URLs." + [text state] + [(str/replace text #"src=\"/" "src=\"https://www.loicb.dev/") state]) + +(def feed-configs + "Configuration for different RSS feeds." + {"clojure" {:title "Loic Blanchard - Clojure Blog Feed" + :description "Articles related to Clojure"} + "all" {:title "Loic Blanchard - Blog Feed" + :description "All blog articles"}}) + +(defn rss-feed + "Generate an RSS feed for a specific feed-name. + + Args: + - feed-name: string identifier for the feed (e.g., \"clojure\", \"all\") + + The function: + 1. Loads all blog posts + 2. Filters posts that have feed-name in their :post/rss-feeds + 3. Sorts by date (newest first) + 4. Generates RSS XML + 5. Writes to resources/public/blog/rss/{feed-name}-feed.xml" + [feed-name] + (let [config (get feed-configs feed-name) + _ (when-not config + (throw (ex-info (str "Unknown feed: " feed-name) + {:feed-name feed-name + :available-feeds (keys feed-configs)}))) + filtered-posts (->> (md/files-of "blog") + (mapv md/load-post) + (filter #(some #{feed-name} (:post/rss-feeds %))) + (sort-by #(first (:post/date %))) + reverse) + base-url "https://www.loicb.dev" + blog-url (str base-url "/blog") + channel {:title (:title config) + :link base-url + :feed-url (str blog-url "/rss/" feed-name "-feed.xml") + :description (:description config) + :language "en-us" + :lastBuildDate (t/now)} + items (for [{:post/keys [id date md-content md-content-short title]} filtered-posts] + {:title title + :link (str blog-url "/" id) + :guid (str blog-url "/" id) + :pubDate (-> (t/time "18:00") (t/on (first date)) (t/in "Asia/Singapore") t/instant) + :description md-content-short + "content:encoded" (str "absolute]) + "]]>")}) + output-path (str "resources/public/blog/rss/" feed-name "-feed.xml")] + (->> (apply rss/channel-xml (conj items channel)) + (spit output-path)) + (println (str "Generated RSS feed: " output-path " with " (count filtered-posts) " posts")))) + +(defn rss-feeds + "Generate all configured RSS feeds." + [] + (doseq [feed-name (keys feed-configs)] + (rss-feed feed-name)) + (println "All RSS feeds generated successfully!")) + +(defn -main + "Generate all configured RSS feeds." + [& _] + (rss-feeds)) diff --git a/src/loicb/client/core/md.cljs b/src/loicb/client/core/md.cljs index 1d79dce..0cc8d5e 100644 --- a/src/loicb/client/core/md.cljs +++ b/src/loicb/client/core/md.cljs @@ -1,5 +1,5 @@ (ns loicb.client.core.md - (:require-macros [loicb.server.md :as macro])) + (:require-macros [loicb.build.md :as macro])) (def posts "Read posts config and content from the md files" diff --git a/src/loicb/server/core.clj b/src/loicb/dev/core.clj similarity index 68% rename from src/loicb/server/core.clj rename to src/loicb/dev/core.clj index 8c5ec7b..1dfb8a4 100644 --- a/src/loicb/server/core.clj +++ b/src/loicb/dev/core.clj @@ -1,8 +1,8 @@ -(ns loicb.server.core +(ns loicb.dev.core (:require [clojure.java.io :as io])) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn handler [_] {:status 200 :headers {"Content-Type" "text/html"} - :body (slurp (io/resource "public/index.html"))}) \ No newline at end of file + :body (slurp (io/resource "public/index.html"))}) diff --git a/src/loicb/server/rss.clj b/src/loicb/server/rss.clj deleted file mode 100644 index ff1d45e..0000000 --- a/src/loicb/server/rss.clj +++ /dev/null @@ -1,6 +0,0 @@ -(ns loicb.server.rss - (:require [loicb.server.md :as md])) - -(defn -main - [& _] - (md/blog-rss-clojure-feed)) diff --git a/test/loicb/server/md_test.clj b/test/loicb/build/md_test.clj similarity index 98% rename from test/loicb/server/md_test.clj rename to test/loicb/build/md_test.clj index 6f4e068..e19aab6 100644 --- a/test/loicb/server/md_test.clj +++ b/test/loicb/build/md_test.clj @@ -1,8 +1,8 @@ -(ns loicb.server.md-test +(ns loicb.build.md-test (:require [clojure.string :as s] [clojure.test :refer [deftest is testing]] - [loicb.server.md :as sut] + [loicb.build.md :as sut] [markdown.core :as md])) ;; Test data From 8c9ff4a629207219fc3f27c6a6f9062194eb02c9 Mon Sep 17 00:00:00 2001 From: skydread1 Date: Mon, 13 Oct 2025 16:16:07 +0800 Subject: [PATCH 10/11] Remove from clojure feed --- src/loicb/build/content/blog/hibou-platform.md | 1 - 1 file changed, 1 deletion(-) diff --git a/src/loicb/build/content/blog/hibou-platform.md b/src/loicb/build/content/blog/hibou-platform.md index 13012bd..41a137f 100644 --- a/src/loicb/build/content/blog/hibou-platform.md +++ b/src/loicb/build/content/blog/hibou-platform.md @@ -15,7 +15,6 @@ tags: - Full-Stack rss-feeds: - all - - clojure image: src: https://www.flybot.sg/assets/flybot-logo.png src-dark: https://www.flybot.sg/assets/flybot-logo.png From da4e3ab4c396b009d19de6732899c05e5d18c102 Mon Sep 17 00:00:00 2001 From: skydread1 Date: Mon, 13 Oct 2025 20:03:43 +0800 Subject: [PATCH 11/11] Update about me tags --- src/loicb/build/content/about/aboutme.md | 4 ++-- src/loicb/build/content/blog/hibou-platform.md | 2 +- src/loicb/client/core/dom/header.cljs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/loicb/build/content/about/aboutme.md b/src/loicb/build/content/about/aboutme.md index 50db659..89c3ff1 100644 --- a/src/loicb/build/content/about/aboutme.md +++ b/src/loicb/build/content/about/aboutme.md @@ -2,7 +2,7 @@ id: about-me page: about date: - - 2024-11-12 + - 2025-10-13 title: About Me employer: My journey so far css-class: about-me @@ -10,7 +10,7 @@ tags: - πŸ› οΈ Staff SE - πŸ“ Singapore - "🎫 Pass: EP" - - πŸ’» 5+ years of experience + - πŸ’» 6+ years of experience - πŸŽ“ Master in CS - πŸ‡«πŸ‡· French Citizen rss-feeds: diff --git a/src/loicb/build/content/blog/hibou-platform.md b/src/loicb/build/content/blog/hibou-platform.md index 41a137f..7397d2e 100644 --- a/src/loicb/build/content/blog/hibou-platform.md +++ b/src/loicb/build/content/blog/hibou-platform.md @@ -3,7 +3,7 @@ id: hibou-platform page: blog date: - 2025-10-13 -title: Building a Generic Analytics Platform with Hibou +title: Generic Analytics Platform with Rama home-page?: true employer: Flybot Pte Ltd css-class: hibou-platform diff --git a/src/loicb/client/core/dom/header.cljs b/src/loicb/client/core/dom/header.cljs index c707fdb..15946ba 100644 --- a/src/loicb/client/core/dom/header.cljs +++ b/src/loicb/client/core/dom/header.cljs @@ -15,7 +15,7 @@ (for [tag ["πŸ› οΈ Staff SE" "πŸ“ Singapore" "🎫 Pass: EP" - "πŸ’» 5+ years of experience" + "πŸ’» 6+ years of experience" "πŸŽ“ Master in CS" "πŸ‡«πŸ‡· French Citizen"]] [:div.tag