Skip to content

feat: create related posts and products#1584

Open
pedrophzp wants to merge 5 commits into
deco-cx:mainfrom
pedrophzp:main
Open

feat: create related posts and products#1584
pedrophzp wants to merge 5 commits into
deco-cx:mainfrom
pedrophzp:main

Conversation

@pedrophzp

@pedrophzp pedrophzp commented May 11, 2026

Copy link
Copy Markdown

What is this Contribution About?

Please provide a brief description of the changes or enhancements you are proposing in this pull request.

Issue Link

Please link to the relevant issue that this pull request addresses:

Loom Video

Record a quick screencast describing your changes to help the team understand and review your contribution. This will greatly assist in the review process.

Demonstration Link

Provide a link to a branch or environment where this pull request can be tested and seen in action.


Summary by cubic

Add related posts and products to blog posts and add categories with an optional parent to improve navigation and merchandising. Reverted earlier post template styling changes; no visual template changes in this PR.

  • New Features

    • Related content: relatedPosts via apps/blog/loaders/BlogPostList.ts and relatedProducts from the catalog; dynamic options for selecting posts.
    • Categories: Category.parent with dynamic options via apps/blog/loaders/CategoryList.ts.
  • Refactors

    • Corrected CMS type comments for clearer dynamic options.

Written for commit d9f0695. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features
    • Blog posts can display related posts to improve discovery.
    • Category editor now allows selecting a parent category from a dynamic list for better organization.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds dynamic-options JSDoc/metadata to Category.parent and a new optional BlogPost.relatedPosts field; restores a missing }; terminator at the end of the loader export in blog/sections/Template.tsx.

Changes

Blog types metadata and Template fix

Layer / File(s) Summary
Type metadata updates
blog/types.ts
Adds JSDoc/metadata to Category.parent marking it dynamic-options with @options apps/blog/loaders/CategoryList.ts; adds BlogPost.relatedPosts?: BlogPost[] with dynamic-options metadata and @options apps/blog/loaders/BlogPostList.ts.
Template loader terminator fix
blog/sections/Template.tsx
Restores the trailing }; terminator on the exported loader arrow-function.

Possibly Related PRs

Suggested Reviewers

  • guitavano

Poem

