Robert J. Brotherus • 2025-08-13 • 18 min read • 792 views

Tech stack of this blog application

Tech stack

In 2025-05 I moved this blog from Wix to custom blog-platform. I started development of this custom webapp 2025-04-29, working 1-2 hours on few evenings per week for four weeks - perhaps 20 hours total - before the first production deployment on 2025-05-31. The blog platform code is at https://github.com/rbrother/blog and the blog articles as Markdown-files in a separate https://github.com/rbrother/articles repo.

SPA with ClojureScript and Re-frame

I implemented the blog viewer as Single-Page-Application (SPA) running in the browser as JavaScript (compiled from ClojureScript) and deployed to AWS S3 blob store. Alternatives to SPA would have been (1) Server-Side Rendering (SSR) implementation (with Clojure on JVM as AWS Lambda function) or (2) Static Site Generation (SSG) where final HTML-files are generated at compile time.

re-frame-colour.png

I implemented the Single-Page-App with ClojureScript, React and Re-Frame using pure functions where possible. Clojure(Script) has good support for writing apps using pure functions which are functions that return result computed only from function input parameters. Such functional logic is widely re-usable since it does not depend on the environment. Hence the app could be adapted for backend-based execution with reasonably small effort.

In terms of building React front-end, working with JavaScript and ClojureScript is very similar:

  1. Both represent React Components as functions
  2. Both support reactive local state (with Hooks in JS and with Atoms in ClojureScript)
  3. Both support reactive global state with 3rd party libraries (Redux, Zustand, Jotai, Recoil, etc. for JS and Re-Frame for ClojureScript)

The main practical difference between JS and ClojureScript in React applications is that while JS applications create HTML-definitions with embedded JSX-syntax whereas ClojureScript supports HTML-definitions with Hiccup library which uses Clojure native vector/map syntaxes. The Hiccup approach has the advantage of having more uniform syntax across the whole source file, avoiding syntactical elements like {...} necessary for switching between JS and XML syntaxes in JSX. Also because Hiccup is simply Clojure data, it can be manipulated and post-processed like any other data.

On core language level, Clojure(Script) has stronger support for high-performance immutable persistent data structures important for functional programming and LISP-style macros that can be used for performant compile-time language-extensions.

In terms of library support, Clojure has good interoperability with its native Java platform libraries and ClojureScript has good interoperability with JavaScript NPM libraries. So one does not run out of options for 3rd party functionality even if one does not find perfect library from the wide selection of Clojure native libraries.

Local routing

