add SEO enhancements: OG images, meta robots, FAQ block, image sitemap
All checks were successful
deploy / deploy (push) Successful in 4m59s
All checks were successful
deploy / deploy (push) Successful in 4m59s
- Per-page SEO controls: meta robots directives, focus keyword, OG image - Site-wide default OG image in admin settings - FAQ block type with FAQPage JSON-LD schema - Enhanced Organization JSON-LD with business info, contact, address - Image sitemap with product images - SEO preview panel with Google/social card mockups - SEO checklist with real-time scoring - Business info section in site editor - GSC integration scaffolding (OAuth, client, cache) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
defmodule BerrypodWeb.Admin.Settings do
|
||||
use BerrypodWeb, :live_view
|
||||
|
||||
alias Berrypod.Media
|
||||
alias Berrypod.Products
|
||||
alias Berrypod.Settings
|
||||
alias Berrypod.Stripe.Setup, as: StripeSetup
|
||||
@@ -19,7 +20,17 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
|> assign(:signing_secret_status, :idle)
|
||||
|> assign_stripe_state()
|
||||
|> assign_products_state()
|
||||
|> assign_url_prefixes()}
|
||||
|> assign_url_prefixes()
|
||||
|> assign_og_image_state()}
|
||||
end
|
||||
|
||||
defp assign_og_image_state(socket) do
|
||||
og_image = Media.get_default_og_image()
|
||||
|
||||
socket
|
||||
|> assign(:og_image, og_image)
|
||||
|> assign(:og_picker_open, false)
|
||||
|> assign(:og_picker_images, [])
|
||||
end
|
||||
|
||||
defp assign_url_prefixes(socket) do
|
||||
@@ -170,6 +181,44 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
end
|
||||
end
|
||||
|
||||
# -- Events: OG image --
|
||||
|
||||
def handle_event("show_og_picker", _params, socket) do
|
||||
images = Media.list_images() |> Enum.take(50)
|
||||
{:noreply, assign(socket, og_picker_open: true, og_picker_images: images)}
|
||||
end
|
||||
|
||||
def handle_event("hide_og_picker", _params, socket) do
|
||||
{:noreply, assign(socket, og_picker_open: false)}
|
||||
end
|
||||
|
||||
def handle_event("pick_og_image", %{"id" => id}, socket) do
|
||||
image = Media.get_image(id)
|
||||
|
||||
if image do
|
||||
Media.update_image_type(image, "default_og")
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:og_image, image)
|
||||
|> assign(:og_picker_open, false)
|
||||
|> put_flash(:info, "Default social image set")}
|
||||
else
|
||||
{:noreply, put_flash(socket, :error, "Image not found")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("clear_og_image", _params, socket) do
|
||||
if socket.assigns.og_image do
|
||||
Media.update_image_type(socket.assigns.og_image, "media")
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:og_image, nil)
|
||||
|> put_flash(:info, "Default social image removed")}
|
||||
end
|
||||
|
||||
# -- Events: Stripe --
|
||||
|
||||
def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do
|
||||
@@ -502,10 +551,113 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%!-- Default social image --%>
|
||||
<section class="admin-section">
|
||||
<h2 class="admin-section-title">Default social image</h2>
|
||||
<p class="admin-section-desc">
|
||||
The image shown when pages are shared on social media.
|
||||
Individual pages can override this in their settings.
|
||||
</p>
|
||||
<div class="admin-section-body">
|
||||
<%= if @og_image do %>
|
||||
<div class="page-settings-og-preview">
|
||||
<img
|
||||
src={og_image_url(@og_image)}
|
||||
alt="Current social image"
|
||||
class="page-settings-og-thumb"
|
||||
/>
|
||||
<div class="page-settings-og-actions">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="show_og_picker"
|
||||
class="admin-btn admin-btn-outline admin-btn-sm"
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="clear_og_image"
|
||||
class="admin-link-danger"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="show_og_picker"
|
||||
class="admin-btn admin-btn-outline admin-btn-sm"
|
||||
>
|
||||
<.icon name="hero-photo" class="size-4" /> Choose image
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<.og_picker_modal
|
||||
:if={@og_picker_open}
|
||||
images={@og_picker_images}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp og_picker_modal(assigns) do
|
||||
~H"""
|
||||
<div class="admin-modal-backdrop" phx-click="hide_og_picker">
|
||||
<div class="admin-modal admin-modal-lg" phx-click-away="hide_og_picker">
|
||||
<div class="admin-modal-header">
|
||||
<h3>Choose social image</h3>
|
||||
<button type="button" phx-click="hide_og_picker" class="admin-modal-close">
|
||||
<.icon name="hero-x-mark" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="admin-modal-body">
|
||||
<p class="admin-help-text" style="margin-bottom: 1rem;">
|
||||
Choose an image from your media library.
|
||||
Recommended size: 1200×630 pixels.
|
||||
</p>
|
||||
<%= if @images == [] do %>
|
||||
<p class="admin-text-secondary">
|
||||
No images in your media library.
|
||||
<.link navigate={~p"/admin/media"} class="admin-link">Upload images</.link>
|
||||
first.
|
||||
</p>
|
||||
<% else %>
|
||||
<div class="og-picker-grid">
|
||||
<%= for image <- @images do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="pick_og_image"
|
||||
phx-value-id={image.id}
|
||||
class="og-picker-item"
|
||||
>
|
||||
<img src={og_image_url(image)} alt={image.filename} />
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp og_image_url(image) do
|
||||
if image.is_svg do
|
||||
"/image_cache/#{image.id}.webp"
|
||||
else
|
||||
applicable_width =
|
||||
image.source_width
|
||||
|> Berrypod.Images.Optimizer.applicable_widths()
|
||||
|> Enum.find(&(&1 >= 400))
|
||||
|
||||
"/image_cache/#{image.id}-#{applicable_width || 400}.webp"
|
||||
end
|
||||
end
|
||||
|
||||
# -- Function components --
|
||||
|
||||
attr :color, :string, required: true
|
||||
|
||||
Reference in New Issue
Block a user