-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add React Server Components with shared MarkdownViewer pattern #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Add react_on_rails_pro gem (local path for development) - Add react-on-rails-rsc npm package (v19.0.2) - Link @shakacode-tools/react-on-rails-pro-node-renderer locally - Add .claude/ to .gitignore This enables React Server Components support for the demo app. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Configure complete RSC infrastructure following React on Rails Pro documentation: **Webpack Configuration:** - Create rscWebpackConfig.js with RSC loader before babel-loader - Add react-server condition to resolve config - Add RSCWebpackPlugin to client and server webpack configs - Update generateWebpackConfigs to include RSC bundle in default build - Support RSC_BUNDLE_ONLY environment variable **Rails Configuration:** - Add rsc_bundle_js_file = "rsc-bundle.js" to react_on_rails initializer - Create react_on_rails_pro initializer with enable_rsc_support = true - Add rsc_payload_route to routes for RSC payload handling **Development Workflow:** - Add wp-rsc process to Procfile.dev for RSC bundle watch mode This sets up the foundation for implementing RSC components with proper bundle splitting and server-side component execution. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Demonstrate React Server Components with a shared viewer pattern: **Shared MarkdownViewer Component:** - Lightweight display component with no markdown processing dependencies - Accepts pre-processed HTML as prop - Reusable by both server and client components - Minimal bundle impact (~2KB) **RSCMarkdownPage Server Component:** - Processes markdown server-side using 'marked' library - Heavy markdown library stays on server, never ships to client - Uses async/await for server-side data fetching - Renders using shared MarkdownViewer component **Rails Integration:** - Created RscMarkdownPageController with ReactOnRailsPro::Stream - Uses stream_view_containing_react_components for streaming SSR - View uses stream_react_component helper for RSC rendering - Added route at /rsc_markdown_page **Dependencies:** - Added 'marked' npm package for server-side markdown processing This demonstrates the key RSC benefit: markdown processing happens entirely on the server, keeping client bundles minimal while providing full markdown rendering capabilities. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Set up .gitignore to exclude .npmrc credentials - Install react-on-rails-pro-node-renderer from GitHub packages - Add client/node-renderer.js configuration file - Update Procfile.dev to run node-renderer process - Configure Node renderer in react_on_rails_pro.rb initializer This switches from local npm link to published packages, resolving dependency issues with fastify and other packages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Ignore /ssr-generated directory containing server-side webpack bundles - Ignore RSC_CONFIG_AUDIT.md analysis file 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Set server_bundle_output_path to "ssr-generated" - Enable enforce_private_server_bundles to prevent fallback to public/packs - Required for React on Rails Pro node renderer to locate bundles 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Update bundlePath from public/packs to ssr-generated - Aligns with Rails configuration for private server bundles 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Enable target: 'node' for Node.js runtime compatibility - Enable libraryTarget: 'commonjs2' for module export format - Add babel-loader SSR caller flag for proper React transforms - Output to ssr-generated directory instead of public/packs - Remove publicPath as server bundles are not web-served Required for React on Rails Pro node renderer to execute server bundles. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Alias react-dom/server to false in RSC bundle - Prevents runtime errors from unused server rendering imports - RSC payload generation uses react-on-rails-rsc instead 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Add directive to HelloWorld for SSR registration - Add directive to HeavyMarkdownEditor for client-side rendering Auto-bundler uses "use client" to determine component registration: - WITH directive → ReactOnRails.register() (SSR/client components) - WITHOUT directive → registerServerComponent() (RSC components) Fixes component mis-registration that caused node renderer crashes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Add foreman gem for managing dev processes via bin/dev. Temporary solution until system-level foreman is installed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Replace local development path with GitHub Packages source for react_on_rails_pro gem. Makes the setup portable and production-ready. Configured via bundle config (credentials not in source code). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Remove foreman from Gemfile (use system-installed foreman instead) - Remove RSC_CONFIG_AUDIT.md from gitignore (file untracked for reference) Keeps the branch cleaner without temporary dependencies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Add padding-bottom to body to prevent content from touching viewport edge, matching the existing top padding. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Add RSC Markdown Page links to HelloWorld and HeavyMarkdownEditor - Add flexbox with gap for proper spacing between navigation buttons - Remove redundant margin-bottom in favor of gap - All three pages now have bidirectional navigation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
CSS modules are not currently supported for React Server Components. Convert RSCMarkdownPage to use inline styles and embedded <style> tag for markdown content styling. - Replace CSS module imports with inline style objects - Add embedded <style> tag for markdown content (tables, code, etc.) - Remove unused RSCMarkdownPage.module.css - Maintain visual consistency with previous CSS module styles Note: MarkdownViewer.module.css is kept as it's still used by client components that import MarkdownViewer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Refactor HeavyMarkdownEditor to use the shared MarkdownViewer component while maintaining the heavy bundle demonstration. Changes: - Import shared MarkdownViewer component - Add react-dom/server to convert ReactMarkdown output to HTML string - Store processedHtml instead of MarkdownComponent in state - Re-process markdown on every change for live preview - Pass HTML to shared MarkdownViewer for display Bundle impact (still heavy): - Transferred: ~315KB (react-markdown + remark-gfm + react-dom/server) - Uncompressed: ~1.53MB - This demonstrates client-side bundle weight vs RSC's server-side processing The shared component pattern is now complete: - HeavyMarkdownEditor: Client-side processing → MarkdownViewer - RSCMarkdownPage: Server-side processing → MarkdownViewer 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Add comprehensive setup instructions for GitHub Packages authentication required to install React on Rails Pro dependencies. Changes: - Add Prerequisites section with step-by-step token creation - Document npm configuration for @shakacode-tools packages - Document Bundler configuration for rubygems.pkg.github.com - Add note about installation failure without authentication - Clarify that each developer needs their own GitHub token This addresses best practices for token management in demo repositories where each developer should use their own credentials rather than sharing tokens. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
WalkthroughAdds React Server Components support and server-side markdown rendering: new gem/source, node renderer, initializers, webpack RSC configs and bundle, routes, controller, view, RSC component and MarkdownViewer, editor and navigation updates, package/gem dependencies, Procfile and .gitignore updates, README and layout padding. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant B as Browser
participant R as Rails (RscMarkdownPageController)
participant S as ReactOnRailsPro::Stream
participant NR as Node Renderer
participant RB as rsc-bundle (server RSC bundle)
B->>R: GET /rsc_markdown_page
R->>S: stream_view_containing_react_components(...)
S->>NR: request render RSC component with props
NR->>RB: execute rsc-bundle.js (server-only code)
RB-->>NR: streamed HTML chunks
NR-->>S: rendered stream
S-->>B: incremental HTML stream to client
note right of NR: renderer auth (password/URL) per initializer/env
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (3 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (5)
Gemfile (1)
61-64
: Consider pinning the gem version.The
react_on_rails_pro
gem has no version constraint, which may lead to unexpected updates. Consider specifying a version or range:# React on Rails Pro from GitHub Packages source "https://rubygems.pkg.github.com/shakacode-tools" do - gem "react_on_rails_pro" + gem "react_on_rails_pro", "~> 4.0" endREADME.md (2)
81-95
: Consider adding token security guidance.The authentication setup is well-documented. Consider adding a note about token security:
### Prerequisites: GitHub Packages Authentication This demo uses **React on Rails Pro** from GitHub Packages. While RORP will be open source soon, it currently requires authentication with your Pro subscription token. +**Security Note:** Never commit your authentication token. The `.npmrc` file is already ignored by `.gitignore`. + 1. **Configure npm for GitHub Packages:**
86-89
: Clarify token environment variable option.The npm config commands store the token in the user's global npm config. Consider documenting the
.npmrc
file option for project-specific auth:1. **Configure npm for GitHub Packages:** ```bash npm config set @shakacode-tools:registry https://npm.pkg.github.com npm config set //npm.pkg.github.com/:_authToken YOUR_PRO_TOKEN ``` + + **Alternative:** Create a local `.npmrc` file (already ignored): + ``` + @shakacode-tools:registry=https://npm.pkg.github.com + //npm.pkg.github.com/:_authToken=YOUR_PRO_TOKEN + ```config/webpack/generateWebpackConfigs.js (1)
13-15
: Include RSC config in envSpecific if needed
Current env-specific callbacks (productionEnvOnly
,testOnly
,developmentEnvOnly
) are defined as(clientConfig, serverConfig)
and do not receiverscConfig
. If you need to apply environment-specific tweaks to the RSC bundle, updategenerateWebpackConfigs
to callenvSpecific(clientConfig, serverConfig, rscConfig)
and revise each env config file’s callback signature accordingly.app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx (1)
22-90
: Consider extracting the default markdown content.The default markdown content (69 lines) is embedded directly in the component, which makes it harder to maintain and test.
Consider moving it to a separate file:
// defaultMarkdownContent.js export const defaultMarkdownContent = `# React Server Components Demo ... `;import React from 'react'; import { marked } from 'marked'; import MarkdownViewer from '../../MarkdownViewer/ror_components/MarkdownViewer'; +import { defaultMarkdownContent } from './defaultMarkdownContent'; async function RSCMarkdownPage({ initialText, title, author, lastModified }) { - const defaultMarkdown = `# React Server Components Demo - ... - `; - - const markdown = initialText || defaultMarkdown; + const markdown = initialText || defaultMarkdownContent;This separation improves readability and makes it easier to update the demo content independently.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
Gemfile.lock
is excluded by!**/*.lock
package-lock.json
is excluded by!**/package-lock.json
📒 Files selected for processing (23)
.gitignore
(2 hunks)Gemfile
(1 hunks)Procfile.dev
(1 hunks)README.md
(2 hunks)app/controllers/rsc_markdown_page_controller.rb
(1 hunks)app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.jsx
(4 hunks)app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.module.css
(1 hunks)app/javascript/src/HelloWorld/ror_components/HelloWorld.jsx
(2 hunks)app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css
(1 hunks)app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx
(1 hunks)app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.module.css
(1 hunks)app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx
(1 hunks)app/views/layouts/application.html.erb
(1 hunks)app/views/rsc_markdown_page/index.html.erb
(1 hunks)client/node-renderer.js
(1 hunks)config/initializers/react_on_rails.rb
(1 hunks)config/initializers/react_on_rails_pro.rb
(1 hunks)config/routes.rb
(1 hunks)config/webpack/clientWebpackConfig.js
(2 hunks)config/webpack/generateWebpackConfigs.js
(2 hunks)config/webpack/rscWebpackConfig.js
(1 hunks)config/webpack/serverWebpackConfig.js
(4 hunks)package.json
(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
config/webpack/generateWebpackConfigs.js (6)
config/webpack/serverWebpackConfig.js (3)
require
(4-4)require
(9-9)serverWebpackConfig
(16-16)config/webpack/clientWebpackConfig.js (2)
require
(5-5)clientConfig
(8-8)config/webpack/development.js (2)
require
(4-4)generateWebpackConfigs
(6-6)config/webpack/test.js (1)
generateWebpackConfigs
(4-4)config/webpack/production.js (1)
generateWebpackConfigs
(4-4)config/webpack/rscWebpackConfig.js (2)
serverWebpackConfig
(2-2)rscConfig
(20-20)
config/initializers/react_on_rails_pro.rb (1)
client/node-renderer.js (1)
config
(7-33)
config/webpack/serverWebpackConfig.js (1)
config/webpack/clientWebpackConfig.js (1)
require
(5-5)
app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.jsx (1)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1)
MarkdownViewer
(16-23)
app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx (1)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1)
MarkdownViewer
(16-23)
config/webpack/clientWebpackConfig.js (2)
config/webpack/serverWebpackConfig.js (3)
commonWebpackConfig
(5-5)require
(4-4)require
(9-9)config/webpack/generateWebpackConfigs.js (1)
clientConfig
(9-9)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1)
app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx (1)
processedHtml
(96-99)
config/webpack/rscWebpackConfig.js (3)
config/webpack/serverWebpackConfig.js (4)
serverWebpackConfig
(16-16)require
(4-4)require
(9-9)rules
(73-73)config/webpack/generateWebpackConfigs.js (2)
serverWebpackConfig
(5-5)rscConfig
(11-11)config/webpack/clientWebpackConfig.js (1)
require
(5-5)
🪛 Biome (2.1.2)
app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx
[error] 103-103: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx
[error] 20-20: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
🔇 Additional comments (25)
.gitignore (1)
56-57
: LGTM! Critical security pattern added.Ignoring
.npmrc
is essential to prevent accidental commits of GitHub Packages authentication tokens. This aligns with the README's GitHub Packages authentication setup.app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css (1)
72-78
: LGTM! Consistent navigation pattern.The flexbox layout provides better vertical spacing for navigation links. This pattern is consistent with similar changes in
HeavyMarkdownEditor.module.css
.package.json (3)
20-20
: LGTM! Node renderer dependency added.The
@shakacode-tools/react-on-rails-pro-node-renderer@^4.0.0
aligns with the React on Rails Pro integration documented in the README.
29-29
: LGTM! Markdown processing dependency added.The
marked@^16.3.0
package provides fast server-side markdown-to-HTML conversion for the RSCMarkdownPage component.Based on learnings
36-36
: LGTM! RSC support dependency added.The
react-on-rails-rsc@^19.0.2
version aligns correctly with React 19.x (react@^19.1.1
andreact-dom@^19.1.1
).Based on learnings
app/views/layouts/application.html.erb (1)
34-34
: LGTM! Symmetric vertical padding added.The
padding-bottom: 2rem
matches the existingpadding-top: 2rem
, providing consistent spacing for the new RSC markdown pages.app/views/rsc_markdown_page/index.html.erb (1)
1-3
: Props Initialization VerifiedRscMarkdownPageController#index
sets@rsc_markdown_page_props
as expected.app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.module.css (1)
24-30
: No contrast issues detected
Inline code background (#f1c40f) with black text achieves a 12.64:1 contrast ratio, exceeding WCAG AAA requirements.Procfile.dev (1)
6-7
: LGTM!The new processes correctly set up the RSC development workflow:
wp-rsc
enables HMR for RSC-only bundle rebuilds during development.node-renderer
starts the renderer service on port 3800 with debug logging, matching the configuration inclient/node-renderer.js
.app/javascript/src/HelloWorld/ror_components/HelloWorld.jsx (2)
1-1
: LGTM!The
"use client"
directive is correctly placed, as this component uses client-side state (useState
) and cannot be a server component.
36-38
: LGTM!The navigation link to the new RSC Markdown Page is correctly implemented and follows the existing pattern.
config/webpack/clientWebpackConfig.js (2)
5-5
: LGTM!The import correctly references the RSCWebpackPlugin from
react-on-rails-rsc/WebpackPlugin
, consistent with the server webpack configuration.
16-17
: LGTM!The RSCWebpackPlugin is correctly configured with
isServer: false
for the client bundle, enabling proper manifest generation for client-side RSC boundaries.app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.module.css (1)
225-228
: LGTM!The navigation layout change to a vertical flex container improves the presentation when multiple navigation links are present and maintains consistency with similar changes in other components.
config/routes.rb (2)
12-13
: LGTM!The
rsc_payload_route
declaration is correctly placed to enable RSC payload routing. This is a helper method provided by ReactOnRailsPro.
19-19
: LGTM!The new route correctly maps to the
RscMarkdownPageController#index
action, enabling navigation to the RSC markdown demo page.config/webpack/generateWebpackConfigs.js (4)
6-6
: LGTM!The import of
rscWebpackConfig
is correctly placed alongside the existing client and server webpack configurations.
11-11
: LGTM!Creating the
rscConfig
before the conditional logic ensures it's available for all bundling scenarios.
27-30
: LGTM!The
RSC_BUNDLE_ONLY
conditional correctly mirrors the existing patterns forCLIENT_BUNDLE_ONLY
andSERVER_BUNDLE_ONLY
, enabling isolated RSC bundle builds during development.
34-35
: LGTM!The default bundling behavior correctly includes all three bundles (client, server, and RSC), with accurate console logging reflecting the change.
app/controllers/rsc_markdown_page_controller.rb (2)
3-4
: LGTM!The controller correctly includes
ReactOnRailsPro::Stream
to enable streaming behavior for React Server Components.
6-14
: LGTM!The
index
action properly initializes props and invokes streaming with the correct template path. The use ofTime.current
forlastModified
is appropriate for server-side rendering.client/node-renderer.js (1)
7-33
:bundlePath
alignment verified. Bothclient/node-renderer.js
andconfig/webpack/serverWebpackConfig.js
resolve tossr-generated
, so no changes are needed.app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx (2)
1-19
: LGTM! Clear documentation of the RSC pattern.The JSDoc comment effectively explains the shared component architecture and the benefits of server-side markdown processing. The distinction between heavy server-side processing and lightweight client rendering is well articulated.
103-340
: Inline styles are extensive but align with RSC constraints.The component uses inline styles throughout due to CSS modules not being supported with React Server Components (as noted in the PR objectives). While this makes the component verbose, it's a necessary trade-off for RSC compatibility.
The styling is consistent and the visual hierarchy is clear. If inline styles become unmanageable as the component grows, consider extracting style objects to constants at the top of the file to improve maintainability.
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.module.css
Outdated
Show resolved
Hide resolved
const processedHtml = await marked(markdown, { | ||
gfm: true, // GitHub Flavored Markdown | ||
breaks: true, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Sanitize markdown output to prevent XSS attacks.
The marked
library processes markdown without sanitization before passing it to MarkdownViewer
, which renders the HTML using dangerouslySetInnerHTML
. If initialText
originates from user input or any untrusted source, this creates an XSS vulnerability where malicious markdown could inject arbitrary HTML/JavaScript.
According to the marked
documentation, you should pair its output with an HTML sanitizer when processing untrusted content. Based on learnings:
Apply this fix to sanitize the HTML output:
import React from 'react';
import { marked } from 'marked';
+import DOMPurify from 'isomorphic-dompurify';
import MarkdownViewer from '../../MarkdownViewer/ror_components/MarkdownViewer';
const processedHtml = await marked(markdown, {
gfm: true, // GitHub Flavored Markdown
breaks: true,
});
+ const sanitizedHtml = DOMPurify.sanitize(processedHtml);
return (
<>
{/* ... */}
- <MarkdownViewer processedHtml={processedHtml} className="rsc-markdown-content" />
+ <MarkdownViewer processedHtml={sanitizedHtml} className="rsc-markdown-content" />
</>
);
If this is purely a demo with hardcoded content and initialText
never comes from users, document this assumption clearly in the component's JSDoc to prevent future misuse.
Also applies to: 290-290
🏁 Script executed:
#!/bin/bash
set -e
echo "1. Locating RSCMarkdownPage.jsx..."
fd -e jsx RSCMarkdownPage.jsx
echo "2. Inspecting RSCMarkdownPage.jsx (lines 1-200)..."
sed -n '1,200p' app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx
echo "3. Locating MarkdownViewer component..."
fd -e jsx -e tsx MarkdownViewer.jsx
echo "4. Searching for dangerouslySetInnerHTML across codebase..."
rg -n 'dangerouslySetInnerHTML' -C3
echo "5. Searching for DOMPurify usage..."
rg -n 'DOMPurify' -C3 || true
echo "6. Searching for sanitize calls..."
rg -n 'sanitize' -C3 || true
Length of output: 11069
Sanitize marked output before rendering.
Wrap processedHtml
in a sanitizer before passing it to MarkdownViewer
(which uses dangerouslySetInnerHTML
):
import { marked } from 'marked';
+import DOMPurify from 'isomorphic-dompurify';
const processedHtml = await marked(markdown, {
gfm: true,
breaks: true,
});
+ const sanitizedHtml = DOMPurify.sanitize(processedHtml);
return (
<>
{/* … */}
- <MarkdownViewer processedHtml={processedHtml} className="rsc-markdown-content" />
+ <MarkdownViewer processedHtml={sanitizedHtml} className="rsc-markdown-content" />
</>
);
If initialText
is always demo content and never comes from users, add a JSDoc note to that effect.
🤖 Prompt for AI Agents
In app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx around
lines 96 to 99, the HTML produced by marked is passed straight to MarkdownViewer
which uses dangerouslySetInnerHTML; sanitize processedHtml first (e.g., import
and use DOMPurify to call DOMPurify.sanitize(processedHtml) or a configured
sanitize-html equivalent) and pass the sanitized string into MarkdownViewer
instead, and if initialText is truly static demo content add a JSDoc comment
above the component or the prop explaining it never contains user input so it's
safe.
<style dangerouslySetInnerHTML={{ __html: ` | ||
.rsc-markdown-content h1, | ||
.rsc-markdown-content h2, | ||
.rsc-markdown-content h3 { | ||
color: #2c3e50; | ||
margin-top: 1.5rem; | ||
margin-bottom: 0.5rem; | ||
} | ||
.rsc-markdown-content h1 { | ||
border-bottom: 2px solid #e67e22; | ||
padding-bottom: 0.3rem; | ||
} | ||
.rsc-markdown-content code { | ||
background-color: #f1c40f; | ||
padding: 2px 4px; | ||
border-radius: 3px; | ||
font-size: 0.9em; | ||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | ||
} | ||
.rsc-markdown-content pre { | ||
background-color: #2c3e50; | ||
color: #ecf0f1; | ||
padding: 1rem; | ||
border-radius: 4px; | ||
overflow-x: auto; | ||
} | ||
.rsc-markdown-content pre code { | ||
background: none; | ||
color: inherit; | ||
padding: 0; | ||
} | ||
.rsc-markdown-content table { | ||
border-collapse: collapse; | ||
width: 100%; | ||
margin: 1rem 0; | ||
} | ||
.rsc-markdown-content th, | ||
.rsc-markdown-content td { | ||
border: 1px solid #bdc3c7; | ||
padding: 0.5rem; | ||
text-align: left; | ||
} | ||
.rsc-markdown-content th { | ||
background-color: #ecf0f1; | ||
font-weight: bold; | ||
} | ||
.rsc-markdown-content blockquote { | ||
border-left: 4px solid #e67e22; | ||
margin: 1rem 0; | ||
padding-left: 1rem; | ||
color: #7f8c8d; | ||
} | ||
.rsc-markdown-content p { | ||
margin-bottom: 1rem; | ||
line-height: 1.6; | ||
} | ||
` }} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CSS injection via inline styles (low risk).
Static analysis flagged dangerouslySetInnerHTML
for the inline CSS. While CSS injection is less dangerous than HTML injection, it can still be a vector for certain attacks (e.g., data exfiltration via CSS selectors, UI redressing).
Since the CSS is static and hardcoded in this component, the risk is minimal. However, if you ever need to make these styles dynamic or configurable, ensure they are validated and escaped appropriately. For truly dynamic styles, consider using a CSS-in-JS solution that provides automatic escaping.
🧰 Tools
🪛 Biome (2.1.2)
[error] 103-103: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
🤖 Prompt for AI Agents
In app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx around
lines 103 to 159 the component injects static CSS via dangerouslySetInnerHTML
which flags CSS injection; since the styles are static, remove the inline
dangerouslySetInnerHTML and move the CSS into a safer place — e.g. a dedicated
.css/.module.css file imported into the component or a
styled-components/CSS-in-JS definition — and if any styles ever become dynamic,
validate/sanitize values or switch to a CSS-in-JS approach that scopes and
escapes style values instead of concatenating raw strings.
- Add isomorphic-dompurify for HTML sanitization - Sanitize all HTML in MarkdownViewer to prevent XSS attacks - Fix task list CSS selector from li[data-task] to li.task-list-item (matches actual HTML output from marked/react-markdown) Addresses CodeRabbit security review comments 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Remove default password fallback for production environments - Keep "devPassword" fallback for development/test convenience - Production deployments will now fail fast if RENDERER_PASSWORD is unset Addresses CodeRabbit security review comment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (4)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.module.css (2)
1-21
: Consider using CSS custom properties for colors.The stylesheet uses hard-coded color values throughout (e.g.,
#2c3e50
,#e67e22
). For better maintainability and theme consistency, consider defining these as CSS custom properties at the:root
or.markdownContent
level.Example refactor:
+/* Color palette */ +.markdownContent { + --color-heading: #2c3e50; + --color-accent: #e67e22; + --color-text-muted: #7f8c8d; + --color-link: #3498db; + --color-code-bg: #f1c40f; + --color-pre-bg: #2c3e50; + --color-pre-text: #ecf0f1; + line-height: 1.6; +} .markdownContent h1, .markdownContent h2, .markdownContent h3, .markdownContent h4, .markdownContent h5, .markdownContent h6 { - color: #2c3e50; + color: var(--color-heading); margin-top: 1.5rem; margin-bottom: 0.5rem; } .markdownContent h1 { - border-bottom: 2px solid #e67e22; + border-bottom: 2px solid var(--color-accent); padding-bottom: 0.3rem; }
24-26
: Consider a more subtle background for inline code.The bright yellow background (
#f1c40f
) for inline code may be visually distracting. Consider using a more subtle color like a light gray (#f4f4f4
or#e8e8e8
) for better readability and less visual noise.Example:
.markdownContent code { - background-color: #f1c40f; + background-color: #f4f4f4; padding: 2px 4px; border-radius: 3px; font-size: 0.9em; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; }app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (2)
19-19
: Consider adding prop validation.Adding PropTypes or TypeScript types would improve type safety and provide better developer experience with IDE autocomplete and inline documentation.
Example with PropTypes:
import React from 'react'; +import PropTypes from 'prop-types'; import DOMPurify from 'isomorphic-dompurify'; import * as style from './MarkdownViewer.module.css'; const MarkdownViewer = ({ processedHtml, className }) => { const sanitizedHtml = DOMPurify.sanitize(processedHtml); return ( <div className={`${style.markdownContent} ${className || ''}`} dangerouslySetInnerHTML={{ __html: sanitizedHtml }} /> ); }; +MarkdownViewer.propTypes = { + processedHtml: PropTypes.string.isRequired, + className: PropTypes.string, +}; + export default MarkdownViewer;
24-24
: Simplify className concatenation.The
className || ''
fallback is not strictly necessary since undefined will be coerced to an empty string in string concatenation, and trailing whitespace is typically trimmed by browsers.Apply this diff for cleaner code:
- className={`${style.markdownContent} ${className || ''}`} + className={`${style.markdownContent} ${className ?? ''}`.trim()}Or use a dedicated library like
classnames
for more robust className handling if the project already has it as a dependency.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.json
is excluded by!**/package-lock.json
📒 Files selected for processing (5)
app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.module.css
(2 hunks)app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx
(1 hunks)app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.module.css
(1 hunks)config/initializers/react_on_rails_pro.rb
(1 hunks)package.json
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- package.json
- config/initializers/react_on_rails_pro.rb
- app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.module.css
🧰 Additional context used
🧬 Code graph analysis (1)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1)
app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx (1)
processedHtml
(96-99)
🪛 Biome (2.1.2)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx
[error] 25-25: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
🔇 Additional comments (2)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.module.css (1)
66-69
: Task list selector correctly updated!The selector now uses
.task-list-item
, which correctly matches the HTML structure generated byremark-gfm
. This addresses the previous concern about the incompatibleli[data-task]
selector.app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1)
19-27
: XSS concern addressed with DOMPurify sanitization!The component now sanitizes
processedHtml
usingDOMPurify.sanitize()
on line 20 before rendering it withdangerouslySetInnerHTML
. This addresses the previous critical XSS concern and implements the recommended pattern.Note: The Biome static analysis warning about
dangerouslySetInnerHTML
is a false positive in this case, as the HTML is properly sanitized before injection. The use ofisomorphic-dompurify
ensures safe sanitization on both client and server.
Summary
Key Changes
marked
react-markdown
output to HTML viarenderToString
Bundle Sizes
Test Plan
🤖 Generated with Claude Code
Summary by CodeRabbit