{"id":17130823,"url":"https://github.com/erikmueller/jelly_shot","last_synced_at":"2025-04-13T07:28:06.897Z","repository":{"id":16914452,"uuid":"80860504","full_name":"erikmueller/jelly_shot","owner":"erikmueller","description":"Elixir blog engine based on Phoenix powered by markdown","archived":false,"fork":false,"pushed_at":"2022-12-09T08:14:24.000Z","size":4523,"stargazers_count":8,"open_issues_count":33,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-04-14T16:01:43.619Z","etag":null,"topics":["agent","blog","elixir","elixir-flow","elixir-phoenix","genserver","markdown"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/erikmueller.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-02-03T19:09:55.000Z","updated_at":"2023-11-14T19:23:08.000Z","dependencies_parsed_at":"2022-08-19T14:31:14.704Z","dependency_job_id":null,"html_url":"https://github.com/erikmueller/jelly_shot","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erikmueller%2Fjelly_shot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erikmueller%2Fjelly_shot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erikmueller%2Fjelly_shot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erikmueller%2Fjelly_shot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/erikmueller","download_url":"https://codeload.github.com/erikmueller/jelly_shot/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248677364,"owners_count":21144092,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","blog","elixir","elixir-flow","elixir-phoenix","genserver","markdown"],"created_at":"2024-10-14T19:13:20.171Z","updated_at":"2025-04-13T07:28:06.842Z","avatar_url":"https://github.com/erikmueller.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"---\ntitle: Jelly Shot\nseparator: \u003c!--s--\u003e\nverticalSeparator: \u003c!--v--\u003e\ntheme: black\nrevealOptions:\n    controls: false\n    transition: 'slide'\n---\n\n![CircleCI](https://circleci.com/gh/erikmueller/jelly_shot/tree/master.svg?style=svg)  \u003c!-- .element style=\"border: none; background: none;\" --\u003e ![codecov](https://codecov.io/gh/erikmueller/jelly_shot/branch/master/graph/badge.svg) \u003c!-- .element style=\"border: none; background: none;\" --\u003e\n\n# Jelly Shot\n\nA semi static blog engine\n\n\u003c!--s--\u003e\n\n## starting point\n\nStatic markdown blog by\n[@seilund](http://www.sebastianseilund.com/static-markdown-blog-posts-with-elixir-phoenix)\n\n\u003c!--s--\u003e\n\n### A million static site generators...\n### 💁 let's write another one \u003c!-- .element: class=\"fragment\" --\u003e\n\n\u003c!--s--\u003e\n\n* Phoenix\n* No database \u003c!-- .element: class=\"fragment\" --\u003e\n* No static file generation \u003c!-- .element: class=\"fragment\" --\u003e\n\n\u003c!--s--\u003e\n\n* Compiling [markdown](https://github.com/pragdave/earmark) into `%Post{}`s\n* Have a [GenServer](https://hexdocs.pm/elixir/GenServer.html) deliver them\n\n\u003c!--s--\u003e\n\n### Let's take this further\n\n* Generator __`-\u003e` markdown `|\u003e` struct__ \u003c!-- .element: class=\"fragment\" --\u003e\n* Repository __`-\u003e` [Agent](https://hexdocs.pm/elixir/Agent.html) storing data__ \u003c!-- .element: class=\"fragment\" --\u003e\n* Watcher __`-\u003e` [GenServer](https://hexdocs.pm/elixir/GenServer.html) for auto update__ \u003c!-- .element: class=\"fragment\" --\u003e\n\n\u003c!--s--\u003e\n\n#### Generator\n\n```elixir\ndefp compile_file(file) do\n  with{:ok, matter, body} \u003c- split_frontmatter(file),\n      {:ok, html, _} \u003c- Earmark.as_html(body),\n  do: {:ok, into_post(file, matter, html)}\nend\n```\n\n```elixir\ndefp into_post(file, meta, html) do\n  data = %{\n    slug: file_to_slug(file),\n    content: html,\n  } |\u003e Map.merge(meta)\n\n  struct(JellyShot.Post, data)\nend\n```\n\n\u003c!--s--\u003e\n\n#### Repository\n\n```elixir\ndef start_link do\n  Agent.start_link(\u0026get_initial_state/0, name: __MODULE__)\nend\n```\n\n```elixir\nposts = File.ls!(\"priv/posts\")\n|\u003e Enum.filter(\u0026(Path.extname(\u00261) == \".md\"))\n|\u003e Enum.map(\u0026compile_async/1)\n|\u003e Enum.map(\u0026Task.await/1)\n|\u003e Enum.reduce([], \u0026valid_into_list/2)\n|\u003e Enum.sort(\u0026sort/2)\n```\n\n\u003c!--s--\u003e\n\n#### Watcher\n\n```elixir\ndef init(state) do\n  path = Path.expand(\"priv/posts\")\n\n  :fs.start_link(:fs_watcher, path)\n  :fs.subscribe(:fs_watcher)\n\n  {:ok, state}\nend\n```\n\n```elixir\ndef handle_info({_pid, {:fs, :file_event}, {path, ev}}, _) do\n  new_state = cond do\n    Enum.member?(ev, :modified) -\u003e\n      path\n      |\u003e JellyShot.Post.file_to_slug\n      |\u003e JellyShot.Repo.upsert_by_slug\n  end\nend\n```\n\n\u003c!--s--\u003e\n\n### Integrating into Phoenix 🐣🔥\n\n\u003c!--s--\u003e\n\n#### Listing posts\n\n```elixir\ndef index(conn, params) do\n  {tmpl, headline, {:ok, posts}} = case params do\n    %{\"author\" =\u003e author} -\u003e\n      {\"list\", \"by author\",  Repo.get_by_author(author)}\n    %{\"category\" =\u003e category} -\u003e\n      {\"list\", \"by category\", Repo.get_by_category(category)}\n    _ -\u003e\n      {\"index\", \"recent posts\", Repo.list()}\n  end\n\n  render conn, \"#{tmpl}.html\", head: head, posts: posts\nend\n```\n\n\u003c!--s--\u003e\n\n![JellySHot](./jelly_shot.png)\n\n\u003c!--s--\u003e\n\n## Limitations\n\n\u003c!--s--\u003e\n\n### Filling the repository...\n\n~ 250 sloc / file\n\n* 12 posts in 406ms 🐰 \u003c!-- .element: class=\"fragment\" --\u003e\n* 384 posts in 3844ms 🐢 \u003c!-- .element: class=\"fragment\" --\u003e\n* ... 🐌 \u003c!-- .element: class=\"fragment\" --\u003e\n\nWe might hit a cap at some point \u003c!-- .element: class=\"fragment\" --\u003e\n\u003c!--s--\u003e\n\n### Anyway, I Learned a lot\n\n* __Pattern matching__ \u003c!-- .element: class=\"fragment\" --\u003e\n* `Agents` \u003c!-- .element: class=\"fragment\" --\u003e\n* `GenServer` \u003c!-- .element: class=\"fragment\" --\u003e\n* `with {:ok}` \u003c!-- .element: class=\"fragment\" --\u003e\n\n\u003c!--s--\u003e\n\n# Thanks\n\n[https://github.com/erikmueller/jelly_shot](https://github.com/erikmueller/jelly_shot)\n\n\u003cstyle\u003e\n  .reveal code {font-family: hasklig, monospace}\n\u003c/style\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferikmueller%2Fjelly_shot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ferikmueller%2Fjelly_shot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferikmueller%2Fjelly_shot/lists"}