Skip to content

Commit 286e3e8

Browse files
authored
feat(tags): tag extension (#127)
1 parent 682bb2d commit 286e3e8

File tree

12 files changed

+243
-37
lines changed

12 files changed

+243
-37
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Static Site Generator for Elixir.
1919
- [x] Posts
2020
- [x] RSS
2121
- [x] Sitemap
22+
- [x] Tags
2223
- [ ] SEO
2324
- [x] Project generator
2425

config/test.exs

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ config :tableau, Mix.Tasks.Tableau.LogExtension, enabled: true
77
config :tableau, Tableau.PostExtension, enabled: true
88
config :tableau, Tableau.RSSExtension, enabled: false
99
config :tableau, Tableau.SitemapExtension, enabled: false
10+
config :tableau, Tableau.TagExtension, enabled: false
1011
config :tableau, :config, url: "http://localhost:4999"

lib/tableau/extensions/post_extension.ex

+7
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ defmodule Tableau.PostExtension do
7777

7878
alias Tableau.Extension.Common
7979

80+
@type post :: %{
81+
body: String.t(),
82+
file: String.t(),
83+
layout: module(),
84+
date: DateTime.t()
85+
}
86+
8087
@impl Tableau.Extension
8188
def config(config) do
8289
unify(
+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
defmodule Tableau.TagExtension do
2+
@moduledoc ~S'''
3+
Creates pages for tags found in posts created by the `Tableau.PostExtension`.
4+
5+
The `:tags` key provided on every page in the assigns is described by `t:tags/0`.
6+
7+
The `@page` assign passed to the `layout` provided in the configuration is described by `t:page/0`.
8+
9+
## Configuration
10+
11+
- `:enabled` - boolean - Extension is active or not.
12+
* `:layout` - module - The `Tableau.Layout` implementation to use.
13+
* `:permalink` - string - The permalink prefix to use for the tag page, will be joined with the tag name.
14+
15+
16+
## Layout and Page
17+
18+
To take advantage of tag extension, you'll need to define a layout that will render each "tag page" and a normal `Tableau.Page` that lists all tags on your site.
19+
20+
### Layout to render a tag page
21+
22+
```elixir
23+
defmodule MySite.TagLayout do
24+
use Tableau.Layout, layout: MySite.RootLayout
25+
26+
def template(assigns) do
27+
~H"""
28+
<div>
29+
<h1>Tag: #{@page.tag}</h1>
30+
31+
<ul>
32+
<li :for={post <- @page.posts}>
33+
<a href={post.permalink}> {post.title}</a>
34+
</li>
35+
</ul>
36+
</div>
37+
"""
38+
end
39+
end
40+
```
41+
42+
### Page to render all tags
43+
44+
This example page shows listing all takes, sorting them by the number of posts for each tag.
45+
46+
```elixir
47+
defmodule MySite.TagPage do
48+
use Tableau.Page,
49+
layout: MySite.RootLayout,
50+
permalink: "/tags",
51+
title: "Tags"
52+
53+
54+
def template(assigns) do
55+
sorted_tags = Enum.sort_by(assigns.tags, fn {_, p} -> length(p) end, :desc)
56+
assigns = Map.put(assigns, :tags, sorted_tags)
57+
58+
~H"""
59+
<div>
60+
<h1>Tags</h1>
61+
62+
<ul>
63+
<li :for={{tag, posts} <- @tags}>
64+
<a href={tag.permalink}>tag.tag</a>
65+
66+
<span>- {length(posts)}</span>
67+
</li>
68+
</ul>
69+
</div>
70+
"""
71+
end
72+
end
73+
```
74+
'''
75+
use Tableau.Extension, key: :tag, priority: 200, type: :pre_build
76+
77+
import Schematic
78+
79+
@type page :: %{
80+
title: String.t(),
81+
tag: String.t(),
82+
permalink: String.t(),
83+
posts: [Tableau.PostExtension.post()]
84+
}
85+
86+
@type tag :: %{
87+
title: String.t(),
88+
tag: String.t(),
89+
permalink: String.t()
90+
}
91+
92+
@type tags :: %{
93+
tag() => [Tableau.PostExtension.post()]
94+
}
95+
96+
@impl Tableau.Extension
97+
def config(config) do
98+
unify(
99+
oneof([
100+
map(%{enabled: false}),
101+
map(%{
102+
enabled: true,
103+
layout: atom(),
104+
permalink: str()
105+
})
106+
]),
107+
config
108+
)
109+
end
110+
111+
@impl Tableau.Extension
112+
def run(token) do
113+
posts = token.posts
114+
layout = token.extensions.tag.config.layout
115+
permalink = token.extensions.tag.config.permalink
116+
117+
tags =
118+
for post <- posts, tag <- post |> Map.get(:tags, []) |> Enum.uniq(), reduce: Map.new() do
119+
acc ->
120+
permalink = Path.join(permalink, tag)
121+
122+
tag = %{title: tag, permalink: permalink, tag: tag}
123+
Map.update(acc, tag, [post], &[post | &1])
124+
end
125+
126+
graph =
127+
Tableau.Graph.insert(
128+
token.graph,
129+
for {tag, posts} <- tags do
130+
posts = Enum.sort_by(posts, & &1.date, {:desc, DateTime})
131+
132+
opts = Map.put(tag, :posts, posts)
133+
134+
%Tableau.Page{
135+
parent: layout,
136+
permalink: tag.permalink,
137+
template: fn _ -> "" end,
138+
opts: opts
139+
}
140+
end
141+
)
142+
143+
{:ok, token |> Map.put(:graph, graph) |> Map.put(:tags, tags)}
144+
end
145+
end

lib/tableau_dev_server/build_exception.ex

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ defmodule TableauDevServer.BuildException do
1414

1515
@impl true
1616
def message(%__MODULE__{page: page, exception: exception}) do
17-
exception = String.replace(exception, ~r/\x1B\[[0-9;]*m/, "")
17+
exception =
18+
if is_binary(exception) do
19+
String.replace(exception, ~r/\x1B\[[0-9;]*m/, "")
20+
else
21+
exception
22+
end
1823

1924
"""
2025
An exception was raised:

mix.exs

+5-4
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ defmodule Tableau.MixProject do
3838
{:html_entities, "~> 0.5.2"},
3939
{:libgraph, "~> 0.16.0"},
4040
{:mdex, "~> 0.4.0"},
41-
{:schematic, "~> 0.5"},
41+
{:schematic, "~> 0.5.1"},
4242
{:tz, "~> 0.28.1"},
4343
{:web_dev_utils, "~> 0.3"},
4444
{:websock_adapter, "~> 0.5"},
@@ -71,19 +71,20 @@ defmodule Tableau.MixProject do
7171
Tableau,
7272
Tableau.Layout,
7373
Tableau.Page,
74-
Tableau.Document.Helper
74+
Tableau.Document.Helper,
75+
Tableau.Extension
7576
],
7677
Converters: [
7778
Tableau.Converter,
7879
Tableau.MDExConverter
7980
],
8081
Extensions: [
81-
Tableau.Extension,
8282
Tableau.PostExtension,
8383
Tableau.PageExtension,
8484
Tableau.SitemapExtension,
8585
Tableau.RSSExtension,
86-
Tableau.DataExtension
86+
Tableau.DataExtension,
87+
Tableau.TagExtension
8788
]
8889
]
8990
]

mix.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
2222
"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"},
2323
"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"},
24-
"schematic": {:hex, :schematic, "0.5.0", "e9df4e9ad2074ad803cc82612cdfddaa57fbded9698b27f94e9d3aeb0b48e88e", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6cc50951999548506b9f99bfd5c707af70624ec2c4cb85a772da1be12be1b06"},
24+
"schematic": {:hex, :schematic, "0.5.1", "be4b2c03115d5a593459c11a7249a6fbb45855947d9653e9250455dcd7df1d42", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02f913c97e6e04ccdaa02004679a7a16bb16fe0449583ad647e296d8e8961546"},
2525
"styler": {:hex, :styler, "1.1.1", "ccb55763316915b5de532bf14c587c211ddc86bc749ac676e74dfacd3894cc0d", [:mix], [], "hexpm", "80ce12fb862e13d998589eea7c1932f4e6ce9d6ded2182cb322f8f9b2b8d3632"},
2626
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
2727
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},

test/support/helpers.ex

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
defmodule Tableau.Support.Helpers do
2+
@moduledoc false
3+
def post(idx, overrides) do
4+
base = %{
5+
title: "Post #{idx}",
6+
permalink: "/posts/post-#{1}",
7+
date: DateTime.utc_now(),
8+
body: """
9+
## Welcome to Post #{idx}
10+
11+
Here, we post like crazy.
12+
"""
13+
}
14+
15+
Map.merge(base, Map.new(overrides))
16+
end
17+
18+
def page_with_permalink?(page, permalink) do
19+
is_struct(page, Tableau.Page) and page.permalink == permalink
20+
end
21+
end

test/support/my_data.ex

-11
This file was deleted.

test/tableau/extensions/post_extension_test.exs

+9-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ end
1919
defmodule Tableau.PostExtensionTest do
2020
use ExUnit.Case, async: true
2121

22+
import Tableau.Support.Helpers
23+
2224
alias Tableau.PostExtension
2325

2426
@moduletag :tmp_dir
@@ -113,9 +115,10 @@ defmodule Tableau.PostExtensionTest do
113115

114116
vertices = Graph.vertices(graph)
115117

116-
assert Enum.any?(vertices, fn v -> is_struct(v, Tableau.Page) and v.permalink == post_1.permalink end)
117-
assert Enum.any?(vertices, fn v -> is_struct(v, Tableau.Page) and v.permalink == post_2.permalink end)
118-
assert Enum.any?(vertices, fn v -> v == Blog.PostLayout end)
118+
assert Enum.any?(vertices, &page_with_permalink?(&1, post_1.permalink))
119+
assert Enum.any?(vertices, &page_with_permalink?(&1, post_2.permalink))
120+
121+
assert Blog.PostLayout in vertices
119122
end
120123

121124
test "future: true will render future posts", %{tmp_dir: dir, token: token} do
@@ -155,8 +158,9 @@ defmodule Tableau.PostExtensionTest do
155158

156159
vertices = Graph.vertices(graph)
157160

158-
assert Enum.any?(vertices, fn v -> is_struct(v, Tableau.Page) and v.permalink == post.permalink end)
159-
assert Enum.any?(vertices, fn v -> v == Blog.PostLayout end)
161+
assert Enum.any?(vertices, &page_with_permalink?(&1, post.permalink))
162+
163+
assert Blog.PostLayout in vertices
160164
end
161165

162166
test "configured permalink works when you dont specify one", %{tmp_dir: dir, token: token} do

test/tableau/extensions/rss_extension_test.exs

+2-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule Tableau.RSSExtensionTest do
22
use ExUnit.Case, async: true
33

4+
import Tableau.Support.Helpers
5+
46
alias Tableau.RSSExtension
57

68
describe "run/1" do
@@ -192,19 +194,4 @@ defmodule Tableau.RSSExtensionTest do
192194
refute not_casual_content =~ "Post 3"
193195
end
194196
end
195-
196-
defp post(idx, overrides) do
197-
base = %{
198-
title: "Post #{idx}",
199-
permalink: "/posts/post-#{1}",
200-
date: DateTime.utc_now(),
201-
body: """
202-
## Welcome to Post #{idx}
203-
204-
Here, we post like crazy.
205-
"""
206-
}
207-
208-
Map.merge(base, Map.new(overrides))
209-
end
210197
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
defmodule Tableau.TagExtensionTest do
2+
use ExUnit.Case, async: true
3+
4+
import Tableau.Support.Helpers
5+
6+
alias Tableau.TagExtension
7+
alias Tableau.TagExtensionTest.Layout
8+
9+
describe "run" do
10+
test "creates tag pages and tags key" do
11+
posts = [
12+
# dedups tags
13+
post(1, tags: ["post", "post"]),
14+
# post can have multiple tags, includes posts from same tag
15+
post(2, tags: ["til", "post"]),
16+
post(3, tags: ["recipe"])
17+
]
18+
19+
token = %{
20+
posts: posts,
21+
graph: Graph.new(),
22+
extensions: %{tag: %{config: %{layout: Layout, permalink: "/tags"}}}
23+
}
24+
25+
assert {:ok, token} = TagExtension.run(token)
26+
27+
assert %{
28+
tags: %{
29+
%{tag: "post", title: "post", permalink: "/tags/post"} => [%{title: "Post 2"}, %{title: "Post 1"}],
30+
%{tag: "recipe", title: "recipe", permalink: "/tags/recipe"} => [%{title: "Post 3"}],
31+
%{tag: "til", title: "til", permalink: "/tags/til"} => [%{title: "Post 2"}]
32+
},
33+
graph: graph
34+
} = token
35+
36+
vertices = Graph.vertices(graph)
37+
38+
assert Enum.any?(vertices, &page_with_permalink?(&1, "/tags/post"))
39+
assert Enum.any?(vertices, &page_with_permalink?(&1, "/tags/recipe"))
40+
assert Enum.any?(vertices, &page_with_permalink?(&1, "/tags/til"))
41+
42+
assert Layout in vertices
43+
end
44+
end
45+
end

0 commit comments

Comments
 (0)