diff --git a/hub/services/templatetags/json_ld_tags.py b/hub/services/templatetags/json_ld_tags.py index 6efa78c..2f1e56d 100644 --- a/hub/services/templatetags/json_ld_tags.py +++ b/hub/services/templatetags/json_ld_tags.py @@ -217,87 +217,78 @@ def json_ld_structured_data(context): offering = context["offering"] offering_url = request.build_absolute_uri() - data = { - "@context": "https://schema.org", - "@type": "Product", - "name": f"Managed {offering.service.name} on {offering.cloud_provider.name}", - "description": offering.description or offering.service.description, - "url": offering_url, - "category": "Cloud Service", - } - - # Add brand (service) - data["brand"] = {"@type": "Brand", "name": offering.service.name} - - # Add image if available - if hasattr(offering.service, "get_logo") and offering.service.get_logo: - data["image"] = request.build_absolute_uri(offering.service.get_logo.url) - - # Add offers if available + # Check if we have pricing data available + has_pricing_data = False if hasattr(offering, "plans") and offering.plans.exists(): # Get all plans with pricing plans_with_prices = offering.plans.filter( plan_prices__isnull=False ).distinct() + has_pricing_data = plans_with_prices.exists() - if plans_with_prices.exists(): - # Create individual offers for each plan - offers = [] - all_prices = [] + if has_pricing_data: + # Use Product type with complete pricing information + data = { + "@context": "https://schema.org", + "@type": "Product", + "name": f"Managed {offering.service.name} on {offering.cloud_provider.name}", + "description": offering.description or offering.service.description, + "url": offering_url, + "category": "Cloud Service", + } - for plan in plans_with_prices: - plan_prices = plan.plan_prices.all() - if plan_prices.exists(): - first_price = plan_prices.first() - all_prices.extend([p.amount for p in plan_prices]) + # Add brand (service) + data["brand"] = {"@type": "Brand", "name": offering.service.name} - offer = { - "@type": "Offer", - "name": plan.name, - "price": str(first_price.amount), - "priceCurrency": first_price.currency, - "availability": "https://schema.org/InStock", - "url": offering_url + "#plan-order-form", - "seller": {"@type": "Organization", "name": "VSHN"}, - } - offers.append(offer) + # Add image if available + if hasattr(offering.service, "get_logo") and offering.service.get_logo: + data["image"] = request.build_absolute_uri(offering.service.get_logo.url) + + # Create individual offers for each plan with pricing + offers = [] + all_prices = [] + + for plan in plans_with_prices: + plan_prices = plan.plan_prices.all() + if plan_prices.exists(): + first_price = plan_prices.first() + all_prices.extend([p.amount for p in plan_prices]) + + offer = { + "@type": "Offer", + "name": plan.name, + "price": str(first_price.amount), + "priceCurrency": first_price.currency, + "availability": "https://schema.org/InStock", + "url": offering_url + "#plan-order-form", + "seller": {"@type": "Organization", "name": "VSHN"}, + } + offers.append(offer) + + # Add aggregate offer with all required pricing fields + if all_prices and offers: + # Use the currency from the first plan's first price + first_plan_with_prices = plans_with_prices.first() + first_currency = first_plan_with_prices.plan_prices.first().currency - # Add aggregate offer with all individual offers data["offers"] = { "@type": "AggregateOffer", "availability": "https://schema.org/InStock", "offerCount": len(offers), "offers": offers, + "lowPrice": str(min(all_prices)), + "highPrice": str(max(all_prices)), + "priceCurrency": first_currency, "seller": {"@type": "Organization", "name": "VSHN"}, } - # Add lowPrice, highPrice and priceCurrency if we have prices - if all_prices: - data["offers"]["lowPrice"] = str(min(all_prices)) - data["offers"]["highPrice"] = str(max(all_prices)) - # Use the currency from the first plan's first price - first_plan_with_prices = plans_with_prices.first() - first_currency = first_plan_with_prices.plan_prices.first().currency - data["offers"]["priceCurrency"] = first_currency - # Note: aggregateRating and review fields are not included as this is a B2B # service marketplace without a review system. These could be added in the future # if customer reviews/ratings are implemented. - # Example structure for future implementation: - # if hasattr(offering, 'reviews') and offering.reviews.exists(): - # data["aggregateRating"] = { - # "@type": "AggregateRating", - # "ratingValue": "4.5", - # "reviewCount": "10" - # } - else: - # No pricing available, just basic offer info - data["offers"] = { - "@type": "AggregateOffer", - "availability": "https://schema.org/InStock", - "offerCount": offering.plans.count(), - "seller": {"@type": "Organization", "name": "VSHN"}, - } + else: + # No pricing data available - use Organization data instead of Product + # to avoid Google Search Console errors for missing required Product fields + data = organization_data elif view_name == "article_list": data = {