🐰 I nudged a semicolon back in place,
and taught the types to show a friendly face.
Related posts line up to say hello,
small fixes hop fast, gentle and mellow.

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes a primary change: adding related posts and products to blog posts, which aligns with the key features in the changeset.
Description check ✅ Passed The description template sections are largely unfilled (contributor info missing), but an auto-generated Cubic summary provides comprehensive context about the changes, rationale, and implementation details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 0.148.2 update
  • 🎉 for Minor 0.149.0 update
  • 🚀 for Major 1.0.0 update

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@blog/sections/Template.tsx`:
- Around line 55-58: The JSX currently injects raw HTML via
dangerouslySetInnerHTML using the content variable in Template.tsx; import the
existing sanitizeHtml utility from spire/utils/sanitizeHtml.ts and run content
through it before passing into dangerouslySetInnerHTML (e.g., const safeHtml =
sanitizeHtml(content as string)), then use safeHtml in the __html field to strip
scripts, event handlers and dangerous protocols and eliminate the XSS risk.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d70eea3f-51e9-407b-9c3d-5edb8d0b64ea

📥 Commits

Reviewing files that changed from the base of the PR and between 18d422e and db0ea3f.

📒 Files selected for processing (2)
  • blog/sections/Template.tsx
  • blog/types.ts

Comment thread blog/sections/Template.tsx Outdated
Comment on lines +55 to +58
<div
class="prose max-w-none mb-10"
dangerouslySetInnerHTML={{ __html: content as string }}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether HTML content is sanitized before render.

# 1) Find all raw HTML injection points.
rg -n -C3 'dangerouslySetInnerHTML' --type=ts --type=tsx

# 2) Find sanitization utilities/usages in the blog rendering pipeline.
rg -n -C3 'sanitize|sanitiz|DOMPurify|xss|rehype-sanitize|sanitize-html' --type=ts --type=tsx

# 3) Trace where blog post content is loaded/transformed before Template.
rg -n -C3 '\bcontent\b' blog --type=ts --type=tsx

Repository: deco-cx/apps

Length of output: 140


🏁 Script executed:

#!/bin/bash
# 1) Find all raw HTML injection points in blog with correct file pattern
echo "=== Searching for dangerouslySetInnerHTML ==="
rg -n -C3 'dangerouslySetInnerHTML' -g '*.tsx' -g '*.ts' blog/

# 2) Find sanitization utilities in the entire blog directory
echo "=== Searching for sanitization patterns ==="
rg -n 'sanitize|DOMPurify|xss|rehype-sanitize|sanitize-html' -g '*.tsx' -g '*.ts' blog/

# 3) Search for imports/requires in Template.tsx specifically
echo "=== Template.tsx imports ==="
head -50 blog/sections/Template.tsx

Repository: deco-cx/apps

Length of output: 2246


🏁 Script executed:

#!/bin/bash
# Find the loader function and BlogPost type definition
echo "=== BlogPost type definition ==="
cat blog/types.ts

echo ""
echo "=== Searching for loader function in Template.tsx ==="
rg -n 'loader|export' blog/sections/Template.tsx

echo ""
echo "=== Looking for loader file in blog directory ==="
find blog -name '*loader*' -o -name '*load*' 2>/dev/null | head -20

echo ""
echo "=== Check blog directory structure ==="
ls -la blog/

Repository: deco-cx/apps

Length of output: 6561


🏁 Script executed:

#!/bin/bash
# Read the loader function from Template.tsx
echo "=== Loader function from Template.tsx ==="
sed -n '81,130p' blog/sections/Template.tsx

echo ""
echo "=== Check loaders directory ==="
ls -la blog/loaders/

echo ""
echo "=== Search for rich-text or content processing ==="
rg -n 'rich-text|content.*sanitize|sanitize.*content' -g '*.ts' -g '*.tsx' blog/

Repository: deco-cx/apps

Length of output: 1108


🏁 Script executed:

#!/bin/bash
# Check mod.ts and other framework setup
echo "=== mod.ts (framework setup) ==="
cat blog/mod.ts

echo ""
echo "=== Search for other rich-text format usages ==="
rg -n '@format rich-text' -g '*.ts' -g '*.tsx' 

echo ""
echo "=== Check if Deco has sanitization in dependencies ==="
find . -name 'deno.json' -o -name 'deno.lock' -o -name 'package.json' 2>/dev/null | head -5

Repository: deco-cx/apps

Length of output: 1283


🏁 Script executed:

#!/bin/bash
# Check deno.json for `@deco/deco` and any sanitization dependencies
echo "=== deno.json (first 100 lines) ==="
head -100 deno.json

echo ""
echo "=== admin/widgets.ts rich-text context ==="
rg -n -B5 -A5 '@format rich-text' admin/widgets.ts

Repository: deco-cx/apps

Length of output: 3139


🏁 Script executed:

#!/bin/bash
# Final verification: search for any comments about content safety
echo "=== Search for content safety comments/docs ==="
rg -n -i 'sanitize|xss|danger|safe|trust|content.*html' -g '*.ts' -g '*.tsx' blog/ | head -30

echo ""
echo "=== Check if similar patterns exist elsewhere ==="
rg -n 'dangerouslySetInnerHTML' -g '*.tsx' -g '*.ts' . --max-count=10

Repository: deco-cx/apps

Length of output: 4359


🏁 Script executed:

#!/bin/bash
# Critical: Found that spire app DOES sanitize. Let's find the sanitizeHtml function
echo "=== Finding sanitizeHtml implementation ==="
rg -n 'function sanitizeHtml|export.*sanitizeHtml|const sanitizeHtml' -g '*.ts' -g '*.tsx'

echo ""
echo "=== Check spire app's sanitization imports ==="
head -30 spire/sections/blocks/Paragraph.tsx

Repository: deco-cx/apps

Length of output: 779


🏁 Script executed:

#!/bin/bash
# Check the sanitizeHtml implementation
echo "=== sanitizeHtml implementation ==="
cat spire/utils/sanitizeHtml.ts

echo ""
echo "=== Check if blog app imports or uses sanitizeHtml ==="
rg -n 'sanitizeHtml' blog/

Repository: deco-cx/apps

Length of output: 1468


Sanitize content before dangerouslySetInnerHTML (Line 57).

Rendering post.content directly here is an XSS risk. The content comes from the CMS (@format rich-text) and is injected without sanitization. A sanitizeHtml utility already exists in this codebase at spire/utils/sanitizeHtml.ts that can be reused to strip XSS vectors (scripts, event handlers, dangerous protocols).

🧰 Tools
🪛 ast-grep (0.42.2)

[warning] 56-56: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blog/sections/Template.tsx` around lines 55 - 58, The JSX currently injects
raw HTML via dangerouslySetInnerHTML using the content variable in Template.tsx;
import the existing sanitizeHtml utility from spire/utils/sanitizeHtml.ts and
run content through it before passing into dangerouslySetInnerHTML (e.g., const
safeHtml = sanitizeHtml(content as string)), then use safeHtml in the __html
field to strip scripts, event handlers and dangerous protocols and eliminate the
XSS risk.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@blog/types.ts`:
- Line 4: The BlogPost type is missing the relatedProducts field though Product
is imported; update the BlogPost interface to include a relatedProducts property
(e.g., relatedProducts?: Product[] or relatedProducts: Product[] as appropriate
for optionality) alongside the existing relatedPosts to satisfy the post
contract and editor schema; locate the BlogPost interface and add the
relatedProducts field using the imported Product type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fe234206-cfef-41ee-a8b1-d9264fe84709

📥 Commits

Reviewing files that changed from the base of the PR and between db0ea3f and be5dd79.

📒 Files selected for processing (1)
  • blog/types.ts

Comment thread blog/types.ts Outdated
import { ImageWidget } from "../admin/widgets.ts";
import { PageInfo, Person, Thing } from "../commerce/types.ts";
import { type Section } from "@deco/deco/blocks";
import { Product } from "../commerce/types.ts";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether relatedProducts is already modeled/consumed elsewhere.
rg -n -C3 --type=ts '\brelatedProducts\b'
rg -n -C3 --type=ts '\binterface\s+BlogPost\b'
rg -n -C3 --type=ts '`@options` .*Product|ProductList|Catalog'

Repository: deco-cx/apps

Length of output: 50370


🏁 Script executed:

cat -n blog/types.ts

Repository: deco-cx/apps

Length of output: 7062


Add the missing relatedProducts field to complete the post contract.

Line 4 imports Product, but the BlogPost interface only includes relatedPosts and lacks a relatedProducts field. This type gap can break editor schema integration for related products functionality.

Proposed change
 export interface BlogPost {
   relatedPosts?: BlogPost[];
+  /**
+   * `@title` Produtos Relacionados
+   * `@description` Selecione produtos para exibir no post
+   * `@format` dynamic-options
+   * `@options` <use the same product loader path used by this PR>
+   */
+  relatedProducts?: Product[];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blog/types.ts` at line 4, The BlogPost type is missing the relatedProducts
field though Product is imported; update the BlogPost interface to include a
relatedProducts property (e.g., relatedProducts?: Product[] or relatedProducts:
Product[] as appropriate for optionality) alongside the existing relatedPosts to
satisfy the post contract and editor schema; locate the BlogPost interface and
add the relatedProducts field using the imported Product type.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread blog/types.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
blog/types.ts (1)

