~mika/www.m1kadev.nl

e5ef6146526cba076e1901f7d93010d1f36fe935 — mika 5 months ago 1a4d481 + 3020fb3
Merge pull request #1 from m1kadev/kethel

Nicer build system: kethel
M .vscode/settings.json => .vscode/settings.json +1 -1
@@ 1,5 1,5 @@
{
    "files.associations": {
        "*.html": "mustache"
        "*.thtml": "mustache"
    }
}
\ No newline at end of file

R bricks/base-header.html => bricks/base-header.thtml +0 -0
R bricks/extended-footer.html => bricks/extended-footer.thtml +0 -0
R bricks/footer.html => bricks/footer.thtml +0 -0
R bricks/highlight.html => bricks/highlight.thtml +0 -0
R bricks/navbar.html => bricks/navbar.thtml +1 -2
@@ 6,8 6,7 @@
        <a href="/">home</a>
        <a href="/projects.html">projects</a>
        <a href="/thoughts/">thoughts</a>
        <a href="/pastes/">pastes</a>
        <hr>
        <a href="/colophon.html">colophon</a>
    </nav>
</div>
\ No newline at end of file
</div>

D build.exs => build.exs +0 -371
@@ 1,371 0,0 @@
require Mix
require EEx

Mix.install([
  {:tzdata, "~> 1.1"},
  {:mustache, "~> 0.5.0"}
])

Calendar.put_time_zone_database(Tzdata.TimeZoneDatabase)

defmodule Git do
  def get_file_commit(path) do
    {ts_string, 0} = System.cmd("git", ["log", "--format=%ct", path])
    String.to_integer(String.trim_trailing(ts_string))
  end
end

defmodule Templates do
  binfo_template = """
  commit=<%= commit %>lightningcss=<%= lightningcss %>html_minifier_next=<%= html_minifier_next %>uglifyjs=<%= uglifyjs %>build_time=<%= build_time %>
  """

  pastelink_template = """
  <a href="/<%= location %>.html"><%= name %></a> | <%= date %>
  """

  EEx.function_from_string(
    :def,
    :build_info,
    binfo_template,
    [
      :commit,
      :lightningcss,
      :html_minifier_next,
      :uglifyjs,
      :build_time
    ]
  )

  EEx.function_from_string(
    :def,
    :pastelink,
    pastelink_template,
    [
      :location,
      :name,
      :date
    ]
  )

  def ogp(title, url, description) do
    """
      <meta property="og:type" content="website">
      <meta property="og:title" content="#{title}">
      <meta property="og:url" content="#{url}">
      <meta property="og:image" content="https://m1kadev.nl/static/logo.png">
      <meta property="og:description" content="#{description}">
      <meta property="og:locale" content="en_GB" />
      <meta property="og:site_name" content="m1kadev.nl" />
    """
  end
end

defmodule Builders do
  def html(input_path) do
    output_path = String.replace_prefix(input_path, "src", "build")

    {_, 0} =
      System.cmd("html-minifier-next", [
        "--preset",
        "conservative",
        "-o",
        output_path,
        input_path
      ])
  end

  def css(input_path) do
    output_path = "build/" <> input_path

    {_, 0} =
      System.cmd("lightningcss", [
        "-m",
        "-o",
        output_path,
        input_path
      ])
  end

  def js(input_path) do
    output_path = "build/" <> input_path

    {_, 0} =
      System.cmd("uglifyjs", [
        "-c",
        "-o",
        output_path,
        input_path
      ])
  end

  def fxg(input_path, template, bricks) do
    own_template =
      input_path
      |> String.replace_prefix("pages", "templates/pages")
      |> String.replace_suffix("fxg", "html")

    case File.read(own_template) do
      {:ok, ctemplate} -> _fxg(input_path, ctemplate, bricks)
      _ -> _fxg(input_path, template, bricks)
    end
  end

  def paste(input_path, template, bricks) do
    {:ok, code} = File.read(input_path)
    IO.inspect(code)

    output = "build/" <> input_path <> ".html"

    lang = Path.extname(input_path) |> String.slice(1..-1//1)
    file = Path.basename(input_path)
    time = FileX.createdf(input_path)

    result =
      Mustache.render(
        template,
        Map.merge(
          %{
            filename: file,
            about_paste: "Created on " <> time,
            language: "<script src=\"static/" <> lang <> ".min.js\"></script>",
            codetag: "<pre><code class=\"language-" <> lang <> "\">",
            code: code,
            ogp:
              Templates.ogp(file, "https://m1kadev.nl/pastes/#{file}.html", "Created on " <> time)
          },
          bricks
        )
      )

    IO.inspect(input_path)

    :ok = File.write(output, result)
  end

  defp _fxg(input_path, template, bricks) do
    output_path =
      String.replace_prefix(input_path, "pages", "build")
      |> String.replace_suffix("fxg", "html")

    name =
      case Path.basename(input_path) |> Path.rootname() do
        "index" -> "home"
        x -> x
      end

    {content, 0} = System.cmd("fxg", [input_path])

    ogp =
      Templates.ogp(
        name,
        "https://m1kadev.nl/#{String.replace_prefix(output_path, "build/", "")}",
        ""
      )

    output =
      Mustache.render(
        template,
        Map.merge(
          %{
            fxg_content: content,
            location: name,
            ogp: ogp
          },
          bricks
        )
      )

    IO.inspect(output_path)
    :ok = File.write(output_path, output)
  end

  def thought(path, unix) do
    thought = File.read!(path)
    name = String.replace_prefix(path, "thoughts/", "")

    date = DateTime.from_unix!(unix) |> Calendar.strftime("%d/%m/%y (%H:%M)")

    """
    <div class="thought">
    <h3><a href="/thoughts/#{FileX.sanitised_name(name)}.html">#{name}</a></h3>
    <span>#{thought}</span>
    <small> -mika, #{date}</small>
    </div>
    """
  end
end

defmodule FileX do
  def createdf(path) do
    {:ok, %File.Stat{mtime: {{y, m, d}, _}}} = File.stat(path, time: :local)
    "#{d}/#{m}/#{y}"
  end

  def created(path) do
    {:ok, %File.Stat{mtime: x}} = File.stat(path, time: :posix)
    x
  end

  def sanitised_name(path) do
    Path.basename(path)
    |> String.trim_trailing(".html")
    |> String.replace("-", "_")
    |> String.replace(" ", "_")
  end
end

{commit, 0} = System.cmd("git", ["rev-parse", "HEAD"])

{lightningcss, 0} = System.cmd("lightningcss", ["-V"])

{html_minifier_next, 0} = System.cmd("html-minifier-next", ["-V"])

{uglifyjs, 0} = System.cmd("uglifyjs", ["-V"])

File.rm_rf("build")

File.mkdir_p!("build")

bricks =
  Path.wildcard("bricks/*.html")
  |> Map.new(fn x -> {FileX.sanitised_name(x), File.read!(x)} end)

{:ok, main_template} = File.read("templates/base.html")

Path.wildcard("pages/**/*.fxg")
|> Task.async_stream(fn file -> Builders.fxg(file, main_template, bricks) end)
|> Enum.to_list()

File.mkdir_p!("build/static")

Path.wildcard("static/**/*")
|> Task.async_stream(fn file -> File.cp(file, "build/" <> file) end)
|> Enum.to_list()

File.mkdir_p!("build/scripts")

Path.wildcard("scripts/**/*.js")
|> Task.async_stream(fn file -> Builders.js(file) end)
|> Enum.to_list()

File.mkdir_p!("build/styles")

Path.wildcard("styles/**/*.css")
|> Task.async_stream(fn file -> Builders.css(file) end)
|> Enum.to_list()

File.mkdir_p("build/pastes")

{:ok, paste_template} = File.read("templates/paste.html")

pastes =
  Path.wildcard("pastes/**/*")
  |> Enum.map(fn path -> {path, Git.get_file_commit(path)} end)
  |> Enum.sort_by(fn {_, t} -> t end, :desc)
  |> Enum.map(fn {path, _} -> path end)

pastes
|> Task.async_stream(fn file -> Builders.paste(file, paste_template, bricks) end)
|> Enum.to_list()

paste_data =
  Map.new(pastes, fn x -> {x, FileX.createdf(x)} end)
  |> Enum.map(fn {x, y} -> Templates.pastelink(x, x, y) end)
  |> Enum.join("<br>")

pastes_header = """
<h1>my pastes</h1>
<p>random code snippets i found useful or wanted to save :))</p>
<hr>
"""

# main_template
# |> String.replace("<content />", pastes_header <> "<p>" <> paste_data <> "</p>")
paste_index =
  Mustache.render(
    main_template,
    Map.merge(bricks, %{
      fxg_content: pastes_header <> "<p>" <> paste_data <> "</p>",
      location: "pastes",
      ogp:
        Templates.ogp(
          "thoughts",
          "https://m1kadev.nl/pastes",
          "random code snippets i found useful or wanted to save :))"
        )
    })
  )

File.write("build/pastes/index.html", paste_index)

File.mkdir_p("build/thoughts")

thoughts_template = File.read!("templates/thoughts.html")
thought_template = File.read!("templates/thought.html")

thought_paths =
  Path.wildcard("thoughts/**/*")
  |> Enum.map(fn path -> {path, Git.get_file_commit(path)} end)
  |> Enum.sort_by(fn {_, t} -> t end, :desc)

thoughts = Enum.map(thought_paths, fn {path, date} -> Builders.thought(path, date) end)

thoughts_html =
  Mustache.render(
    thoughts_template,
    Map.merge(bricks, %{
      fxg_content: Enum.join(thoughts, "<hr>"),
      location: "thoughts",
      ogp:
        Templates.ogp(
          "thoughts",
          "https://m1kadev.nl/thoughts",
          "important thoughts that needed publishing"
        )
    })
  )

for {thought, {path, time}} <- Enum.zip(thoughts, thought_paths) do
  thought = FileX.sanitised_name(path)
  output_path = "build/thoughts/" <> thought <> ".html"

  when_said = DateTime.from_unix!(time) |> Calendar.strftime("%H:%M, %d/%m/%y")
  thought_txt = File.read!(path)

  thought_html =
    Mustache.render(
      thought_template,
      Map.merge(bricks, %{
        fxg_content: thought_txt,
        thought: thought,
        date: when_said,
        ogp:
          Templates.ogp(
            thought,
            "https://m1kadev.nl/#{String.replace_prefix(output_path, "build/", "")}",
            String.replace(thought_txt, "\"", "&quot;")
          )
      })
    )

  File.write(output_path, thought_html)
end

File.write("build/thoughts/index.html", thoughts_html)

File.copy("favicon.ico", "build/favicon.ico")

{:ok, date} = DateTime.now("Europe/Amsterdam")
build_time = Calendar.strftime(date, "%H:%M:%S %d/%m/%y")

File.write(
  "build/info.txt",
  Templates.build_info(
    commit,
    lightningcss |> String.split_at(13) |> elem(1),
    html_minifier_next,
    uglifyjs |> String.split_at(10) |> elem(1),
    build_time
  )
)

A kethel/.formatter.exs => kethel/.formatter.exs +4 -0
@@ 0,0 1,4 @@
# Used by "mix format"
[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

A kethel/.gitignore => kethel/.gitignore +25 -0
@@ 0,0 1,25 @@
/node_modules

# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
kethel-*.tar

# Temporary files, for example, from tests.
/tmp/

A kethel/lib/bricks.ex => kethel/lib/bricks.ex +18 -0
@@ 0,0 1,18 @@
defmodule Bricks do
  @typedoc "A path, relative to mix.exs"
  @type relative_path() :: binary()

  @typedoc "The name of the brick"
  @type brick_name() :: binary()

  @typedoc "A valid mustache template"
  @type mustache() :: binary()

  @type bricks() :: %{brick_name() => mustache()}

  @spec collect(relative_path()) :: bricks()
  def collect(folder) do
    Path.wildcard(folder <> "/bricks/*.thtml")
    |> Map.new(fn x -> {FileX.trimmed_filename(x), File.read!(x)} end)
  end
end

A kethel/lib/common.ex => kethel/lib/common.ex +3 -0
@@ 0,0 1,3 @@
defmodule Context do
  defstruct project_root: "/", fxg: "fxg"
end

A kethel/lib/filex.ex => kethel/lib/filex.ex +9 -0
@@ 0,0 1,9 @@
defmodule FileX do
  @typedoc "Any path"
  @type path() :: binary()

  @spec trimmed_filename(path()) :: binary()
  def trimmed_filename(path) do
    Path.basename(path) |> Path.rootname() |> String.replace("-", "_")
  end
end

A kethel/lib/pages.ex => kethel/lib/pages.ex +66 -0
@@ 0,0 1,66 @@
require Rambo

defmodule Pages do
  defstruct [:source_files, :main_template, :project_root, :bricks]

  defp trim_abs_path(path, project_root) do
    # ROOT + pages/
    begin = byte_size(project_root) + byte_size("pages/")
    # remove extension
    binary_part(path, begin, byte_size(path) - begin - byte_size(".fxg"))
  end

  @spec collect(%Context{}, Bricks.bricks()) :: %Pages{}
  def collect(project_root, bricks) do
    pages_root = project_root <> "/pages"

    %Pages{
      project_root: project_root,
      source_files:
        Path.wildcard(pages_root <> "/**/*.fxg")
        |> Task.async_stream(fn path -> {trim_abs_path(path, project_root), File.read!(path)} end)
        # we assume every file read succeeds :)) always be positive
        |> Enum.map(fn {:ok, val} -> val end),
      main_template: File.read!(project_root <> "/templates/base.thtml"),
      bricks: bricks
    }
  end

  @spec compile(%Pages{}) :: none()
  def compile(pages) do
    t_begin = System.monotonic_time(:millisecond)
    Task.async_stream(pages.source_files, fn page -> compile_file(pages, page) end)
    |> Enum.to_list()
    t_end = System.monotonic_time(:millisecond)
    t_end - t_begin 
  end

  # doc "compiles a file, already read into process memory. Does not return the binary, but instantly writes it to disk"
  @spec compile_file(%Pages{}, {}) :: none()
  defp compile_file(pages, {page_path, page_data}) do
    output_path = "#{pages.project_root}/build#{page_path}.html"
    # check for a custom template
    custom_template_path = "#{pages.project_root}/templates/pages#{page_path}.thtml"

    template =
      case File.read(custom_template_path) do
        {:ok, custom_template} -> custom_template
        {:error, :enoent} -> pages.main_template
        {:error, err} -> raise err
      end

    mustache_context =
      Map.merge(%{fxg_content: invoke_fxg(page_data), location: page_path}, pages.bricks)

    page_rendered = Mustache.render(template, mustache_context)
    File.write!(output_path, page_rendered)
    IO.puts("[COMPILED] #{pages.project_root}/pages#{page_path}.fxg -> #{output_path}")
  end

  # doc "Invokes the fxg compiler, a la `fxg -`, piping `data` into the process stdin, and reading the stdout. "
  @spec invoke_fxg(binary()) :: binary()
  defp invoke_fxg(data) do
    {:ok, res} = Rambo.run("fxg", ["-"], in: data)
    res.out
  end
end

A kethel/lib/scripts.ex => kethel/lib/scripts.ex +50 -0
@@ 0,0 1,50 @@
require Rambo

defmodule Scripts do
  defstruct [:scripts, :project_root]

  defp ensure_file_structure(project_root) do
    File.mkdir_p!("#{project_root}/build/scripts")
  end

  defp trim_abs_path(path, project_root) do
    # ROOT + scripts/
    begin = byte_size(project_root) + byte_size("scripts/")
    # remove extension
    binary_part(path, begin, byte_size(path) - begin)
  end

  def collect(project_root) do
    %Scripts{
      scripts:
        Path.wildcard(project_root <> "/scripts/**/*.js")
        |> Task.async_stream(fn script ->
          {trim_abs_path(script, project_root), File.read!(script)}
        end)
        |> Enum.map(fn {:ok, val} -> val end),
      project_root: project_root
    }
  end

  def compile(scripts) do
    t_begin = System.monotonic_time(:millisecond)

    ensure_file_structure(scripts.project_root)

    scripts.scripts
    |> Task.async_stream(fn script -> compile_file(script, scripts.project_root) end)
    |> Enum.map(fn {:ok, val} -> val end)

    t_end = System.monotonic_time(:millisecond)
    t_end - t_begin
  end

  # data | npx lightningcss -m
  defp compile_file({path, data}, project_root) do
    {:ok, %Rambo{out: out}} = Rambo.run("npx", ["uglifyjs"], in: data)
    output_path = "#{project_root}/build/scripts#{path}"
    File.write!(output_path, out)

    IO.puts("[COMPILED] #{project_root}/scripts#{path} -> #{output_path}")
  end
end

A kethel/lib/statics.ex => kethel/lib/statics.ex +31 -0
@@ 0,0 1,31 @@
defmodule Statics do
  @moduledoc """
    Statics is an exception, because we only copy the files. Therefore, the performance gain from reading files into memory for processing isn't worth it. The compile fuction gets called at load-time for all other asset types.
  """

  @spec ensure_file_structure(binary()) :: :ok
  defp ensure_file_structure(project_root) do
    File.mkdir_p!("#{project_root}/build/static/")
  end

  defp trim_abs_path(path, project_root) do
    # ROOT + static
    begin = byte_size(project_root) + byte_size("static/")
    # remove extension
    binary_part(path, begin, byte_size(path) - begin)
  end

  # we dont async_stream here because copying is (relatively speaking) inexpensive, and we want to keep more threads open for other (more expensive) compilation processes
  def compile(project_root) do
    ensure_file_structure(project_root)

    for path <- Path.wildcard("#{project_root}/static/**/*") do
      rel_path = trim_abs_path(path, project_root)
      output_path = "#{project_root}/build/static#{rel_path}"

      File.copy!(path, output_path)

      IO.puts("[ COPIED ] #{path} -> #{output_path}")
    end
  end
end

A kethel/lib/styles.ex => kethel/lib/styles.ex +50 -0
@@ 0,0 1,50 @@
require Rambo

defmodule Styles do
  defstruct [:styles, :project_root]

  defp ensure_file_structure(project_root) do
    File.mkdir_p!("#{project_root}/build/styles")
  end

  defp trim_abs_path(path, project_root) do
    # ROOT + styles/
    begin = byte_size(project_root) + byte_size("styles/")
    # remove extension
    binary_part(path, begin, byte_size(path) - begin)
  end

  def collect(project_root) do
    %Styles{
      styles:
        Path.wildcard(project_root <> "/styles/**/*.css")
        |> Task.async_stream(fn style ->
          {trim_abs_path(style, project_root), File.read!(style)}
        end)
        |> Enum.map(fn {:ok, val} -> val end),
      project_root: project_root
    }
  end

  def compile(styles) do
    t_begin = System.monotonic_time(:millisecond)

    ensure_file_structure(styles.project_root)

    styles.styles
    |> Task.async_stream(fn style -> compile_file(style, styles.project_root) end)
    |> Enum.map(fn {:ok, val} -> val end)

    t_end = System.monotonic_time(:millisecond)
    t_end - t_begin
  end

  # data | npx lightningcss -m
  defp compile_file({path, data}, project_root) do
    {:ok, %Rambo{out: out}} = Rambo.run("npx", ["lightningcss", "-m"], in: data)
    output_path = "#{project_root}/build/styles#{path}"
    File.write!(output_path, out)

    IO.puts("[COMPILED] #{project_root}/styles#{path} -> #{output_path}")
  end
end

A kethel/lib/thoughts.ex => kethel/lib/thoughts.ex +89 -0
@@ 0,0 1,89 @@
defmodule Thoughts do
  defstruct [:thoughts, :template, :bricks, :project_root, :index_template, :inline_template ]

  @spec urlify(binary()) :: binary()
  defp urlify(name) do
    name
    |> String.replace(" ", "_")
  end

  @spec trim_abs_path(binary(), binary()) :: binary()
  defp trim_abs_path(path, project_root) do
    # ROOT + thoughts/
    begin = byte_size(project_root) + byte_size("/thoughts/")
    binary_part(path, begin, byte_size(path) - begin)
  end

  @spec collect(binary(), %{binary() => binary()}) :: %Thoughts{}
  def collect(project_root, bricks) do
    %Thoughts{
      thoughts:
        Path.wildcard("#{project_root}/thoughts/*")
        |> Enum.map(fn path -> {path, get_file_commit_date(path), File.read!(path)} end)
        |> Enum.sort_by(fn {_, date, _ } -> date end, :desc),
      inline_template: File.read!("#{project_root}/templates/inline_thought.thtml"),
      template: File.read!("#{project_root}/templates/thought.thtml"),
      index_template: File.read!("#{project_root}/templates/thoughts.thtml"),
      bricks: bricks,
      project_root: project_root
    }
  end

  @spec ensure_folders(binary()) :: :ok
  def ensure_folders(project_root) do
    File.mkdir_p!("#{project_root}/build/thoughts/")
  end

  @spec compile(%Thoughts{}) :: pos_integer()
  def compile(thoughts) do
    t_begin = System.monotonic_time(:millisecond)
    ensure_folders(thoughts.project_root)

    thoughts.thoughts
    |> Task.async_stream(fn thought ->
      compile_file(thought, thoughts.template, thoughts.bricks, thoughts.project_root) end)
    |> Enum.to_list()

    # generate index
    index_content = thoughts.thoughts
      |> Enum.map(fn thought -> inline_thought(thought, thoughts) end)
      |> Enum.join()
    index = Mustache.render(thoughts.index_template, Map.merge(thoughts.bricks, %{fxg_content: index_content}))
    File.write!("#{thoughts.project_root}/build/thoughts/index.html", index)
    IO.puts("[COMPILED] #{thoughts.project_root}/build/thoughts/index.html")

    t_end = System.monotonic_time(:millisecond)
    t_end - t_begin
  end

  defp inline_thought({path, date, thought}, context) do
    stripped_name = String.replace_prefix(path, "#{context.project_root}/thoughts/", "")
    location = "https://m1kadev.nl/thoughts/#{urlify(stripped_name)}.html"
    title = "<a href=\"#{location}\">#{stripped_name}</a>"
    Mustache.render(context.inline_template, %{name: title, date: date, thought: thought})
  end

  defp compile_file({path, date, thought}, template, bricks, project_root) do
    name = trim_abs_path(path, project_root)
    output_path = "#{project_root}/build/thoughts/#{urlify(name)}.html"
    mustache_context = Map.merge(bricks, %{thought: thought, name: name, date: date})

    File.write!(output_path, Mustache.render(template, mustache_context))
    IO.puts("[COMPILED] #{path} -> #{output_path}")
  end

  def get_file_commit_date(path) do
    {ts_string, 0} = System.cmd("git", ["log", "--format=%ct", path])
    String.trim_trailing(ts_string)
      |> String.to_integer()
  end

  def to_rss({path, date, thought}, template, project_root) do
    location = String.replace_prefix(path, project_root <> "/thoughts/", "")
    link = "https://m1kadev.nl/thoughts/" <> urlify(location) <> ".html"
    rfc_date = date
      |> DateTime.from_unix!()
      |> Calendar.strftime("%a, %d %b %Y %H:%M:%S %Z")
    Mustache.render(template, %{title: location, date: rfc_date, description: thought, category: "thoughts", link: link } ) 
  end
end

A kethel/main.exs => kethel/main.exs +92 -0
@@ 0,0 1,92 @@
args = System.argv()

folder = Enum.at(args, 1)

if folder != nil do
  project_root = Path.expand(folder)

  File.rm_rf!("#{project_root}/build")
  File.mkdir_p("#{project_root}/build")

  collection_time_start = System.monotonic_time(:millisecond)

  # other contents depend on this, so we cant async it
  bricks = Bricks.collect(project_root)

  # special case, see module documentation
  statics_task = Task.async(fn -> Statics.compile(project_root) end)

  styles_task = Task.async(fn -> Styles.collect(project_root) end)
  scripts_task = Task.async(fn -> Scripts.collect(project_root) end)
  pages_task = Task.async(fn -> Pages.collect(project_root, bricks) end)
  thoughts_task = Task.async(fn -> Thoughts.collect(project_root, bricks) end)

  [styles, pages, scripts, thoughts] =
    Task.await_many([styles_task, pages_task, scripts_task, thoughts_task], :infinity)

  collection_time_end = System.monotonic_time(:millisecond)

  compilation_time_start = System.monotonic_time(:millisecond)
  
  pages_compile_task = Task.async(fn -> Pages.compile(pages) end)
  styles_compile_task = Task.async(fn -> Styles.compile(styles) end)
  scripts_compile_task = Task.async(fn -> Scripts.compile(scripts) end)
  thoughts_compile_task = Task.async(fn -> Thoughts.compile(thoughts) end)

  [ pages_time, styles_time, scripts_time, thoughts_time] = Task.await_many(
    [
      pages_compile_task,
      styles_compile_task,
      scripts_compile_task,
      thoughts_compile_task
    ],
    :infinity
  )

  compilation_time_end = System.monotonic_time(:millisecond)

  rss_header = File.read!("#{project_root}/rss/feed.txml")
  rss_footer = "</channel></rss>"
  rss_item_template = File.read!("#{project_root}/rss/item.txml")

  rss_thoughts = thoughts.thoughts
    |> Enum.take(10)
    |> Enum.map(fn thought -> Thoughts.to_rss(thought, rss_item_template, project_root) end)
    |> Enum.join();

  File.write!("#{project_root}/build/rss.xml", rss_header <> rss_thoughts <> rss_footer)

  IO.puts("[COMPILED] #{project_root}/build/rss.xml")
  
  { lcss_version, 0 }= System.cmd("npx", [ "lightningcss", "-V" ])
  { hmn_version, 0 } = System.cmd("npx", [ "html-minifier-next", "-V" ])
  { ujs_version, 0 } = System.cmd("npx", [ "uglifyjs", "-V" ])
  { git_commit, 0 } = System.cmd("git", [ "rev-parse", "HEAD" ])

  { build_date, 0 } = System.cmd("date", ["+%H:%M %d/%m/%y"])

  info_txt = """
  commit=#{git_commit}
  lightningcss=#{lcss_version}
  html_minifier_next=#{hmn_version}
  uglifyjs=#{ujs_version}
  build_time=#{build_date}
  """
  
  File.write!("#{project_root}/build/info.txt", info_txt)

  IO.puts("[COMPILED] #{project_root}/build/info.txt")

  IO.puts("Finalising...");

  File.cp!("#{project_root}/favicon.ico", "#{project_root}/build/favicon.ico")
  IO.puts("")
  IO.puts(:stderr, "===== BUILD RESULTS =====")
  IO.puts(:stderr, "FILE READING (*.collect()) | #{collection_time_end - collection_time_start}ms") 
  IO.puts(:stderr, "Root page (compile)        | #{pages_time}ms")
  IO.puts(:stderr, "Styles (compile)           | #{styles_time}ms")
  IO.puts(:stderr, "Scripts (compile)          | #{scripts_time}ms")
  IO.puts(:stderr, "Thoughts (compile)         | #{thoughts_time}ms")
else
  IO.puts(:stderr, "The root project folder should be provided on the command line")
end

A kethel/mix.exs => kethel/mix.exs +20 -0
@@ 0,0 1,20 @@
defmodule Kethel.MixProject do
  use Mix.Project

  def project do
    [
      app: :kethel,
      version: "0.1.0",
      elixir: "~> 1.18",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  defp deps do
    [
      {:mustache, "~> 0.5.0"},
      {:rambo, "~> 0.3.4"}
    ]
  end
end

A kethel/mix.lock => kethel/mix.lock +13 -0
@@ 0,0 1,13 @@
%{
  "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
  "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
  "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
  "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
  "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
  "mustache": {:hex, :mustache, "0.5.1", "1bfee8a68a8e72d47616541a93a632ae1684c1abc17c9eb5f7bfecb78d7496e5", [:mix], [], "hexpm", "524c9bbb6080a52d7b6806436b4e269e0224c785a228faf3293ef30a75016bfa"},
  "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
  "rambo": {:hex, :rambo, "0.3.4", "8962ac3bd1a633ee9d0e8b44373c7913e3ce3d875b4151dcd060886092d2dce7", [:mix], [], "hexpm", "0cc54ed089fbbc84b65f4b8a774224ebfe60e5c80186fafc7910b3e379ad58f1"},
  "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
  "tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"},
  "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
}

A kethel/package-lock.json => kethel/package-lock.json +878 -0
@@ 0,0 1,878 @@
{
  "name": "kethel",
  "version": "0.1.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "kethel",
      "version": "0.1.0",
      "license": "MIT",
      "dependencies": {
        "html-minifier-next": "^4.17.0",
        "lightningcss-cli": "^1.30.2",
        "uglify-js": "^3.19.3"
      }
    },
    "node_modules/@jridgewell/gen-mapping": {
      "version": "0.3.13",
      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
      "license": "MIT",
      "dependencies": {
        "@jridgewell/sourcemap-codec": "^1.5.0",
        "@jridgewell/trace-mapping": "^0.3.24"
      }
    },
    "node_modules/@jridgewell/resolve-uri": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
      "license": "MIT",
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@jridgewell/source-map": {
      "version": "0.3.11",
      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
      "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
      "license": "MIT",
      "dependencies": {
        "@jridgewell/gen-mapping": "^0.3.5",
        "@jridgewell/trace-mapping": "^0.3.25"
      }
    },
    "node_modules/@jridgewell/sourcemap-codec": {
      "version": "1.5.5",
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
      "license": "MIT"
    },
    "node_modules/@jridgewell/trace-mapping": {
      "version": "0.3.31",
      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
      "license": "MIT",
      "dependencies": {
        "@jridgewell/resolve-uri": "^3.1.0",
        "@jridgewell/sourcemap-codec": "^1.4.14"
      }
    },
    "node_modules/@types/relateurl": {
      "version": "0.2.33",
      "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz",
      "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==",
      "license": "MIT"
    },
    "node_modules/acorn": {
      "version": "8.15.0",
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
      "license": "MIT",
      "bin": {
        "acorn": "bin/acorn"
      },
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/buffer-from": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
      "license": "MIT"
    },
    "node_modules/camel-case": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
      "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
      "license": "MIT",
      "dependencies": {
        "pascal-case": "^3.1.2",
        "tslib": "^2.0.3"
      }
    },
    "node_modules/capital-case": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz",
      "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==",
      "license": "MIT",
      "dependencies": {
        "no-case": "^3.0.4",
        "tslib": "^2.0.3",
        "upper-case-first": "^2.0.2"
      }
    },
    "node_modules/change-case": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz",
      "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==",
      "license": "MIT",
      "dependencies": {
        "camel-case": "^4.1.2",
        "capital-case": "^1.0.4",
        "constant-case": "^3.0.4",
        "dot-case": "^3.0.4",
        "header-case": "^2.0.4",
        "no-case": "^3.0.4",
        "param-case": "^3.0.4",
        "pascal-case": "^3.1.2",
        "path-case": "^3.0.4",
        "sentence-case": "^3.0.4",
        "snake-case": "^3.0.4",
        "tslib": "^2.0.3"
      }
    },
    "node_modules/commander": {
      "version": "14.0.2",
      "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
      "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
      "license": "MIT",
      "engines": {
        "node": ">=20"
      }
    },
    "node_modules/constant-case": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz",
      "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==",
      "license": "MIT",
      "dependencies": {
        "no-case": "^3.0.4",
        "tslib": "^2.0.3",
        "upper-case": "^2.0.2"
      }
    },
    "node_modules/detect-libc": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
      "license": "Apache-2.0",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/dot-case": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
      "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
      "license": "MIT",
      "dependencies": {
        "no-case": "^3.0.4",
        "tslib": "^2.0.3"
      }
    },
    "node_modules/entities": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz",
      "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==",
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=0.12"
      },
      "funding": {
        "url": "https://github.com/fb55/entities?sponsor=1"
      }
    },
    "node_modules/header-case": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz",
      "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==",
      "license": "MIT",
      "dependencies": {
        "capital-case": "^1.0.4",
        "tslib": "^2.0.3"
      }
    },
    "node_modules/html-minifier-next": {
      "version": "4.17.0",
      "resolved": "https://registry.npmjs.org/html-minifier-next/-/html-minifier-next-4.17.0.tgz",
      "integrity": "sha512-7REp/offCDTw5QdWd2D14vv5MsTslrakD1LT5Nnows2praQvuFkP/pjV3YwtE9H+xRG9Mjr32UDE9zS2s+YUaw==",
      "license": "MIT",
      "dependencies": {
        "@types/relateurl": "^0.2.33",
        "change-case": "^4.1.2",
        "commander": "^14.0.2",
        "entities": "^7.0.0",
        "lightningcss": "^1.28.2",
        "relateurl": "^0.2.7",
        "terser": "^5.44.1"
      },
      "bin": {
        "html-minifier-next": "cli.js"
      },
      "funding": {
        "url": "https://github.com/j9t/html-minifier-next?sponsor=1"
      },
      "peerDependencies": {
        "@swc/core": "^1.15.7"
      },
      "peerDependenciesMeta": {
        "@swc/core": {
          "optional": true
        }
      }
    },
    "node_modules/lightningcss": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
      "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
      "license": "MPL-2.0",
      "dependencies": {
        "detect-libc": "^2.0.3"
      },
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      },
      "optionalDependencies": {
        "lightningcss-android-arm64": "1.30.2",
        "lightningcss-darwin-arm64": "1.30.2",
        "lightningcss-darwin-x64": "1.30.2",
        "lightningcss-freebsd-x64": "1.30.2",
        "lightningcss-linux-arm-gnueabihf": "1.30.2",
        "lightningcss-linux-arm64-gnu": "1.30.2",
        "lightningcss-linux-arm64-musl": "1.30.2",
        "lightningcss-linux-x64-gnu": "1.30.2",
        "lightningcss-linux-x64-musl": "1.30.2",
        "lightningcss-win32-arm64-msvc": "1.30.2",
        "lightningcss-win32-x64-msvc": "1.30.2"
      }
    },
    "node_modules/lightningcss-android-arm64": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
      "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
      "cpu": [
        "arm64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-cli": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli/-/lightningcss-cli-1.30.2.tgz",
      "integrity": "sha512-vTm/775SqvQ74T0y4twiqjopXV6SnAFguuRpnbKUE9aXxqJGxWJoHS77ZRkJj3dKmnD61ejOawg3FBiTT8/3Tw==",
      "hasInstallScript": true,
      "license": "MPL-2.0",
      "dependencies": {
        "detect-libc": "^2.0.3"
      },
      "bin": {
        "lightningcss": "lightningcss"
      },
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      },
      "optionalDependencies": {
        "lightningcss-cli-android-arm64": "1.30.2",
        "lightningcss-cli-darwin-arm64": "1.30.2",
        "lightningcss-cli-darwin-x64": "1.30.2",
        "lightningcss-cli-freebsd-x64": "1.30.2",
        "lightningcss-cli-linux-arm-gnueabihf": "1.30.2",
        "lightningcss-cli-linux-arm64-gnu": "1.30.2",
        "lightningcss-cli-linux-arm64-musl": "1.30.2",
        "lightningcss-cli-linux-x64-gnu": "1.30.2",
        "lightningcss-cli-linux-x64-musl": "1.30.2",
        "lightningcss-cli-win32-arm64-msvc": "1.30.2",
        "lightningcss-cli-win32-x64-msvc": "1.30.2"
      }
    },
    "node_modules/lightningcss-cli-android-arm64": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli-android-arm64/-/lightningcss-cli-android-arm64-1.30.2.tgz",
      "integrity": "sha512-CcntRK9yNjFRiZKwW9m4sf5WYYNOGxlD4ROymyIb+KPbsrEBAZSaknoUM1aIhuhvB/TUTtVmPKvt/Zu0LeSR7g==",
      "cpu": [
        "arm64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-cli-darwin-arm64": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli-darwin-arm64/-/lightningcss-cli-darwin-arm64-1.30.2.tgz",
      "integrity": "sha512-Z2STTSes07R2R5tOPY85HipU+z1SwUBV2Yp5emAcqL+o5RsOAE1JWbkloQWa++8R6b/gxzD11VPM3WtCCqL8Gg==",
      "cpu": [
        "arm64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-cli-darwin-x64": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli-darwin-x64/-/lightningcss-cli-darwin-x64-1.30.2.tgz",
      "integrity": "sha512-N0IGWWsLnv81sDw9i/vvaisp55uDnhoapGBEJ8lMED064CsUHpLCK7L/5B8kFj8UI97zL+xb7KTTGOHkcC8skg==",
      "cpu": [
        "x64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-cli-freebsd-x64": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli-freebsd-x64/-/lightningcss-cli-freebsd-x64-1.30.2.tgz",
      "integrity": "sha512-17pN+hnxU2fPOGhGzMhPf2KZrIFFwEtrIZYshGDM/jhskJmDq57DAt5esh7PVDUjNkD7JDt3a7E9xJEVltc2Kg==",
      "cpu": [
        "x64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-cli-linux-arm-gnueabihf": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli-linux-arm-gnueabihf/-/lightningcss-cli-linux-arm-gnueabihf-1.30.2.tgz",
      "integrity": "sha512-aaeEtZXqvNWDyHYCThugev1YPGlouvY9lE+o9QJg/Z5GTNb/1EM2TaTBGEwdG5m1ChYukc2lD5CPIEHjQ7qzSA==",
      "cpu": [
        "arm"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-cli-linux-arm64-gnu": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli-linux-arm64-gnu/-/lightningcss-cli-linux-arm64-gnu-1.30.2.tgz",
      "integrity": "sha512-XhgByx2H85MeEWGtI0XR+ugtQRBc/bvRN8gSGdXa3YqVONmACsLj7shatVHmqVOaPgtnevKVKl7wtqfN9pW96A==",
      "cpu": [
        "arm64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-cli-linux-arm64-musl": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli-linux-arm64-musl/-/lightningcss-cli-linux-arm64-musl-1.30.2.tgz",
      "integrity": "sha512-UR7/N/33b8sG9e07TccxJvWxvNdlMFglesdgv1fi+wMa8sbN3tBPXOGj67B5nlVWHskYD9bHLPahemRWxnNuHQ==",
      "cpu": [
        "arm64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-cli-linux-x64-gnu": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli-linux-x64-gnu/-/lightningcss-cli-linux-x64-gnu-1.30.2.tgz",
      "integrity": "sha512-W6U9LNo+4mCsEB22AIvtRVGp8NETN4by9U2AIRDzZbYZYb7W9+1H56xKzb/2ABQEAoDrh/7027I/XDheuGrlZg==",
      "cpu": [
        "x64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-cli-linux-x64-musl": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli-linux-x64-musl/-/lightningcss-cli-linux-x64-musl-1.30.2.tgz",
      "integrity": "sha512-Qe6hwTXmc37LnKPIyoY20EQgHsVoAeTUF389Bt7sJfK5JZPLm7EyVvqkO4DukCx4Dv4DSskU03o+qT1+s6UeVw==",
      "cpu": [
        "x64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-cli-win32-arm64-msvc": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli-win32-arm64-msvc/-/lightningcss-cli-win32-arm64-msvc-1.30.2.tgz",
      "integrity": "sha512-PZrkKHhYncBRh/Xe2Voa7t7ors+zUMU8hm5qOLgR0W5NfhbwwjLBXa3+fFF+BVdpTeyjLGF1ovzKIWSF1rMN/g==",
      "cpu": [
        "arm64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-cli-win32-x64-msvc": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-cli-win32-x64-msvc/-/lightningcss-cli-win32-x64-msvc-1.30.2.tgz",
      "integrity": "sha512-5VUIS1Dcbkcy2SDr7vBlKfwpSx2BUlZwmvoRVWA+OkkLsQhVUtLUKheJJXsOFruBn9uV8FH8KPLkvTJzuGFLyQ==",
      "cpu": [
        "x64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-darwin-arm64": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
      "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
      "cpu": [
        "arm64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-darwin-x64": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
      "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
      "cpu": [
        "x64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-freebsd-x64": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
      "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
      "cpu": [
        "x64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-linux-arm-gnueabihf": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
      "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
      "cpu": [
        "arm"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-linux-arm64-gnu": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
      "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
      "cpu": [
        "arm64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-linux-arm64-musl": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
      "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
      "cpu": [
        "arm64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-linux-x64-gnu": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
      "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
      "cpu": [
        "x64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-linux-x64-musl": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
      "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
      "cpu": [
        "x64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-win32-arm64-msvc": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
      "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
      "cpu": [
        "arm64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lightningcss-win32-x64-msvc": {
      "version": "1.30.2",
      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
      "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
      "cpu": [
        "x64"
      ],
      "license": "MPL-2.0",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">= 12.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/parcel"
      }
    },
    "node_modules/lower-case": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
      "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
      "license": "MIT",
      "dependencies": {
        "tslib": "^2.0.3"
      }
    },
    "node_modules/no-case": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
      "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
      "license": "MIT",
      "dependencies": {
        "lower-case": "^2.0.2",
        "tslib": "^2.0.3"
      }
    },
    "node_modules/param-case": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
      "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
      "license": "MIT",
      "dependencies": {
        "dot-case": "^3.0.4",
        "tslib": "^2.0.3"
      }
    },
    "node_modules/pascal-case": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
      "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
      "license": "MIT",
      "dependencies": {
        "no-case": "^3.0.4",
        "tslib": "^2.0.3"
      }
    },
    "node_modules/path-case": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz",
      "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==",
      "license": "MIT",
      "dependencies": {
        "dot-case": "^3.0.4",
        "tslib": "^2.0.3"
      }
    },
    "node_modules/relateurl": {
      "version": "0.2.7",
      "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
      "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
      "license": "MIT",
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/sentence-case": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz",
      "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==",
      "license": "MIT",
      "dependencies": {
        "no-case": "^3.0.4",
        "tslib": "^2.0.3",
        "upper-case-first": "^2.0.2"
      }
    },
    "node_modules/snake-case": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
      "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
      "license": "MIT",
      "dependencies": {
        "dot-case": "^3.0.4",
        "tslib": "^2.0.3"
      }
    },
    "node_modules/source-map": {
      "version": "0.6.1",
      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/source-map-support": {
      "version": "0.5.21",
      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
      "license": "MIT",
      "dependencies": {
        "buffer-from": "^1.0.0",
        "source-map": "^0.6.0"
      }
    },
    "node_modules/terser": {
      "version": "5.44.1",
      "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
      "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
      "license": "BSD-2-Clause",
      "dependencies": {
        "@jridgewell/source-map": "^0.3.3",
        "acorn": "^8.15.0",
        "commander": "^2.20.0",
        "source-map-support": "~0.5.20"
      },
      "bin": {
        "terser": "bin/terser"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/terser/node_modules/commander": {
      "version": "2.20.3",
      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
      "license": "MIT"
    },
    "node_modules/tslib": {
      "version": "2.8.1",
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
      "license": "0BSD"
    },
    "node_modules/uglify-js": {
      "version": "3.19.3",
      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
      "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
      "license": "BSD-2-Clause",
      "bin": {
        "uglifyjs": "bin/uglifyjs"
      },
      "engines": {
        "node": ">=0.8.0"
      }
    },
    "node_modules/upper-case": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz",
      "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==",
      "license": "MIT",
      "dependencies": {
        "tslib": "^2.0.3"
      }
    },
    "node_modules/upper-case-first": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz",
      "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==",
      "license": "MIT",
      "dependencies": {
        "tslib": "^2.0.3"
      }
    }
  }
}

A kethel/package.json => kethel/package.json +12 -0
@@ 0,0 1,12 @@
{
  "name": "kethel",
  "version": "0.1.0",
  "description": "My own build system",
  "author": "m1kadev",
  "license": "MIT",
  "dependencies": {
    "html-minifier-next": "^4.17.0",
    "lightningcss-cli": "^1.30.2",
    "uglify-js": "^3.19.3"
  }
}

M pages/colophon.fxg => pages/colophon.fxg +7 -1
@@ 2,4 2,10 @@

== about == 

you're looking at a <#https://en.wiktionary.org/wiki/beautiful#English beautiful> site, with <#https://caddyserver.com/ caddy> as its backend. all source pages are written in my own markup language, <#https://github.com/m1kadev/fxg fxg>. site generation is done by a homebrewn <#https://github.com/m1kadev/www.m1kadev.nl/blob/main/build.exs elixir script>. all code (including <#https://github.com/m1kadev/fxg fxg>) is licenced under the MIT licence.
\ No newline at end of file
you're looking at a <#https://en.wiktionary.org/wiki/beautiful#English beautiful> site, with <#https://caddyserver.com/ caddy> as its backend.

the site generator is a home-grown non-gmo <#https://github.com/m1kadev/www.m1kadev.nl/tree/main/kethel/ elixir app>. the internal markup language is <#https://github.com/m1kadev/fxg fxg>, made to be more legible than markdown. 

templating the final html is handled by <#https://mustache.github.io mustache>, a stateless templating library.

css, js, and html parsing are handled by lightningcss, uglifyjs, and html-minifier-next, respectively.

A rss/feed.txml => rss/feed.txml +1 -0
@@ 0,0 1,1 @@
<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>m1kadev.nl updates :3</title><description>placeholder description</description><language>en</language>

A rss/item.txml => rss/item.txml +1 -0
@@ 0,0 1,1 @@
<item><title>{{title}}</title><description>{{description}}</description><link>{{link}}</link><pubDate>{{date}}</pubDate><category>{{category}}</category></item>

M styles/thoughts.css => styles/thoughts.css +5 -1
@@ 18,4 18,8 @@
.thought>small {
    text-align: right;
    display: block;
}
\ No newline at end of file
}

main hr:first-of-type {
    display: none;
}

R templates/base.html => templates/base.thtml +0 -0
A templates/inline_thought.thtml => templates/inline_thought.thtml +8 -0
@@ 0,0 1,8 @@
<hr>
<div class="thought">
    <h2> {{ &name }} </h2>
    <span>
        {{ thought }}
    </span>
    <small>-mika, {{ date }}</small>
</div>

R templates/pages/colophon.html => templates/pages/colophon.thtml +0 -0
R templates/pages/index.html => templates/pages/index.thtml +0 -0
R templates/paste.html => templates/paste.thtml +0 -0
R templates/thought.html => templates/thought.thtml +3 -3
@@ 7,7 7,7 @@

    <link rel="stylesheet" href="/styles/thought.css">

    <title> {{ thought }} - m1kadev</title>
    <title> {{ name }} - m1kadev</title>

    {{ &ogp }}



@@ 19,9 19,9 @@

    <div id="content">
        <main>
            <h2> {{ thought }} </h2>
            <h2> {{ name }} </h2>
            <span id="thought">
                {{ &fxg_content }}
                {{ thought }}
            </span>
            <small>-mika, {{ date }}</small>
            <hr>

R templates/thoughts.html => templates/thoughts.thtml +0 -0