Skip to content

Commit a2ab279

Browse files
committed
[IMP] website, *: introduce JsonLd builder and structured data
*=website_blog Body: This commit introduces a reusable JsonLd builder for Schema.org payloads and integrates structured data generation in website and website_blog. - add a JsonLd helper with snake_case to camelCase normalization, nested schema support, datetime normalization, and safe rendering for single or multiple schemas - add website structured data foundations (organization schema default and breadcrumb helper) through a dedicated mixin - expose website-level structured data generation and inject structured_data in template rendering context - render JSON-LD payload in website layout head - add images_from_html utility to collect post images from blog content - generate blog schemas for listing and detail pages (Blog, CollectionPage, BlogPosting, BreadcrumbList) - pass structured_data from blog controllers for both list and detail routes - add dedicated tests validating JsonLd behavior and serialization rules This change enables consistent, extensible structured-data generation across website and blog pages. task-4655276 [IMP] website_blog: WIP
1 parent 2983a26 commit a2ab279

9 files changed

Lines changed: 120 additions & 135 deletions

File tree

addons/website/helpers/jsonld_builder.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,7 @@ def to_iso_datetime(dt) -> str | None:
113113
return as_datetime.isoformat()
114114

115115
def get(self, key: str, default=None):
116-
"""Retrieve a stored value by its snake_case key.
117-
The key is normalised exactly like :meth:`set` / :meth:`add_nested`
118-
so callers never have to know the internal camelCase representation.
116+
"""Retrieve a stored value by key.
119117
Args:
120118
key: Property name
121119
default: Value returned when the key is absent.
@@ -144,7 +142,7 @@ def set(self, values: dict[str, Any]) -> JsonLd:
144142
self.values[key] = value
145143
return self
146144

147-
def add_nested(self, values: dict[str, JsonLd | list[JsonLd | None] | None]) -> JsonLd:
145+
def add_nested(self, values: dict[str, JsonLd | list[JsonLd] | None]) -> JsonLd:
148146
"""Add nested schema builder(s).
149147
A single nested value is stored as-is; values are converted to a list
150148
only when multiple nested values exist for the same key. None values

addons/website/models/mixins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,7 @@ class WebsiteStructuredDataMixin(models.AbstractModel):
877877
_name = 'website.structured_data.mixin'
878878
_description = 'Website Structured Data Mixin'
879879

880-
def get_jsonLD(self, is_detail_page=False):
880+
def get_json_ld(self, is_detail_page=False):
881881
"""Return the JSON-LD structured data for this record.
882882
:param is_detail_page: whether the structured data is for a detail page
883883
:return: string containing the JSON-LD structured data

addons/website/models/website.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2493,7 +2493,7 @@ def _is_tag_domains_watchlisted(self, tagName, atts):
24932493
def _is_tag_classes_watchlisted(self, tagName, atts):
24942494
return self._get_blocked_iframe_containers_classes().intersection((atts.get('class') or '').split(' '))
24952495

2496-
def get_jsonLD(self):
2496+
def get_json_ld(self):
24972497
"""Generate structured data for the website."""
24982498
self.ensure_one()
24992499
return JsonLd.render_structured_data([self.organization_structured_data()])
@@ -2504,7 +2504,9 @@ def organization_structured_data(self):
25042504
base_url = self.get_base_url()
25052505
logo_url = f"{base_url}/logo.png?company={self.company_id.id}"
25062506
return JsonLd("Organization",
2507-
{"name": self.name,
2508-
"url": base_url,
2509-
"@id": f"{base_url}/#organization"},
2507+
{
2508+
"name": self.name,
2509+
"url": base_url,
2510+
"@id": f"{base_url}/#organization",
2511+
},
25102512
).add_nested({"logo": JsonLd("ImageObject", {"url": logo_url})})

addons/website/tests/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from . import test_iap
2020
from . import test_import_files
2121
from . import test_ir_asset
22+
from . import test_jsonld_builder
2223
from . import test_lang_url
2324
from . import test_menu
2425
from . import test_multi_website
@@ -32,7 +33,6 @@
3233
from . import test_sitemap
3334
from . import test_skip_website_configurator
3435
from . import test_snippets
35-
from . import test_structure_data_defination
3636
from . import test_theme
3737
from . import test_ui
3838
from . import test_unsplash_beacon
File renamed without changes.

addons/website/tools.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,27 +94,24 @@ def text_from_html(html_fragment, collapse_whitespace=False):
9494
return content
9595

9696

97-
def images_from_html(html_fragment, website_url):
97+
def images_from_html(html_fragment, base_url):
9898
"""
9999
Extract unique image URLs from an HTML fragment.
100-
Preserves order.
100+
101101
:param html_fragment: document from which image URLs must be extracted
102-
:param website_url: base URL of the website to resolve relative URLs
102+
:param base_url: base URL of the website to resolve relative URLs
103103
:return: list of image URLs extracted from the html
104104
"""
105105
if not html_fragment:
106106
return []
107107

108108
tree = html.fromstring(html_fragment)
109-
image_paths = []
110109
seen = set()
111110

112111
for img in tree.xpath("//img[@src]"):
113-
src = urljoin(website_url, img.get("src"))
114-
if src not in seen:
115-
seen.add(src)
116-
image_paths.append(src)
117-
return image_paths
112+
src = urljoin(base_url, img.get("src"))
113+
seen.add(src)
114+
return list(seen)
118115

119116

120117
def get_base_domain(url, strip_www=False):

addons/website/views/website_templates.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@
177177
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin=""/>
178178
<link rel="apple-touch-icon" t-att-href="x_icon"/>
179179
<!-- Render structured data from context or fallback to website-level schema -->
180-
<script type="application/ld+json" t-out="structured_data or website.get_jsonLD()"/>
180+
<script type="application/ld+json" t-out="structured_data or website.get_json_ld()"/>
181181
</xpath>
182182

183183
<xpath expr="//head/script" position="before">

addons/website_blog/controllers/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def blog(self, blog=None, tag=None, page=1, search=None, **opt):
227227
posts = values['posts']
228228
if blog:
229229
posts = posts.with_context(blog_id=blog.id)
230-
values['structured_data'] = posts.get_jsonLD()
230+
values['structured_data'] = posts.get_json_ld()
231231

232232
return request.render("website_blog.blog_post_short", values)
233233

@@ -339,7 +339,7 @@ def blog_post(self, blog, blog_post, tag_id=None, page=1, enable_editor=None, **
339339
'is_next_post_recommended': is_next_post_recommended,
340340
'date': date_begin,
341341
'blog_url': blog_url,
342-
'structured_data': blog_post.get_jsonLD(is_detail_page=True),
342+
'structured_data': blog_post.get_json_ld(is_detail_page=True),
343343
}
344344
response = request.render("website_blog.blog_post_complete", values)
345345

0 commit comments

Comments
 (0)