200 lines
5.9 kB
1
defmodule BlogWeb.PostLive do
2
use BlogWeb, :live_view
3
alias BlogWeb.Presence
4
require Logger
5
6
@presence_topic "blog_presence"
7
8
def mount(%{"slug" => slug}, _session, socket) do
9
if connected?(socket) do
10
# Subscribe first to get presence updates
11
Phoenix.PubSub.subscribe(Blog.PubSub, @presence_topic)
12
13
# Generate a random ID for this reader
14
reader_id = "reader_#{:crypto.strong_rand_bytes(8) |> Base.encode16()}"
15
16
# Track presence with the slug
17
{:ok, _} =
18
Presence.track(self(), @presence_topic, reader_id, %{
19
name: "Reader-#{:rand.uniform(999)}",
20
anonymous: true,
21
joined_at: DateTime.utc_now(),
22
slug: slug
23
})
24
end
25
26
require Logger
27
Logger.debug("Mounting PostLive with slug: #{slug}")
28
29
case Blog.Content.Post.get_by_slug(slug) do
30
nil ->
31
Logger.debug("No post found for slug: #{slug}")
32
{:ok, push_navigate(socket, to: "/")}
33
34
post ->
35
# Set meta tags for the page
36
meta_attrs = [
37
%{name: "title", content: post.title},
38
%{name: "description", content: truncated_post(post.body)},
39
%{property: "og:title", content: post.title},
40
%{property: "og:description", content: truncated_post(post.body)},
41
%{property: "og:type", content: "website"}
42
]
43
44
socket =
45
socket
46
|> assign_meta_tags(post)
47
|> assign(:post, post)
48
|> assign(meta_attrs: meta_attrs)
49
|> assign(page_title: post.title)
50
51
Logger.debug("Found post: #{inspect(post, pretty: true)}")
52
53
# Filter out tags line and process markdown
54
content_without_tags = remove_tags_line(post.body)
55
case Earmark.as_html(content_without_tags, code_class_prefix: "language-") do
56
{:ok, html, _} ->
57
socket =
58
assign(socket,
59
html: html,
60
reader_count: get_reader_count(slug)
61
)
62
63
{:ok, socket}
64
65
{:error, html, errors} ->
66
# Still show the content even if there are markdown errors
67
Logger.error("Markdown parsing warnings: #{inspect(errors)}")
68
69
socket =
70
assign(socket,
71
html: html,
72
post: post
73
)
74
75
{:ok, socket}
76
end
77
end
78
end
79
80
# Remove the tags line from the content
81
defp remove_tags_line(content) when is_binary(content) do
82
content
83
|> String.split("\n")
84
|> Enum.reject(&is_tags_line?/1)
85
|> Enum.join("\n")
86
end
87
88
# Check if a line is a tags line
89
defp is_tags_line?(line) do
90
String.starts_with?(String.trim(line), "tags:")
91
end
92
93
defp truncated_post(body) do
94
body
95
|> remove_tags_line()
96
|> String.slice(0, 250)
97
|> Kernel.<>("...")
98
end
99
100
def handle_info(%{event: "presence_diff"} = _diff, socket) do
101
socket =
102
assign(socket,
103
reader_count: get_reader_count(socket.assigns.post.slug)
104
)
105
106
{:noreply, socket}
107
end
108
109
defp get_reader_count(slug) do
110
Presence.list(@presence_topic)
111
|> Enum.count(fn {_key, %{metas: [meta | _]}} ->
112
# Only count readers that have a slug and are reading this post
113
Map.get(meta, :slug) == slug
114
end)
115
end
116
117
def render(assigns) do
118
~H"""
119
<div class="px-8 py-12 font-mono text-gray-700">
120
<div class="max-w-7xl mx-auto">
121
<div class="mb-4 text-sm text-gray-500">
122
<%= if @reader_count > 1 do %>
123
{@reader_count - 1} other {if @reader_count == 2, do: "person", else: "people"} reading this post
124
<% end %>
125
</div>
126
127
<article class="p-8 bg-white rounded-lg border-2 border-gray-200">
128
<div
129
id="post-content"
130
phx-hook="Highlight"
131
class="prose prose-lg prose-headings:font-mono prose-headings:font-bold prose-h1:text-4xl prose-h2:text-3xl prose-h3:text-2xl max-w-none"
132
>
133
{raw(@html)}
134
</div>
135
</article>
136
</div>
137
</div>
138
"""
139
end
140
141
defp assign_meta_tags(socket, post) do
142
description = get_preview(post.body)
143
144
# Get image path, fallback to nil if generation fails
145
image_path = Blog.Content.ImageGenerator.ensure_post_image(post.slug)
146
image_url = if image_path, do: BlogWeb.Endpoint.url() <> image_path
147
148
meta_tags = [
149
%{name: "description", content: description},
150
%{property: "og:title", content: post.title},
151
%{property: "og:description", content: description},
152
%{property: "og:type", content: "article"},
153
%{property: "og:site_name", content: "Thoughts and Tidbits"},
154
%{
155
property: "article:published_time",
156
content: DateTime.from_naive!(post.written_on, "Etc/UTC") |> DateTime.to_iso8601()
157
},
158
%{name: "twitter:card", content: "summary_large_image"},
159
%{name: "twitter:title", content: post.title},
160
%{name: "twitter:description", content: description}
161
]
162
163
# Add image tags only if we have an image
164
meta_tags =
165
if image_url do
166
meta_tags ++
167
[
168
%{property: "og:image", content: image_url},
169
%{property: "og:image:width", content: "1200"},
170
%{property: "og:image:height", content: "630"},
171
%{name: "twitter:image", content: image_url}
172
]
173
else
174
meta_tags
175
end
176
177
assign(socket,
178
page_title: post.title,
179
meta_tags: meta_tags ++ tag_meta_tags(post.tags)
180
)
181
end
182
183
defp tag_meta_tags(tags) do
184
Enum.map(tags, fn tag ->
185
%{property: "article:tag", content: tag.name}
186
end)
187
end
188
189
defp get_preview(content, max_length \\ 200) do
190
content
191
|> remove_tags_line()
192
|> String.split("\n")
193
|> Enum.join(" ")
194
|> String.replace(~r/[#*`]/, "")
195
|> String.replace(~r/\s+/, " ")
196
|> String.trim()
197
|> String.slice(0, max_length)
198
|> Kernel.<>("...")
199
end
200
end
201