Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Used by "mix format"
[
import_deps: [:phoenix],
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
inputs: [
"{mix,.formatter}.exs",
"{config,lib,test}/**/*.{ex,exs}",
"priv/wrappers/*.ex"
]
]
25 changes: 11 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,26 @@ jobs:
- uses: actions/checkout@v2
- uses: erlef/setup-beam@v1
with:
otp-version: '27'
elixir-version: '1.18'
otp-version: '28'
elixir-version: '1.19'
- run: mix deps.get --only dev
- run: mix format --check-formatted
- run: mix credo --strict

test:
runs-on: ubuntu-latest
name: Tests on latest version
name: Tests on oldest version
env:
MIX_ENV: test
CI: "true"
steps:
- uses: actions/checkout@v2
- uses: erlef/setup-beam@v1
with:
otp-version: '27'
elixir-version: '1.18'
otp-version: '25'
elixir-version: '1.17'
- run: rm mix.lock
- run: mix deps.get --only test
- run: mix deps.get
- run: mix compile --force --warnings-as-errors
- run: mix test --warnings-as-errors

Expand All @@ -49,14 +49,11 @@ jobs:
- uses: actions/checkout@v2
- uses: erlef/setup-beam@v1
with:
otp-version: '27'
elixir-version: '1.18'
otp-version: '28'
elixir-version: '1.19'
- run: rm mix.lock
- run: mix deps.get --only test
- run: mix deps.get
- run: mix compile --force --warnings-as-errors
- run: mix blend.get
- run: BLEND=phoenix_live_view_v0_18 mix test
- run: BLEND=phoenix_live_view_v0_19 mix test
- run: BLEND=phoenix_live_view_v0_20 mix test
- run: BLEND=phoenix_live_view_v1_0 mix test
- run: BLEND=phoenix_live_view_v1_1 mix test
- run: BLEND=phoenix_live_view_v1_0 mix deps.get && mix test
- run: BLEND=phoenix_live_view_v1_1 mix deps.get && mix test
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## 0.3.0 (2026-04-16)

- **breaking** Require Elixir >=1.17
- **breaking** LLMs behaviours changed arity to thread the Plug.Conn through
- **breaking** JSONLD support. See `SEO.JSONLD` modules. Thanks @Flo0807. Anyone using `SEO.Breadcrumb` will need to replace with `SEO.JSONLD.Breadcrumbs`
- JSON-LD modules (`SEO.JSONLD.*`) are now generated at compile time via a
Mix compiler instead of being prebuilt in the package. Register the
compiler in your `mix.exs` (`compilers: [:seo_jsonld] ++ Mix.compilers()`)
and pick which Schema.org types to materialize via
`config :phoenix_seo, json_ld_types: :all` (default is `:google` — the
~24 rich-result types plus their closure). Accepts a single entry or a
list. See the README "Installation" section for details.
- The `:config` attr on `<SEO.JSONLD.meta />` is removed along with the
site-wide `json_ld:` config slot. `"@context"` is now applied to the
top-level node by `SEO.JSONLD.meta/1` only, so nested typed builders no
longer emit redundant per-node contexts.
- Convenience wrappers (`SEO.JSONLD.Breadcrumbs`, `SEO.JSONLD.FAQ`,
`SEO.JSONLD.Actions`) are emitted only when their dependent modules are
present in the compiled closure.
## 0.2.1 (2026-04-13)

