@@ -43,7 +43,6 @@ defmodule Beacon.Content do
4343 alias Beacon.Content.LayoutEvent
4444 alias Beacon.Content.LayoutSnapshot
4545 alias Beacon.Content.Page
46- alias Beacon.Content.Author
4746 alias Beacon.Content.InternalLink
4847 alias Beacon.Content.PageEvent
4948 alias Beacon.Content.Redirect
@@ -793,11 +792,11 @@ defmodule Beacon.Content do
793792 "extra" => page . extra ,
794793 "ast" => ast ,
795794 "date_modified" => page . date_modified ,
796- "faq_items " => page . faq_items || [ ] ,
797- "author_id " => page . author_id
795+ "template_type_id " => page . template_type_id ,
796+ "fields " => page . fields || % { }
798797 }
799798
800- fields = [ :site , :schema_version , :event_id , :page , :page_id , :path , :title , :template , :format , :extra , :ast , :date_modified , :faq_items , :author_id ]
799+ fields = [ :site , :schema_version , :event_id , :page , :page_id , :path , :title , :template , :format , :extra , :ast , :date_modified , :template_type_id , :fields ]
801800
802801 result =
803802 % PageSnapshot { }
@@ -1298,6 +1297,19 @@ defmodule Beacon.Content do
12981297 |> maybe_add_leading_slash ( )
12991298 end
13001299
1300+ defp extract_page_snapshot ( % { schema_version: 6 , page: % Page { } = page , ast: ast } ) do
1301+ page = maybe_add_leading_slash ( page )
1302+ case unwrap_ast ( ast ) do
1303+ nodes when is_list ( nodes ) -> % { page | ast: nodes }
1304+ _ -> page
1305+ end
1306+ end
1307+
1308+ defp extract_page_snapshot ( % { schema_version: 6 , page: % Page { } = page } ) do
1309+ page
1310+ |> maybe_add_leading_slash ( )
1311+ end
1312+
13011313 defp extract_page_snapshot ( _snapshot ) , do: nil
13021314
13031315 defp maybe_add_leading_slash ( % { path: << "/" , _rest :: binary >> } = page ) , do: page
@@ -4383,13 +4395,30 @@ defmodule Beacon.Content do
43834395 case insert_published_page ( page ) do
43844396 { :ok , page } ->
43854397 :ok = Beacon.PubSub . page_published ( page )
4398+ # Async: extract internal links from the published page
4399+ maybe_rebuild_links_async ( page )
43864400 { :reply , { :ok , page } , config }
43874401
43884402 error ->
43894403 { :reply , error , config }
43904404 end
43914405 end
43924406
4407+ defp maybe_rebuild_links_async ( page ) do
4408+ Task . start ( fn ->
4409+ try do
4410+ template = Beacon.Lifecycle.Template . load_template ( page )
4411+ ast = Beacon.Template.Parser . parse ( template )
4412+ component_registry = build_component_registry_for_ast ( page . site )
4413+ expanded = Beacon.Template.ComponentExpander . expand ( ast , component_registry )
4414+ html = Beacon.Client.LiveViewCompiler . render_to_string ( expanded , % { } )
4415+ rebuild_links_for_page ( page . site , page . id , html )
4416+ rescue
4417+ _ -> :ok
4418+ end
4419+ end )
4420+ end
4421+
43934422 @ doc false
43944423 def handle_call ( { :unpublish_page , page } , _from , config ) do
43954424 case insert_unpublished_page ( page ) do
@@ -4846,53 +4875,65 @@ defmodule Beacon.Content do
48464875 end
48474876
48484877 # ---------------------------------------------------------------------------
4849- # Authors
4878+ # Template Types
48504879 # ---------------------------------------------------------------------------
48514880
4852- @ doc "Creates an author."
4853- @ doc type: :authors
4854- @ spec create_author ( map ( ) ) :: { :ok , Author . t ( ) } | { :error , Ecto.Changeset . t ( ) }
4855- def create_author ( attrs ) do
4856- attrs = attrs |> Beacon.Types.Attrs . ensure_string_keys ( )
4857- site = Beacon.Types.Attrs . get_site ( attrs )
4858- % Author { } |> Author . changeset ( attrs ) |> repo ( site ) . insert ( )
4881+ alias Beacon.Content.TemplateType
4882+
4883+ @ doc "Creates a template type. Pass `site` for site-specific, or set `site: nil` in attrs for global."
4884+ @ doc type: :template_types
4885+ @ spec create_template_type ( Site . t ( ) , map ( ) ) :: { :ok , TemplateType . t ( ) } | { :error , Ecto.Changeset . t ( ) }
4886+ def create_template_type ( site , attrs ) when is_atom ( site ) and is_map ( attrs ) do
4887+ % TemplateType { } |> TemplateType . changeset ( attrs ) |> repo ( site ) . insert ( )
48594888 end
48604889
4861- @ doc "Updates an author ."
4862- @ doc type: :authors
4863- @ spec update_author ( Author . t ( ) , map ( ) ) :: { :ok , Author . t ( ) } | { :error , Ecto.Changeset . t ( ) }
4864- def update_author ( % Author { } = author , attrs ) do
4865- author |> Author . changeset ( attrs ) |> repo ( author ) . update ( )
4890+ @ doc "Updates a template type ."
4891+ @ doc type: :template_types
4892+ @ spec update_template_type ( TemplateType . t ( ) , map ( ) ) :: { :ok , TemplateType . t ( ) } | { :error , Ecto.Changeset . t ( ) }
4893+ def update_template_type ( % TemplateType { } = tt , attrs ) do
4894+ tt |> TemplateType . changeset ( attrs ) |> repo ( tt ) . update ( )
48664895 end
48674896
4868- @ doc "Deletes an author ."
4869- @ doc type: :authors
4870- @ spec delete_author ( Author . t ( ) ) :: { :ok , Author . t ( ) } | { :error , Ecto.Changeset . t ( ) }
4871- def delete_author ( % Author { } = author ) do
4872- repo ( author ) . delete ( author )
4897+ @ doc "Deletes a template type ."
4898+ @ doc type: :template_types
4899+ @ spec delete_template_type ( TemplateType . t ( ) ) :: { :ok , TemplateType . t ( ) } | { :error , Ecto.Changeset . t ( ) }
4900+ def delete_template_type ( % TemplateType { } = tt ) do
4901+ repo ( tt ) . delete ( tt )
48734902 end
48744903
4875- @ doc "Lists all authors for a site."
4876- @ doc type: :authors
4877- @ spec list_authors ( Site . t ( ) , keyword ( ) ) :: [ Author . t ( ) ]
4878- def list_authors ( site , opts \\ [ ] ) when is_atom ( site ) do
4879- per_page = Keyword . get ( opts , :per_page , 100 )
4880- query = from ( a in Author , where: a . site == ^ site , order_by: [ asc: a . name ] )
4881- query = if per_page == :infinity , do: query , else: limit ( query , ^ per_page )
4882- repo ( site ) . all ( query )
4904+ @ doc "Lists template types available to a site (includes global types where site is nil)."
4905+ @ doc type: :template_types
4906+ @ spec list_template_types ( Site . t ( ) , keyword ( ) ) :: [ TemplateType . t ( ) ]
4907+ def list_template_types ( site , opts \\ [ ] ) when is_atom ( site ) do
4908+ from ( tt in TemplateType ,
4909+ where: tt . site == ^ site or is_nil ( tt . site ) ,
4910+ order_by: [ asc: tt . name ]
4911+ )
4912+ |> repo ( site ) . all ( )
48834913 end
48844914
4885- @ doc "Gets an author by ID."
4886- @ doc type: :authors
4887- @ spec get_author ( Site . t ( ) , String . t ( ) ) :: Author . t ( ) | nil
4888- def get_author ( site , id ) when is_atom ( site ) do
4889- repo ( site ) . get_by ( Author , site: site , id: id )
4915+ @ doc "Gets a template type by ID."
4916+ @ doc type: :template_types
4917+ @ spec get_template_type ( Site . t ( ) , String . t ( ) ) :: TemplateType . t ( ) | nil
4918+ def get_template_type ( site , id ) when is_atom ( site ) do
4919+ repo ( site ) . get ( TemplateType , id )
48904920 end
48914921
4892- @ doc "Returns a changeset for tracking author changes."
4893- @ doc type: :authors
4894- def change_author ( % Author { } = author , attrs \\ % { } ) do
4895- Author . changeset ( author , attrs )
4922+ @ doc "Gets a template type by slug (checks site-specific first, then global)."
4923+ @ doc type: :template_types
4924+ @ spec get_template_type_by_slug ( Site . t ( ) , String . t ( ) ) :: TemplateType . t ( ) | nil
4925+ def get_template_type_by_slug ( site , slug ) when is_atom ( site ) do
4926+ # Prefer site-specific over global
4927+ case repo ( site ) . get_by ( TemplateType , site: site , slug: slug ) do
4928+ nil -> repo ( site ) . get_by ( TemplateType , site: nil , slug: slug )
4929+ tt -> tt
4930+ end
4931+ end
4932+
4933+ @ doc "Returns a changeset for tracking template type changes."
4934+ @ doc type: :template_types
4935+ def change_template_type ( % TemplateType { } = tt , attrs \\ % { } ) do
4936+ TemplateType . changeset ( tt , attrs )
48964937 end
48974938
48984939 # ---------------------------------------------------------------------------
@@ -4966,6 +5007,40 @@ defmodule Beacon.Content do
49665007 |> repo ( site ) . all ( )
49675008 end
49685009
5010+ @ doc "Returns per-page inbound and outbound link counts."
5011+ @ doc type: :links
5012+ @ spec list_link_stats ( Site . t ( ) ) :: [ % { page_id: String . t ( ) , path: String . t ( ) , inbound: integer ( ) , outbound: integer ( ) } ]
5013+ def list_link_stats ( site ) when is_atom ( site ) do
5014+ inbound_q =
5015+ from ( l in InternalLink ,
5016+ where: l . site == ^ site and not is_nil ( l . target_page_id ) ,
5017+ group_by: l . target_page_id ,
5018+ select: % { page_id: l . target_page_id , count: count ( l . id ) }
5019+ )
5020+
5021+ outbound_q =
5022+ from ( l in InternalLink ,
5023+ where: l . site == ^ site ,
5024+ group_by: l . source_page_id ,
5025+ select: % { page_id: l . source_page_id , count: count ( l . id ) }
5026+ )
5027+
5028+ inbound_map = repo ( site ) . all ( inbound_q ) |> Map . new ( & { & 1 . page_id , & 1 . count } )
5029+ outbound_map = repo ( site ) . all ( outbound_q ) |> Map . new ( & { & 1 . page_id , & 1 . count } )
5030+
5031+ list_published_pages ( site , per_page: :infinity )
5032+ |> Enum . map ( fn page ->
5033+ % {
5034+ page_id: page . id ,
5035+ path: page . path ,
5036+ title: page . title ,
5037+ inbound: Map . get ( inbound_map , page . id , 0 ) ,
5038+ outbound: Map . get ( outbound_map , page . id , 0 )
5039+ }
5040+ end )
5041+ |> Enum . sort_by ( & & 1 . inbound )
5042+ end
5043+
49695044 # ---------------------------------------------------------------------------
49705045 # SEO Measurement
49715046 # ---------------------------------------------------------------------------
@@ -5016,9 +5091,12 @@ defmodule Beacon.Content do
50165091 """
50175092 @ doc type: :redirects
50185093 @ spec create_redirect ( map ( ) ) :: { :ok , Redirect . t ( ) } | { :error , Ecto.Changeset . t ( ) }
5019- def create_redirect ( attrs ) do
5020- attrs = attrs |> Beacon.Types.Attrs . ensure_string_keys ( )
5021- site = Beacon.Types.Attrs . get_site ( attrs )
5094+ def create_redirect ( attrs ) when is_map ( attrs ) do
5095+ attrs = Map . new ( attrs , fn
5096+ { key , val } when is_binary ( key ) -> { key , val }
5097+ { key , val } -> { Atom . to_string ( key ) , val }
5098+ end )
5099+ { :ok , site } = Beacon.Types.Site . cast ( attrs [ "site" ] )
50225100
50235101 # Flatten chains: if destination is another redirect's source, point to final destination
50245102 attrs = flatten_redirect_chain ( site , attrs )
0 commit comments