Skip to content

Commit 575a9b9

Browse files
committed
Replace Template Types with Collections architecture
- New `beacon_collections` table replaces `beacon_template_types` - Collections support two modes: `managed` (individual pages) and `template` (dynamic routes) - Collections define layout, field definitions, JSON-LD mapping, meta tag mapping, starter template, path prefix/pattern, and icon - Pages reference `collection_id` instead of `template_type_id` - Migration V013 creates collections, migrates data, drops template_types - Renamed `Beacon.TemplateType.*` modules to `Beacon.Collection.*` - Updated runtime renderer, loader, SEO resolvers, and metrics - Fixed Phoenix 1.8.5 compatibility: `Phoenix.Endpoint.Supervisor.config/2` → `Application.get_env/3` - Updated deps: Phoenix 1.8.5, LiveView 1.1.28, TailwindCompiler with plugin_css API - Fixed LiveView 1.1 Comprehension struct change (dynamics → entries)
1 parent 40e0f78 commit 575a9b9

17 files changed

Lines changed: 358 additions & 117 deletions

File tree

lib/beacon/template_type/field_validator.ex renamed to lib/beacon/collection/field_validator.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
defmodule Beacon.TemplateType.FieldValidator do
1+
defmodule Beacon.Collection.FieldValidator do
22
@moduledoc """
3-
Validates a page's `fields` JSONB against its template type's `field_definitions`.
3+
Validates a page's `fields` JSONB against its collection's `field_definitions`.
44
"""
55

