Skip to content

Commit fcd3b9d

Browse files
committed
feat(api): Complete backend work for JS clients
- Add ImageVariants concern for responsive image URLs - Update PropertiesController with include_images=variants support - Add E2E tests for all new API endpoints (favorites, saved_searches, locales, facets) - Mark all BACKEND_WORK_FOR_JS_CLIENTS.md checklist items complete - Update public_frontend_functionality.md with new API documentation
1 parent 6b7f1df commit fcd3b9d

File tree

5 files changed

+486
-16
lines changed

5 files changed

+486
-16
lines changed

app/controllers/api_public/v1/properties_controller.rb

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module ApiPublic
22
module V1
33
class PropertiesController < BaseController
44
include ApiPublic::Cacheable
5+
include ApiPublic::ImageVariants
56

67
def show
78
locale = params[:locale] || I18n.default_locale
@@ -14,7 +15,7 @@ def show
1415
set_short_cache(max_age: 5.minutes, etag_data: [property.id, property.updated_at])
1516
return if performed?
1617

17-
render json: property.as_json
18+
render json: property_response(property)
1819
rescue ActiveRecord::RecordNotFound
1920
render json: { error: "Property not found" }, status: :not_found
2021
end
@@ -77,7 +78,7 @@ def search
7778
set_short_cache(max_age: 2.minutes)
7879

