Skip to content

Commit

Permalink
feat: blog release (#2)
Browse files Browse the repository at this point in the history
## New Features

- Implemented blog functionality:
  - Added blog page HTML (843d2b4)
  - Created post page HTML (707da17)
  - Developed an action for parsing and displaying posts (6450161)
  - Implemented list posts feature (1de0b9a)
  - Added a route for `/blog/:slug` (02e1443)

- Enhanced GitHub integration:
  - Added cache for GitHub API calls (794a972)
  - Implemented logic to fetch posts from GitHub (3a572bd)

- Improved blog actions and cache logic (a38f5a6)

## Bug Fixes

- Added ability to use Phoenix links (81576a0)
- Updated robots.txt to disallow certain routes (8aff117)

## Maintenance

- Updated packages and added new ones for blog functionality (f9f4c98)
  • Loading branch information
RemoteRabbit authored Nov 5, 2024
1 parent 2902184 commit 697e9e1
Show file tree
Hide file tree
Showing 11 changed files with 196 additions and 61 deletions.
21 changes: 7 additions & 14 deletions lib/portfolio/application.ex
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
defmodule Portfolio.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false

use Application

@impl true
def start(_type, _args) do
children = [
# Start the Telemetry supervisor
PortfolioWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Portfolio.PubSub},
# Start Finch
{Finch, name: Portfolio.Finch},
# Start the Endpoint (http/https)
PortfolioWeb.Endpoint
# Start a worker by calling: Portfolio.Worker.start_link(arg)
# {Portfolio.Worker, arg}
PortfolioWeb.Endpoint,
{ConCache, [
name: :blog_cache,
ttl_check_interval: :timer.seconds(1),
global_ttl: :timer.hours(1)
]}
]

# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Portfolio.Supervisor]
Supervisor.start_link(children, opts)
end

# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
@impl true
def config_change(changed, _new, removed) do
PortfolioWeb.Endpoint.config_change(changed, removed)
:ok
end
end

24 changes: 24 additions & 0 deletions lib/portfolio/blog.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Portfolio.Blog do
@cache_ttl :timer.hours(1)

def list_posts do
case ConCache.get_or_store(:blog_cache, :posts, fn ->
posts = Portfolio.Blog.Post.fetch_posts("remoterabbit", "blog-posts")
%{value: posts, ttl: @cache_ttl}
end) do
%{value: posts} -> posts
posts -> posts
end
end

def get_post_by_slug(slug) do
list_posts()
|> Enum.find(&(&1.slug == slug))
end

def refresh_cache do
ConCache.delete(:blog_cache, :posts)
list_posts()
end
end

44 changes: 44 additions & 0 deletions lib/portfolio/blog/post.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule Portfolio.Blog.Post do
defstruct [:title, :content, :date, :slug, :description, :status]

def fetch_posts(owner, repo, path \\ "posts/") do
case Tentacat.Contents.find(owner, repo, path) do
{200, contents, _} ->
contents
|> Enum.filter(&(&1["type"] == "file" && String.ends_with?(&1["name"], ".md")))
|> Enum.map(fn file ->
{200, content, _} = Tentacat.Contents.find(owner, repo, file["path"])
content["content"]
|> String.replace("\n", "")
|> Base.decode64!()
|> parse_content(file["path"])
end)
|> Enum.filter(& &1.status == "published")
|> Enum.sort_by(& &1.date, {:desc, Date})

{404, _, _} ->
[]

{status, body, _} ->
IO.puts "GitHub API returned status #{status}: #{inspect(body)}"
[]
end
end

defp parse_content(content, path) do
case YamlFrontMatter.parse(content) do
{:ok, metadata, markdown_content} ->
%__MODULE__{
title: metadata["title"],
date: Date.from_iso8601!(metadata["date"]),
description: metadata["description"],
content: Earmark.as_html!(markdown_content),
slug: Path.basename(path, ".md"),
status: metadata["status"] || "published"
}
_ ->
raise "Invalid blog post format"
end
end
end