66
@doc """

lib/beacon/template_type/json_ld_resolver.ex renamed to lib/beacon/collection/json_ld_resolver.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
defmodule Beacon.TemplateType.JsonLdResolver do
1+
defmodule Beacon.Collection.JsonLdResolver do
22
@moduledoc """
3-
Resolves a template type's `json_ld_mapping` into a concrete JSON-LD map
3+
Resolves a collection's `json_ld_mapping` into a concrete JSON-LD map
44
by substituting `{field}` references with actual values from the page.
55
"""
66

@@ -10,7 +10,7 @@ defmodule Beacon.TemplateType.JsonLdResolver do
1010
Resolves a JSON-LD mapping template into a concrete JSON-LD map.
1111
1212
Reference syntax: `{name}` where `name` is looked up in this order:
13-
1. `fields[name]` — the page's template-type-defined fields
13+
1. `fields[name]` — the page's collection-defined fields
1414
2. `manifest[name]` — page manifest fields (title, path, description, etc.)
1515
3. Config values: `site_name`, `site_url`
1616

lib/beacon/template_type/meta_tag_resolver.ex renamed to lib/beacon/collection/meta_tag_resolver.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
defmodule Beacon.TemplateType.MetaTagResolver do
1+
defmodule Beacon.Collection.MetaTagResolver do
22
@moduledoc """
3-
Resolves a template type's `meta_tag_mapping` into concrete meta tag maps
3+
Resolves a collection's `meta_tag_mapping` into concrete meta tag maps
44
by substituting `{field}` references with actual values from the page.
55
"""
66

@@ -9,7 +9,7 @@ defmodule Beacon.TemplateType.MetaTagResolver do
99
@doc """
1010
Resolves a list of meta tag mapping templates into concrete meta tag maps.
1111
12-
Same reference syntax as `Beacon.TemplateType.JsonLdResolver`.
12+
Same reference syntax as `Beacon.Collection.JsonLdResolver`.
1313
Returns an empty list if the mapping is empty.
1414
"""
1515
@spec resolve([map()], map(), map(), Beacon.Config.t()) :: [map()]

lib/beacon/content.ex

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -792,11 +792,11 @@ defmodule Beacon.Content do
792792
"extra" => page.extra,
793793
"ast" => ast,
794794
"date_modified" => page.date_modified,
795-
"template_type_id" => page.template_type_id,
795+
"collection_id" => page.collection_id,
796796
"fields" => page.fields || %{}
797797
}
798798

799-
fields = [:site, :schema_version, :event_id, :page, :page_id, :path, :title, :template, :format, :extra, :ast, :date_modified, :template_type_id, :fields]
799+
fields = [:site, :schema_version, :event_id, :page, :page_id, :path, :title, :template, :format, :extra, :ast, :date_modified, :collection_id, :fields]
800800

801801
result =
802802
%PageSnapshot{}
@@ -4879,62 +4879,80 @@ defmodule Beacon.Content do
48794879
# Template Types
48804880
# ---------------------------------------------------------------------------
48814881

4882-
alias Beacon.Content.TemplateType
4882+
alias Beacon.Content.Collection
48834883

4884-
@doc "Creates a template type. Pass `site` for site-specific, or set `site: nil` in attrs for global."
4885-
@doc type: :template_types
4886-
@spec create_template_type(Site.t(), map()) :: {:ok, TemplateType.t()} | {:error, Ecto.Changeset.t()}
4887-
def create_template_type(site, attrs) when is_atom(site) and is_map(attrs) do
4888-
%TemplateType{} |> TemplateType.changeset(attrs) |> repo(site).insert()
4884+
@doc "Creates a collection."
4885+
@doc type: :collections
4886+
@spec create_collection(map()) :: {:ok, Collection.t()} | {:error, Ecto.Changeset.t()}
4887+
def create_collection(attrs) when is_map(attrs) do
4888+
site = attrs[:site] || attrs["site"]
4889+
%Collection{} |> Collection.changeset(attrs) |> repo(site).insert()
48894890
end
48904891

4891-
@doc "Updates a template type."
4892-
@doc type: :template_types
4893-
@spec update_template_type(TemplateType.t(), map()) :: {:ok, TemplateType.t()} | {:error, Ecto.Changeset.t()}
4894-
def update_template_type(%TemplateType{} = tt, attrs) do
4895-
tt |> TemplateType.changeset(attrs) |> repo(tt).update()
4892+
@doc "Updates a collection."
4893+
@doc type: :collections
4894+
@spec update_collection(Collection.t(), map()) :: {:ok, Collection.t()} | {:error, Ecto.Changeset.t()}
4895+
def update_collection(%Collection{} = col, attrs) do
4896+
col |> Collection.changeset(attrs) |> repo(col).update()
48964897
end
48974898

4898-
@doc "Deletes a template type."
4899-
@doc type: :template_types
4900-
@spec delete_template_type(TemplateType.t()) :: {:ok, TemplateType.t()} | {:error, Ecto.Changeset.t()}
4901-
def delete_template_type(%TemplateType{} = tt) do
4902-
repo(tt).delete(tt)
4899+
@doc "Deletes a collection."
4900+
@doc type: :collections
4901+
@spec delete_collection(Collection.t()) :: {:ok, Collection.t()} | {:error, Ecto.Changeset.t()}
4902+
def delete_collection(%Collection{} = col) do
4903+
repo(col).delete(col)
49034904
end
49044905

4905-
@doc "Lists template types available to a site (includes global types where site is nil)."
4906-
@doc type: :template_types
4907-
@spec list_template_types(Site.t(), keyword()) :: [TemplateType.t()]
4908-
def list_template_types(site, _opts \\ []) when is_atom(site) do
4909-
from(tt in TemplateType,
4910-
where: tt.site == ^site or is_nil(tt.site),
4911-
order_by: [asc: tt.name]
4912-
)
4906+
@doc "Lists collections available to a site (includes global collections where site is nil)."
4907+
@doc type: :collections
4908+
@spec list_collections(Site.t(), keyword()) :: [Collection.t()]
4909+
def list_collections(site, opts \\ []) when is_atom(site) do
4910+
query =
4911+
cond do
4912+
opts[:globals_only] ->
4913+
from(c in Collection, where: is_nil(c.site))
4914+
4915+
opts[:mode] ->
4916+
mode = to_string(opts[:mode])
4917+
from(c in Collection, where: (c.site == ^site or is_nil(c.site)) and c.mode == ^mode)
4918+
4919+
true ->
4920+
from(c in Collection, where: c.site == ^site or is_nil(c.site))
4921+
end
4922+
4923+
query
4924+
|> order_by([c], [asc: c.sort_order, asc: c.name])
49134925
|> repo(site).all()
49144926
end
49154927

4916-
@doc "Gets a template type by ID."
4917-
@doc type: :template_types
4918-
@spec get_template_type(Site.t(), String.t()) :: TemplateType.t() | nil
4919-
def get_template_type(site, id) when is_atom(site) do
4920-
repo(site).get(TemplateType, id)
4928+
@doc "Gets a collection by ID."
4929+
@doc type: :collections
4930+
@spec get_collection(Site.t(), String.t()) :: Collection.t() | nil
4931+
def get_collection(site, id) when is_atom(site) do
4932+
repo(site).get(Collection, id)
4933+
end
4934+
4935+
@doc "Gets a collection by ID, raises if not found."
4936+
@doc type: :collections
4937+
@spec get_collection!(Site.t(), String.t()) :: Collection.t()
4938+
def get_collection!(site, id) when is_atom(site) do
4939+
repo(site).get!(Collection, id)
49214940
end
49224941

4923-
@doc "Gets a template type by slug (checks site-specific first, then global)."
4924-
@doc type: :template_types
4925-
@spec get_template_type_by_slug(Site.t(), String.t()) :: TemplateType.t() | nil
4926-
def get_template_type_by_slug(site, slug) when is_atom(site) do
4927-
# Prefer site-specific over global
4928-
case repo(site).get_by(TemplateType, site: site, slug: slug) do
4929-
nil -> repo(site).get_by(TemplateType, site: nil, slug: slug)
4930-
tt -> tt
4942+
@doc "Gets a collection by slug (checks site-specific first, then global)."
4943+
@doc type: :collections
4944+
@spec get_collection_by_slug(Site.t(), String.t()) :: Collection.t() | nil
4945+
def get_collection_by_slug(site, slug) when is_atom(site) do
4946+
case repo(site).get_by(Collection, site: site, slug: slug) do
4947+
nil -> repo(site).get_by(Collection, site: nil, slug: slug)
4948+
col -> col
49314949
end
49324950
end
49334951

4934-
@doc "Returns a changeset for tracking template type changes."
4935-
@doc type: :template_types
4936-
def change_template_type(%TemplateType{} = tt, attrs \\ %{}) do
4937-
TemplateType.changeset(tt, attrs)
4952+
@doc "Returns a changeset for tracking collection changes."
4953+
@doc type: :collections
4954+
def change_collection(%Collection{} = col, attrs \\ %{}) do
4955+
Collection.changeset(col, attrs)
49384956
end
49394957

49404958
# ---------------------------------------------------------------------------
Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
defmodule Beacon.Content.TemplateType do
1+
defmodule Beacon.Content.Collection do
22
@moduledoc """
3-
Defines a content type schema for Beacon pages.
3+
Defines a content collection for Beacon pages.
44
5-
A template type formalizes the data contract for a class of pages: what fields
6-
they expect, how those fields map to JSON-LD structured data, and how they map
7-
to meta tags.
5+
A Collection is the primary organizational concept in Beacon. It defines
6+
"what kind of content am I making?" — the data contract, layout, SEO mappings,
7+
starter template, and URL pattern for a class of pages.
8+
9+
## Modes
10+
11+
* `:managed` — Beacon stores each page as a separate content entry.
12+
Each blog post, landing page, etc. is a row in `beacon_pages`.
13+
14+
* `:template` — Beacon stores a single template for dynamic routes.
15+
The consuming client provides data at render time (e.g., `/blog/:slug`
16+
renders from external data). Beacon doesn't store individual content items.
817
918
## Tiers
1019
11-
* **Global** (`site: nil`) — Available to all sites. Created by Beacon-level admins.
20+
* **Global** (`site: nil`) — Available to all sites.
1221
* **Site-specific** (`site: :my_site`) — Available only to that site.
1322
1423
## Field Definitions
@@ -49,56 +58,73 @@ defmodule Beacon.Content.TemplateType do
4958

5059
@type t :: %__MODULE__{}
5160

52-
schema "beacon_template_types" do
61+
@valid_modes ~w(managed template)
62+
63+
schema "beacon_collections" do
5364
field :site, Beacon.Types.Site
5465
field :name, :string
5566
field :slug, :string
56-
field :field_definitions, {:array, :map}, default: []
67+
field :description, :string
68+
field :mode, :string, default: "managed"
69+
field :layout_id, :binary_id
70+
field :fields, {:array, :map}, default: []
5771
field :json_ld_mapping, :map, default: %{}
5872
field :meta_tag_mapping, {:array, :map}, default: []
73+
field :starter_template, :string
74+
field :path_prefix, :string
75+
field :path_pattern, :string
76+
field :icon, :string
77+
field :sort_order, :integer, default: 0
5978

6079
timestamps()
6180
end
6281

6382
@supported_field_types ~w(string text integer float boolean datetime date url select list reference)
6483

6584
@doc false
66-
def changeset(template_type \\ %__MODULE__{}, attrs) do
67-
template_type
68-
|> cast(attrs, [:site, :name, :slug, :field_definitions, :json_ld_mapping, :meta_tag_mapping])
69-
|> validate_required([:name, :slug])
85+
def changeset(collection \\ %__MODULE__{}, attrs) do
86+
collection
87+
|> cast(attrs, [
88+
:site, :name, :slug, :description, :mode, :layout_id,
89+
:fields, :json_ld_mapping, :meta_tag_mapping,
90+
:starter_template, :path_prefix, :path_pattern,
91+
:icon, :sort_order
92+
])
93+
|> validate_required([:name, :slug, :mode])
94+
|> validate_inclusion(:mode, @valid_modes, message: "must be one of: #{Enum.join(@valid_modes, ", ")}")
7095
|> validate_format(:slug, ~r/^[a-z0-9\-]+$/, message: "must be lowercase alphanumeric with hyphens")
7196
|> unique_constraint([:site, :slug])
7297
|> validate_field_definitions()
7398
end
7499