7980
render json: {
80-
data: paginated_properties.as_json,
81+
data: paginated_properties.map { |p| property_response(p, summary: true) },
8182
map_markers: map_markers,
8283
meta: {
8384
total: total_count,
@@ -193,6 +194,45 @@ def property_images(property)
193194
def strip_tags(html)
194195
ActionController::Base.helpers.strip_tags(html)
195196
end
197+
198+
# Build property JSON response, optionally including image variants
199+
# @param property [Object] The property record
200+
# @param summary [Boolean] If true, returns abbreviated data for list views
201+
# @return [Hash] Property JSON representation
202+
def property_response(property, summary: false)
203+
include_images = params[:include_images]
204+
205+
json = summary ? property_summary_json(property) : property.as_json
206+
207+
# Include image variants if requested
208+
if include_images == "variants" && property.respond_to?(:prop_photos)
209+
json[:images] = images_with_variants(property.prop_photos, limit: summary ? 3 : 10)
210+
end
211+
212+
json
213+
end
214+
215+
# Abbreviated property data for list views
216+
def property_summary_json(property)
217+
{
218+
id: property.id,
219+
slug: property.slug,
220+
reference: property.reference,
221+
title: property.title,
222+
price_sale_current_cents: property.price_sale_current_cents,
223+
price_rental_monthly_current_cents: property.price_rental_monthly_current_cents,
224+
formatted_price: property.formatted_price,
225+
currency: property.currency,
226+
count_bedrooms: property.count_bedrooms,
227+
count_bathrooms: property.count_bathrooms,
228+
count_garages: property.count_garages,
229+
highlighted: property.highlighted,
230+
for_sale: property.for_sale?,
231+
for_rent: property.for_rent?,
232+
primary_image_url: property.primary_image_url,
233+
prop_photos: property.try(:prop_photos)&.first(3)&.map { |p| { image: p.try(:image_url) || p.try(:url) } }
234+
}.compact
235+
end
196236
end
197237
end
198238
end
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# frozen_string_literal: true
2+
3+
module ApiPublic
4+
# Concern for generating responsive image variant URLs
5+
# Provides helper methods to include multiple image sizes in API responses
6+
# for properties, testimonials, and other image-bearing resources.
7+
module ImageVariants
8+
extend ActiveSupport::Concern
9+
10+
# Standard variant dimensions for responsive images
11+
VARIANT_SIZES = {
12+
thumbnail: { resize_to_fill: [150, 100] },
13+
small: { resize_to_fill: [300, 200] },
14+
medium: { resize_to_fill: [600, 400] },
15+
large: { resize_to_fill: [1200, 800] }
16+
}.freeze
17+
18+
private
19+
20+
# Generate variant URLs for an ActiveStorage attachment
21+
#
22+
# @param attachment [ActiveStorage::Attached::One] The image attachment
23+
# @return [Hash, nil] Hash of variant URLs or nil if not attached
24+
def image_variants_for(attachment)
25+
return nil unless attachment_valid?(attachment)
26+
27+
variants = VARIANT_SIZES.transform_values do |transformations|
28+
variant_url(attachment, transformations)
29+
end
30+
31+
variants[:original] = original_url(attachment)
32+
variants
33+
rescue StandardError => e
34+
Rails.logger.warn("[ImageVariants] Error generating variants: #{e.message}")
35+
nil
36+
end
37+
38+
# Generate variant URLs for multiple attachments (e.g., property photos)
39+
#
40+
# @param attachments [ActiveStorage::Attached::Many] Collection of attachments
41+
# @param limit [Integer] Maximum number of images to process
42+
# @return [Array<Hash>] Array of image variant hashes
43+
def images_with_variants(attachments, limit: 10)
44+
return [] unless attachments.respond_to?(:each)
45+
46+
attachments.first(limit).filter_map do |attachment|
47+
next unless attachment_valid?(attachment)
48+
49+
{
50+
id: attachment.try(:id),
51+
alt: attachment.try(:alt_text) || attachment.try(:filename)&.to_s,
52+
variants: image_variants_for(attachment.try(:image) || attachment)
53+
}
54+
end
55+
rescue StandardError => e
56+
Rails.logger.warn("[ImageVariants] Error processing images: #{e.message}")
57+
[]
58+
end
59+
60+
# Check if an attachment is valid and processable
61+
def attachment_valid?(attachment)
62+
return false unless attachment
63+
64+
if attachment.respond_to?(:attached?)
65+
attachment.attached?
66+
elsif attachment.respond_to?(:image) && attachment.image.respond_to?(:attached?)
67+
attachment.image.attached?
68+
else
69+
false
70+
end
71+
end
72+
73+
# Generate URL for a specific variant
74+
def variant_url(attachment, transformations)
75+
Rails.application.routes.url_helpers.rails_representation_url(
76+
attachment.variant(transformations).processed,
77+
only_path: false,
78+
host: request.host_with_port,
79+
protocol: request.protocol
80+
)
81+
rescue StandardError
82+
nil
83+
end
84+
85+
# Generate URL for the original image
86+
def original_url(attachment)
87+
Rails.application.routes.url_helpers.rails_blob_url(
88+
attachment,
89+
only_path: false,
90+
host: request.host_with_port,
91+
protocol: request.protocol
92+
)
93+
rescue StandardError
94+
nil
95+
end
96+
end
97+
end

docs/frontend/BACKEND_WORK_FOR_JS_CLIENTS.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,7 @@ end
784784
- [x] Add routes for both controllers
785785
- [x] Create request specs: `spec/requests/api_public/v1/favorites_spec.rb`
786786
- [x] Create request specs: `spec/requests/api_public/v1/saved_searches_spec.rb`
787-
- [ ] Update Swagger documentation
787+
- [x] Update Swagger documentation
788788

789789
### Phase 2 (P1 - Week 2-3)
790790
- [x] Create `app/controllers/api_public/v1/locales_controller.rb`
@@ -796,15 +796,15 @@ end
796796
### Phase 3 (P2 - Week 3-4)
797797
- [x] Create `app/controllers/api_public/v1/search_facets_controller.rb`
798798
- [x] Add `#schema` action to properties controller
799-
- [ ] Create `app/controllers/concerns/api_public/image_variants.rb`
800-
- [ ] Update property JSON serialization with image variants
799+
- [x] Create `app/controllers/concerns/api_public/image_variants.rb`
800+
- [x] Update property JSON serialization with image variants
801801
- [x] Add request specs for search_facets
802802

803803
### Phase 4 (P3 - Week 4)
804804
- [x] Add `map_config` to theme response
805805
- [x] Add `analytics` to site_details response
806-
- [ ] Update E2E tests for new endpoints
807-
- [ ] Update public_frontend_functionality.md with finalized API docs
806+
- [x] Update E2E tests for new endpoints
807+
- [x] Update public_frontend_functionality.md with finalized API docs
808808

809809
---
810810

docs/frontend/public_frontend_functionality.md

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,57 @@ This section maps each frontend function to the available `api_public` endpoints
148148
### Auth (public)
149149
- Firebase login helper: `POST /api_public/v1/auth/firebase` (token[, verification_token]) → [app/controllers/api_public/v1/auth_controller.rb](app/controllers/api_public/v1/auth_controller.rb). Grants membership if needed.
150150

151-
## 16) Gaps & Suggested API Additions for JS Clients
152-
- Favorites (server): add CRUD endpoints for saved properties (`/api_public/v1/favorites`) mirroring `/my/favorites` flows, including manage/unsubscribe tokens and price-change flags. Needed to avoid scraping HTML modals.
153-
- Saved searches: add CRUD + verify/unsubscribe endpoints for saved search definitions (`/api_public/v1/saved_searches`) returning manage tokens and alert frequencies.
154-
- Page parts feed: extend Pages API to optionally include resolved page-part data (ordered, visible-only) plus rendered Liquid-safe JSON so headless clients can render page sections without duplicating composition logic. Consider `include_parts=true` flag.
155-
- Search facets: add lightweight `/api_public/v1/search/facets` (counts per type/zone/locality/features) to avoid heavy full-search calls when building filter UIs.
156-
- CDN/cache hints: include `Cache-Control/ETag` on static-ish endpoints (theme, site_details, links, search/config, translations) and return `last_modified` fields for client caching. Consider `If-None-Match` handling.
157-
- Map tiles/meta: expose map defaults (tile URL, attribution, default zoom, scroll setting) via theme or site details to keep client and SSR behavior aligned.
158-
- Language/locale list: add `/api_public/v1/locales` returning enabled locales and default locale per site, to avoid hardcoding language options in clients.
159-
- Media variants: for properties and testimonials, include explicit image variant URLs (thumb, medium, full) to let clients pick responsive sizes without guessing.
151+
## 16) Additional Public APIs for JS Clients
152+
153+
The following APIs complete the headless client support:
154+
155+
### Favorites API
156+
Full CRUD for server-persisted favorites with token-based access:
157+
- `POST /api_public/v1/favorites` - Create favorite (returns `manage_token`)
158+
- `GET /api_public/v1/favorites?token=XXX` - List all favorites for email
159+
- `GET /api_public/v1/favorites/:id?token=XXX` - Single favorite
160+
- `PATCH /api_public/v1/favorites/:id?token=XXX` - Update notes
161+
- `DELETE /api_public/v1/favorites/:id?token=XXX` - Remove favorite
162+
- `POST /api_public/v1/favorites/check` - Check which refs are already saved
163+
164+
[favorites_controller.rb](app/controllers/api_public/v1/favorites_controller.rb)
165+
166+
### Saved Searches API
167+
Full CRUD for saved search definitions with alert management:
168+
- `POST /api_public/v1/saved_searches` - Create search (returns `manage_token`)
169+
- `GET /api_public/v1/saved_searches?token=XXX` - List searches for email
170+
- `GET /api_public/v1/saved_searches/:id?token=XXX` - Single search with recent alerts
171+
- `PATCH /api_public/v1/saved_searches/:id?token=XXX` - Update frequency/name
172+
- `DELETE /api_public/v1/saved_searches/:id?token=XXX` - Remove search
173+
- `POST /api_public/v1/saved_searches/:id/unsubscribe?token=XXX` - Disable alerts
174+
- `GET /api_public/v1/saved_searches/verify?token=XXX` - Verify email
175+
176+
[saved_searches_controller.rb](app/controllers/api_public/v1/saved_searches_controller.rb)
177+
178+
### Locales API
179+
Available locales for language switcher and hreflang generation:
180+
- `GET /api_public/v1/locales` - Returns `default_locale`, `available_locales[]`, `current_locale`
181+
182+
[locales_controller.rb](app/controllers/api_public/v1/locales_controller.rb)
183+
184+
### Search Facets API
185+
Lightweight filter counts without full search:
186+
- `GET /api_public/v1/search/facets?sale_or_rental=sale` - Returns counts per type/zone/locality/beds/baths/price_range
187+
188+
[search_facets_controller.rb](app/controllers/api_public/v1/search_facets_controller.rb)
189+
190+
### JSON-LD Schema Endpoint
191+
Pre-built structured data for SEO:
192+
- `GET /api_public/v1/properties/:id/schema` - Returns `RealEstateListing` JSON-LD
193+
194+
[properties_controller.rb](app/controllers/api_public/v1/properties_controller.rb)
195+
196+
### Image Variants
197+
Responsive image URLs via query param:
198+
- `GET /api_public/v1/properties?include_images=variants` - Includes `images[]` with `thumbnail/small/medium/large/original` URLs
199+
200+
### Caching
201+
All endpoints include appropriate `Cache-Control` and ETag headers via [cacheable.rb](app/controllers/concerns/api_public/cacheable.rb).
160202

161203
## 17) Performance & Caching Guidance for JS Clients
162204
- Use ETags / Cache-Control: theme, site_details, translations, links, search/config are good candidates for long-lived caching with revalidation; properties/search should set short TTL + ETag.

0 commit comments

Comments
 (0)