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
85 changes: 59 additions & 26 deletions lib/mcp_registry_web/components/layouts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -231,23 +231,33 @@ defmodule McpRegistryWeb.Layouts do
The book is a live affiliate link and carries `rel="sponsored"`, which is
what Google asks for on paid or affiliate placements — without it the link
reads as an editorial endorsement and puts the whole page's ranking at risk.
The other three are unsold placeholders and are not links at all.
The other three are unsold, and open a pre-filled enquiry email so an
interested buyer can name a price without leaving the page.

Image paths go through `~p`, which appends the digest that
`Plug.Static` needs before it will answer with
`cache-control: max-age=31536000, immutable`. Referenced as plain strings
they come back with a bare `cache-control: public`, and every repeat visit
re-fetches the artwork.

## Examples

<Layouts.sponsors />
"""
attr :class, :any, default: nil

@enquiry_subject "Interested Sponsor for MCP Harbor"
@enquiry_body "I'm interested in bidding $____ for a Sponsor position on MCP Harbor."

@sponsor_slots [
%{
image: "/images/Book.png",
alt: "Book cover — buy on Amazon",
href: "https://amzn.to/4cPvd4j"
file: "Book.png",
alt: "Sponsored: book cover — opens Amazon",
kind: :affiliate
},
%{image: "/images/sponsor-1.png", alt: "Sponsor slot one", href: nil},
%{image: "/images/sponsor-2.png", alt: "Sponsor slot two", href: nil},
%{image: "/images/sponsor-3.png", alt: "Sponsor slot three", href: nil}
%{file: "sponsor-1.png", alt: "Sponsor slot available — email to enquire", kind: :enquiry},
%{file: "sponsor-2.png", alt: "Sponsor slot available — email to enquire", kind: :enquiry},
%{file: "sponsor-3.png", alt: "Sponsor slot available — email to enquire", kind: :enquiry}
]

def sponsors(assigns) do
Expand Down Expand Up @@ -279,40 +289,63 @@ defmodule McpRegistryWeb.Layouts do
`scale: 1.4` while `transform` stays `none`. --%>
<ul class="flex flex-col items-center gap-3">
<li :for={slot <- @slots} class="relative z-0 hover:z-20">
<%!-- The affiliate link opens in a new tab and is marked sponsored.
The enquiry links are mailto: -- no target, because a new tab
for a mail client leaves a blank window behind, and no
rel="sponsored", which describes paid outbound links and means
nothing on a mailto. --%>
<a
:if={slot.href}
href={slot.href}
target="_blank"
rel="sponsored noopener noreferrer"
class={[sponsor_tile(), "border-rule hover:border-brand/60"]}
href={slot_href(slot)}
target={if slot.kind == :affiliate, do: "_blank"}
rel={if slot.kind == :affiliate, do: "sponsored noopener noreferrer"}
class={[
sponsor_tile(),
if(slot.kind == :affiliate,
do: "border-rule hover:border-brand/60",
else: "border-dashed border-rule hover:border-brand/60"
)
]}
>
<img
src={slot.image}
src={~p"/images/#{slot.file}"}
alt={slot.alt}
loading="lazy"
decoding="async"
class="size-full object-contain"
/>
</a>

<div
:if={is_nil(slot.href)}
class={[sponsor_tile(), "border-dashed border-rule hover:border-rule-strong"]}
>
<img
src={slot.image}
alt={slot.alt}
loading="lazy"
decoding="async"
class="size-full object-contain"
/>
</div>
</li>
</ul>

<p class="text-center text-[11px] text-pretty text-dim">
Want a slot?
<a
href={enquiry_mailto()}
class="underline decoration-rule-strong underline-offset-4 transition-colors hover:decoration-brand"
>
Make an offer
</a>
</p>
</section>
"""
end

defp slot_href(%{kind: :affiliate}), do: "https://amzn.to/4cPvd4j"
defp slot_href(%{kind: :enquiry}), do: enquiry_mailto()

# Built rather than written out, so the subject and body are escaped once and
# correctly. URI.encode_www_form/1 is wrong here: it encodes a space as "+",
# which mail clients paste into the subject line literally.
defp enquiry_mailto do
query =
URI.encode_query(
[subject: @enquiry_subject, body: @enquiry_body],
:rfc3986
)

"mailto:me@loganbesecker.com?" <> query
end

# The whole tile scales, border and all, so it reads as the slot growing
# rather than the artwork straining against a fixed frame.
defp sponsor_tile do
Expand Down
43 changes: 43 additions & 0 deletions test/mcp_registry_web/components/sponsors_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
defmodule McpRegistryWeb.SponsorsTest do
use McpRegistryWeb.ConnCase, async: true

import Phoenix.LiveViewTest
import McpRegistry.RegistryFixtures

test "the sponsored block links the affiliate slot and offers the rest for sale", %{conn: conn} do
server = server_fixture()
{:ok, _view, html} = live(conn, "/servers/#{server.name}")

# The paid placement must carry rel="sponsored", or the link reads to
# search engines as an editorial endorsement.
assert html =~ "https://amzn.to/4cPvd4j"
assert html =~ ~s(rel="sponsored noopener noreferrer")

# Unsold slots open a pre-filled enquiry rather than going nowhere.
assert html =~ "mailto:me@loganbesecker.com"
assert html =~ "Interested%20Sponsor%20for%20MCP%20Harbor"
assert html =~ "bidding%20%24____"

# Spaces must not be encoded as "+", which mail clients paste literally
# into the subject line.
refute html =~ "Interested+Sponsor"
end

test "sponsor artwork is addressed through the static path helper", %{conn: conn} do
server = server_fixture()
{:ok, _view, html} = live(conn, "/servers/#{server.name}")

# Plug.Static only answers with `max-age=31536000, immutable` for a request
# carrying the asset digest; a bare path gets `cache-control: public` and
# every repeat visit refetches roughly 1.8MB of artwork.
#
# The digest itself only exists once `mix phx.digest` has run, which is the
# release build and not the test run — so what is pinned here is that the
# markup asks the endpoint for the path rather than hardcoding it. Get that
# wrong and production silently loses the caching.
for file <- ~w(Book.png sponsor-1.png sponsor-2.png sponsor-3.png) do
expected = McpRegistryWeb.Endpoint.static_path("/images/#{file}")
assert html =~ ~s(src="#{expected}"), "#{file} was not addressed via static_path/1"
end
end
end
Loading