feat(blog): add shoppable product block sections#1603
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR integrates Spire as a live blog post source alongside native Deco posts. It adds webhook authentication to receive Spire events, a gate resolution action for admin approvals, UI components for sync status, conversion utilities for Spire posts and blocks, hybrid loaders that fetch native posts first with Spire fallback, and data loaders for authors, tags, and filtered posts. All loaders cache responses for 60 seconds to enable near-real-time updates while maintaining performance. Native Blog Loaders with Spire Fallback
🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Tagging OptionsShould a new tag be published when this PR is merged?
|
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
spire/sections/SpirePendingApprovals.tsx (1)
1-190:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRun
deno fmtfor this file before merge.CI is currently failing on formatting for this module.
🤖 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 `@spire/sections/SpirePendingApprovals.tsx` around lines 1 - 190, Run the formatter (deno fmt) on this file to fix CI formatting errors; specifically format the module containing the loader function and the default export SpirePendingApprovals so imports, JSX attributes, spacing, and trailing commas match the project's style — after running deno fmt, re-run the tests/CI and commit the updated file.spire/sections/SpireDashboard.tsx (1)
1-65:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRun
deno fmtfor this file before merge.CI is currently failing on formatting for this module.
🤖 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 `@spire/sections/SpireDashboard.tsx` around lines 1 - 65, Run the formatter on this file (e.g., run `deno fmt`) to fix CI formatting errors for the SpireDashboard component; ensure the import and JSX/TSX formatting, indentation, and trailing semicolons in the SpireDashboard function (including the useId import, Props interface, default props, the conditional early-return block, and the embeddedUrl/iframe JSX) are normalized by the formatter before merging.blog/actions/importSpirePost.ts (1)
1-197:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRun
deno fmtfor this file before merge.CI is currently failing on formatting for this module.
🤖 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/actions/importSpirePost.ts` around lines 1 - 197, The file fails formatting; run the formatter and commit the changes: execute `deno fmt` (or your repo's formatting script) to format this module, ensuring functions like importSpirePost and compileBlocksToHtml (and their surrounding imports/exports) are consistently formatted, then stage and commit the resulting changes so the CI formatting check passes.
🤖 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/actions/importSpirePost.ts`:
- Around line 148-152: The JSON.parse of serialized block arrays (e.g., when
building items in importSpirePost.ts) is unguarded and can throw, aborting
webhook processing; wrap each JSON.parse usage (the assignment to items and the
similar parses at the other mentioned spots) in a try/catch and on failure fall
back to an empty array (or safe default) and optionally log the error via the
existing logger, so parsing errors do not throw—update the parsing sites (the
items variable and the other two parse locations noted) to validate typeof
content.items === "string" then try JSON.parse(...) inside try/catch and use the
safe fallback on catch.
- Around line 44-45: Validate and sanitize postSlug before using it to build
filesystem paths: ensure postSlug contains only allowed characters (e.g.,
alphanumerics, hyphens/underscores) and does not include path separators or
segments like ".."; if validation fails, throw an error. Replace direct uses of
postSlug when constructing blocksDir/filePath (and the other paths referenced
around lines 89-92) with a validated/sanitizedSlug (or use path.basename after
validation) so no crafted slug can escape the intended posts directory;
reference variables to update: postSlug, blocksDir, filePath and any other
path-building that uses postSlug. Ensure the validation runs early in the
importSpirePost flow so all subsequent file operations use the safe slug.
- Around line 30-40: The handler currently allows unauthenticated requests when
expectedSecret is falsy (the expectedSecret check/log block in
importSpirePost.ts), which is unsafe; change the logic so that if expectedSecret
is not set you return a failure response (e.g., success: false with
"Unauthorized: webhook secret not configured.") instead of logging a warning and
allowing the request, and keep the existing Authorization header check
(authHeader !== `Bearer ${expectedSecret}`) intact to reject mismatches; locate
the block referencing expectedSecret and req.headers.get("Authorization") and
replace the else-branch to return an unauthorized response rather than
permitting the request.
- Line 59: The fetch call that builds the Spire API URL with raw
blogSlug/postSlug and no timeout is brittle; URL-encode blogSlug and postSlug
(via encodeURIComponent) before interpolating into the path and enforce a
request timeout using an AbortController: create controller, start a timer
(e.g., 5s) that calls controller.abort(), pass controller.signal into fetch (the
existing response = await fetch(...) call), and clear the timer after fetch
completes; also handle the abort/timeout error path where the request can throw.
Use the variables blogSlug, postSlug, response and the fetch invocation in
importSpirePost.ts to locate and update the code.
- Around line 137-193: The map over sorted blocks interpolates many content
fields directly (see sorted.map callback and SpireBlockContent) causing stored
XSS; replace direct interpolation by running all user-provided strings
(content.text, content.html, content.items entries, content.quote,
content.attribution, content.title, content.body, content.caption, content.alt,
content.href, content.code, etc.) through a canonical sanitizer/escaper before
embedding, e.g., use a shared escapeHtml(value) for plain-text fields and a
vetted HTML sanitizer (e.g., DOMPurify or sanitizeHtml) for content.html if raw
HTML must be allowed; update each case branch
("paragraph","heading","list","quote","callout","checklist","steps","image","video","code","cta",
and the default) to call the appropriate sanitizer/escaper on the referenced
fields instead of interpolating them directly.
In `@spire/sections/SpireDashboard.tsx`:
- Around line 44-55: The iframe URL currently builds embeddedUrl using blogSlug
raw, which can produce malformed routes; update the construction of embeddedUrl
(used for the iframe src in SpireDashboard) to encode the slug by applying
encodeURIComponent(blogSlug) so the path/query are safe and preserved when
embedded; locate where embeddedUrl is defined and replace the raw blogSlug usage
with the encoded value before passing embeddedUrl to the iframe.
In `@spire/sections/SpirePendingApprovals.tsx`:
- Around line 47-68: The catch block in SpirePendingApprovals.tsx currently
returns synthetic mockGates (PendingGate[]), which can surface false approvals;
remove the mockGates fallback and instead handle fetch failures safely by
logging the error and returning a non-misleading result (e.g., return {
...props, gates: [] } or rethrow the error so the caller can handle it). Update
the catch in the function that fetches gates (the try/catch that references
mockGates and returns { ...props, gates: mockGates }) to eliminate the mock data
path; if you still want local developer convenience, gate that mock-only
behavior behind an explicit DEV flag (e.g., process.env.NODE_ENV ===
'development') so production never receives synthetic approvals.
- Line 41: The fetch call in SpirePendingApprovals.tsx (where `response` is
assigned) must URL-encode the blog slug and use a timeout via AbortController;
replace the raw template `${spireUrl}/api/blog/${blogSlug}/gates/pending` with a
safe URL built using encodeURIComponent(blogSlug) (or new URL()) and wrap the
fetch with an AbortController whose signal is passed to fetch and which is
aborted after a short timeout (e.g. 3–10s) using setTimeout; also add an
explicit UTF-8 header like Accept-Charset: 'utf-8' (or ensure headers include
'Accept': 'application/json; charset=utf-8') so the loader won’t hang or break
on special characters.
---
Outside diff comments:
In `@blog/actions/importSpirePost.ts`:
- Around line 1-197: The file fails formatting; run the formatter and commit the
changes: execute `deno fmt` (or your repo's formatting script) to format this
module, ensuring functions like importSpirePost and compileBlocksToHtml (and
their surrounding imports/exports) are consistently formatted, then stage and
commit the resulting changes so the CI formatting check passes.
In `@spire/sections/SpireDashboard.tsx`:
- Around line 1-65: Run the formatter on this file (e.g., run `deno fmt`) to fix
CI formatting errors for the SpireDashboard component; ensure the import and
JSX/TSX formatting, indentation, and trailing semicolons in the SpireDashboard
function (including the useId import, Props interface, default props, the
conditional early-return block, and the embeddedUrl/iframe JSX) are normalized
by the formatter before merging.
In `@spire/sections/SpirePendingApprovals.tsx`:
- Around line 1-190: Run the formatter (deno fmt) on this file to fix CI
formatting errors; specifically format the module containing the loader function
and the default export SpirePendingApprovals so imports, JSX attributes,
spacing, and trailing commas match the project's style — after running deno fmt,
re-run the tests/CI and commit the updated file.
🪄 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: f2d2c9cb-0831-414a-8e43-f486b1eecdc8
📒 Files selected for processing (7)
blog/actions/importSpirePost.tsblog/manifest.gen.tsblog/mod.tsblog/types.tsspire/manifest.gen.tsspire/sections/SpireDashboard.tsxspire/sections/SpirePendingApprovals.tsx
There was a problem hiding this comment.
8 issues found across 7 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…gApprovals section component
…anual sync watcher
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 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/actions/importSpirePost.ts`:
- Around line 269-271: The heading level and variant values (content.level and
content.variant) are interpolated directly into tag/attribute contexts which can
enable markup injection; validate these tokens against strict whitelists before
use (e.g., allow only digits 1-6 for level and a fixed set of variant CSS
names), normalize/fallback to safe defaults (e.g., "2" for level, "default" for
variant) if validation fails, and use the validated variables in the template
generation where the current string interpolation occurs (the lines building
`<h${level}>...</h${level}>` and the variant-related template at 293-296) so no
unvalidated user input is placed into tag names or attributes.
- Around line 241-245: safeJsonParse<T> currently returns whatever JSON.parse
yields and callers assume specific shapes (e.g., arrays), causing runtime errors
when Spire returns malformed JSON; change safeJsonParse to accept an optional
runtime validator (a (v: unknown) => v is T) or a flag for arrays, run
JSON.parse inside the try and if the validator (or Array.isArray for array
expectations) returns false, return the fallback instead, and update all call
sites that expect arrays (the locations using .map) to pass a validator or check
Array.isArray on the result before mapping so malformed shapes safely fall back.
In `@blog/actions/resolveSpireGate.ts`:
- Around line 40-55: The POST to Spire in resolveSpireGate should use an
AbortController with a timeout (e.g., 8000ms) so the request cannot hang
indefinitely: create an AbortController, pass controller.signal into fetch in
resolveSpireGate, set a setTimeout to call controller.abort() after the timeout
and clear that timer after fetch completes; in the catch block detect an
AbortError and handle it (return a specific error/result or throw a clearer
error) so callers (and SpirePendingApprovals.tsx which uses an 8s loader)
receive a timely failure instead of waiting forever.
In `@blog/mod.ts`:
- Around line 111-124: The fetch call that constructs `response` to POST to
`${spireUrl}/api/blog/posts/sync-manual` lacks timeout/abort handling; wrap the
request with an AbortController, pass `controller.signal` into the fetch
options, start a timer (e.g., using setTimeout) to call `controller.abort()`
after a reasonable timeout, and clear the timeout when the fetch completes or
fails; ensure you handle the abort error case (thrown when aborted) and
propagate or log it appropriately while still including the existing headers and
JSON body built from `post`, `expectedSecret`, and `spireUrl`.
In `@spire/sections/SpirePendingApprovals.tsx`:
- Around line 217-227: The Quick Approve <button> element (identified by class
"gate-approve-btn" and data attributes like data-gate-id) is missing an explicit
type and fails the Deno lint; add type="button" to that button element in
SpirePendingApprovals.tsx so it does not default to "submit" inside forms.
- Around line 234-282: The inline script in SpirePendingApprovals.tsx unsafely
injects blogSlug into the template string, creating an XSS risk; fix it by
serializing/escaping blogSlug before embedding (e.g. use
JSON.stringify(blogSlug) when constructing the script string) so the value
assigned to the local blogSlug variable is safely escaped, then keep the rest of
the handler logic (querySelectorAll('.gate-approve-btn') and the fetch to
"/live/action/blog/actions/resolveSpireGate") unchanged.
🪄 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: 43363455-1387-4f1f-a6ed-9bb5b4c0f48b
📒 Files selected for processing (5)
blog/actions/importSpirePost.tsblog/actions/resolveSpireGate.tsblog/mod.tsspire/sections/SpireDashboard.tsxspire/sections/SpirePendingApprovals.tsx
There was a problem hiding this comment.
10 issues found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="spire/sections/SpirePendingApprovals.tsx">
<violation number="1" location="spire/sections/SpirePendingApprovals.tsx:250">
P1: The `resolveSpireGate` action lacks inbound request authorization validation, creating a privilege bypass where anyone can call the endpoint to approve Spire gates.</violation>
</file>
<file name="blog/mod.ts">
<violation number="1" location="blog/mod.ts:51">
P2: Missing debounce/dedup for filesystem watcher events can trigger duplicate sync requests per single edit.</violation>
<violation number="2" location="blog/mod.ts:94">
P1: Unbounded watcher restart loop will spam logs and churn CPU if `Deno.watchFs` fails permanently (missing permission, unsupported runtime, etc.). The catch block unconditionally retries after 5 seconds with no max retry count or backoff.</violation>
</file>
<file name="blog/actions/importSpirePost.ts">
<violation number="1" location="blog/actions/importSpirePost.ts:172">
P2: Transient sync flag written to `Deno.env` is never cleared, causing stale entries to accumulate indefinitely in the process environment.</violation>
<violation number="2" location="blog/actions/importSpirePost.ts:392">
P1: Timing attack vulnerability: HMAC signature comparison uses `===` which short-circuits on the first differing byte, potentially leaking information to an attacker. Use a constant-time comparison (e.g., compare byte-by-byte with `crypto.subtle.verify` directly, or implement a timing-safe equals function) for cryptographic signature validation.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…dized webhook router
…s, and XSS escaping in Spire integration
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="blog/actions/resolveSpireGate.ts">
<violation number="1" location="blog/actions/resolveSpireGate.ts:24">
P1: Authorization relies on spoofable Origin/Referer headers instead of authenticated user context, enabling possible privilege bypass.</violation>
</file>
<file name="spire/sections/SpirePendingApprovals.tsx">
<violation number="1" location="spire/sections/SpirePendingApprovals.tsx:250">
P1: The `resolveSpireGate` action lacks inbound request authorization validation, creating a privilege bypass where anyone can call the endpoint to approve Spire gates.</violation>
</file>
<file name="blog/mod.ts">
<violation number="1" location="blog/mod.ts:51">
P2: Missing debounce/dedup for filesystem watcher events can trigger duplicate sync requests per single edit.</violation>
</file>
<file name="blog/actions/importSpirePost.ts">
<violation number="1" location="blog/actions/importSpirePost.ts:172">
P2: Transient sync flag written to `Deno.env` is never cleared, causing stale entries to accumulate indefinitely in the process environment.</violation>
<violation number="2" location="blog/actions/importSpirePost.ts:392">
P1: Timing attack vulnerability: HMAC signature comparison uses `===` which short-circuits on the first differing byte, potentially leaking information to an attacker. Use a constant-time comparison (e.g., compare byte-by-byte with `crypto.subtle.verify` directly, or implement a timing-safe equals function) for cryptographic signature validation.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
- blog/webhook.ts: HMAC auth via canonical payload reconstruction, Bearer fallback, tenant isolation (allowedBlogSlug), path-traversal guard - blog/utils/spireImport.ts: shared importSpirePost + compileBlocksToHtml (full block type support: card-group, stat, comparison, etc.) - blog/actions/syncAllPosts.ts: admin-only bulk sync with pagination - blog/sections/SpireSync.tsx: sync dashboard section (registered in manifest) - blog/components/SpireSyncPreviewTab.tsx: interactive preview tab shown in Deco Studio app settings (shows connection status + Sync All Posts button) - blog/mod.ts: preview() now receives state and renders SpireSyncPreviewTab; activeSyncs Set exported; Deno.watchFs guard for serverless environments - spire/types.ts: SpirePost.tags → SpirePostTag[], added SpireAuthorFull, SpireTagWithCount; full block type union - spire/utils/client.ts: added authors, tags, authors/:slug, tags/:slug endpoints - spire/loaders: BlogPostPage tags→categories; new GetAuthors, GetTags, BlogsByAuthor, BlogsByTag loaders - spire/sections/SpirePendingApprovals.tsx: fixed route /live/invoke/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
blog/actions/webhook.ts (1)
79-82: 💤 Low valueConsider timing-safe comparison for bearer token verification.
The direct
===comparison for the bearer token (line 81) may be vulnerable to timing attacks. While modern JS engines often optimize string comparisons, using a constant-time comparison is a security best practice for secret verification.Proposed fix using crypto.subtle.timingSafeEqual
+function timingSafeEqual(a: string, b: string): boolean { + const encoder = new TextEncoder(); + const aBytes = encoder.encode(a); + const bBytes = encoder.encode(b); + if (aBytes.length !== bBytes.length) return false; + return crypto.subtle.timingSafeEqual(aBytes, bBytes); +} + ... if (!isAuthorized) { const token = authHeader?.replace("Bearer ", "") || querySecret; - isAuthorized = !!token && token === expectedSecret; + isAuthorized = !!token && timingSafeEqual(token, expectedSecret); }🤖 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/actions/webhook.ts` around lines 79 - 82, Replace the direct === comparison for bearer token verification with a timing-safe comparison: import Node's crypto, build Buffers from token and expectedSecret (or empty Buffers if missing), check lengths first and only call crypto.timingSafeEqual when lengths match, and set isAuthorized to the boolean result of that check (falling back to false for missing values); update the conditional around isAuthorized/authHeader/querySecret to use this timing-safe routine (refer to the symbols isAuthorized, authHeader, querySecret, expectedSecret, and token).blog/components/SpireSyncPreviewTab.tsx (1)
245-254: 💤 Low valueConsider escaping server response values before innerHTML assignment.
While
data.messageand error strings originate from the controlledsyncAllPostsaction, the errors array may include content from external API responses (e.g., Spire error messages). UsingtextContentor escaping HTML entities before innerHTML assignment provides defense-in-depth against potential XSS if upstream responses change.Example text-escaping helper
function escapeHtml(s) { return String(s).replace(/[&<>"']/g, function(m) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": '&`#39`;' }[m]; }); } // Then use: escapeHtml(data.message)🤖 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/components/SpireSyncPreviewTab.tsx` around lines 245 - 254, Replace unsafe innerHTML assignments in SpireSyncPreviewTab (where resultEl is updated) with safe text assignments: use resultEl.textContent (or apply an escapeHtml helper) when setting messages for success ('✓ ' + ...), failure ('✗ ' + ...), and the catch block ('Error: ' + ...). Ensure you convert data.message and err to strings before assigning (e.g., String(data.message) / String(err)) and use the same symbols/prefixes but never insert unescaped HTML into innerHTML.blog/sections/SpireSync.tsx (1)
185-196: 💤 Low valueSame text-escaping consideration as SpireSyncPreviewTab.
The error details are rendered in a
<pre>tag via innerHTML. If any error message contains HTML characters, they'll be interpreted as markup. Escaping would be a minor hardening measure.🤖 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/SpireSync.tsx` around lines 185 - 196, The code in SpireSync.tsx builds HTML into resultEl.innerHTML using data.message and data.errors (and mirrors the same concern as SpireSyncPreviewTab); to fix, stop injecting raw strings and either escape HTML entities in data.message and each entry of data.errors (replace &, <, >, ", ' ) before concatenation or, better, create DOM nodes/text nodes programmatically (createElement for <span>, <details>, <summary>, <pre> and append createTextNode(error) for each error) so error text is never interpreted as markup; update the code paths that set resultEl.innerHTML (both success and error branches and the catch block) to use the sanitized text or DOM-node approach referencing resultEl, data.message and data.errors.
🤖 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/utils/spireImport.ts`:
- Around line 265-279: In the "steps" case of the switch in spireImport.ts (the
block using type Step, safeJsonParse, escapeHtml and sanitizeHtml), move the
heading out of the ordered list so the <h3> is not a direct child of <ol>;
render the title (escapeHtml(content.title)) above the <ol> when content.title
exists and then render the <ol class="steps"> containing only <li> elements
created from steps.map(...). Ensure the HTML string concatenation in the "steps"
case produces "<h3>...</h3>" (when present) followed by "<ol
class=\"steps\">...li items...</ol>" rather than embedding the <h3> inside the
<ol>.
- Around line 252-263: The checklist branch currently injects the checklist
title inside the <ul>, producing invalid HTML; update the "checklist" case (in
blog/utils/spireImport.ts) to render the title outside the list: if
content.title exists output the escaped title (use escapeHtml) before opening
the <ul class="checklist">, then render the items as <li> entries (using
sanitizeHtml for each item) inside the <ul>, joining them as before; ensure the
overall return concatenates title + ul rather than placing the <h3> inside the
ul.
---
Nitpick comments:
In `@blog/actions/webhook.ts`:
- Around line 79-82: Replace the direct === comparison for bearer token
verification with a timing-safe comparison: import Node's crypto, build Buffers
from token and expectedSecret (or empty Buffers if missing), check lengths first
and only call crypto.timingSafeEqual when lengths match, and set isAuthorized to
the boolean result of that check (falling back to false for missing values);
update the conditional around isAuthorized/authHeader/querySecret to use this
timing-safe routine (refer to the symbols isAuthorized, authHeader, querySecret,
expectedSecret, and token).
In `@blog/components/SpireSyncPreviewTab.tsx`:
- Around line 245-254: Replace unsafe innerHTML assignments in
SpireSyncPreviewTab (where resultEl is updated) with safe text assignments: use
resultEl.textContent (or apply an escapeHtml helper) when setting messages for
success ('✓ ' + ...), failure ('✗ ' + ...), and the catch block ('Error: ' +
...). Ensure you convert data.message and err to strings before assigning (e.g.,
String(data.message) / String(err)) and use the same symbols/prefixes but never
insert unescaped HTML into innerHTML.
In `@blog/sections/SpireSync.tsx`:
- Around line 185-196: The code in SpireSync.tsx builds HTML into
resultEl.innerHTML using data.message and data.errors (and mirrors the same
concern as SpireSyncPreviewTab); to fix, stop injecting raw strings and either
escape HTML entities in data.message and each entry of data.errors (replace &,
<, >, ", ' ) before concatenation or, better, create DOM nodes/text nodes
programmatically (createElement for <span>, <details>, <summary>, <pre> and
append createTextNode(error) for each error) so error text is never interpreted
as markup; update the code paths that set resultEl.innerHTML (both success and
error branches and the catch block) to use the sanitized text or DOM-node
approach referencing resultEl, data.message and data.errors.
🪄 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: ad158b97-f66a-425b-a6b5-eb1f58755699
📒 Files selected for processing (17)
blog/actions/resolveSpireGate.tsblog/actions/syncAllPosts.tsblog/actions/webhook.tsblog/components/SpireSyncPreviewTab.tsxblog/manifest.gen.tsblog/mod.tsblog/sections/SpireSync.tsxblog/utils/spireImport.tsspire/loaders/BlogPostPage.tsspire/loaders/BlogsByAuthor.tsspire/loaders/BlogsByTag.tsspire/loaders/GetAuthors.tsspire/loaders/GetTags.tsspire/manifest.gen.tsspire/sections/SpirePendingApprovals.tsxspire/types.tsspire/utils/client.ts
✅ Files skipped from review due to trivial changes (2)
- blog/manifest.gen.ts
- spire/manifest.gen.ts
There was a problem hiding this comment.
10 issues found across 17 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="blog/actions/resolveSpireGate.ts">
<violation number="1" location="blog/actions/resolveSpireGate.ts:24">
P1: Authorization relies on spoofable Origin/Referer headers instead of authenticated user context, enabling possible privilege bypass.</violation>
</file>
<file name="spire/sections/SpirePendingApprovals.tsx">
<violation number="1" location="spire/sections/SpirePendingApprovals.tsx:250">
P1: The `resolveSpireGate` action lacks inbound request authorization validation, creating a privilege bypass where anyone can call the endpoint to approve Spire gates.</violation>
</file>
<file name="blog/mod.ts">
<violation number="1" location="blog/mod.ts:51">
P2: Missing debounce/dedup for filesystem watcher events can trigger duplicate sync requests per single edit.</violation>
</file>
<file name="blog/actions/importSpirePost.ts">
<violation number="1" location="blog/actions/importSpirePost.ts:172">
P2: Transient sync flag written to `Deno.env` is never cleared, causing stale entries to accumulate indefinitely in the process environment.</violation>
<violation number="2" location="blog/actions/importSpirePost.ts:392">
P1: Timing attack vulnerability: HMAC signature comparison uses `===` which short-circuits on the first differing byte, potentially leaking information to an attacker. Use a constant-time comparison (e.g., compare byte-by-byte with `crypto.subtle.verify` directly, or implement a timing-safe equals function) for cryptographic signature validation.</violation>
</file>
<file name="blog/sections/SpireSync.tsx">
<violation number="1" location="blog/sections/SpireSync.tsx:185">
P1: DOM XSS via unescaped innerHTML: server response fields (data.message, data.errors) are injected into innerHTML without escaping, creating a XSS vector in the admin sync panel.</violation>
</file>
<file name="blog/utils/spireImport.ts">
<violation number="1" location="blog/utils/spireImport.ts:92">
P1: Result-contract violation: `response.json()`, `Deno.mkdir`, and `Deno.writeTextFile` can throw unhandled exceptions instead of resolving to the documented `{ success: false, message }` structured failure.</violation>
<violation number="2" location="blog/utils/spireImport.ts:256">
P2: Invalid HTML: `<h3>` is rendered as a direct child of `<ul>`, which only allows `<li>` children. Move the title element before the `<ul>` to produce valid markup.</violation>
<violation number="3" location="blog/utils/spireImport.ts:270">
P2: Invalid HTML: `<h3>` is rendered as a direct child of `<ol>`, which only allows `<li>` children. Move the title element before the `<ol>` to produce valid markup.</violation>
<violation number="4" location="blog/utils/spireImport.ts:320">
P1: Runtime crash risk in `comparison` block renderer: `safeJsonParse` is called without a validator for `left`/`right`, and `(s.items ?? [])` does not protect against malformed non-array `items` values (e.g., a string or object). If Spire sends malformed JSON, calling `.map()` on a non-array throws `TypeError` and aborts the entire HTML compilation/post import.</violation>
</file>
<file name="spire/loaders/BlogPostPage.ts">
<violation number="1" location="spire/loaders/BlogPostPage.ts:81">
P1: Potential runtime crash if Spire API response omits `tags` field</violation>
</file>
<file name="spire/loaders/BlogsByTag.ts">
<violation number="1" location="spire/loaders/BlogsByTag.ts:26">
P2: Cache key uses raw account/slug concatenation, which can cause ambiguous collisions and high-cardinality cache growth.</violation>
</file>
<file name="blog/actions/webhook.ts">
<violation number="1" location="blog/actions/webhook.ts:102">
P2: `activeSyncs` loop-prevention flag is keyed only by `postSlug` without a `blogSlug` qualifier, which can cause cross-blog collisions and incorrect sync suppression when `allowedBlogSlug` is unset or misconfigured.</violation>
</file>
<file name="blog/actions/syncAllPosts.ts">
<violation number="1" location="blog/actions/syncAllPosts.ts:94">
P2: HTTP listing failures are not counted as failures, causing aborted sync runs to report success</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Replace isAdmin() origin check with ctx.spireWebhookSecret validation so the Sync All Posts button in the preview tab works when a webhook secret is configured — the preview iframe runs under the site's own origin, not an admin domain. Passes the resolved secret through SpireSyncPreviewTab props into the inline fetch as Authorization: Bearer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="blog/actions/resolveSpireGate.ts">
<violation number="1" location="blog/actions/resolveSpireGate.ts:24">
P1: Authorization relies on spoofable Origin/Referer headers instead of authenticated user context, enabling possible privilege bypass.</violation>
</file>
<file name="spire/sections/SpirePendingApprovals.tsx">
<violation number="1" location="spire/sections/SpirePendingApprovals.tsx:250">
P1: The `resolveSpireGate` action lacks inbound request authorization validation, creating a privilege bypass where anyone can call the endpoint to approve Spire gates.</violation>
</file>
<file name="blog/mod.ts">
<violation number="1" location="blog/mod.ts:51">
P2: Missing debounce/dedup for filesystem watcher events can trigger duplicate sync requests per single edit.</violation>
</file>
<file name="blog/actions/importSpirePost.ts">
<violation number="1" location="blog/actions/importSpirePost.ts:172">
P2: Transient sync flag written to `Deno.env` is never cleared, causing stale entries to accumulate indefinitely in the process environment.</violation>
<violation number="2" location="blog/actions/importSpirePost.ts:392">
P1: Timing attack vulnerability: HMAC signature comparison uses `===` which short-circuits on the first differing byte, potentially leaking information to an attacker. Use a constant-time comparison (e.g., compare byte-by-byte with `crypto.subtle.verify` directly, or implement a timing-safe equals function) for cryptographic signature validation.</violation>
</file>
<file name="spire/loaders/BlogPostPage.ts">
<violation number="1" location="spire/loaders/BlogPostPage.ts:81">
P1: Potential runtime crash if Spire API response omits `tags` field</violation>
</file>
<file name="blog/actions/webhook.ts">
<violation number="1" location="blog/actions/webhook.ts:102">
P2: `activeSyncs` loop-prevention flag is keyed only by `postSlug` without a `blogSlug` qualifier, which can cause cross-blog collisions and incorrect sync suppression when `allowedBlogSlug` is unset or misconfigured.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
- Remove webhook secret from client HTML; use X-Requested-With header for preview-tab auth (non-simple CORS header blocks cross-origin forgery) - Escape innerHTML injections in SpireSync and SpireSyncPreviewTab (XSS) - Move checklist/steps <h3> title outside <ul>/<ol> (invalid HTML spec) - Guard comparison block renderSide against non-array items (crash fix) - Wrap importSpirePost response.json/mkdir/writeTextFile in try/catch - Count HTTP listing failures in syncAllPosts (was silently returning success) - Add missing-fields guard in webhook action (catches empty test calls) - Fix BlogsByTag cache key collision (use :: separator with encodeURIComponent) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ield Aligns spireImport with how Deco Admin stores CMS blocks so synced posts, categories, and authors appear correctly in the Studio collections browser: - Use URL-encoded flat filename convention (.deco/blocks/collections%2Fblog%2F...) instead of nested subdirectories — matches files created by Deco Admin - Add mandatory "name" field (decoded collection path) to every written block - Upsert separate Category blocks (insert-only, never overwrite admin edits) - Upsert separate Author blocks (insert-only, slugified name as identifier) - Migrate: remove legacy subdirectory-format file after writing new one - webhook removePostBlock now uses postBlockPath() from spireImport Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
5 issues found across 7 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
- date: slice ISO timestamp to YYYY-MM-DD to satisfy @Format date JSON Schema validation (was causing 'Invalid Date format' and 'must match anyOf' errors) - spireWarning: change from string to boolean — shows as a checkbox in the admin instead of a confusing optional text field; set to true on sync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move paginated Spire sync loop into shared syncBlogPosts() in spireImport.ts,
used by both syncAllPosts action and the new background auto-sync paths
- Add SyncResult interface to spireImport.ts (removed duplicate from syncAllPosts)
- On cold start, trigger a background sync 5s after App() initialises to catch any
posts that arrived before the webhook was configured (fires once per process)
- Register Deno.cron("spire-auto-sync", every 6h) when Deno Deploy cron is available
as a periodic reconciliation safety-net for missed webhooks
- All background syncs are fire-and-forget; they never block the server
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/utils/spireImport.ts`:
- Around line 78-86: The diacritic-stripping regex in function toId is using
literal combining characters (`/[̀-ͯ]/g`) which is error-prone; replace it with
the Unicode escape range for the Combining Diacritical Marks block
(`\u0300-\u036f`) so after .normalize("NFD") you call .replace(/\u0300-\u036f/g,
"") (use a proper regex with the escape range like /\u0300-\u036f/g) to reliably
remove accents, keeping the rest of the pipeline (.toLowerCase(),
.replace(/[^a-z0-9]+/g, "-"), .replace(/^-+|-+$/g, "")) unchanged in the toId
function.
- Around line 306-309: The JSON parse of listResponse in
blog/utils/spireImport.ts (the const { posts, pagination } = await
listResponse.json() ...) can throw and crash the sync loop; wrap the await
listResponse.json() call in a try/catch, mirror the individual post import error
handling by catching JSON/parse errors, log the error with context (include
listResponse.status/url), and then gracefully return or set posts = [] and
pagination = { totalPages: 0 } (or abort this sync iteration without throwing)
so the sync loop continues instead of terminating.
- Around line 58-61: The empty catch around Deno.stat(filePath) is too broad and
hides permission/other errors; change it to catch the thrown error as e and only
treat a Deno.errors.NotFound (or e?.name === "NotFound") as the "file not
present" case (so continue and create the file), but rethrow or surface any
other errors (e.g., permission denied) instead of swallowing them; update the
block that currently references Deno.stat and filePath to explicitly inspect the
caught error and conditionally handle NotFound vs rethrow.
🪄 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: 1a310506-1f65-4558-b583-c788c6263af4
📒 Files selected for processing (8)
blog/actions/syncAllPosts.tsblog/actions/webhook.tsblog/components/SpireSyncPreviewTab.tsxblog/mod.tsblog/sections/SpireSync.tsxblog/types.tsblog/utils/spireImport.tsspire/loaders/BlogsByTag.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- blog/types.ts
- spire/loaders/BlogsByTag.ts
- blog/actions/webhook.ts
- blog/mod.ts
- blog/components/SpireSyncPreviewTab.tsx
There was a problem hiding this comment.
4 issues found across 4 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
New architecture: blog loaders transparently merge native Deco posts with
live Spire API posts when allowedBlogSlug is configured. No sync machinery,
no file writes, no GitHub commits — just real-time API integration with
60-second CDN caching.
blog app:
- Remove block-sync machinery (syncAllPosts, SpirePost loader, github.ts,
spireImport write utilities, startup/cron sync, file watcher, activeSyncs)
- Simplify mod.ts State (githubRepo/githubToken removed); derive typed
spireApi client in App() when allowedBlogSlug is set — single config field
- Update all four loaders to merge native .deco/blocks with Spire API posts:
BlogpostList, BlogpostListing, BlogPostPage, BlogPostItem
- Fix category filtering: use GET /tags/:slug so Spire posts appear on
category pages with category metadata injected for handlePosts filtering
- Add cache { maxAge: 60 } + cacheKey to all loaders (prevents per-request
API calls; key includes allowedBlogSlug + all filter params)
- Simplify webhook action to auth-only (HMAC-SHA256 + Bearer + tenant guard
+ slug path-traversal prevention); no storage side-effects
- Remove spireWarning field; keep spirePostId @hide/@readonly in types
- Simplify SpireSync section and SpireSyncPreviewTab to status display only
- Regenerate manifest via deno task bundle
spire app:
- Add BlogPostItem.ts loader (single post without full page SEO metadata)
- Reduce cache TTL 24h → 60s across all loaders for near-real-time freshness
- Add apiKey?: Secret to Props for private blog support
- Remove SpireDashboard.tsx, SpirePendingApprovals.tsx (unused)
Security: HMAC-SHA256 preserved, tenant isolation, slug path-traversal guard,
sanitizeHtml/sanitizeHref on all compiled block HTML.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s + CallToAction rename)
Declares the shared HMAC secret as @hide true / @readonly in Props so Deco Studio preserves the field on edits without showing it. Written by admin's bulkSync during integration setup; validated by handleWebhook for local HMAC authentication without a round-trip to autonomous-blog. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…etadata now Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s util - SpireBlogPost was defined but never used anywhere in the blog app. - utils/blocksToSections.ts was defined but never imported — dead code. - spirePostId stays in BlogPost (used by Blogpost.ts + BlogPostItem.ts loaders). - spireBlogSlug stays in mod.ts State (config field for the blog app). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keeps only what's necessary per Tavano's review: - NEW: ProductCard, ProductHighlight, ProductShelf block sections - RENAME: Cta.tsx → CallToAction.tsx - blog/mod.ts: add spireBlogSlug + fix logo (weather→blog) + better description - blog/types.ts: add spirePostId to BlogPost + typo fixes + remove SpireBlogPost - blog/utils/blocksToSections.ts: deleted (orphaned code) - blog/manifest.gen.ts: regenerated - fmt: reformat airtable+slack HTML templates (pre-commit requirement) Reverted to main: loaders, actions, core, utils/dataURI.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- blog/mod.ts: revert logo back to weather URL (as in main) - deno.json: exclude **/ui-templates/ from fmt/lint/check so those HTML files never show as changed vs main - airtable/slack templates: reset to exact main content Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moving from global exclude to fmt.exclude prevents the bundle script from potentially skipping files during manifest generation, which was causing uncommitted changes to appear after the Bundle Apps CI step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move multi-platform product resolution into blog/utils, cache platform detection per request, and wire ProductCard/Highlight/Shelf to dynamic options with inline Image usage. Restore Cta block and add shared product display helpers. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
7 issues found across 25 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Restore records guards, BlogpostList cache/cacheKey, legacy product block fallbacks, site-scoped platform detection, and BlogpostListing JSDoc. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
3 issues found across 8 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="blog/loaders/BlogpostList.ts">
<violation number="1" location="blog/loaders/BlogpostList.ts:66">
P1: The new `cacheKey` omits `AppContext` (`_ctx`) entirely, but the loader result depends on it through `getRecordsByPath(ctx, ...)`. If the cache storage is shared across sites or tenants within the same runtime, identical query params from different contexts will collide, potentially serving cross-tenant data or stale content. Consider including a site or tenant identifier from `ctx` in the cache key, or confirming that the framework automatically scopes loader caches.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| /** | ||
| * @description Overrides the query term at url | ||
| */ | ||
| query?: string; |
There was a problem hiding this comment.
P1: The new cacheKey omits AppContext (_ctx) entirely, but the loader result depends on it through getRecordsByPath(ctx, ...). If the cache storage is shared across sites or tenants within the same runtime, identical query params from different contexts will collide, potentially serving cross-tenant data or stale content. Consider including a site or tenant identifier from ctx in the cache key, or confirming that the framework automatically scopes loader caches.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At blog/loaders/BlogpostList.ts, line 66:
<comment>The new `cacheKey` omits `AppContext` (`_ctx`) entirely, but the loader result depends on it through `getRecordsByPath(ctx, ...)`. If the cache storage is shared across sites or tenants within the same runtime, identical query params from different contexts will collide, potentially serving cross-tenant data or stale content. Consider including a site or tenant identifier from `ctx` in the cache key, or confirming that the framework automatically scopes loader caches.</comment>
<file context>
@@ -48,6 +48,25 @@ export interface Props {
+ props.sortBy ?? url.searchParams.get("sortBy") ?? "date_desc",
+ );
+ const query = String(props.query ?? url.searchParams.get("q") ?? "");
+ const slugs = JSON.stringify(props.postSlugs ?? []);
+ return `blog-list-p${page}-c${count}-s${slug}-${sort}-q${query}-${slugs}`;
+};
</file context>
Add ProductCard, ProductHighlight, and ProductShelf with dynamic product picker and multi-platform resolution (VTEX, Shopify, Wake, WAP). Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
3 issues found across 11 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="blog/loaders/BlogpostList.ts">
<violation number="1" location="blog/loaders/BlogpostList.ts:66">
P1: The new `cacheKey` omits `AppContext` (`_ctx`) entirely, but the loader result depends on it through `getRecordsByPath(ctx, ...)`. If the cache storage is shared across sites or tenants within the same runtime, identical query params from different contexts will collide, potentially serving cross-tenant data or stale content. Consider including a site or tenant identifier from `ctx` in the cache key, or confirming that the framework automatically scopes loader caches.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Remove unused blocksToSections util and short-circuit ProductShelf loader before platform detection when products list is empty. Co-authored-by: Cursor <cursoragent@cursor.com>
Accept string refs, loader-ref shapes, and pre-resolved Products so Spire/admin posts and Studio-edited blocks both render without format loss. Co-authored-by: Cursor <cursoragent@cursor.com>
Skip platform probes when the commerce app is not installed and require VTEX suggestions to return products so Shopify sites are not misdetected. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="blog/utils/productResolver.ts">
<violation number="1" location="blog/utils/productResolver.ts:161">
P1: `probePlatform` now requires non-empty product results from VTEX, Wake, and WAP (`(res?.products?.length ?? 0) > 0`), and an array result from Shopify (`Array.isArray(res)`), to consider a platform "installed." This creates false negatives for correctly configured integrations with empty catalogs or where probe queries return no matches, and the indefinite `platformBySite` cache in `getPlatform` then persists the misclassification for the process lifetime.
A safer approach is to keep the original contract — if `hasInvokePlatform` passes and the loader invocation succeeds without throwing, the platform is available. Platform presence should not depend on whether the hardcoded probe query `"a"` happens to return products.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| query: "a", | ||
| count: 1, | ||
| }); | ||
| return (res?.products?.length ?? 0) > 0; |
There was a problem hiding this comment.
P1: probePlatform now requires non-empty product results from VTEX, Wake, and WAP ((res?.products?.length ?? 0) > 0), and an array result from Shopify (Array.isArray(res)), to consider a platform "installed." This creates false negatives for correctly configured integrations with empty catalogs or where probe queries return no matches, and the indefinite platformBySite cache in getPlatform then persists the misclassification for the process lifetime.
A safer approach is to keep the original contract — if hasInvokePlatform passes and the loader invocation succeeds without throwing, the platform is available. Platform presence should not depend on whether the hardcoded probe query "a" happens to return products.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At blog/utils/productResolver.ts, line 161:
<comment>`probePlatform` now requires non-empty product results from VTEX, Wake, and WAP (`(res?.products?.length ?? 0) > 0`), and an array result from Shopify (`Array.isArray(res)`), to consider a platform "installed." This creates false negatives for correctly configured integrations with empty catalogs or where probe queries return no matches, and the indefinite `platformBySite` cache in `getPlatform` then persists the misclassification for the process lifetime.
A safer approach is to keep the original contract — if `hasInvokePlatform` passes and the loader invocation succeeds without throwing, the platform is available. Platform presence should not depend on whether the hardcoded probe query `"a"` happens to return products.</comment>
<file context>
@@ -141,26 +151,34 @@ async function probePlatform(
count: 1,
});
- return true;
+ return (res?.products?.length ?? 0) > 0;
}
if (platform === "shopify") {
</file context>
Summary
Adds three shoppable product blocks to the blog app for autonomous-blog content, with a searchable admin product picker backed by the site storefront integration.
Supporting changes
blog/loaders/options/productsByTerm.ts— dynamic-options picker for the Deco adminblog/utils/productReference.ts— canonical refs (platform:kind:id)blog/utils/productResolver.ts— multi-platform resolve/search viactx.invoke(VTEX, Shopify, Wake, WAP)blog/utils/productData.ts— image, price, and link display helpersblog/manifest.gen.ts— registers loader and sectionsTest plan
deno task checkpasses on CI