100+
def valid_modes, do: @valid_modes
101+
75102
defp validate_field_definitions(changeset) do
76-
validate_change(changeset, :field_definitions, fn :field_definitions, definitions ->
103+
validate_change(changeset, :fields, fn :fields, definitions ->
77104
errors =
78105
definitions
79106
|> Enum.with_index()
80107
|> Enum.flat_map(fn {def_map, idx} ->
81108
cond do
82109
not is_map(def_map) ->
83-
[{:field_definitions, "item #{idx} must be a map"}]
110+
[{:fields, "item #{idx} must be a map"}]
84111

85112
not is_binary(def_map["name"]) or def_map["name"] == "" ->
86-
[{:field_definitions, "item #{idx} missing 'name'"}]
113+
[{:fields, "item #{idx} missing 'name'"}]
87114

88115
not is_binary(def_map["type"]) or def_map["type"] not in @supported_field_types ->
89-
[{:field_definitions, "item #{idx} has invalid type '#{def_map["type"]}'. Supported: #{Enum.join(@supported_field_types, ", ")}"}]
116+
[{:fields, "item #{idx} has invalid type '#{def_map["type"]}'. Supported: #{Enum.join(@supported_field_types, ", ")}"}]
90117

91118
true ->
92119
[]
93120
end
94121
end)
95122

96-
# Check for duplicate names
97123
names = Enum.map(definitions, & &1["name"])
98124
dupes = names -- Enum.uniq(names)
99125

100126
if dupes != [] do
101-
[{:field_definitions, "duplicate field names: #{Enum.join(Enum.uniq(dupes), ", ")}"} | errors]
127+
[{:fields, "duplicate field names: #{Enum.join(Enum.uniq(dupes), ", ")}"} | errors]
102128
else
103129
errors
104130
end

lib/beacon/content/page.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ defmodule Beacon.Content.Page do
2525
alias Beacon.Content
2626
alias Beacon.Content.Page.Helper
2727

28-
@version 6
28+
@version 7
2929

3030
@type t :: %__MODULE__{}
3131

@@ -45,7 +45,7 @@ defmodule Beacon.Content.Page do
4545
field :og_image, :string
4646
field :twitter_card, :string
4747
field :date_modified, :utc_datetime_usec
48-
field :template_type_id, Ecto.UUID
48+
field :collection_id, Ecto.UUID
4949
field :fields, :map, default: %{}
5050
field :order, :integer, default: 1
5151
field :format, Beacon.Types.Atom, default: :heex
@@ -91,7 +91,7 @@ defmodule Beacon.Content.Page do
9191
:og_image,
9292
:twitter_card,
9393
:date_modified,
94-
:template_type_id,
94+
:collection_id,
9595
:fields,
9696
:order,
9797
:layout_id,
@@ -141,7 +141,7 @@ defmodule Beacon.Content.Page do
141141
:og_image,
142142
:twitter_card,
143143
:date_modified,
144-
:template_type_id,
144+
:collection_id,
145145
:fields,
146146
:format
147147
])

lib/beacon/content/page_snapshot.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ defmodule Beacon.Content.PageSnapshot do
3838
field :og_image, :string
3939
field :twitter_card, :string
4040
field :date_modified, :utc_datetime_usec
41-
field :template_type_id, Ecto.UUID
41+
field :collection_id, Ecto.UUID
4242
field :fields, :map, default: %{}
4343
belongs_to :event, Beacon.Content.PageEvent
4444
timestamps updated_at: false

lib/beacon/migration.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ defmodule Beacon.Migration do
5252
"""
5353

5454
@initial_version 1
55-
@current_version 12
55+
@current_version 13
5656

5757
@doc """
5858
Upgrades Beacon database schemas.

0 commit comments

Comments
 (0)