1-4: ⚠️ Potential issue | 🟠 Major

PR description says “relatedProducts” but BlogPost only supports relatedPosts.

blog/types.ts’s export interface BlogPost defines relatedPosts?: BlogPost[] and has no relatedProducts field (and no Product import). There are no relatedProducts occurrences anywhere under blog/ (the only matches are in vtex/loaders/legacy/relatedProductsLoader.ts).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blog/types.ts` around lines 1 - 4, The PR claims support for
"relatedProducts" but the BlogPost interface only has relatedPosts; either add a
relatedProducts?: Product[] field to export interface BlogPost and import
Product from "../commerce/types.ts" (ensure the new field is used where needed),
or update the PR and any callers to consistently use relatedPosts; locate the
interface named BlogPost and the relatedPosts property to apply the chosen fix
so the type and documentation match.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@blog/types.ts`:
- Around line 1-4: The PR claims support for "relatedProducts" but the BlogPost
interface only has relatedPosts; either add a relatedProducts?: Product[] field
to export interface BlogPost and import Product from "../commerce/types.ts"
(ensure the new field is used where needed), or update the PR and any callers to
consistently use relatedPosts; locate the interface named BlogPost and the
relatedPosts property to apply the chosen fix so the type and documentation
match.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31c49377-8e68-4c4c-9ee3-2d2c8820540d

