this repo has no description

think links are working and stuff

= e5cf9ef7 8b1c1929

Changed files
+262 -25
lib
blog
blog_web
priv
rel
+89
Dockerfile
··· 1 + # Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian 2 + # instead of Alpine to avoid DNS resolution issues in production. 3 + # 4 + # https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu 5 + # https://hub.docker.com/_/ubuntu?tab=tags 6 + # 7 + # This file is based on these images: 8 + # 9 + # - https://hub.docker.com/r/hexpm/elixir/tags - for the build image 10 + # - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20230612-slim - for the release image 11 + # - https://pkgs.org/ - resource for finding needed packages 12 + # - Ex: hexpm/elixir:1.15.7-erlang-26.0.2-debian-bullseye-20230612-slim 13 + # 14 + ARG ELIXIR_VERSION=1.15.7 15 + ARG OTP_VERSION=26.0.2 16 + ARG DEBIAN_VERSION=bullseye-20230612-slim 17 + 18 + ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" 19 + ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" 20 + 21 + FROM ${BUILDER_IMAGE} as builder 22 + 23 + # Install build dependencies 24 + RUN apt-get update -y && apt-get install -y build-essential git \ 25 + && apt-get clean && rm -f /var/lib/apt/lists/*_* 26 + 27 + # Prepare build dir 28 + WORKDIR /app 29 + 30 + # Install hex + rebar 31 + RUN mix local.hex --force && \ 32 + mix local.rebar --force 33 + 34 + # Set build ENV 35 + ENV MIX_ENV="prod" 36 + 37 + # Install mix dependencies 38 + COPY mix.exs mix.lock ./ 39 + RUN mix deps.get --only $MIX_ENV 40 + RUN mkdir config 41 + 42 + # Copy config/runtime.exs from your project 43 + COPY config/config.exs config/${MIX_ENV}.exs config/ 44 + RUN mix deps.compile 45 + 46 + COPY priv priv 47 + 48 + COPY lib lib 49 + 50 + COPY assets assets 51 + 52 + # Compile assets 53 + RUN mix assets.deploy 54 + 55 + # Compile the release 56 + RUN mix compile 57 + 58 + # Changes to config/runtime.exs don't require recompiling the code 59 + COPY config/runtime.exs config/ 60 + 61 + COPY rel rel 62 + RUN mix release 63 + 64 + # Start a new build stage so that the final image will only contain 65 + # the compiled release and other runtime necessities 66 + FROM ${RUNNER_IMAGE} 67 + 68 + RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \ 69 + && apt-get clean && rm -f /var/lib/apt/lists/*_* 70 + 71 + # Set the locale 72 + RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen 73 + 74 + ENV LANG en_US.UTF-8 75 + ENV LANGUAGE en_US:en 76 + ENV LC_ALL en_US.UTF-8 77 + 78 + WORKDIR "/app" 79 + RUN chown nobody /app 80 + 81 + # Set runner ENV 82 + ENV MIX_ENV="prod" 83 + 84 + # Only copy the final release from the build stage 85 + COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/blog ./ 86 + 87 + USER nobody 88 + 89 + CMD ["/app/bin/server"]
+32
fly.toml
··· 1 + # fly.toml app configuration file generated for thoughts-and-tidbits on 2025-02-01T10:53:39-05:00 2 + # 3 + # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 + # 5 + 6 + app = "thoughts-and-tidbits" 7 + primary_region = "ord" 8 + kill_signal = "SIGTERM" 9 + 10 + [build] 11 + 12 + [env] 13 + PHX_HOST = "thoughts-and-tidbits.fly.dev" 14 + PORT = "8080" 15 + 16 + [http_service] 17 + internal_port = 8080 18 + force_https = true 19 + auto_stop_machines = true 20 + auto_start_machines = true 21 + min_machines_running = 0 22 + processes = ["app"] 23 + 24 + [http_service.concurrency] 25 + type = "connections" 26 + hard_limit = 1000 27 + soft_limit = 1000 28 + 29 + [[vm]] 30 + cpu_kind = "shared" 31 + cpus = 1 32 + memory_mb = 1024
+14 -5
lib/blog/content/post.ex
··· 2 2 defstruct [:body, :title, :written_on, :tags, :slug] 3 3 4 4 def all do 5 - "priv/posts/*.md" 5 + "priv/static/posts/*.md" 6 6 |> Path.wildcard() 7 7 |> Enum.map(&parse_post_file/1) 8 8 |> Enum.sort_by(& &1.written_on, {:desc, NaiveDateTime}) 9 9 end 10 10 11 11 def get_by_slug(slug) do 12 - all() 13 - |> Enum.find(&(&1.slug == slug)) 12 + require Logger 13 + all_posts = all() 14 + Logger.debug("Looking for slug: #{slug}") 15 + Logger.debug("Available slugs: #{inspect(Enum.map(all_posts, & &1.slug))}") 16 + Logger.debug("First post for debugging: #{inspect(List.first(all_posts), pretty: true)}") 17 + 18 + found = Enum.find(all_posts, &(&1.slug == slug)) 19 + Logger.debug("Found post?: #{inspect(found != nil)}") 20 + found 14 21 end 15 22 16 23 defp parse_post_file(file) do 24 + require Logger 17 25 filename = Path.basename(file, ".md") 26 + Logger.debug("Parsing file: #{filename}") 18 27 [year, month, day, hour, minute, second | title_words] = String.split(filename, "-") 19 - _title = Enum.join(title_words, " ") 20 28 datetime = parse_datetime(year, month, day, hour, minute, second) 21 29 slug = Enum.join(title_words, "-") 30 + Logger.debug("Generated slug: #{slug}") 22 31 23 32 %__MODULE__{ 24 33 body: File.read!(file), ··· 38 47 String.to_integer(minute), 39 48 String.to_integer(second) 40 49 ) 41 - IO.inspect(datetime, label: "parsed datetime for post") 50 + datetime # Make sure we're returning the datetime 42 51 end 43 52 44 53 defp humanize_title(slug) do
+28
lib/blog/release.ex
··· 1 + defmodule Blog.Release do 2 + @moduledoc """ 3 + Used for executing DB release tasks when run in production without Mix 4 + installed. 5 + """ 6 + @app :blog 7 + 8 + def migrate do 9 + load_app() 10 + 11 + for repo <- repos() do 12 + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) 13 + end 14 + end 15 + 16 + def rollback(repo, version) do 17 + load_app() 18 + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) 19 + end 20 + 21 + defp repos do 22 + Application.fetch_env!(@app, :ecto_repos) 23 + end 24 + 25 + defp load_app do 26 + Application.load(@app) 27 + end 28 + end
+3 -4
lib/blog_web/components/layouts.ex
··· 22 22 def posts_by_tag do 23 23 Blog.Content.Post.all() 24 24 |> Enum.flat_map(fn post -> 25 - Enum.map(post.tags, fn tag -> {tag.name, {post.title, format_date(post.written_on), post.slug}} end) 25 + Enum.map(post.tags, fn tag -> {tag.name, {post.title, post.slug}} end) 26 26 end) 27 27 |> Enum.group_by( 28 - fn {tag_name, _post} -> tag_name end, 29 - fn {_tag_name, post} -> post end 28 + fn {tag_name, _} -> tag_name end, 29 + fn {_, post_info} -> post_info end 30 30 ) 31 31 |> Enum.map(fn {tag_name, posts} -> {tag_name, Enum.take(posts, 2)} end) 32 32 end 33 - 34 33 35 34 defp format_date(datetime) do 36 35 Calendar.strftime(datetime, "%B %d, %Y")
-10
lib/blog_web/components/layouts/root.html.heex
··· 53 53 Posts by Tag 54 54 </h2> 55 55 <%= for {tag, posts} <- posts_by_tag() do %> 56 - 57 56 <div class="space-y-6"> 58 57 <div class="group"> 59 58 <a href={~p"/post/#{tag}"} class="block"> 60 59 <h3 class="text-sm font-medium text-gray-900 group-hover:text-blue-600 transition-colors"> 61 60 <%= tag %> 62 61 </h3> 63 - <p class="text-xs text-gray-500 mt-1 font-normal"> 64 - <%= for {title, slug} <- posts do %> 65 - <a href={~p"/post/#{slug}"} class="block"> 66 - <h6 class="text-sm font-medium text-gray-900 group-hover:text-blue-600 transition-colors"> 67 - <%= title %> 68 - </h6> 69 - </a> 70 - <% end %> 71 - </p> 72 62 </a> 73 63 </div> 74 64 </div>
+54
lib/blog_web/live/index.ex
··· 1 + defmodule BlogWeb.PostLive.Index do 2 + use BlogWeb, :live_view 3 + 4 + def mount(_params, _session, socket) do 5 + posts = Blog.Content.Post.all() 6 + {:ok, assign(socket, posts: posts)} 7 + end 8 + 9 + def render(assigns) do 10 + ~H""" 11 + <div class="px-8 py-12"> 12 + <div class="max-w-7xl mx-auto"> 13 + <h1 class="text-4xl font-bold text-gray-900 mb-8">All Posts</h1> 14 + 15 + <div class="space-y-10"> 16 + <%= for post <- @posts do %> 17 + <article class="p-6 bg-white rounded-lg border-2 border-gray-200"> 18 + <h2 class="text-2xl font-bold text-gray-900 mb-2"> 19 + <.link href={~p"/post/#{post.slug}"} class="hover:text-blue-600 transition-colors"> 20 + <%= post.title %> 21 + </.link> 22 + </h2> 23 + 24 + <div class="flex items-center space-x-4 text-sm text-gray-500 mb-4"> 25 + <time datetime={post.written_on}> 26 + <%= Calendar.strftime(post.written_on, "%B %d, %Y") %> 27 + </time> 28 + <div class="flex items-center space-x-2"> 29 + <%= for tag <- post.tags do %> 30 + <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100"> 31 + <%= tag.name %> 32 + </span> 33 + <% end %> 34 + </div> 35 + </div> 36 + 37 + <div class="prose prose-lg max-w-none"> 38 + <%= # Show first paragraph or excerpt %> 39 + <%= String.split(post.body, "\n\n") |> List.first() %> 40 + </div> 41 + 42 + <div class="mt-4"> 43 + <.link href={~p"/post/#{post.slug}"} class="text-blue-600 hover:text-blue-800 transition-colors"> 44 + Read more → 45 + </.link> 46 + </div> 47 + </article> 48 + <% end %> 49 + </div> 50 + </div> 51 + </div> 52 + """ 53 + end 54 + end
+15 -5
lib/blog_web/live/post_live.ex
··· 2 2 use BlogWeb, :live_view 3 3 4 4 def mount(%{"slug" => slug}, _session, socket) do 5 + require Logger 6 + Logger.debug("Mounting PostLive with slug: #{slug}") 7 + 5 8 case Blog.Content.Post.get_by_slug(slug) do 6 9 nil -> 10 + Logger.debug("No post found for slug: #{slug}") 7 11 {:ok, push_navigate(socket, to: "/")} 8 12 9 13 post -> 14 + Logger.debug("Found post: #{inspect(post, pretty: true)}") 10 15 case Earmark.as_html(post.body) do 11 16 {:ok, html, _} -> 12 17 headers = extract_headers(post.body) ··· 17 22 ) 18 23 {:ok, socket} 19 24 20 - {:error, _html, errors} -> 21 - # Log the error and show a friendly message 22 - require Logger 23 - Logger.error("Failed to parse markdown: #{inspect(errors)}") 24 - {:ok, push_navigate(socket, to: "/")} 25 + {:error, html, errors} -> 26 + # Still show the content even if there are markdown errors 27 + Logger.error("Markdown parsing warnings: #{inspect(errors)}") 28 + headers = extract_headers(post.body) 29 + socket = assign(socket, 30 + html: html, 31 + headers: headers, 32 + post: post 33 + ) 34 + {:ok, socket} 25 35 end 26 36 end 27 37 end
+1 -1
priv/static/posts/2024-03-10-14-45-00-pattern-matching-in-elixir.md
··· 79 79 80 80 Posts are routed by slug, which is implicit by title in filename. 81 81 82 - For example, `priv/static/posts/2024-03-10-14-45-00-pattern-matching-in-elixir.md` would be `pattern 82 + For example, `priv/static/posts/2024-03-10-14-45-00-pattern-matching-in-elixir.md` would be routed as `/post/pattern-matching-in-elixir`.
+13
rel/env.sh.eex
··· 1 + #!/bin/sh 2 + 3 + # configure node for distributed erlang with IPV6 support 4 + export ERL_AFLAGS="-proto_dist inet6_tcp" 5 + export ECTO_IPV6="true" 6 + export DNS_CLUSTER_QUERY="${FLY_APP_NAME}.internal" 7 + export RELEASE_DISTRIBUTION="name" 8 + export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}" 9 + 10 + # Uncomment to send crash dumps to stderr 11 + # This can be useful for debugging, but may log sensitive information 12 + # export ERL_CRASH_DUMP=/dev/stderr 13 + # export ERL_CRASH_DUMP_BYTES=4096
+5
rel/overlays/bin/migrate
··· 1 + #!/bin/sh 2 + set -eu 3 + 4 + cd -P -- "$(dirname -- "$0")" 5 + exec ./blog eval Blog.Release.migrate
+1
rel/overlays/bin/migrate.bat
··· 1 + call "%~dp0\blog" eval Blog.Release.migrate
+5
rel/overlays/bin/server
··· 1 + #!/bin/sh 2 + set -eu 3 + 4 + cd -P -- "$(dirname -- "$0")" 5 + PHX_SERVER=true exec ./blog start
+2
rel/overlays/bin/server.bat
··· 1 + set PHX_SERVER=true 2 + call "%~dp0\blog" start