Skip to content

Commit 783d201

Browse files
committed
feat(api): Update API endpoints for Astro.js headless frontend
- Property images: Use absolute URLs (rails_blob_url) with variants - Added small/medium/large image variants for responsive images - Added meta_title and meta_description fields for SEO - Configured resolve_asset_host for ASSET_HOST/APP_HOST env vars - Site details: Expanded as_json with frontend-required fields - Added logo_url, favicon_url, css_variables, dark_mode_setting - Added contact_info helper (phone, email, address from agency) - Added social_links helper (structured social media links) - Added top_nav_links and footer_links using as_api_json format - Links: Standardized JSON format for API consumption - Added as_api_json method with consistent field names - Added resolved_url helper for URL generation - Updated LinksController to accept both placement and position params - Theme endpoint: New GET /api_public/v1/theme - Returns CSS variables, colors, fonts, palette info - Enables frontend CSS variable injection for theming
1 parent 1946372 commit 783d201

File tree

7 files changed

+217
-16
lines changed

7 files changed

+217
-16
lines changed

app/controllers/api_public/v1/links_controller.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ module V1
33
class LinksController < BaseController
44

55
def index
6-
placement = params[:placement]
6+
# Accept both 'placement' and 'position' for API compatibility
7+
placement = params[:placement] || params[:position]
78
locale = params[:locale]
89

910
if locale
@@ -18,11 +19,12 @@ def index
1819

1920
# Filter by visibility if needed, similar to GraphQL implementation
2021
if params[:visible_only] == 'true' || placement == 'top_nav' || placement == 'footer'
21-
links = links.where(visible: true)
22+
links = links.where(visible: true).order('sort_order asc')
2223
end
2324

24-
render json: links.as_json
25+
render json: { data: links.map(&:as_api_json) }
2526
end
2627
end
2728
end
2829
end
30+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
module ApiPublic
4+
module V1
5+
# ThemeController provides theming configuration for headless frontends
6+
# Returns CSS variables, colors, and fonts for dynamic theme injection
7+
class ThemeController < BaseController
8+
def index
9+
website = Pwb::Current.website
10+
11+
render json: {
12+
data: {
13+
theme_name: website.theme_name,
14+
dark_mode: website.dark_mode_setting,
15+
colors: website.style_variables,
16+
css_variables: website.css_variables,
17+
css_with_dark_mode: website.css_variables_with_dark_mode,
18+
fonts: extract_fonts(website),
19+
palette: {
20+
selected: website.selected_palette,
21+
available: website.available_palettes.keys
22+
}
23+
}
24+
}
25+
end
26+
27+
private
28+
29+
def extract_fonts(website)
30+
vars = website.style_variables || {}
31+
{
32+
primary: vars["font_primary"] || "Open Sans",
33+
secondary: vars["font_secondary"] || "Vollkorn"
34+
}
35+
end
36+
end
37+
end
38+
end

app/models/concerns/listed_property/photo_accessors.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,26 @@ def ordered_photo(number)
1818
def primary_image_url
1919
first_photo = ordered_photo(1)
2020
if prop_photos.any? && first_photo&.image&.attached?
21-
Rails.application.routes.url_helpers.rails_blob_path(first_photo.image, only_path: true)
21+
Rails.application.routes.url_helpers.rails_blob_url(
22+
first_photo.image,
23+
host: resolve_asset_host
24+
)
2225
else
2326
""
2427
end
2528
end
29+
30+
private
31+
32+
def resolve_asset_host
33+
ENV.fetch('ASSET_HOST') do
34+
ENV.fetch('APP_HOST') do
35+
Rails.application.config.action_controller.asset_host ||
36+
Rails.application.routes.default_url_options[:host] ||
37+
'http://localhost:3000'
38+
end
39+
end
40+
end
2641
end
2742
end
43+

app/models/pwb/link.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,37 @@ def as_json(options = nil)
6969
}.merge(options || {}))
7070
end
7171

72+
# API-formatted JSON for frontend consumption
73+
# Uses consistent field names matching the frontend contract
74+
def as_api_json
75+
{
76+
"id" => id,
77+
"slug" => slug,
78+
"title" => link_title,
79+
"url" => resolved_url,
80+
"position" => placement,
81+
"order" => sort_order,
82+
"visible" => visible,
83+
"external" => is_external || false
84+
}
85+
end
86+
87+
# Resolves the URL for this link
88+
# Prefers link_url (absolute), falls back to generating from link_path
89+
def resolved_url
90+
if link_url.present?
91+
link_url
92+
elsif link_path.present?
93+
# Generate a basic path from the route helper name
94+
path_name = link_path.gsub('_path', '')
95+
"/#{I18n.locale}/#{path_name}"
96+
elsif page_slug.present?
97+
"/#{I18n.locale}/#{page_slug}"
98+
else
99+
"#"
100+
end
101+
end
102+
72103
def admin_attribute_names
73104
mobility_attribute_names
74105
end
@@ -84,3 +115,4 @@ def mobility_attribute_names
84115
end
85116
end
86117
end
118+