16 changes: 15 additions & 1 deletion lib/portfolio_web/controllers/page_controller.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
defmodule PortfolioWeb.PageController do
use PortfolioWeb, :controller
alias Portfolio.Blog

def home(conn, _params) do
render(conn, :home)
end

def blog(conn, _params) do
render(conn, :blog)
posts = Blog.list_posts()
render(conn, :blog, posts: posts)
end

def show_posts(conn, %{"slug" => slug}) do
case Blog.get_post_by_slug(slug) do
nil ->
conn
|> put_status(:not_found)
|> render("404.html")

post ->
render(conn, :show_posts, post: post)
end
end

def projects(conn, _params) do
Expand Down
1 change: 1 addition & 0 deletions lib/portfolio_web/controllers/page_html.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule PortfolioWeb.PageHTML do
use PortfolioWeb, :html
import Phoenix.HTML.Link

embed_templates "page_html/*"
end
34 changes: 33 additions & 1 deletion lib/portfolio_web/controllers/page_html/blog.html.heex
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
<h1>as;dlkf</h1>
<% posts = if is_map(@posts) && Map.has_key?(@posts, :value), do: @posts.value, else: @posts %>

<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 class="text-4xl font-bold text-gray-900 mb-8">Blog Posts</h1>

<div class="space-y-8">
<%= for post <- posts do %>
<article class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
<div class="p-6">
<div class="flex items-center justify-between mb-3">
<time class="text-sm text-gray-500">
<%= Calendar.strftime(post.date, "%B %d, %Y") %>
</time>
</div>

<h2 class="text-2xl font-semibold text-gray-900 mb-3">
<%= link post.title, to: ~p"/blog/#{post.slug}", class: "hover:text-blue-600" %>
</h2>

<p class="text-gray-600 mb-4">
<%= post.description %>
</p>

<div class="flex items-center">
<%= link "Read more →", to: ~p"/blog/#{post.slug}",
class: "text-blue-600 hover:text-blue-800 font-medium" %>
</div>
</div>
</article>
<% end %>
</div>
</div>

19 changes: 19 additions & 0 deletions lib/portfolio_web/controllers/page_html/show_posts.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<% post = if is_map(@post) && Map.has_key?(@post, :value), do: @post.value, else: @post %>

<article class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<header class="mb-8">
<time class="text-sm text-gray-500">
<%= Calendar.strftime(post.date, "%B %d, %Y") %>
</time>
<h1 class="text-4xl font-bold text-gray-900 mt-2"><%= post.title %></h1>
</header>

<div class="prose prose-lg max-w-none">
<%= raw(post.content) %>
</div>

<div class="mt-8">
<%= link "← Back to all posts", to: ~p"/blog", class: "text-blue-600 hover:text-blue-800" %>
</div>
</article>

1 change: 1 addition & 0 deletions lib/portfolio_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ defmodule PortfolioWeb.Router do

get "/", PageController, :home
get "/blog", PageController, :blog
get "/blog/:slug", PageController, :show_posts
get "/projects", PageController, :projects
end

Expand Down
20 changes: 6 additions & 14 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,16 @@ defmodule Portfolio.MixProject do
]
end

# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {Portfolio.Application, []},
extra_applications: [:logger, :runtime_tools]
]
end

# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.7.7"},
Expand All @@ -46,19 +39,18 @@ defmodule Portfolio.MixProject do
{:telemetry_poller, "~> 1.0"},
{:gettext, "~> 0.20"},
{:jason, "~> 1.2"},
{:plug_cowboy, "~> 2.5"}
{:plug_cowboy, "~> 2.5"},
{:tentacat, "~> 2.2"},
{:earmark, "~> 1.4"},
{:yaml_front_matter, "~> 1.0"},
{:con_cache, "~> 1.0"}
]
end

# Aliases are shortcuts or tasks specific to the current project.
# For example, to install project dependencies and perform other setup tasks, run:
#
# $ mix setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
setup: ["deps.get", "assets.setup", "assets.build"],
server: ["phx.server"],
"assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
"assets.build": ["tailwind default", "esbuild default"],
"assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
Expand Down
Loading

0 comments on commit 697e9e1

Please sign in to comment.