Content struct and parsing functions.
Represents a fully parsed piece of content with HTML body, metadata, and all front matter fields resolved. This is the struct passed to templates.
Parsing
Content can be parsed from a raw string (with YAML front matter) or from
a Sayfa.Content.Raw struct:
iex> {:ok, content} = Sayfa.Content.parse("---\ntitle: Hello\n---\n# World")
iex> content.title
"Hello"
iex> content.body =~ "World</h1>"
trueFront Matter
Known fields are mapped to struct keys. Unknown fields are collected
into the meta map:
---
title: "My Article"
custom_field: "value"
---Results in content.title == "My Article" and content.meta["custom_field"] == "value".
Summary
Functions
Filters contents by content type string.
Extracts a Date from a filename's YYYY-MM-DD- prefix.
Transforms a Sayfa.Content.Raw struct into a Sayfa.Content struct.
Groups contents by category.
Groups contents by {category, lang_prefix} tuple.
Groups contents by tag.
Groups contents by {tag, lang_prefix} tuple.
Parses a raw string containing YAML front matter and Markdown body.
Parses a raw string, raising on error.
Reads and parses a content file from disk.
Reads a file and returns a Sayfa.Content.Raw struct without rendering Markdown.
Returns the N most recent contents (sorted by date descending).
Generates a URL-friendly slug from a filename.
Sorts contents by date.
Returns the URL path for a content item.
Filters contents that have the given category.
Filters contents that have the given tag.
Types
Functions
Filters contents by content type string.
Examples
iex> articles = [%Sayfa.Content{title: "A", body: "", meta: %{"content_type" => "articles"}},
...> %Sayfa.Content{title: "B", body: "", meta: %{"content_type" => "pages"}}]
iex> Sayfa.Content.all_of_type(articles, "articles") |> length()
1
Extracts a Date from a filename's YYYY-MM-DD- prefix.
Returns nil if the filename has no date prefix or is nil.
Examples
iex> Sayfa.Content.date_from_filename("2024-01-15-hello-world.md")
~D[2024-01-15]
iex> Sayfa.Content.date_from_filename("about.md")
nil
iex> Sayfa.Content.date_from_filename(nil)
nil
@spec from_raw(Sayfa.Content.Raw.t(), map()) :: {:ok, t()} | {:error, term()}
Transforms a Sayfa.Content.Raw struct into a Sayfa.Content struct.
Renders the Markdown body to HTML and maps front matter fields.
An optional config map may be passed as the second argument. The
highlight_theme key selects the syntax highlighting theme (default: "github_light").
Examples
iex> raw = %Sayfa.Content.Raw{
...> path: "content/articles/hello.md",
...> front_matter: %{"title" => "Hello"},
...> body_markdown: "# World"
...> }
iex> {:ok, content} = Sayfa.Content.from_raw(raw)
iex> content.title
"Hello"
Groups contents by category.
Returns a map where each key is a category and the value is a list of contents that have that category.
Examples
iex> contents = [%Sayfa.Content{title: "A", body: "", categories: ["programming"]},
...> %Sayfa.Content{title: "B", body: "", categories: ["programming", "elixir"]}]
iex> groups = Sayfa.Content.group_by_category(contents)
iex> length(groups["programming"])
2
Groups contents by {category, lang_prefix} tuple.
Returns a map where each key is a {category, lang_prefix} tuple and the value
is a list of contents that have that category in that language.
Examples
iex> contents = [
...> %Sayfa.Content{title: "A", body: "", categories: ["programming"], meta: %{"lang_prefix" => ""}},
...> %Sayfa.Content{title: "B", body: "", categories: ["programming"], meta: %{"lang_prefix" => "tr"}}
...> ]
iex> groups = Sayfa.Content.group_by_category_and_lang(contents)
iex> length(groups[{"programming", ""}])
1
iex> length(groups[{"programming", "tr"}])
1
Groups contents by tag.
Returns a map where each key is a tag and the value is a list of contents that have that tag.
Examples
iex> contents = [%Sayfa.Content{title: "A", body: "", tags: ["elixir", "otp"]},
...> %Sayfa.Content{title: "B", body: "", tags: ["elixir"]}]
iex> groups = Sayfa.Content.group_by_tag(contents)
iex> length(groups["elixir"])
2
iex> length(groups["otp"])
1
Groups contents by {tag, lang_prefix} tuple.
Returns a map where each key is a {tag, lang_prefix} tuple and the value
is a list of contents that have that tag in that language.
Examples
iex> contents = [
...> %Sayfa.Content{title: "A", body: "", tags: ["elixir"], meta: %{"lang_prefix" => ""}},
...> %Sayfa.Content{title: "B", body: "", tags: ["elixir"], meta: %{"lang_prefix" => "tr"}}
...> ]
iex> groups = Sayfa.Content.group_by_tag_and_lang(contents)
iex> length(groups[{"elixir", ""}])
1
iex> length(groups[{"elixir", "tr"}])
1
Parses a raw string containing YAML front matter and Markdown body.
The string must have front matter delimited by --- lines at the top.
Examples
iex> {:ok, content} = Sayfa.Content.parse("---\ntitle: Hello\n---\n# World")
iex> content.title
"Hello"
Parses a raw string, raising on error.
Examples
iex> content = Sayfa.Content.parse!("---\ntitle: Hello\n---\n# World")
iex> content.title
"Hello"
Reads and parses a content file from disk.
Examples
Sayfa.Content.parse_file("content/articles/2024-01-15-hello.md")
@spec parse_raw_file(String.t()) :: {:ok, Sayfa.Content.Raw.t()} | {:error, term()}
Reads a file and returns a Sayfa.Content.Raw struct without rendering Markdown.
This is used by the builder to allow hooks to modify the raw content before Markdown rendering.
Examples
{:ok, raw} = Sayfa.Content.parse_raw_file("content/articles/hello.md")
raw.front_matter["title"]
#=> "Hello"
@spec recent([t()], pos_integer()) :: [t()]
Returns the N most recent contents (sorted by date descending).
Examples
iex> contents = [%Sayfa.Content{title: "A", body: "", date: ~D[2024-01-01]},
...> %Sayfa.Content{title: "B", body: "", date: ~D[2024-06-01]},
...> %Sayfa.Content{title: "C", body: "", date: ~D[2024-03-01]}]
iex> Sayfa.Content.recent(contents, 2) |> Enum.map(& &1.title)
["B", "C"]
Generates a URL-friendly slug from a filename.
Strips date prefixes (e.g., 2024-01-15-) and the .md extension.
Examples
iex> Sayfa.Content.slug_from_filename("2024-01-15-hello-world.md")
"hello-world"
iex> Sayfa.Content.slug_from_filename("about.md")
"about"
iex> Sayfa.Content.slug_from_filename(nil)
nil
Sorts contents by date.
Items with nil dates are pushed to the end.
Options
:desc(default) — newest first:asc— oldest first
Examples
iex> contents = [%Sayfa.Content{title: "Old", body: "", date: ~D[2024-01-01]},
...> %Sayfa.Content{title: "New", body: "", date: ~D[2024-06-01]}]
iex> sorted = Sayfa.Content.sort_by_date(contents)
iex> hd(sorted).title
"New"
Returns the URL path for a content item.
Combines lang_prefix, url_prefix, and slug from the content's metadata
to build the correct path. This is the single source of truth for content URLs.
Examples
iex> content = %Sayfa.Content{title: "T", body: "", slug: "hello", meta: %{"url_prefix" => "articles", "lang_prefix" => ""}}
iex> Sayfa.Content.url(content)
"/articles/hello"
iex> content = %Sayfa.Content{title: "T", body: "", slug: "merhaba", meta: %{"url_prefix" => "articles", "lang_prefix" => "tr"}}
iex> Sayfa.Content.url(content)
"/tr/articles/merhaba"
iex> content = %Sayfa.Content{title: "T", body: "", slug: "index", meta: %{"url_prefix" => "", "lang_prefix" => ""}}
iex> Sayfa.Content.url(content)
"/"
Filters contents that have the given category.
Examples
iex> contents = [%Sayfa.Content{title: "A", body: "", categories: ["programming"]},
...> %Sayfa.Content{title: "B", body: "", categories: ["cooking"]}]
iex> Sayfa.Content.with_category(contents, "programming") |> length()
1
Filters contents that have the given tag.
Examples
iex> contents = [%Sayfa.Content{title: "A", body: "", tags: ["elixir", "otp"]},
...> %Sayfa.Content{title: "B", body: "", tags: ["rust"]}]
iex> Sayfa.Content.with_tag(contents, "elixir") |> length()
1