Single-Page-Apps (SPA), as the name implies, do not trigger browser HTML-page reload from server. For some SPAs it's perfectly fine to just single one URL in the browser like https://myapp.com and have all the different states and views of the application play out within that URL. However, for some applications it is important to be able to link to individual parts of the application with different URLs like https://myapp.com/one-page and https://myapp.com/another-page. In a blog-application it is quite essential for each article to have separate clear URL which readers can bookmark or share in social media and which search-engines can index. Since I was moving to custom blog-application from Wix, I wanted to preserve the exact URLs of the existing posts with form https://www.brotherus.net/post/</code> so that existing bookmarks would continue to work after the transition.</p><p><em>Routing</em> (mapping URLs to content) can be done also in SPA in which case it's called <em>local routing</em>: mapping happening in JavaScript at the users browser. Local routing uses <a href="https://developer.mozilla.org/en-US/docs/Web/API/History_API" target="_blank">Browser history API</a> which allows manipulation of browser URL and reacting to URL changes without triggering page reload. ClojureScript has a handy library <a href="https://github.com/venantius/accountant" target="_blank">Accountant</a> wrapping the history API and allowing functional mapping of browser paths to re-frame events defining application internal state (like the blog post to display). </p><p><a href="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/blog-tech-stack/history-api.png"><img alt="history-api.png" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/blog-tech-stack/history-api.png" /></a></p><p>In <a href="https://github.com/rbrother/blog/blob/master/src/brotherus/blog/core.cljs" target="_blank">core.cljs</a> I define the few simple routes of the blog with regular expressions and map them to corresponding re-frame <em>dispatch function</em> which performs appropriate change for the app state:</p><pre><code class="hljs language-clojure">(<span class="hljs-keyword">def</span> <span class="hljs-title">routes</span> [{<span class="hljs-symbol">:regex</span> <span class="hljs-regex">#"/about"</span><span class="hljs-punctuation">,</span> <span class="hljs-symbol">:dispatch</span> [<span class="hljs-symbol">:brotherus.blog.info/show-info</span>]} {<span class="hljs-symbol">:regex</span> <span class="hljs-regex">#"/post/(.+)"</span><span class="hljs-punctuation">,</span> <span class="hljs-symbol">:dispatch</span> [<span class="hljs-symbol">:brotherus.blog.item-page/select-item</span>]} {<span class="hljs-symbol">:regex</span> <span class="hljs-regex">#"/posts/(.+)"</span><span class="hljs-punctuation">,</span> <span class="hljs-symbol">:dispatch</span> [<span class="hljs-symbol">:brotherus.blog.filters/select-items</span>]} {<span class="hljs-symbol">:regex</span> <span class="hljs-regex">#".*"</span><span class="hljs-punctuation">,</span> <span class="hljs-symbol">:dispatch</span> [<span class="hljs-symbol">::home/home</span>]} <span class="hljs-comment">;; Default route, match anything</span> ]) </code></pre><p>Then I connect accountant navigation handler to find first route matching the regex and then dispatching the corresponding event-handler with possible parameters extracted from the regular expression <em>capturing groups</em> <code>(...)</code> :</p><pre><code class="hljs language-clojure">(<span class="hljs-keyword">defn</span> <span class="hljs-title">dispatch-route!</span> [{<span class="hljs-symbol">:keys</span> [matches dispatch]}] (<span class="hljs-name"><span class="hljs-built_in">let</span></span> [params (<span class="hljs-name"><span class="hljs-built_in">->></span></span> matches rest (<span class="hljs-name"><span class="hljs-built_in">map</span></span> js/decodeURIComponent))] (<span class="hljs-name">rf/dispatch</span> (<span class="hljs-name"><span class="hljs-built_in">vec</span></span> (<span class="hljs-name"><span class="hljs-built_in">concat</span></span> dispatch params))))) (<span class="hljs-keyword">defn</span> <span class="hljs-title">setup-routes</span> [] (<span class="hljs-name">accountant/configure-navigation!</span> {<span class="hljs-symbol">:nav-handler</span> (<span class="hljs-name"><span class="hljs-built_in">fn</span></span> [raw-path] (<span class="hljs-name">js/window.scrollTo</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>) <span class="hljs-comment">;; Filter away query parameters from the path</span> (<span class="hljs-name"><span class="hljs-built_in">let</span></span> [path (<span class="hljs-name"><span class="hljs-built_in">re-find</span></span> <span class="hljs-regex">#"^[^\?]+"</span> raw-path)] (<span class="hljs-name"><span class="hljs-built_in">->></span></span> routes (<span class="hljs-name"><span class="hljs-built_in">map</span></span> (<span class="hljs-name"><span class="hljs-built_in">fn</span></span> [{<span class="hljs-symbol">:keys</span> [regex] <span class="hljs-symbol">:as</span> route}] (<span class="hljs-name"><span class="hljs-built_in">assoc</span></span> route <span class="hljs-symbol">:matches</span> (<span class="hljs-name">re-matches</span> regex path)))) (<span class="hljs-name">find-first</span> <span class="hljs-symbol">:matches</span>) dispatch-route!))) ... </code></pre><p>Notes about the preceding code:</p><ol><li>I don't currently have any paths that would support URL-parameters like <code>...?a=1&b=2</code> so I filter those away with regex <code>^[^\?]+</code> to simplify regex matching. This is because when posting to social media like Facebook vanilla-links without URL-parameters, they often automatically add URL-parameters with their own metadata to such links.</li><li>I map each captured path parameter through <code>decodeURIComponent</code> function which converts URL-encoded path parameters like <code>my%20blog%20post</code> to original strings like <code>my blog post</code>.</li></ol><a class="heading-anchor" href="#markdown-to-html-processing-pipeline" id="markdown-to-html-processing-pipeline"><h2>Markdown to HTML processing pipeline</h2></a><p><a href="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/blog-tech-stack/md-to-html.png"><img alt="Markdown to HTML" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/blog-tech-stack/md-to-html.png" /></a></p><p>The core Blog-functionality of loading, transforming and displaying article to the blog reader is implemented in <a href="https://github.com/rbrother/blog/blob/master/src/brotherus/blog/item_page.cljs" target="_blank">item_page.cljs</a> and <a href="https://github.com/rbrother/blog/blob/master/src/brotherus/blog/article.cljs" target="_blank">article.cljs</a>. It starts by event-handler <code>::select-item</code> when user clicks on some article in the article list. This sets the selected item id to re-frame global state <code>db</code> and dispatches event <code>::load-article</code> to load the markdown from Github:</p><pre><code class="hljs language-clojure">(<span class="hljs-name">rf/reg-event-fx</span> <span class="hljs-symbol">::select-item</span> (<span class="hljs-name"><span class="hljs-built_in">fn</span></span> [{<span class="hljs-symbol">:keys</span> [db]} [_ id-raw]] (<span class="hljs-name"><span class="hljs-built_in">if-let</span></span> [info (<span class="hljs-name"><span class="hljs-built_in">get</span></span> db/articles-index id-raw)] {<span class="hljs-symbol">:db</span> (<span class="hljs-name"><span class="hljs-built_in">-></span></span> db (<span class="hljs-name"><span class="hljs-built_in">dissoc</span></span> <span class="hljs-symbol">:page</span> <span class="hljs-symbol">:error</span>) (<span class="hljs-name"><span class="hljs-built_in">assoc</span></span> <span class="hljs-symbol">:selected-item</span> id-raw)) <span class="hljs-symbol">:dispatch</span> [<span class="hljs-symbol">::load-article</span> (<span class="hljs-name"><span class="hljs-built_in">str</span></span> id-raw <span class="hljs-string">"/article.md"</span>)]} ...))) </code></pre><p>The <code>::load-article</code> event-handler uses re-frame extension <a href="https://github.com/day8/re-frame-http-fx" target="_blank">http-xhrio</a> to trigger HTTP GET-request for the Markdown data in functional declarative way:</p><pre><code class="hljs language-clojure">(<span class="hljs-name">rf/reg-event-fx</span> <span class="hljs-symbol">::load-article</span> (<span class="hljs-name"><span class="hljs-built_in">fn</span></span> [{<span class="hljs-symbol">:keys</span> [db]} [_ url]] {<span class="hljs-symbol">:db</span> db <span class="hljs-symbol">:http-xhrio</span> {<span class="hljs-symbol">:method</span> <span class="hljs-symbol">:get</span> <span class="hljs-symbol">:uri</span> (<span class="hljs-name"><span class="hljs-built_in">str</span></span> components/articles-base-url url) <span class="hljs-symbol">:timeout</span> <span class="hljs-number">8000</span> <span class="hljs-symbol">:response-format</span> (<span class="hljs-name">ajax.core/text-response-format</span>) <span class="hljs-symbol">:on-success</span> [<span class="hljs-symbol">::set-article-content</span>] <span class="hljs-symbol">:on-failure</span> [<span class="hljs-symbol">::set-article-content-failed</span>]}})) </code></pre><p>The <code>:on-success</code> attribute is set to further trigger <code>::set-article-content</code> event handler when content has been loaded so that the raw content is stored in app-db under <code>:article-content</code> key:</p><pre><code class="hljs language-clojure">(<span class="hljs-name">rf/reg-event-db</span> <span class="hljs-symbol">::set-article-content</span> (<span class="hljs-name"><span class="hljs-built_in">fn</span></span> [db [_ content]] (<span class="hljs-name"><span class="hljs-built_in">assoc</span></span> db <span class="hljs-symbol">:article-content</span> content))) </code></pre><p>The Markdown -> HTML processing pipeline is implemented as Re-frame as a set of <em>subscriptions</em> which are triggered by the Re-frame framework automatically when the <code>:article-content</code> in the app-db changes. These can be compared to Excel cells with formulas referring to other cells: Excel automatically re-calculates their resulting values when their source cells change. Re-frame subscriptions are created with <code>rf/reg-sub</code> macro, the first one in the chain being simple subscription that draws the raw content from the app-db:</p><pre><code class="hljs language-clojure">(<span class="hljs-name">rf/reg-sub</span> <span class="hljs-symbol">::article-content</span> (<span class="hljs-name"><span class="hljs-built_in">fn</span></span> [db _] (<span class="hljs-symbol">:article-content</span> db))) </code></pre><p>The main subscription doing the heavy lifting of the transform is <code>::article-html</code> shown below. It is set to depend on the lower-level subscriptions <code>::article-content</code> and <code>::selected-item</code> using the <code>:<-</code> arrow-syntax of Re-frame. Such <em>higher-level subscription</em> is triggered for re-execution automatically by Re-frame if and only if any value of it's source-subscriptions change.</p><pre><code class="hljs language-clojure">(<span class="hljs-name">rf/reg-sub</span> <span class="hljs-symbol">::article-html</span> <span class="hljs-symbol">:<-</span> [<span class="hljs-symbol">::article-content</span>] <span class="hljs-symbol">:<-</span> [<span class="hljs-symbol">::selected-item</span>] (<span class="hljs-name"><span class="hljs-built_in">fn</span></span> [[markdown item-id] _] (<span class="hljs-name">article/markdown-to-hiccup</span> markdown {<span class="hljs-symbol">:item-id</span> item-id}))) </code></pre><p>The subscription calls pure function <code>article/markdown-to-hiccup</code> which contains the main processing pipeline:</p><pre><code class="hljs language-clojure">(<span class="hljs-keyword">defn</span> <span class="hljs-title">markdown-to-hiccup</span> [markdown context] (<span class="hljs-name">binding</span> [*rendering-context* context] (<span class="hljs-name"><span class="hljs-built_in">let</span></span> [mark (<span class="hljs-name">Marked.</span> (<span class="hljs-name">markedHighlight</span> marked-options))] (<span class="hljs-name">some->></span> markdown (<span class="hljs-name">.parse</span> mark) html->hiccup (<span class="hljs-name"><span class="hljs-built_in">into</span></span> [<span class="hljs-symbol">:div</span>]) postprocess)))) </code></pre><p>In the code above:</p><ol><li><code>(let [mark (Marked. ...)]</code> creates an instance of <a href="https://www.npmjs.com/package/marked" target="_blank">Marked Markdown-parser</a> and assigns it to symbol <code>mark</code>. Marked is one of the most commonly used Markdown-parsers with 15 million weekly downloads. Its particular advantage in my case is that is supports GitHub-flavor of markdown, making it a good pair for my GitHub-based blog strategy.</li><li><code>(some->> ...)</code> is a Clojure <a href="https://clojure.org/guides/threading_macros" target="_blank">threading-macro</a>. Threading-macros or "arrow macros" come in several flavors, but they all allow deeply nested function calls to be written in simpler list form read from top to bottom. For example <code>(-> x a b c)</code> is equivalent to <code>(c (b (a x)))</code> which in Javascript-syntax would be equivalent to <code>c(b(a(x)))</code>. Although Clojure is a Lisp and Lisps are infamous for a lot of parenthesis, the threading macros often allow syntax which is has actually less parenthesis than the equivalent Javascript, Python or Java.</li><li>In the chain / pipeline of processing, the first step <code>(.parse mark)</code> uses the Marked parser to create HTML. </li><li>The created HTML string could be immediately set at the content of our react element but I wanted to make some post-processing to the generated HTML. Such processing is challenging to do to HTML string, but becomes easy when we have the HTML converted first to the equivalent native Clojure data-structure called <a href="https://github.com/weavejester/hiccup" target="_blank">Hiccup</a>. This is done with <code>html->hiccup</code> function call, which uses <a href="https://cljdoc.org/d/taipei.404/html-to-hiccup/0.1.8/api/taipei-404.html" target="_blank">taipei-404.html library</a>. For example HTML <code><span class="foo">bar</span></code> is equivalent in Hiccup to the Clojure data structure <code>[:span {:class "foo"} "bar"]</code></li><li><code>postprocess</code> calls my custom postprocessing function for the resulting Hiccup data structure to preform desired manipulations (described below)</li></ol><a class="heading-anchor" href="#post-processing-for-html-tweaks" id="post-processing-for-html-tweaks"><h2>Post-processing for HTML-tweaks</h2></a><p>The postprocess-function that is last part of the Markdown-processing pipeline applies additional improvements to the generated HTML-tree (in form of the equivalent Hiccup-data-structure):</p><pre><code class="hljs language-clojure">(<span class="hljs-keyword">defn</span> <span class="hljs-title">postprocess</span> [hiccup] (<span class="hljs-name"><span class="hljs-built_in">->></span></span> hiccup (<span class="hljs-name">walk/postwalk</span> (<span class="hljs-name"><span class="hljs-built_in">fn</span></span> [node] (<span class="hljs-name">cond-></span> node (<span class="hljs-name">is-element?</span> node <span class="hljs-symbol">:a</span>) set-link-new-tab (<span class="hljs-name">is-element?</span> node <span class="hljs-symbol">:img</span>) fix-image-links (<span class="hljs-name"><span class="hljs-built_in">string?</span></span> node) (<span class="hljs-name">he/decode</span> node) <span class="hljs-comment">;; < to < etc</span> ))))) </code></pre><p>The processing uses from the Clojure core library the powerful functional <code>postwalk</code>-function which recurses through any nested Clojure data structure and calls the given transformation function for each part / node. The node transformation function uses conditional pipeline macro <code>cond-></code> to perform selective post-processing.</p><p>The test <code>(is-element? node :a)</code> triggers for HTML link-elements such as <code><a href="url">text</a></code> (generated from Markup <code>[text](url)</code> syntax), postprocessing function <code>set-link-new-tab</code>. This adds <code>target="_blank"</code> attribute to links, which causes them to be opened in new tab, which I consider desirable in blog-context. Markdown itself does not have syntax for specifying such behavior, so it is reasonable to add as a post-processing step.</p><p>For <code><img src="..."></code> elements inserting images to the page, I do post-processing to support simple syntax for images that are part of the blog-posts content. In the GitHub articles repository I store image-files of the article in the same folder as the articles main <code>article.md</code> markdown-file. This allows referencing such images with simple relative syntax like <code>![Tech stack](tech_stack_of_blog_application.jpg)</code> without full domain in the URL. Both VSCode and GitHub are able to display files with such simple syntax when they are in the same folder as the markdown file. But when the article is viewed through my custom blog-application in URLs like <code>https://www.brotherus.net/post/llm-understanding</code> such simple image-URLs don't work since the images exists only in the Github domain of <code>https://raw.githubusercontent.com/rbrother/articles/</code>.</p><p><code>fix-image-links</code> applies post-processing to the <code>src</code> attributes of images in a way that relative links are converted to absolute ones by prefixing them with the articles base URL:</p><pre><code class="hljs language-clojure">(<span class="hljs-keyword">def</span> <span class="hljs-title">content-base</span> <span class="hljs-string">"https://raw.githubusercontent.com/rbrother/articles/refs/heads/main"</span>) (<span class="hljs-keyword">defn</span> <span class="hljs-title">relative-link?</span> [url] (<span class="hljs-name"><span class="hljs-built_in">not</span></span> (<span class="hljs-name"><span class="hljs-built_in">re-find</span></span> <span class="hljs-regex">#"^https?://"</span> url))) (<span class="hljs-keyword">defn</span> <span class="hljs-title">make-absolute-link</span> [src] (<span class="hljs-name"><span class="hljs-built_in">str</span></span> content-base <span class="hljs-string">"/"</span> (<span class="hljs-symbol">:item-id</span> *rendering-context*) <span class="hljs-string">"/"</span> src)) (<span class="hljs-keyword">defn</span> <span class="hljs-title">fix-image-src</span> [src] (<span class="hljs-name">cond-></span> src (<span class="hljs-name">relative-link?</span> src) make-absolute-link)) (<span class="hljs-keyword">defn</span> <span class="hljs-title">fix-image-links</span> [img] (<span class="hljs-name"><span class="hljs-built_in">update-in</span></span> img [<span class="hljs-number">1</span> <span class="hljs-symbol">:src</span>] fix-image-src)) </code></pre><a class="heading-anchor" href="#syntax-highlighting-for-code-samples" id="syntax-highlighting-for-code-samples"><h2>Syntax highlighting for code samples</h2></a><p>Since this is a blog about Software Development, many articles will contain source code samples of some kind. The readability of such samples are improved with proper <em>syntax highlighting</em> ie using different colors for different types of syntactic elements of the language in a consistent way.</p><p>Luckily the Marked-library I chose for Markdown parsing has many plugins, and one such plugin is <a href="https://www.npmjs.com/package/marked-highlight" target="_blank">marked-highlight</a> which supports generation of syntax-highlighting with <a href="https://www.npmjs.com/package/highlight.js?activeTab=readme" target="_blank">highlight.js library</a>. Highlight.js supports large number of language syntaxes, including Clojure as you can see from the code samples in this article. The syntax-highlighting for Marked is configured in its constructor with the <code>markedHighlight</code> function: </p><pre><code class="hljs language-Clojure">(<span class="hljs-name">Marked.</span> (<span class="hljs-name">markedHighlight</span> marked-options)) </code></pre><p>This gets as parameter <code>marked-options</code> which configures with <code>:highlight</code> key a function that will be called for code-blocks to perform the syntax highlighting. It does this by calling <code>hljs.highlight()</code> from the highlight.js library:</p><pre><code class="hljs language-Clojure">(<span class="hljs-keyword">def</span> <span class="hljs-title">marked-options</span> #js {<span class="hljs-symbol">:emptyLangClass</span> <span class="hljs-string">"hljs"</span> <span class="hljs-symbol">:langPrefix</span> <span class="hljs-string">"hljs language-"</span> <span class="hljs-symbol">:highlight</span> (<span class="hljs-name"><span class="hljs-built_in">fn</span></span> [code lang] (<span class="hljs-name">.-value</span> (<span class="hljs-name">.highlight</span> hljs code #js {<span class="hljs-symbol">:language</span> (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">=</span></span> lang <span class="hljs-string">""</span>) <span class="hljs-string">"plaintext"</span> lang)})))}) </code></pre><a class="heading-anchor" href="#style-tweaks" id="style-tweaks"><h2>Style tweaks</h2></a><p>For most customizations of the article output, no HTML-postprocessing is needed, instead CSS-rules are enough. For example I wanted small images and their captions to be horizontally centered like the spectrum image in the <a href="/post/entry-to-chemistry-programming" target="_blank">Quantum Chemistry Programming post</a>:</p><p><a href="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/blog-tech-stack/centered-images.png"><img alt="Centered images" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/blog-tech-stack/centered-images.png" /></a></p><p>Markdown image syntax <code>![text](url)</code> has no way of specifying centering and Markdown has no concept of image caption. But these both can be accomplished with set of CSS declarations (on in this case in their equivalent <a href="https://github.com/noprompt/garden" target="_blank">ClojureScript Garden</a>-syntax) using <a href="https://css-tricks.com/snippets/css/a-guide-to-flexbox/" target="_blank">flexbox</a>:</p><pre><code class="hljs language-clojure"> [<span class="hljs-string">"div.article img"</span> {<span class="hljs-symbol">:max-width</span> <span class="hljs-string">"800px"</span> <span class="hljs-symbol">:max-height</span> <span class="hljs-string">"500px"</span> }] [<span class="hljs-string">"div.article p:has(img)"</span> {<span class="hljs-symbol">:display</span> <span class="hljs-string">"flex"</span> <span class="hljs-symbol">:justify-content</span> <span class="hljs-string">"center"</span>}] [<span class="hljs-string">"div.article p:has(img):has(+ p small)"</span> {<span class="hljs-symbol">:margin-bottom</span> <span class="hljs-string">"8px"</span>}] [<span class="hljs-string">"div.article p:has(small)"</span> {<span class="hljs-symbol">:text-align</span> <span class="hljs-string">"center"</span> <span class="hljs-symbol">:margin-top</span> <span class="hljs-string">"8px"</span>}] </code></pre><a class="heading-anchor" href="#deployment" id="deployment"><h2>Deployment</h2></a><p>As I described in my <a href="/post/blog-platform" target="_blank">previous article</a>, I had set myself a requirement for keeping URL of the blog (<a href="https://www.brotherus.net/" target="_blank">www.brotherus.net</a>) and URLs of individual articles (such as <a href="https://www.brotherus.net/post/entry-to-chemistry-programming" target="_blank">https://www.brotherus.net/post/entry-to-chemistry-programming</a>) identical when transitioning from Wix to my custom blog-software.</p><p>In my earlier static JavaScript application deployments I had simply used AWS S3 buckets that I configured for public web-hosting. For example for my <a href="post/ruletti-reborn" target="_blank">Ruletti-reframe game</a> I created "roulette-reframe" bucket with web-hosting which makes the game visible in URL <a href="http://roulette-reframe.s3-website.eu-north-1.amazonaws.com/" target="_blank">http://roulette-reframe.s3-website.eu-north-1.amazonaws.com/</a>. This works fine, but these URLs are ugly, difficult to remember, don't support custom domains and only work with HTTP, not HTTPS which modern browsers prefer.</p><p><a href="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/blog-tech-stack/aws.webp"><img alt="AWS Logo" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/blog-tech-stack/aws.webp" /></a></p><p>Mapping S3 deployment to custom domain and HTTPS in AWS is not possible with the S3 service alone, but requires creation of cloud resources in several connected AWS services:</p><ol><li><a href="https://aws.amazon.com/cloudfront/" target="_blank">Amazon CloudFront</a> content delivery network (CDN) service is first configured with the S3 bucket as content source. Using CloudFront provides the additional benefit of caching and slightly lower transfer latencies than S3 alone.</li><li><a href="https://aws.amazon.com/route53/" target="_blank">Amazon Route 53</a> Domain Name System (DNS) service which allows mapping of the domain (<code>www.brotherus.net</code>) to the CloudFront deployment.</li><li><a href="https://aws.amazon.com/certificate-manager/" target="_blank">AWS Certificate Manager</a> service for creating SSL-certificate which is needed for the secure HTTPS protocol.</li></ol><p>Because AWS has dozens of services, each of these have numerous resource types and each resource numerous attributes, it used to be rather tedious to find out the right set of resources and their attribute-values to accomplish a given goal. When the best available help was Google, Stack Overflow and AWS technical documentation, one could easily end up on several incorrect paths in setting up the cloud resources. But with the aid of modern LLMs, this has become much easier: from a very high-level description of your goal tools like ChatGPT, Gemini, Claude, Llama and DeepSeek provide exhaustive step-by-step guides for reaching your goal by navigating through the numerous screens and settings of the AWS Portal.</p><p>For a project with multiple environments (DEV, QA and PROD), bigger team, longer development life-cycle or more complex cloud resources, it would have made more sense to use some Infrastructure-As-Code (IAC) solution like <a href="https://aws.amazon.com/cloudformation/" target="_blank">AWS CloudFormation</a>. This would involve writing the list of required resources and their attributes with some IAC Coding language (a flavor of YAML in case of using CloudFormation), so that executing this code creates the desired resources.</p><p>Finally I created small PowerShell-script <a href="https://github.com/rbrother/blog/blob/master/deploy.ps1" target="_blank">deploy.ps1</a> which compiles production version of the app and uploads to the S3 bucket (which is then automatically distributed through the CloudFront):</p><pre><code class="hljs language-plaintext">npm install Remove-Item -Recurse -Force .\resources\public\js\compiled npm run release aws s3 cp .\resources\public s3://brotherus.net/ --recursive </code></pre><a class="heading-anchor" href="#future-improvement-possibilities" id="future-improvement-possibilities"><h2>Future improvement possibilities</h2></a><p>While the Single-Page-Application React-architecture I use provides slightly more snappy response when navigating withing the application and very nice development experience, I have noticed some downsides. Because the main content of each article page is generated dynamically with JavaScript, web crawlers and similar automated tools have harder time deciphering content of the page. Luckily the Google web-crawler (powering the Google search) works quite well with dynamic pages by executing JavaScript on the page and waiting a while to get the page final form, but other search engines sometimes use more limited crawlers and would hence miss all the main content.</p><p>More importantly, the "post preview builders" that social media platforms like FaceBook, Linked-In and X use easily miss dynamic content. When posting a link to these platforms, these little agents analyze the content of the target page and amend the post with a small preview that has typically the first image on the page and excerpt from the beginning of the text. On pages where content is generated by dynamic JavaScript, these platforms easily fail to make meaningful preview.</p><p>This limitation has made me consider trading off the snappy local navigation of the React SPA with a more search/preview-friendly architecture nature of server-side rendering. While the high-level architecture of such app would be quite different, transition would be simplified by the fact that core application logic is functional Clojure/JavaScript-code that is independent of any environment.</p></div></div><div style="display: flex; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small margin">Robert J. Brotherus • 2025-08-13 • 18 min read • 792 views</div></div><div class="small"><a href="/posts/Computers">Computers</a> • <a href="/posts/AWS">AWS</a> • <a href="/posts/Markdown">Markdown</a> • <a href="/posts/Clojure">Clojure</a> • <a href="/posts/ClojureScript">ClojureScript</a> • <a href="/posts/Re-Frame">Re-Frame</a> • <a href="/posts/Programming">Programming</a> • <a href="/posts/SPA">SPA</a></div><hr /></div><div class="article-inner"><div class="product-table"><div class="article-box"><a href="/post/i-like-coding-agents"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/i-like-coding-agents/thumbnail.jpg" /></div><div class="margin2">Why do I like Programming, part 2: AI coding agents</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2025-09-25</div></div></div></a></div><div class="article-box"><a href="/post/null-scifi"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/null-scifi/thumbnail.jpg" /></div><div class="margin2">Null and Void</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2025-08-20</div></div></div></a></div><div class="article-box"><a href="/post/blog-tech-stack"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/blog-tech-stack/thumbnail.jpg" /></div><div class="margin2">Tech stack of this blog app</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2025-08-13</div></div></div></a></div><div class="article-box"><a href="/post/blog-platform"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/blog-platform/thumbnail.jpg" /></div><div class="margin2">From Wix to a custom blog-platform</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2025-06-11</div></div></div></a></div><div class="article-box"><a href="/post/resurrect-infia-2025"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/resurrect-infia-2025/thumbnail.jpg" /></div><div class="margin2">Resurrecting Infia in 2025</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2025-05-01</div></div></div></a></div><div class="article-box"><a href="/post/llm-understanding"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/llm-understanding/thumbnail.jpg" /></div><div class="margin2">Don't LLMs really have understanding?</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2025-03-28</div></div></div></a></div><div class="article-box"><a href="/post/infia"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/infia/thumbnail.jpg" /></div><div class="margin2">Creating the INFIA Spectrum Analysis Software in 1996-1998</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2024-12-21</div></div></div></a></div><div class="article-box"><a href="/post/death-by-fortran-common-block"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/death-by-fortran-common-block/thumbnail.jpg" /></div><div class="margin2">Death by Fortran Common Block</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2024-01-17</div></div></div></a></div><div class="article-box"><a href="/post/entry-to-chemistry-programming"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/entry-to-chemistry-programming/thumbnail.jpg" /></div><div class="margin2">My entry to Quantum Chemistry programming</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2023-01-03</div></div></div></a></div><div class="article-box"><a href="/post/logistic-map-fractal"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/logistic-map-fractal/thumbnail.jpg" /></div><div class="margin2">Logistic Map Fractal</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2022-10-14</div></div></div></a></div><div class="article-box"><a href="/post/ovako-excel"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/ovako-excel/thumbnail.jpg" /></div><div class="margin2">Excel-revolution at Steel Factory in 1989</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2022-05-29</div></div></div></a></div><div class="article-box"><a href="/post/i-like-programming"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/i-like-programming/thumbnail.jpg" /></div><div class="margin2">Why do I like Programming so much?</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2022-05-08</div></div></div></a></div><div class="article-box"><a href="/post/ruletti-imperative-to-functional"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/ruletti-imperative-to-functional/thumbnail.jpg" /></div><div class="margin2">From imperative C64 Basic to functional-reactive programming</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2022-02-06</div></div></div></a></div><div class="article-box"><a href="/post/ruletti-developer-experience"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/ruletti-developer-experience/thumbnail.jpg" /></div><div class="margin2">Developer experience in 1987 with C64 vs modern tools</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2021-12-18</div></div></div></a></div><div class="article-box"><a href="/post/ruletti-reborn"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/ruletti-reborn/thumbnail.jpg" /></div><div class="margin2">Ruletti re-born after 34 years</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2021-09-09</div></div></div></a></div><div class="article-box"><a href="/post/ruletti-code"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/ruletti-code/thumbnail.jpg" /></div><div class="margin2">Diving into Ruletti-64 code</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2021-08-25</div></div></div></a></div><div class="article-box"><a href="/post/ruletti-c64"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/ruletti-c64/thumbnail.jpg" /></div><div class="margin2">Blast from my Commodore 64 Past</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2021-08-10</div></div></div></a></div><div class="article-box"><a href="/post/from-a-product-company-to-a-consulting-company"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/from-a-product-company-to-a-consulting-company/thumbnail.jpg" /></div><div class="margin2">From a Product company to a Consulting company</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2020-02-06</div></div></div></a></div><div class="article-box"><a href="/post/why-so-many-languages"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/why-so-many-languages/thumbnail.jpg" /></div><div class="margin2">Why so many languages?</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2019-03-13</div></div></div></a></div><div class="article-box"><a href="/post/humble-beginnings-the-vic-20"><div class="crop-container"><img class="cropped-image" src="https://raw.githubusercontent.com/rbrother/articles/refs/heads/main/humble-beginnings-the-vic-20/thumbnail.jpg" /></div><div class="margin2">Humble Beginnings: the VIC-20</div><div class="grid margin2" style="grid-template-columns: auto 1fr; align-items: center;"><div><img src="https://brotherus-blog-blog-static-assets.s3.eu-north-1.amazonaws.com/images/robert.jpg" style="width: 50px; border-radius: 50%;" /></div><div class="small"><div>Robert J. Brotherus</div><div>2019-02-15</div></div></div></a></div></div></div></div></div></div></div></body></html>