app/models/pwb/listed_property.rb

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,77 @@ def extras_for_display
226226
def as_json(options = nil)
227227
super(options).tap do |hash|
228228
hash['prop_photos'] = prop_photos.map do |photo|
229-
if photo.image.attached?
230-
{ 'image' => Rails.application.routes.url_helpers.rails_blob_path(photo.image, only_path: true) }
231-
else
232-
{ 'image' => nil }
233-
end
234-
end
229+
serialize_photo(photo)
230+
end.compact
235231
hash['title'] = title
236232
hash['description'] = description
233+
hash['meta_title'] = generate_meta_title
234+
hash['meta_description'] = generate_meta_description
235+
end
236+
end
237+
238+
private
239+
240+
def serialize_photo(photo)
241+
return nil unless photo.image.attached?
242+
243+
{
244+
'id' => photo.id,
245+
'url' => absolute_image_url(photo.image),
246+
'alt' => photo.respond_to?(:caption) ? photo.caption.presence : nil,
247+
'position' => photo.sort_order,
248+
'variants' => generate_image_variants(photo.image)
249+
}
250+
end
251+
252+
def absolute_image_url(image)
253+
Rails.application.routes.url_helpers.rails_blob_url(
254+
image,
255+
host: resolve_asset_host
256+
)
257+
rescue StandardError => e
258+
Rails.logger.warn("Failed to generate absolute image URL: #{e.message}")
259+
nil
260+
end
261+
262+
def generate_image_variants(image)
263+
return {} unless image.variable?
264+
265+
{
266+
'small' => variant_url(image, resize_to_limit: [300, 200]),
267+
'medium' => variant_url(image, resize_to_limit: [600, 400]),
268+
'large' => variant_url(image, resize_to_limit: [1200, 800])
269+
}
270+
rescue StandardError => e
271+
Rails.logger.warn("Failed to generate image variants: #{e.message}")
272+
{}
273+
end
274+
275+
def variant_url(image, transformations)
276+
Rails.application.routes.url_helpers.rails_representation_url(
277+
image.variant(transformations).processed,
278+
host: resolve_asset_host
279+
)
280+
rescue StandardError
281+
nil
282+
end
283+
284+
def resolve_asset_host
285+
ENV.fetch('ASSET_HOST') do
286+
ENV.fetch('APP_HOST') do
287+
Rails.application.config.action_controller.asset_host ||
288+
Rails.application.routes.default_url_options[:host] ||
289+
'http://localhost:3000'
290+
end
237291
end
238292
end
293+
294+
def generate_meta_title
295+
title.presence || "Property #{reference}"
296+
end
297+
298+
def generate_meta_description
299+
description&.truncate(160)
300+
end
239301
end
240302
end

app/models/pwb/website.rb

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -353,13 +353,63 @@ def as_json(options = nil)
353353
"company_display_name", "theme_name",
354354
"default_area_unit", "default_client_locale",
355355
"available_currencies", "default_currency",
356-
"supported_locales", "social_media",
357-
"raw_css", "analytics_id", "analytics_id_type",
358-
"sale_price_options_from", "sale_price_options_till",
359-
"rent_price_options_from", "rent_price_options_till",
356+
"supported_locales", "dark_mode_setting"
360357
],
361-
methods: ["style_variables", "admin_page_links", "top_nav_display_links",
362-
"footer_display_links", "agency"] }.merge(options || {}))
358+
methods: [
359+
"logo_url", "favicon_url",
360+
"style_variables", "css_variables",
361+
"contact_info", "social_links",
362+
"top_nav_links", "footer_links",
363+
"agency"
364+
] }.merge(options || {}))
365+
end
366+
367+
# API helper: Returns contact information from agency
368+
def contact_info
369+
return {} unless agency
370+
371+
{
372+
"phone" => agency.phone_number_primary,
373+
"phone_mobile" => agency.phone_number_mobile,
374+
"email" => agency.email_primary,
375+
"address" => format_agency_address
376+
}
377+
end
378+
379+
# API helper: Returns structured social media links
380+
def social_links
381+
{
382+
"facebook" => social_media_facebook,
383+
"twitter" => social_media_twitter,
384+
"instagram" => social_media_instagram,
385+
"linkedin" => social_media_linkedin,
386+
"youtube" => social_media_youtube,
387+
"whatsapp" => social_media_whatsapp,
388+
"pinterest" => social_media_pinterest
389+
}.compact
390+
end
391+
392+
# API helper: Returns navigation links formatted for API consumption
393+
def top_nav_links
394+
links.ordered_visible_top_nav.map(&:as_api_json)
395+
end
396+
397+
# API helper: Returns footer links formatted for API consumption
398+
def footer_links
399+
links.ordered_visible_footer.map(&:as_api_json)
400+
end
401+
402+
private
403+
404+
def format_agency_address
405+
return nil unless agency&.primary_address
406+
407+
[
408+
agency.street_address,
409+
agency.city,
410+
agency.postal_code
411+
].compact.reject(&:blank?).join(", ")
363412
end
364413
end
365414
end
415+

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,7 @@
727727
get "/links" => "links#index"
728728
get "/site_details" => "site_details#index"
729729
get "/select_values" => "select_values#index"
730+
get "/theme" => "theme#index"
730731
post "/auth/firebase" => "auth#firebase"
731732

732733
# Embeddable Widget API

0 commit comments

Comments
 (0)