- Fixup llms.txt rendering with module/function configs.
Expand Down
88 changes: 64 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,50 @@ def deps do
end
```

Register the JSON-LD compiler in your `mix.exs` so `SEO.JSONLD.*` builder
modules are generated at compile time:

```elixir
def project do
[
# ...
compilers: [:seo_jsonld] ++ Mix.compilers()
]
end
```

By default this emits the ~24 types Google has rich-result guides for,
along with the supporting types those guides reference (`Question`,
`Answer`, `ListItem`) and the closure of any types referenced through
field ranges and inheritance — about 200 modules in total.

If you need the full vocabulary (~820 typed builder modules) or a
different slice, override the default via application config — a single
entry or a list of entries:

```elixir
# config/config.exs
config :phoenix_seo, json_ld_types: :all
# or mix-and-match:
config :phoenix_seo, json_ld_types: [:google, SEO.JSONLD.SearchAction]
```

Available entries:

- `:google` — types Google has rich-result guides for plus their
supporting types. **Default if `:json_ld_types` is not configured.**
- `:all` — every regular Schema.org class.
- Category atoms: `:medical`, `:place`, `:travel`, `:shopping`,
`:creative_work`, `:action`, `:financial`, etc. See
`Mix.Tasks.Compile.SeoJsonld.Generator.groups/0` for the full list.
- Module names like `SEO.JSONLD.Article`, or strings like `"Article"` /
`"schema:Article"`.

Inheritance ancestors and referenced types are pulled into the emitted
set automatically — so the typespecs and module links always resolve.
Modules are written to your application's `_build` directory, not into
the dep tree.

## Usage

1. Define an SEO module for your web application and defaults
Expand Down Expand Up @@ -151,30 +195,26 @@ defimpl SEO.Unfurl.Build, for: MyApp.Article do
end
end

defimpl SEO.Breadcrumb.Build, for: MyApp.Article do
defimpl SEO.JSONLD.Build, for: MyApp.Article do
use MyAppWeb, :verified_routes

def build(article, conn) do
# Because of `Phoenix.Param`, structs will assume the key of `:id` when
# interpolating the struct into the verified route.
SEO.Breadcrumb.List.build([
%{name: "Articles", item: url(conn, ~p"/articles")},
%{name: article.title, item: url(conn, ~p"/articles/#{article}")}
])
end
end

defimpl SEO.JsonLD.Build, for: MyApp.Article do
use MyAppWeb, :verified_routes

def build(article, conn) do
SEO.JsonLD.Article.build(
headline: article.title,
description: article.description,
datePublished: article.published_at,
author: %{"@type" => "Person", "name" => article.author},
mainEntityOfPage: url(conn, ~p"/articles/#{article}")
)
# interpolating the struct into the verified route. Emit multiple JSON-LD
# entities by returning a list — breadcrumbs sit alongside the article.
[
SEO.JSONLD.Article.build(%{
headline: article.title,
description: article.description,
date_published: article.published_at,
author: SEO.JSONLD.Person.build(%{name: article.author}),
main_entity_of_page: url(conn, ~p"/articles/#{article}")
}),
SEO.JSONLD.Breadcrumbs.build([
%{name: "Articles", item: url(conn, ~p"/articles")},
%{name: article.title, item: url(conn, ~p"/articles/#{article}")}
])
]
end
end
```
Expand Down Expand Up @@ -277,13 +317,13 @@ defmodule MyAppWeb.ArticleMD do
"""
end

# llms.txt entry — called by your Provider
# llms.txt entry — called by your Provider with the current conn
@impl SEO.LLMs
def entry(article) do
def entry(article, conn) do
SEO.LLMs.Entry.build(
section: "Articles",
title: article.title,
url: ~p"/articles/#{article}",
url: url(conn, ~p"/articles/#{article}"),
description: article.summary
)
end
Expand Down Expand Up @@ -418,4 +458,4 @@ integration, nested sub-sections, and inline markdown content.
> defaults. Technically, your implementation doesn't have to return an
> `SEO.OpenGraph` struct, but it's very handy since documentation is present on the
> build function so your editor can quickly show you what is available. Knowing is
> half the battle!
> half the battle!
Binary file added assets/article-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/book-actions-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/breadcrumb-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/course-list-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/dataset-search-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/discussion-forum-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/education-qa-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/employer-aggregate-rating-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/event-details-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/event-result-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/factcheck-example-result.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/faqpage-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/image-metadata-example-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/image-metadata-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/jobs-search-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/local-business-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/loyalty-program-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/math-solvers-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/movie-carousel-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/organization-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/product-snippet-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/product-variants-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/qa-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/recipe-example-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/recipe-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/return-policy-example.png
Binary file added assets/review-snippet-example.png
Binary file added assets/shipping-policy-example.png
Binary file added assets/shopping-knowledge-panel-example.png
Binary file added assets/software-apps-example.png
Binary file added assets/vacation-rental-example.png
Binary file added assets/video-example.png
3 changes: 0 additions & 3 deletions blend.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
%{
phoenix_live_view_v0_18: [{:phoenix_live_view, "~> 0.18.0"}],
phoenix_live_view_v0_19: [{:phoenix_live_view, "~> 0.19.0"}],
phoenix_live_view_v0_20: [{:phoenix_live_view, "~> 0.20.0"}],
phoenix_live_view_v1_0: [{:phoenix_live_view, "~> 1.0.0"}],
phoenix_live_view_v1_1: [{:phoenix_live_view, "~> 1.1.0"}]
}
28 changes: 0 additions & 28 deletions blend/phoenix_live_view_v0_18.mix.lock

This file was deleted.

Loading
Loading