Close Menu

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    U Mobile deploys ULTRA5G in Kota Kinabalu

    AKASO Launches Keychain 2: A Pocket-Sized 4K Action Camera Built for Creators on the Move

    Huawei Malaysia beings preorders for Pura 80

    Facebook X (Twitter) Instagram
    • Artificial Intelligence
    • Business Technology
    • Cryptocurrency
    • Gadgets
    • Gaming
    • Health
    • Software and Apps
    • Technology
    Facebook X (Twitter) Instagram Pinterest Vimeo
    Tech AI Verse
    • Home
    • Artificial Intelligence

      Blue-collar jobs are gaining popularity as AI threatens office work

      August 17, 2025

      Man who asked ChatGPT about cutting out salt from his diet was hospitalized with hallucinations

      August 15, 2025

      What happens when chatbots shape your reality? Concerns are growing online

      August 14, 2025

      Scientists want to prevent AI from going rogue by teaching it to be bad first

      August 8, 2025

      AI models may be accidentally (and secretly) learning each other’s bad behaviors

      July 30, 2025
    • Business

      Why Certified VMware Pros Are Driving the Future of IT

      August 24, 2025

      Murky Panda hackers exploit cloud trust to hack downstream customers

      August 23, 2025

      The rise of sovereign clouds: no data portability, no party

      August 20, 2025

      Israel is reportedly storing millions of Palestinian phone calls on Microsoft servers

      August 6, 2025

      AI site Perplexity uses “stealth tactics” to flout no-crawl edicts, Cloudflare says

      August 5, 2025
    • Crypto

      Japan Auto Parts Maker Invests US Stablecoin Firm and Its Stock Soars

      August 29, 2025

      Stablecoin Card Firm Rain Raise $58M from Samsung and Sapphire

      August 29, 2025

      Shark Tank Star Kevin O’Leary Expands to Bitcoin ETF

      August 29, 2025

      BitMine Stock Moves Opposite to Ethereum — What Are Analysts Saying?

      August 29, 2025

      Argentina’s Opposition Parties Reactivate LIBRA Investigation Into President Milei

      August 29, 2025
    • Technology

      It’s time we blow up PC benchmarking

      August 29, 2025

      If my Wi-Fi’s not working, here’s how I find answers

      August 29, 2025

      Asus ROG NUC 2025 review: Mini PC in size, massive in performance

      August 29, 2025

      20 free ‘hidden gem’ apps I install on every Windows PC

      August 29, 2025

      Lowest price ever: Microsoft Office at $25 over Labor Day weekend

      August 29, 2025
    • Others
      • Gadgets
      • Gaming
      • Health
      • Software and Apps
    Check BMI
    Tech AI Verse
    You are at:Home»Technology»Crafting your own Static Site Generator using Phoenix (2023)
    Technology

    Crafting your own Static Site Generator using Phoenix (2023)

    TechAiVerseBy TechAiVerseAugust 1, 2025No Comments9 Mins Read1 Views
    Facebook Twitter Pinterest Telegram LinkedIn Tumblr Email Reddit
    Crafting your own Static Site Generator using Phoenix (2023)
    Share
    Facebook Twitter LinkedIn Pinterest WhatsApp Email

    BMI Calculator – Check your Body Mass Index for free!

    Crafting your own Static Site Generator using Phoenix (2023)

    Author
    Name
    Jason Stiebs
    @peregrine

    @peregrine
    Image by


    Annie Ruygt

    This is a post about building up your own Static Site Generator from scratch. If you want to deploy your Phoenix LiveView app right now, then check out how to get started. You could be up and running in minutes.

    The year is 2023, you have many options for building a Static Website. From the OG Jekyll to literally hundreds of JavaScript based options to people suggesting you should just craft HTML by hand. All of these solutions are correct and good, and you know what? You should use them!

    End of post, no need to read on.

    That said… a static website is really just HTML, CSS and JS files. In Elixir, we have wonderful tools for doing that. So let’s do it!

    The Map

    This post is going to assume you are at least a beginner to intermediate in Elixir.

    Starting from scratch with an empty Elixir project, we will build a basic personal website and blog. We’ll add each dependency as we need them and integrate them. We’ll be using well known libraries, and I think we’ll be surprised by how far we get by just following our intuition!

    So let’s begin with the most basic elixir project:

    $ mix new personal_website
    

    * creating README.md
    * creating .formatter.exs
    * creating .gitignore
    * creating mix.exs
    * creating lib
    * creating lib/personal_website.ex
    * creating test
    * creating test/test_helper.exs
    * creating test/personal_website_test.exs
    
    Your Mix project was created successfully.
    You can use "mix" to compile it, test it, and more:
    
        cd personal_website
        mix test
    

    Instead of running the tests, I recommend removing the test/personal_website_test.exs because we’re building a personal website. I also like to do a git init && git commit -am "Make it so", just in case I mess up and want to undo, or show diffs in a blog post.

    Let’s start with our blog content.

    Content

    We want to author in Markdown and publish to HTML, luckily there is a handy library NimblePublisher, just for that, adding to our mix.exs file:

    defp deps do
      [
        {:nimble_publisher, "~> 0.1.3"}
      ]
    end
    

    NimblePublisher is a Dashbit library that will read markdown from a directory, parse the front matter, produce markdown, and build up data structures for creating your own content site. It does not however render it to HTML for you or building any sort of routing.

    It essentially acts like a compile time database for interfacing with a directory of Markdown.

    Luckily for us their docs walk through building a blog and provide some sensible defaults, we want a /posts/YEAR/MONTH-DAY-ID.md file name, and we want to parse that with NimblePublisher into a Post struct. Let’s create our first module,lib/blog.ex

    defmodule PersonalWebsite.Blog do
      alias PersonalWebsite.Post
    
      use NimblePublisher,
        build: Post,
        from: "./posts/**/*.md",
        as: :posts,
        highlighters: [:makeup_elixir, :makeup_erlang]
    
      @posts Enum.sort_by(@posts, & &1.date, {:desc, Date})
    
      # And finally export them
      def all_posts, do: @posts
    end
    

    Here we configure NimblePublisher which will read each markdown file from the posts directory and call the Post.build/3 function on each. Then finally it will assign to the module attribute @posts configured with :as. Then we sort the @posts by date and define a function that returns all_posts.

    Take note that this is all happening at compile time and is embedded into our compiled module. Meaning accessing it will be lighting quick!

    The keen eye’d will be asking, “So what about post? And build/3?” We define those in lib/post.ex:

    defmodule PersonalWebsite.Post do
      @enforce_keys [:id, :author, :title, :body, :description, :tags, :date, :path]
      defstruct [:id, :author, :title, :body, :description, :tags, :date, :path]
    
      def build(filename, attrs, body) do
        path = Path.rootname(filename)
        [year, month_day_id] = path |> Path.split() |> Enum.take(-2)
        path = path <> ".html"
        [month, day, id] = String.split(month_day_id, "-", parts: 3)
        date = Date.from_iso8601!("#{year}-#{month}-#{day}")
        struct!(__MODULE__, [id: id, date: date, body: body, path: path] ++ Map.to_list(attrs))
      end
    end
    

    and before we dive into this, add a test post to posts/2023/04-01-pranks.md:

    %{
      title: "Pranks!",
      author: "Jason Stiebs",
      tags: ~w(april fools),
      description: "Let's learn how to do pranks!"
    }
    ---
    
    ## Gotcha! Not a real post.
    
    This is very funny.
    

    During compile time, NimblePublisher will grab every file from /posts/**/*.md and apply the Post.build/3 function to it. The function build/3 is expected to return a data structure representing a post. In this case, we chose a struct with all the same fields as our front matter and a couple extra we parse from the filename.

    Note that NimblePublisher expects the markdown to have a front-matter formatted as an Elixir Map, followed by ---, finally followed by the post Markdown.

    The build/3 function pulls apart the path to collect the year, month, day and id from the file name and builds a Date struct. It also generates the final path URL, appending .html.

    Let’s test this in iex and see what we’ve got:

    $ iex -S mix
    iex(1)> PersonalWebsite.Blog.all_posts()
    [
      %PersonalWebsite.Post{
        id: "pranks",
        author: "Jason Stiebs",
        title: "Pranks!",
        body: "

    nGotcha!

    n

    nNot a real post. This is very funny.

    n", description: "Let's learn how to do pranks!", tags: ["april", "fools"], date: ~D[2023-04-01], path: "posts/2023/04-01-pranks.html" } ]

    Beautiful.

    From here on out, we have our “context” with all of our posts. If we want a filtered set, or to add paging, we’d do it by adding functions to our Blog and using the built-in Enum functions. Adding more files to /posts will result in this list having one most Post‘s, it’s that simple!

    Don’t worry about scaling this, because if you do hit the point where this takes up too much memory, you will have people who are eager to fix this for you, because they will be tired of generating markdown files. That said, since this is compiled, the cost is paid once at compile time so no big deal!

    Rendering HTML

    Ever since they were announced, I’ve really loved building HTML as Phoenix Components. And even though we only be using 1/10th of the functionality, let’s pull in PhoenixLiveView so we can use HEEX. Editing mix.exs:

    defp deps do
      [
    -   {:nimble_publisher, "~> 0.1.3"}
    +   {:nimble_publisher, "~> 0.1.3"},
    +   {:phoenix_live_view, "~> 0.18.2"}
    

    Now to make a new module responsible for rendering our website into HTML, open up lib/personal_site.ex:

    defmodule PersonalWebsite do
      use Phoenix.Component
      import Phoenix.HTML
    
      def post(assigns) do
        ~H"""
        <.layout>
          <%= raw @post.body %>
        
        """
      end
    
      def index(assigns) do
        ~H"""
        <.layout>
          

    Jason's Personal website!!

    Posts!

    • <%= post.title %>
    """
    end def layout(assigns) do ~H""" <%= render_slot(@inner_block) %> """ end end

    If you are familiar with Phoenix Components, then you will know exactly what’s going on here. We have our base layout/1 function, which builds our base HTML and accepts an inner_block. We have two separate page types, one for index/1 and one for our post/1. Using only the primitives that Phoenix provides us to build our HTML using functions!

    If we wanted a third page like about we’d simply make a new function! If your layout grows unwieldy, move it to its own file. It’s just functions!

    Now it’s a matter of wiring it up to our data! Let’s add a build/0 function to collect all of our data, render it and output it to /output:

    @output_dir "./output"
    File.mkdir_p!(@output_dir)
    
    def build() do
      posts = Blog.all_posts()
    
      render_file("index.html", index(%{posts: posts}))
    
      for post <- posts do
        dir = Path.dirname(post.path)
        if dir != "." do
          File.mkdir_p!(Path.join([@output_dir, dir]))
        end
        render_file(post.path, post(%{post: post}))
      end
    
      :ok
    end
    
    def render_file(path, rendered) do
      safe = Phoenix.HTML.Safe.to_iodata(rendered)
      output = Path.join([@output_dir, path])
      File.write!(output, safe)
    end
    
    

    Stepping through the code we:

    • Create the output_dir if it doesn’t exist
    • Grab all of the posts.
    • Render the index.html, write it to disk.
    • For each post:
      • Build the “year” directory if it doesn’t exist
      • Render the file
      • Write it to disk.

    The render_file/2 function does have one interesting line, Phoenix.HTML.Safe.to_iodata/1 will take a Phoenix rendered component and output it to an HTML safe iodata, which is a weird name for a string in a list, but Erlang knows how to use these to be very efficient. If we were to “dead render” this using a Phoenix Controller, this is the last function Phoenix would call before sending it down the wire.

    Load up iex and see what we get!

    $ iex -S mix
    iex(1)> PersonalWebsite.build()
    :ok
    CTRL-C CTRL-C
    $ open ./output/index.html
    

    We should be greeted by our wonderful website!

    And this for the post

    Hey, this is starting to look like a real website! If you check the ./output all the files are put where they belong. You could deploy this as is, but we’re going to keep going.

    Automation!

    A mix task would be mighty handy here, first $ mkdir -p lib/mix/tasks and then edit lib/mix/tasks/build.ex:

    defmodule Mix.Tasks.Build do
      use Mix.Task
      @impl Mix.Task
      def run(_args) do
        {micro, :ok} = :timer.tc(fn ->
          PersonalWebsite.build()
        end)
        ms = micro / 1000
        IO.puts("BUILT in #{ms}ms")
      end
    end
    

    Running it:

    $ mix build
    BUILT in 13.47ms
    

    Now we’re getting somewhere… but you know there is one thing we haven’t solved yet? CSS and JS. So do that!

    First add a couple familiar deps:

    +    {:esbuild, "~> 0.5"},
    +    {:tailwind, "~> 0.1.8"}
    

    Create a config/config.exs

    import Config
    
    # Configure esbuild (the version is required)
    config :esbuild,
      version: "0.14.41",
      default: [
        args:
          ~w(app.js --bundle --target=es2017 --outdir=../output/assets --external:/fonts/* --external:/images/*),
        cd: Path.expand("../assets", __DIR__),
        env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
      ]
    
    # Configure tailwind (the version is required)
    config :tailwind,
      version: "3.2.4",
      default: [
        args: ~w(
          --config=tailwind.config.js
          --input=css/app.css
          --output=../output/assets/app.css
        ),
        cd: Path.expand("../assets", __DIR__)
      ]
    

    This is copy/pasted from a fresh mix phx.new generated website. I did change the output paths to make it work with out setup. Create an asset’s directory $ mkdir assets create an assets/app.js

    console.log("HELLO WORLD")
    

    Create a assets/tailwind.config.js

    module.exports = {
      content: [
        "./**/*.js",
        "../lib/personal_website.ex",
      ],
      plugins: [
        require("@tailwindcss/typography"),
      ]
    };
    

    Finally, create a mix alias opening up mix.exs again

    +      aliases: aliases(),
           deps: deps()
         ]
    +  defp aliases() do
    +    [
    +      "site.build": ["build", "tailwind default --minify", "esbuild default --minify"]
    +    ]
    +  end
    

    Now when we run mix site.build Elixir will download esbuild and tailwind and execute them outputting to output/ for us. We’re getting nearly the same development experience as a full Phoenix Application!

    Finally, we have to add the CSS and JS we compiled updating our layout/1 in lib/personal_website.ex

    +      
    +      
    
      
    Top Posts

    Ping, You’ve Got Whale: AI detection system alerts ships of whales in their path

    April 22, 2025166 Views

    6.7 Cummins Lifter Failure: What Years Are Affected (And Possible Fixes)

    April 14, 202548 Views

    New Akira ransomware decryptor cracks encryptions keys using GPUs

    March 16, 202530 Views

    Is Libby Compatible With Kobo E-Readers?

    March 31, 202528 Views
    Don't Miss
    Gadgets August 29, 2025

    U Mobile deploys ULTRA5G in Kota Kinabalu

    U Mobile deploys ULTRA5G in Kota Kinabalu After unveiling its new ULTRA5G network for in-building…

    AKASO Launches Keychain 2: A Pocket-Sized 4K Action Camera Built for Creators on the Move

    Huawei Malaysia beings preorders for Pura 80

    It’s time we blow up PC benchmarking

    Stay In Touch
    • Facebook
    • Twitter
    • Pinterest
    • Instagram
    • YouTube
    • Vimeo

    Subscribe to Updates

    Get the latest creative news from SmartMag about art & design.

    About Us
    About Us

    Welcome to Tech AI Verse, your go-to destination for everything technology! We bring you the latest news, trends, and insights from the ever-evolving world of tech. Our coverage spans across global technology industry updates, artificial intelligence advancements, machine learning ethics, and automation innovations. Stay connected with us as we explore the limitless possibilities of technology!

    Facebook X (Twitter) Pinterest YouTube WhatsApp
    Our Picks

    U Mobile deploys ULTRA5G in Kota Kinabalu

    August 29, 20252 Views

    AKASO Launches Keychain 2: A Pocket-Sized 4K Action Camera Built for Creators on the Move

    August 29, 20252 Views

    Huawei Malaysia beings preorders for Pura 80

    August 29, 20252 Views
    Most Popular

    Xiaomi 15 Ultra Officially Launched in China, Malaysia launch to follow after global event

    March 12, 20250 Views

    Apple thinks people won’t use MagSafe on iPhone 16e

    March 12, 20250 Views

    French Apex Legends voice cast refuses contracts over “unacceptable” AI clause

    March 12, 20250 Views
    © 2025 TechAiVerse. Designed by Divya Tech.
    • Home
    • About Us
    • Contact Us
    • Privacy Policy
    • Terms & Conditions

    Type above and press Enter to search. Press Esc to cancel.