📥 Commits

Reviewing files that changed from the base of the PR and between ae1e85a and d9f0695.

📒 Files selected for processing (2)
  • blog/sections/Template.tsx
  • blog/types.ts

@yuriassuncx yuriassuncx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great contribution — this PR adds two well-scoped features to the blog app that fill real gaps in the current type model:

  • Category.parent?: Category — enables a proper category hierarchy (subcategories) where today the Category interface only supports flat structures (name + slug). This unlocks breadcrumbs, nested navigation, and filtered listing by parent. There is no native way to achieve this today.
  • BlogPost.relatedPosts?: BlogPost[] — enables editorial curation of related posts directly from the CMS admin, using a dynamic-options picker backed by the existing BlogPostList.ts loader. While it's possible to render a "related posts" section algorithmically (by filtering on the same category/tag via a separate loader), this approach gives editors fine-grained, per-post control — a different and complementary value.

Both fields use the @format dynamic-options annotation correctly, keeping the admin form clean with a dropdown instead of an unbounded nested editor.

This is approved in principle, but there are a couple of things to address before merging.


Required changes

1. Annotations must be in English

All JSDoc annotations in this codebase follow English conventions. Please translate the Portuguese strings in blog/types.ts:

// Category.parent — current (Portuguese)
/**
 * @title Categoria Pai
 * @description Selecione se esta categoria for uma subcategoria de outra
 * @format dynamic-options
 * @options apps/blog/loaders/CategoryList.ts
 */

// Category.parent — expected (English)
/**
 * @title Parent Category
 * @description Select if this category is a subcategory of another
 * @format dynamic-options
 * @options apps/blog/loaders/CategoryList.ts
 */
// BlogPost.relatedPosts — current (Portuguese)
/**
 * @title Posts Relacionados
 * @description Busque posts já cadastrados pelo título
 * @format dynamic-options
 * @options apps/blog/loaders/BlogPostList.ts
 */

// BlogPost.relatedPosts — expected (English)
/**
 * @title Related Posts
 * @description Search for existing posts by title
 * @format dynamic-options
 * @options apps/blog/loaders/BlogPostList.ts
 */

2. PR title vs. actual scope — please clarify

The PR title is feat: create related posts and products, and the Cubic summary also mentions relatedProducts. However, the final state of blog/types.ts only includes relatedPosts — there is no relatedProducts?: Product[] field in BlogPost.

Please do one of the following:

  • Option A: Rename the PR to feat: add related posts and parent category to match what is actually implemented.
  • Option B: Add relatedProducts?: Product[] to BlogPost with proper JSDoc (if the intent was always to include product linking), and make sure the Product import is present and used.

This was also flagged by @coderabbitai in its last review pass — worth addressing.


Notes on CodeRabbit suggestions

CodeRabbit flagged an XSS concern (dangerouslySetInnerHTML without sanitization in Template.tsx, commit db0ea3f) suggesting the use of the existing sanitizeHtml utility from spire/utils/sanitizeHtml.ts. That finding is no longer applicable since commit d9f0695 reverted the Template.tsx content changes — the final diff leaves that file essentially untouched. No action needed there.

The relatedProducts mismatch flagged in the second and third CodeRabbit passes is the one that still stands and is covered above.


Once the annotation language is updated and the title/scope mismatch is resolved, this is ready to go. 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants