big updates to content
This commit is contained in:
parent
1c17c6f508
commit
464f3a0ae3
26 changed files with 778 additions and 80 deletions
CLAUDE.md
config
lib
core.ex
mix.exsmix.lockcore
web
components
controllers
blog_controller.exblog_html.ex
markdown.exrouter.exblog_html
page_controller.expage_html
post_controller.expost_html
status_controller.exstatus_html.exstatus_html
prompts
|
@ -17,4 +17,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
- Testing: Descriptive test names, fixtures, shared setup, tagged tests
|
||||
- Imports: Organize at top, import frequently used functions, follow boundary pattern
|
||||
- Documentation: `@doc` for public functions, `@moduledoc false` for internal modules
|
||||
- Use `@impl` for callback implementations
|
||||
- Use `@impl` for callback implementations
|
||||
- Styling: Prefer CSS/tailwind based styling using things like group, first:, focus:, hover:, etc. to any sort of dynamic classes using Elixir or JavaScript
|
||||
- All data should be microformat2 compatible
|
||||
- Use lists of classes instead of string interpolation
|
|
@ -30,7 +30,13 @@ config :sloanely_but_surely, Web.Endpoint,
|
|||
config :sloanely_but_surely,
|
||||
namespace: Core,
|
||||
ecto_repos: [Core.Repo],
|
||||
generators: [timestamp_type: :utc_datetime_usec, binary_id: true]
|
||||
generators: [timestamp_type: :utc_datetime_usec, binary_id: true],
|
||||
author: %{
|
||||
name: "Sloane",
|
||||
username: "sloanelybutsurely",
|
||||
avatar_emoji: "💜",
|
||||
avatar_url: nil
|
||||
}
|
||||
|
||||
config :sloanely_but_surely, Core.Repo, migration_timestamps: [type: :utc_datetime_usec]
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Core do
|
||||
@moduledoc false
|
||||
use Boundary, deps: [Schema], exports: [Accounts, Posts]
|
||||
use Boundary, deps: [Schema], exports: [Accounts, Posts, Author]
|
||||
end
|
||||
|
|
19
lib/core/author.ex
Normal file
19
lib/core/author.ex
Normal file
|
@ -0,0 +1,19 @@
|
|||
defmodule Core.Author do
|
||||
@moduledoc false
|
||||
|
||||
@author_config Application.compile_env!(:sloanely_but_surely, :author)
|
||||
|
||||
@doc """
|
||||
Returns the author configuration.
|
||||
"""
|
||||
def get do
|
||||
@author_config
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a specific attribute from the author configuration.
|
||||
"""
|
||||
def get(attribute) when is_atom(attribute) do
|
||||
Map.get(@author_config, attribute)
|
||||
end
|
||||
end
|
|
@ -193,4 +193,22 @@ defmodule Web.CoreComponents do
|
|||
|
||||
defp normalize_posts(posts) when is_list(posts),
|
||||
do: Enum.with_index(posts, &{"#{&1.kind}-#{&2}", &1})
|
||||
|
||||
@doc """
|
||||
Renders markdown content as HTML.
|
||||
|
||||
## Examples
|
||||
|
||||
<.markdown content={@post.body} />
|
||||
"""
|
||||
attr :content, :string, required: true
|
||||
attr :class, :string, default: ""
|
||||
|
||||
def markdown(assigns) do
|
||||
~H"""
|
||||
<div class={["markdown-content", @class]}>
|
||||
{Phoenix.HTML.raw(Web.Markdown.to_html(@content))}
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
<div class="sticky top-0 z-50 flex flex-row bg-white justify-between py-1 md:mb-2 border-b border-gray-200">
|
||||
<div class="sticky top-0 z-50 flex flex-row bg-white justify-between py-1 border-b border-gray-200">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<section class="flex flex-row gap-x-2 md:border-r border-gray-200 px-2">
|
||||
<section class="flex flex-row gap-x-2 px-2">
|
||||
<.link navigate={~p"/"} class="font-bold group">
|
||||
💜 <span class="group-hover:underline">sloanelybutsurely.com</span>
|
||||
</.link>
|
||||
<nav>
|
||||
<ul class="flex flex-row gap-x-2">
|
||||
<li><.link href={~p"/blog"}>writing</.link></li>
|
||||
<li><.link href={~p"/microblog"}>microblog</.link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</section>
|
||||
<section :if={not is_nil(@current_user)} class="ml-2">
|
||||
<nav>
|
||||
|
@ -29,6 +23,6 @@
|
|||
<% end %>
|
||||
</div>
|
||||
|
||||
<main class="p-2 max-w-2xl mx-auto">
|
||||
<main class="mx-auto">
|
||||
{@inner_content}
|
||||
</main>
|
||||
|
|
29
lib/web/controllers/blog_controller.ex
Normal file
29
lib/web/controllers/blog_controller.ex
Normal file
|
@ -0,0 +1,29 @@
|
|||
defmodule Web.BlogController do
|
||||
use Web, :controller
|
||||
|
||||
def index(conn, _params) do
|
||||
blogs = Core.Posts.get_published_recent_posts(:blog)
|
||||
|
||||
blogs_by_year =
|
||||
blogs
|
||||
|> Enum.group_by(fn blog ->
|
||||
date = Core.Posts.publish_date(blog)
|
||||
date && date.year
|
||||
end)
|
||||
|> Enum.sort_by(fn {year, _} -> year end, :desc)
|
||||
|
||||
render(conn, :index, blogs_by_year: blogs_by_year)
|
||||
end
|
||||
|
||||
def show(conn, %{"year" => year, "month" => month, "day" => day, "slug" => slug}) do
|
||||
with {year, ""} <- Integer.parse(year),
|
||||
{month, ""} <- Integer.parse(month),
|
||||
{day, ""} <- Integer.parse(day),
|
||||
{:ok, publish_date} <- Date.new(year, month, day) do
|
||||
blog = Core.Posts.get_published_blog!(publish_date, slug)
|
||||
render(conn, :show, blog: blog)
|
||||
else
|
||||
_ -> raise Ecto.NoResultsError
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Web.PostHTML do
|
||||
defmodule Web.BlogHTML do
|
||||
use Web, :html
|
||||
|
||||
embed_templates "post_html/*"
|
||||
embed_templates "blog_html/*"
|
||||
|
||||
def blog_path(%Schema.Post{} = blog) do
|
||||
if date = Core.Posts.publish_date(blog) do
|
30
lib/web/controllers/blog_html/index.html.heex
Normal file
30
lib/web/controllers/blog_html/index.html.heex
Normal file
|
@ -0,0 +1,30 @@
|
|||
<div class="max-w-2xl mx-auto">
|
||||
<div class="p-4">
|
||||
<h1 class="text-2xl font-bold mb-6 mt-2">Writing</h1>
|
||||
|
||||
<div class="h-feed">
|
||||
<%= for {year, blogs} <- @blogs_by_year do %>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold mb-4">{year}</h2>
|
||||
<ul>
|
||||
<%= for blog <- blogs do %>
|
||||
<li>
|
||||
<article class="h-entry">
|
||||
<.link
|
||||
navigate={blog_path(blog)}
|
||||
class="flex justify-between items-center hover:bg-gray-50 -mx-2 px-2 py-1 rounded"
|
||||
>
|
||||
<h3 class="p-name u-url">{blog.title}</h3>
|
||||
<div class="text-gray-500 dt-published">
|
||||
<.timex value={blog.published_at} format="{Mfull} {D}" />
|
||||
</div>
|
||||
</.link>
|
||||
</article>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
18
lib/web/controllers/blog_html/show.html.heex
Normal file
18
lib/web/controllers/blog_html/show.html.heex
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div class="p-4 max-w-2xl mx-auto">
|
||||
<article class="h-entry mt-2">
|
||||
<header class="mb-4">
|
||||
<h1 class="p-name text-2xl font-bold mb-2">{@blog.title}</h1>
|
||||
<%= if @blog.published_at do %>
|
||||
<div class="text-sm text-gray-500 flex items-center">
|
||||
<span class="p-author h-card">{Core.Author.get(:name)}</span>
|
||||
<span class="mx-2">·</span>
|
||||
<.timex value={@blog.published_at} format="{Mfull} {D}, {YYYY}" class="dt-published" />
|
||||
</div>
|
||||
<% end %>
|
||||
</header>
|
||||
|
||||
<div class="e-content">
|
||||
<.markdown content={@blog.body} />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
|
@ -2,6 +2,12 @@ defmodule Web.PageController do
|
|||
use Web, :controller
|
||||
|
||||
def home(conn, _params) do
|
||||
render(conn, :home)
|
||||
# Get recent blog posts
|
||||
recent_blogs = Core.Posts.get_published_recent_posts(:blog)
|
||||
|
||||
# Get recent statuses
|
||||
recent_statuses = Core.Posts.get_published_recent_posts(:status)
|
||||
|
||||
render(conn, :home, recent_blogs: recent_blogs, recent_statuses: recent_statuses)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1 +1,94 @@
|
|||
<h1>Home</h1>
|
||||
<div class="max-w-2xl mx-auto mt-2">
|
||||
<!-- Recent Blog Posts -->
|
||||
<section class="mb-8 p-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">Writing</h2>
|
||||
<.link navigate={~p"/blog"} class="text-sm text-gray-500 hover:text-gray-800">
|
||||
See all →
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<%= for blog <- @recent_blogs do %>
|
||||
<li>
|
||||
<article class="h-entry">
|
||||
<.link
|
||||
navigate={Web.BlogHTML.blog_path(blog)}
|
||||
class="flex justify-between items-center hover:bg-gray-50 -mx-2 px-2 py-1 rounded"
|
||||
>
|
||||
<h3 class="p-name u-url">{blog.title}</h3>
|
||||
<div class="text-gray-500 dt-published">
|
||||
<.timex value={blog.published_at} format="{Mfull} {D}" />
|
||||
</div>
|
||||
</.link>
|
||||
</article>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Recent Statuses -->
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<section>
|
||||
<div class="flex justify-between items-center mb-4 py-2 px-4">
|
||||
<h2 class="text-xl font-bold">Microblog</h2>
|
||||
<.link navigate={~p"/microblog"} class="text-sm text-gray-500 hover:text-gray-800">
|
||||
See all →
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-200">
|
||||
<%= for status <- @recent_statuses do %>
|
||||
<.link
|
||||
navigate={~p"/status/#{status.id}"}
|
||||
class="block border-t first:border-t-0 border-gray-200 hover:bg-gray-50 u-url"
|
||||
>
|
||||
<article class="h-entry py-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 pl-3 pr-2">
|
||||
<%= if Core.Author.get(:avatar_url) do %>
|
||||
<img
|
||||
src={Core.Author.get(:avatar_url)}
|
||||
alt={Core.Author.get(:name)}
|
||||
class="w-10 h-10 rounded-full u-photo"
|
||||
/>
|
||||
<% else %>
|
||||
<div class="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center text-xl u-photo">
|
||||
{Core.Author.get(:avatar_emoji)}
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow min-w-0 pl-1 pr-3">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="p-name font-bold">
|
||||
{Core.Author.get(:name)}
|
||||
</p>
|
||||
<p class="ml-1 inline">
|
||||
<span class="p-nickname font-normal text-gray-600">
|
||||
@{Core.Author.get(:username)}
|
||||
</span>
|
||||
</p>
|
||||
<span class="mx-2 text-sm text-gray-500">·</span>
|
||||
<div class="text-sm text-gray-500">
|
||||
<.timex
|
||||
value={status.published_at}
|
||||
format="{relative}"
|
||||
formatter={:relative}
|
||||
class="dt-published"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="e-content">
|
||||
<.markdown content={status.body} class="text-gray-900" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</.link>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
defmodule Web.PostController do
|
||||
use Web, :controller
|
||||
|
||||
def show(conn, %{"year" => year, "month" => month, "day" => day, "slug" => slug}) do
|
||||
with {year, ""} <- Integer.parse(year),
|
||||
{month, ""} <- Integer.parse(month),
|
||||
{day, ""} <- Integer.parse(day),
|
||||
{:ok, publish_date} <- Date.new(year, month, day) do
|
||||
post = Core.Posts.get_published_blog!(publish_date, slug)
|
||||
|
||||
conn
|
||||
|> render_post(post)
|
||||
else
|
||||
_ -> raise Ecto.NoResultsError
|
||||
end
|
||||
end
|
||||
|
||||
def show(conn, %{"status_id" => status_id}) do
|
||||
status = Core.Posts.get_published_status!(status_id)
|
||||
|
||||
conn
|
||||
|> render_post(status)
|
||||
end
|
||||
|
||||
def index(%{assigns: %{kind: kind}} = conn, _params) when kind in ~w[blog status]a do
|
||||
posts = Core.Posts.get_published_recent_posts(kind)
|
||||
|
||||
render(conn, :index, posts: posts)
|
||||
end
|
||||
|
||||
defp render_post(conn, %Schema.Post{kind: :blog} = blog) do
|
||||
render(conn, :show_blog, blog: blog)
|
||||
end
|
||||
|
||||
defp render_post(conn, %Schema.Post{kind: :status} = status) do
|
||||
render(conn, :show_status, status: status)
|
||||
end
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
<.post_list :let={post} id="recent-posts" posts={@posts}>
|
||||
<%= case post.kind do %>
|
||||
<% :blog -> %>
|
||||
<.link navigate={blog_path(post)}>{post.title}</.link>
|
||||
<% :status -> %>
|
||||
{post.body}
|
||||
<% end %>
|
||||
</.post_list>
|
|
@ -1,9 +0,0 @@
|
|||
<article>
|
||||
<header class="mb-2">
|
||||
<h1 class="text-2xl font-bold">{@blog.title}</h1>
|
||||
</header>
|
||||
|
||||
<p>{@blog.body}</p>
|
||||
|
||||
<%!-- <footer class="mt-4 border-t border-gray-200"></footer> --%>
|
||||
</article>
|
|
@ -1,5 +0,0 @@
|
|||
<article>
|
||||
<p>{@status.body}</p>
|
||||
|
||||
<%!-- <footer class="mt-4 border-t border-gray-200"></footer> --%>
|
||||
</article>
|
13
lib/web/controllers/status_controller.ex
Normal file
13
lib/web/controllers/status_controller.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Web.StatusController do
|
||||
use Web, :controller
|
||||
|
||||
def index(conn, _params) do
|
||||
statuses = Core.Posts.get_published_recent_posts(:status)
|
||||
render(conn, :index, statuses: statuses)
|
||||
end
|
||||
|
||||
def show(conn, %{"status_id" => status_id}) do
|
||||
status = Core.Posts.get_published_status!(status_id)
|
||||
render(conn, :show, status: status)
|
||||
end
|
||||
end
|
69
lib/web/controllers/status_html.ex
Normal file
69
lib/web/controllers/status_html.ex
Normal file
|
@ -0,0 +1,69 @@
|
|||
defmodule Web.StatusHTML do
|
||||
use Web, :html
|
||||
|
||||
embed_templates "status_html/*"
|
||||
|
||||
attr :status, :map, required: true
|
||||
|
||||
attr :class, :string, default: nil
|
||||
attr :rest, :global
|
||||
|
||||
def status_entry(assigns) do
|
||||
~H"""
|
||||
<.link
|
||||
navigate={~p"/status/#{@status.id}"}
|
||||
class="block border-x border-gray-200 border-t hover:bg-gray-50 u-url"
|
||||
>
|
||||
<article
|
||||
class={[
|
||||
"h-entry py-4",
|
||||
@class
|
||||
]}
|
||||
{@rest}
|
||||
>
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 pl-3 pr-2">
|
||||
<%= if Core.Author.get(:avatar_url) do %>
|
||||
<img
|
||||
src={Core.Author.get(:avatar_url)}
|
||||
alt={Core.Author.get(:name)}
|
||||
class="w-10 h-10 rounded-full u-photo"
|
||||
/>
|
||||
<% else %>
|
||||
<div class="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center text-xl u-photo">
|
||||
{Core.Author.get(:avatar_emoji)}
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow min-w-0 pl-1 pr-3">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="p-name font-bold">
|
||||
{Core.Author.get(:name)}
|
||||
</p>
|
||||
<p class="ml-1 inline">
|
||||
<span class="p-nickname font-normal text-gray-600">
|
||||
@{Core.Author.get(:username)}
|
||||
</span>
|
||||
</p>
|
||||
<span class="mx-2 text-sm text-gray-500">·</span>
|
||||
<div class="text-sm text-gray-500">
|
||||
<.timex
|
||||
value={@status.published_at}
|
||||
format="{relative}"
|
||||
formatter={:relative}
|
||||
class="dt-published"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="e-content">
|
||||
<.markdown content={@status.body} class="text-gray-900" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</.link>
|
||||
"""
|
||||
end
|
||||
end
|
11
lib/web/controllers/status_html/index.html.heex
Normal file
11
lib/web/controllers/status_html/index.html.heex
Normal file
|
@ -0,0 +1,11 @@
|
|||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex items-center py-2 px-4 border-x border-gray-200">
|
||||
<h1 class="text-lg font-bold">Microblog</h1>
|
||||
</div>
|
||||
|
||||
<div class="h-feed border-b border-gray-200">
|
||||
<%= for status <- @statuses do %>
|
||||
<.status_entry status={status} />
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
53
lib/web/controllers/status_html/show.html.heex
Normal file
53
lib/web/controllers/status_html/show.html.heex
Normal file
|
@ -0,0 +1,53 @@
|
|||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex items-center py-2 px-4 border-x border-gray-200">
|
||||
<.link
|
||||
navigate={~p"/microblog"}
|
||||
class="mr-4 text-gray-500 hover:text-gray-800 flex items-center"
|
||||
>
|
||||
<.icon name="hero-arrow-left" class="h-5 w-5" />
|
||||
</.link>
|
||||
<h1 class="text-lg font-bold">Post</h1>
|
||||
</div>
|
||||
|
||||
<article class="h-entry p-4 border border-gray-200">
|
||||
<header class="flex items-center mb-3">
|
||||
<div class="h-card flex items-center gap-3 p-author">
|
||||
<%= if Core.Author.get(:avatar_url) do %>
|
||||
<img
|
||||
src={Core.Author.get(:avatar_url)}
|
||||
alt={Core.Author.get(:name)}
|
||||
class="w-10 h-10 rounded-full u-photo"
|
||||
/>
|
||||
<% else %>
|
||||
<div class="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center text-xl u-photo">
|
||||
{Core.Author.get(:avatar_emoji)}
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex flex-col">
|
||||
<p class="p-name font-bold">
|
||||
{Core.Author.get(:name)}
|
||||
</p>
|
||||
<p>
|
||||
<span class="p-nickname font-normal text-gray-600">
|
||||
@{Core.Author.get(:username)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="e-content my-4">
|
||||
<.markdown content={@status.body} />
|
||||
</div>
|
||||
|
||||
<footer class="mt-4 border-t border-gray-200 pt-2 text-sm text-gray-500">
|
||||
<%= if @status.published_at do %>
|
||||
<.timex
|
||||
value={@status.published_at}
|
||||
format="{Mfull} {D}, {YYYY} at {h12}:{m} {AM}"
|
||||
class="dt-published"
|
||||
/>
|
||||
<% end %>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
323
lib/web/markdown.ex
Normal file
323
lib/web/markdown.ex
Normal file
|
@ -0,0 +1,323 @@
|
|||
defmodule Web.Markdown do
|
||||
@moduledoc """
|
||||
Converts markdown to HTML using MDEx, with custom rendering to add Tailwind CSS classes.
|
||||
Uses iodata for efficient string generation.
|
||||
"""
|
||||
|
||||
alias MDEx.Document
|
||||
|
||||
@doc """
|
||||
Converts markdown to HTML with syntax highlighting, additional extensions,
|
||||
and appropriate Tailwind CSS classes for styling.
|
||||
"""
|
||||
def to_html(markdown) when is_binary(markdown) do
|
||||
# First parse the document to get the AST
|
||||
{:ok, document} =
|
||||
MDEx.parse_document(markdown,
|
||||
extension: [
|
||||
strikethrough: true,
|
||||
table: true,
|
||||
autolink: true,
|
||||
tasklist: true,
|
||||
footnotes: true
|
||||
],
|
||||
features: [syntax_highlight_theme: "catppuccin_latte"]
|
||||
)
|
||||
|
||||
# Render the document to HTML with Tailwind classes
|
||||
render_document(document)
|
||||
end
|
||||
|
||||
def to_html(nil), do: ""
|
||||
|
||||
# Core rendering function for the document
|
||||
defp render_document(%Document{} = document) do
|
||||
document.nodes
|
||||
|> Enum.map(&render_node(&1, nil))
|
||||
end
|
||||
|
||||
# Heading (h1-h6) with anchor links
|
||||
defp render_node(%MDEx.Heading{level: level} = node, _parent) do
|
||||
tag = "h#{level}"
|
||||
|
||||
class =
|
||||
case level do
|
||||
1 -> "text-3xl font-bold mt-6 mb-4 group"
|
||||
2 -> "text-2xl font-bold mt-5 mb-3 group"
|
||||
3 -> "text-xl font-bold mt-4 mb-2 group"
|
||||
4 -> "text-lg font-bold mt-3 mb-2 group"
|
||||
5 -> "text-base font-bold mt-3 mb-2 group"
|
||||
6 -> "text-sm font-bold mt-3 mb-2 group"
|
||||
end
|
||||
|
||||
# Generate a slug from the text content for the ID
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
content_text = extract_text_content(node)
|
||||
slug = Slug.slugify(content_text)
|
||||
|
||||
[
|
||||
"<",
|
||||
tag,
|
||||
" id=\"",
|
||||
slug,
|
||||
"\" class=\"",
|
||||
class,
|
||||
" relative\">",
|
||||
"<a href=\"#",
|
||||
slug,
|
||||
"\" class=\"absolute -left-5 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity\">¶</a>",
|
||||
content,
|
||||
"</",
|
||||
tag,
|
||||
">"
|
||||
]
|
||||
end
|
||||
|
||||
# Paragraph
|
||||
defp render_node(%MDEx.Paragraph{} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
["<p>", content, "</p>"]
|
||||
end
|
||||
|
||||
# Text
|
||||
defp render_node(%MDEx.Text{literal: literal}, _parent) do
|
||||
literal
|
||||
end
|
||||
|
||||
# Strong (bold)
|
||||
defp render_node(%MDEx.Strong{} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
["<strong>", content, "</strong>"]
|
||||
end
|
||||
|
||||
# Emphasis (italic)
|
||||
defp render_node(%MDEx.Emph{} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
["<em>", content, "</em>"]
|
||||
end
|
||||
|
||||
# Strikethrough
|
||||
defp render_node(%MDEx.Strikethrough{} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
["<del>", content, "</del>"]
|
||||
end
|
||||
|
||||
# Link
|
||||
defp render_node(%MDEx.Link{url: url, title: title} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
title_attr = if title, do: [" title=\"", title, "\""], else: []
|
||||
|
||||
[
|
||||
"<a href=\"",
|
||||
url,
|
||||
"\"",
|
||||
title_attr,
|
||||
" class=\"text-blue-600 hover:text-blue-800 hover:underline\">",
|
||||
content,
|
||||
"</a>"
|
||||
]
|
||||
end
|
||||
|
||||
# Image
|
||||
defp render_node(%MDEx.Image{url: url, title: title} = node, _parent) do
|
||||
alt = Enum.map(node.nodes, &render_node(&1, node))
|
||||
title_attr = if title, do: [" title=\"", title, "\""], else: []
|
||||
|
||||
[
|
||||
"<img src=\"",
|
||||
url,
|
||||
"\" alt=\"",
|
||||
alt,
|
||||
"\"",
|
||||
title_attr,
|
||||
" class=\"max-w-full my-4 rounded\">"
|
||||
]
|
||||
end
|
||||
|
||||
# Code (inline)
|
||||
defp render_node(%MDEx.Code{literal: literal}, _parent) do
|
||||
["<code class=\"bg-gray-100 rounded px-1 py-0.5 text-sm\">", literal, "</code>"]
|
||||
end
|
||||
|
||||
# Code blocks - use MDEx's built-in HTML rendering to preserve syntax highlighting
|
||||
defp render_node(%MDEx.CodeBlock{} = node, _parent) do
|
||||
# Use MDEx's built-in HTML rendering for code blocks to preserve syntax highlighting
|
||||
{:ok, html} =
|
||||
MDEx.to_html(
|
||||
%MDEx.Document{nodes: [node]},
|
||||
extension: [],
|
||||
features: [syntax_highlight_theme: "catppuccin_latte"]
|
||||
)
|
||||
|
||||
# Return the HTML as is since it's already properly formatted with syntax highlighting
|
||||
String.replace(html, ~s|class="autumn-hl"|, ~s|class="autumn-hl p-2 rounded"|)
|
||||
end
|
||||
|
||||
# Lists - Bullet
|
||||
defp render_node(%MDEx.List{list_type: :bullet} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
["<ul class=\"list-disc pl-6 my-4\">", content, "</ul>"]
|
||||
end
|
||||
|
||||
# Lists - Ordered
|
||||
defp render_node(%MDEx.List{list_type: :ordered} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
["<ol class=\"list-decimal pl-6 my-4\">", content, "</ol>"]
|
||||
end
|
||||
|
||||
# List items
|
||||
defp render_node(%MDEx.ListItem{} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
["<li class=\"mb-1\">", content, "</li>"]
|
||||
end
|
||||
|
||||
# Task items (checkboxes)
|
||||
defp render_node(%MDEx.TaskItem{checked: checked} = node, _parent) do
|
||||
checkbox =
|
||||
if checked do
|
||||
"<input type=\"checkbox\" checked disabled class=\"rounded mr-1.5 text-blue-600\">"
|
||||
else
|
||||
"<input type=\"checkbox\" disabled class=\"rounded mr-1.5 text-blue-600\">"
|
||||
end
|
||||
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
|
||||
[
|
||||
"<li class=\"mb-1 flex items-start\">",
|
||||
checkbox,
|
||||
"<div class=\"inline\">",
|
||||
content,
|
||||
"</div>",
|
||||
"</li>"
|
||||
]
|
||||
end
|
||||
|
||||
# Blockquote
|
||||
defp render_node(%MDEx.BlockQuote{} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
|
||||
[
|
||||
"<blockquote class=\"border-l-4 border-gray-300 pl-4 py-1 my-4 italic\">",
|
||||
content,
|
||||
"</blockquote>"
|
||||
]
|
||||
end
|
||||
|
||||
# Horizontal Rule
|
||||
defp render_node(%MDEx.ThematicBreak{}, _parent) do
|
||||
"<hr class=\"border-t border-gray-300 my-6\">"
|
||||
end
|
||||
|
||||
# Table
|
||||
defp render_node(%MDEx.Table{} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
|
||||
[
|
||||
"<div class=\"overflow-x-auto my-4\"><table class=\"min-w-full divide-y divide-gray-200\">",
|
||||
content,
|
||||
"</table></div>"
|
||||
]
|
||||
end
|
||||
|
||||
# Table row
|
||||
defp render_node(%MDEx.TableRow{header: true} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
["<tr class=\"bg-gray-50 border-b border-gray-200\">", content, "</tr>"]
|
||||
end
|
||||
|
||||
defp render_node(%MDEx.TableRow{header: false} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
["<tr class=\"border-b border-gray-200\">", content, "</tr>"]
|
||||
end
|
||||
|
||||
# Table cell in header row
|
||||
defp render_node(%MDEx.TableCell{} = node, %MDEx.TableRow{header: true}) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
|
||||
[
|
||||
"<th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">",
|
||||
content,
|
||||
"</th>"
|
||||
]
|
||||
end
|
||||
|
||||
# Table cell in normal row
|
||||
defp render_node(%MDEx.TableCell{} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
["<td class=\"px-6 py-4 whitespace-nowrap text-sm\">", content, "</td>"]
|
||||
end
|
||||
|
||||
# HTML blocks (pass through)
|
||||
defp render_node(%MDEx.HtmlBlock{literal: literal}, _parent) do
|
||||
literal
|
||||
end
|
||||
|
||||
# HTML inline (pass through)
|
||||
defp render_node(%MDEx.HtmlInline{literal: literal}, _parent) do
|
||||
literal
|
||||
end
|
||||
|
||||
# Soft break (line break that doesn't create new paragraph)
|
||||
defp render_node(%MDEx.SoftBreak{}, _parent) do
|
||||
"\n"
|
||||
end
|
||||
|
||||
# Hard break (explicit line break with <br>)
|
||||
defp render_node(%MDEx.LineBreak{}, _parent) do
|
||||
"<br>"
|
||||
end
|
||||
|
||||
# Footnote reference
|
||||
defp render_node(%MDEx.FootnoteReference{name: name}, _parent) do
|
||||
[
|
||||
"<sup class=\"footnote-ref\">",
|
||||
"<a href=\"#fn-",
|
||||
name,
|
||||
"\" id=\"fnref-",
|
||||
name,
|
||||
"\" class=\"text-blue-600 hover:text-blue-800\">",
|
||||
name,
|
||||
"</a>",
|
||||
"</sup>"
|
||||
]
|
||||
end
|
||||
|
||||
# Footnote definition
|
||||
defp render_node(%MDEx.FootnoteDefinition{name: name} = node, _parent) do
|
||||
content = Enum.map(node.nodes, &render_node(&1, node))
|
||||
|
||||
[
|
||||
"<div class=\"footnote flex flex-row\" id=\"fn-",
|
||||
name,
|
||||
"\">",
|
||||
"<p class=\"my-1 flex\">",
|
||||
"<span class=\"mr-2 flex-shrink-0\"><sup>",
|
||||
name,
|
||||
".</sup></span>",
|
||||
"<span class=\"inline\">",
|
||||
content,
|
||||
" <a href=\"#fnref-",
|
||||
name,
|
||||
"\" class=\"text-blue-600 hover:text-blue-800 text-sm\">↩</a>",
|
||||
"</span>",
|
||||
"</p>",
|
||||
"</div>"
|
||||
]
|
||||
end
|
||||
|
||||
# Extract plain text from a node tree (for generating slugs)
|
||||
defp extract_text_content(node) do
|
||||
cond do
|
||||
is_map(node) && Map.has_key?(node, :literal) && is_binary(node.literal) ->
|
||||
node.literal
|
||||
|
||||
is_map(node) && Map.has_key?(node, :nodes) ->
|
||||
node.nodes
|
||||
|> Enum.map(&extract_text_content/1)
|
||||
|> Enum.join("")
|
||||
|
||||
true ->
|
||||
""
|
||||
end
|
||||
end
|
||||
end
|
|
@ -45,10 +45,10 @@ defmodule Web.Router do
|
|||
|
||||
delete "/admin/users/log_out", UserSessionController, :delete
|
||||
|
||||
get "/blog", PostController, :index, assigns: %{kind: :blog}
|
||||
get "/blog/:year/:month/:day/:slug", PostController, :show
|
||||
get "/microblog", PostController, :index, assigns: %{kind: :status}
|
||||
get "/status/:status_id", PostController, :show
|
||||
get "/blog", BlogController, :index
|
||||
get "/blog/:year/:month/:day/:slug", BlogController, :show
|
||||
get "/microblog", StatusController, :index
|
||||
get "/status/:status_id", StatusController, :show
|
||||
|
||||
# live_session :current_user, on_mount: [{Web.UserAuth, :mount_current_user}] do
|
||||
# end
|
||||
|
|
1
mix.exs
1
mix.exs
|
@ -61,6 +61,7 @@ defmodule SlaonelyButSurely.MixProject do
|
|||
{:tzdata, "~> 1.1"},
|
||||
{:slugify, "~> 1.3"},
|
||||
{:timex, "~> 3.7"},
|
||||
{:mdex, "~> 0.5.0"},
|
||||
|
||||
# Added dev and/or test dependencies
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
|
|
5
mix.lock
5
mix.lock
|
@ -25,9 +25,11 @@
|
|||
"hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"mdex": {:hex, :mdex, "0.5.0", "252c83cebc6a089801dfc1e142b4d98c9c358378ec7096a94796bce8bd13b0fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "73e3ddee03130267e3be6aaf47a7f423c6f86add4bb5c62b352465cd9fb87d95"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.19", "36617efe5afbd821099a8b994ff4618a340a5bfb25531a1802c4d4c634017a57", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ba4dc14458278773f905f8ae6c2ec743d52c3a35b6b353733f64f02dfe096cd6"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
|
||||
|
@ -40,6 +42,8 @@
|
|||
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
|
||||
"rustler": {:hex, :rustler, "0.36.1", "2d4b1ff57ea2789a44756a40dbb5fbb73c6ee0a13d031dcba96d0a5542598a6a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.7", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "f3fba4ad272970e0d1bc62972fc4a99809651e54a125c5242de9bad4574b2d02"},
|
||||
"rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"},
|
||||
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"styler": {:hex, :styler, "1.4.0", "5944723d08afe4d38210b674d7e97dd1137a75968a85a633983cc308e86dc5f2", [:mix], [], "hexpm", "07de0e89c27490c8e469bb814d77ddaaa3283d7d8038501021d80a7705cf13e9"},
|
||||
|
@ -49,6 +53,7 @@
|
|||
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.3.10", "a9971ebab1dfb36e2710a86b37c3f54973fbc9470d892035334415521fb53328", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17ab1f1b13aadb1f4b4c8e5b59c06874d701119fed082884c9c6d38addad254f"},
|
||||
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
||||
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
|
||||
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
|
|
39
prompts/00001-markdown-authoring.md
Normal file
39
prompts/00001-markdown-authoring.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Markdown Authoring
|
||||
|
||||
You must update the site to support Markdown content in blog posts and microblog statuses.
|
||||
|
||||
- Use the [mdex](https://hex.pm/packages/mdex) to convert markdown into HTML
|
||||
- [MDEx documentation](https://hexdocs.pm/mdex/MDEx.html)
|
||||
- Keep all content markdown -> HTML in a new module in the `Web` boundary so that any later customizations to markdown to HTML conversion are applied throughout the site
|
||||
|
||||
- Use the existing `body` field of the `Schema.Post`. assume this content is already markdown
|
||||
|
||||
- Ensure that syntax highlighting is supported in codefences
|
||||
- Use the catppuccin latte colorscheme
|
||||
- Make sure support for the following languges is available:
|
||||
- Elixir
|
||||
- Erlang
|
||||
- JavaScript (including React / JSX)
|
||||
- TypeScript
|
||||
- Bash
|
||||
- HTML
|
||||
- Markdown
|
||||
- Lua
|
||||
- JSON
|
||||
- TOML
|
||||
- SQL
|
||||
- YAML
|
||||
|
||||
- Enable these extensions:
|
||||
- strikethrough
|
||||
- table
|
||||
- autolink
|
||||
- tasklist
|
||||
- footnotes
|
||||
|
||||
|
||||
You will need to update the blog show page, the status show page, the status index page, and the home page where recent statuses are shown.
|
||||
|
||||
Use components to reduced code duplication when possible.
|
||||
|
||||
If components are shared between multiple views create a new module in `lib/web/components/` for the components pertaining to a specific thing.
|
28
prompts/00002-pagination.md
Normal file
28
prompts/00002-pagination.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Pagination for Blog posts and Microblog feed
|
||||
|
||||
Your task is to add pagination to the blog posts index page and the statuses index page.
|
||||
|
||||
Requirements:
|
||||
- Must use cursor based pagination NOT limit/offset
|
||||
- Invalid pagination options should just redirect to the first page
|
||||
- h-feed compatibility should be maintained
|
||||
- microformat2 consumers should be able to discover all posts somehow
|
||||
- if the h-feed spec allows for pagination just use the html pages
|
||||
- consider reading the h-feed spec: https://microformats.org/wiki/h-feed to ensure compliance.
|
||||
|
||||
This involves several things
|
||||
|
||||
1. Install the `flop` package
|
||||
- documentation: https://hexdocs.pm/flop/readme.html
|
||||
- make sure to run `mix deps.get` after adding this dependency to the `mix.exs` file.
|
||||
- the current version is `0.26.1`, use this version
|
||||
2. Read through the Flop documentation to understand how the module works and all of the options available
|
||||
3. Derive `Flop.Schema` in `Schema.Post`
|
||||
- Do not support any filtering or sorting for now
|
||||
4. Add new functions to the `Cost.Posts` module that support using Flop
|
||||
5. Add pagination controls to the pages
|
||||
- use the text "Newer posts" and "Older posts" justified with space between at the bottom of the page
|
||||
- only show the pagination controls if there are newer or older posts respectively
|
||||
- the newer posts and older posts links should always be in the same positions even if one is not rendered meaning if "newer posts" is not shown the "older posts" link should not suddenly show on the left-hand side
|
||||
|
||||
Think hard about this problem, review any rerference material, and then ask any clarifying questions or state assumptions for validation before continuing on to complete the task.
|
Loading…
Reference in a new issue