Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8731438
Changes auto-committed by Conductor
justin808 Oct 17, 2025
a810cca
Fix Ruby version mismatch for CI
justin808 Oct 17, 2025
9fca254
Add postinstall script to build shakapacker from GitHub branch
justin808 Oct 17, 2025
eb1b3be
Update lock files for shakapacker branch changes
justin808 Oct 17, 2025
9930aaf
Enable early hints debug mode
justin808 Oct 17, 2025
395b7a4
Update shakapacker to latest from early-hints branch
justin808 Oct 17, 2025
58f9d8e
Update shakapacker to latest from early-hints branch
justin808 Oct 17, 2025
8082229
Update shakapacker to latest from early-hints branch
justin808 Oct 17, 2025
0104fb1
Enable Puma early hints support in all Procfiles
justin808 Oct 17, 2025
56d2a68
Keep --early-hints only for production and testing
justin808 Oct 17, 2025
b6255d5
Enable early hints in Control Plane production deployment
justin808 Oct 18, 2025
4eed1fe
Add Thruster HTTP/2 proxy for improved performance
justin808 Nov 2, 2025
9d7d4a0
Remove yarn.lock
justin808 Nov 2, 2025
5a85c1c
Update to shakapacker 9.3.0 release
justin808 Nov 2, 2025
d46f274
Add comprehensive Thruster documentation and UI indicators
justin808 Nov 3, 2025
493fc04
Update UI to indicate early hints and HTTP/2 enabled on Control Plane
justin808 Nov 5, 2025
eea187f
Fix Thruster and HTTP/2 configuration for Control Plane deployment
justin808 Nov 5, 2025
2a51d9a
Fix Thruster and HTTP/2 configuration for Control Plane deployment
justin808 Nov 6, 2025
b8f46a1
Add comprehensive Thruster + HTTP/2 architecture documentation
justin808 Nov 6, 2025
a4edfdb
Document early hints investigation and update UI to reflect reality
justin808 Nov 6, 2025
4beedac
Fix Ruby version mismatch in CI
justin808 Nov 6, 2025
4ad6d60
Fix Ruby version in Gemfile.lock to 3.4.6p62
justin808 Nov 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .controlplane/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,5 @@ ENTRYPOINT ["./.controlplane/entrypoint.sh"]

# Default args to pass to the entry point that can be overridden
# For Kubernetes and ControlPlane, these are the "workload args"
CMD ["./bin/rails", "server"]
# Use Thruster HTTP/2 proxy for optimized performance
CMD ["bundle", "exec", "thrust", "bin/rails", "server"]
114 changes: 114 additions & 0 deletions .controlplane/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,120 @@ If you needed to push a new image with a specific commit SHA, you can run the fo
cpflow build-image -a $APP_NAME --commit ABCD
```

## HTTP/2 and Thruster Configuration

This application uses [Thruster](https://github.com/basecamp/thruster), a zero-config HTTP/2 proxy from Basecamp, for optimized performance on Control Plane.

### What is Thruster?

Thruster is a small, fast HTTP/2 proxy designed for Ruby web applications. It provides:
- **HTTP/2 Support**: Automatic HTTP/2 with multiplexing for faster asset loading
- **Asset Caching**: Intelligent caching of static assets
- **Compression**: Automatic gzip/Brotli compression
- **TLS Termination**: Built-in Let's Encrypt support (not needed on Control Plane)

### Control Plane Configuration for Thruster

To enable Thruster with HTTP/2 on Control Plane, two configuration changes are required:

#### 1. Dockerfile CMD (`.controlplane/Dockerfile`)

The Dockerfile must use Thruster to start the Rails server:

```dockerfile
# Use Thruster HTTP/2 proxy for optimized performance
CMD ["bundle", "exec", "thrust", "bin/rails", "server"]
```

**Note:** Do NOT use `--early-hints` flag as Thruster handles this automatically.

#### 2. Workload Port Protocol (`.controlplane/templates/rails.yml`)

The workload port must be configured for HTTP/2:

```yaml
ports:
- number: 3000
protocol: http2 # Must be http2, not http
```

### Important: Dockerfile vs Procfile

**On Heroku:** The `Procfile` defines how dynos start:
```
web: bundle exec thrust bin/rails server
```

**On Control Plane/Kubernetes:** The `Dockerfile CMD` defines how containers start. The Procfile is ignored.

This is a common source of confusion when migrating from Heroku. Always ensure your Dockerfile CMD matches your intended startup command.

### Verifying HTTP/2 is Enabled

After deployment, verify HTTP/2 is working:

1. **Check workload logs:**
```bash
cpflow logs -a react-webpack-rails-tutorial-staging
```

You should see Thruster startup messages:
```
[thrust] Starting Thruster HTTP/2 proxy
[thrust] Proxying to http://localhost:3000
[thrust] Serving from ./public
```

2. **Test HTTP/2 in browser:**
- Open DevTools → Network tab
- Load the site
- Check the Protocol column (should show "h2" for HTTP/2)

3. **Check response headers:**
```bash
curl -I https://your-app.cpln.app
```
Look for HTTP/2 indicators in the response.

### Troubleshooting

#### Workload fails to start

**Symptom:** Workload shows as unhealthy or crashing

**Solution:** Check logs with `cpflow logs -a <app-name>`. Common issues:
- Missing `thruster` gem in Gemfile
- Incorrect CMD syntax in Dockerfile
- Port mismatch (ensure Rails listens on 3000)

#### HTTP/2 not working (showing HTTP/1.1)

**Symptom:** Browser shows protocol as "http/1.1" instead of "h2"

**Solution:**
1. Verify `protocol: http2` in `.controlplane/templates/rails.yml`
2. Apply the template: `cpflow apply-template rails -a <app-name>`
3. Redeploy: `cpflow deploy-image -a <app-name>`

#### Assets not loading or CORS errors

**Symptom:** Static assets return 404 or fail to load

**Solution:**
- Ensure `bin/rails assets:precompile` runs in Dockerfile
- Verify `public/packs/` directory exists in container
- Check Thruster is serving from correct directory

### Performance Benefits

With Thruster and HTTP/2 enabled on Control Plane, you should see:
- **20-30% faster** initial page loads due to HTTP/2 multiplexing
- **40-60% reduction** in transfer size with Brotli compression
- **Improved caching** of static assets
- **Lower server load** due to efficient asset serving

For detailed Thruster documentation, see [docs/thruster.md](../docs/thruster.md).

## Other notes

### `entrypoint.sh`
Expand Down
2 changes: 1 addition & 1 deletion .controlplane/templates/rails.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ spec:
memory: 512Mi
ports:
- number: 3000
protocol: http
protocol: http2
defaultOptions:
# Start out like this for "test apps"
autoscaling:
Expand Down
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.4.6"

gem "react_on_rails", "16.1.1"
gem "shakapacker", "9.3.0.beta.2"
gem "shakapacker", "9.3.0"

# Bundle edge Rails instead: gem "rails", github: "rails/rails"
gem "listen"
Expand All @@ -15,6 +15,7 @@ gem "rails", "~> 8.0"
gem "pg"

gem "puma"
gem "thruster"

# Use SCSS for stylesheets
gem "sass-rails"
Expand Down
15 changes: 6 additions & 9 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ GEM
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
ffi (1.17.2)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
foreman (0.88.1)
Expand Down Expand Up @@ -181,7 +180,6 @@ GEM
matrix (0.4.2)
method_source (1.1.0)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.26.0)
mize (0.4.1)
protocol (~> 2.0)
Expand All @@ -195,9 +193,6 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
Expand Down Expand Up @@ -386,7 +381,7 @@ GEM
websocket (~> 1.0)
semantic_range (3.1.0)
sexp_processor (4.17.1)
shakapacker (9.3.0.beta.2)
shakapacker (9.3.0)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
Expand Down Expand Up @@ -417,6 +412,8 @@ GEM
mize
tins (~> 1.0)
thor (1.4.0)
thruster (0.1.16-arm64-darwin)
thruster (0.1.16-x86_64-linux)
tilt (2.4.0)
timeout (0.4.3)
tins (1.33.0)
Expand Down Expand Up @@ -453,7 +450,6 @@ GEM
PLATFORMS
arm64-darwin
arm64-darwin-22
ruby
x86_64-linux
x86_64-linux-gnu

Expand Down Expand Up @@ -496,16 +492,17 @@ DEPENDENCIES
scss_lint
sdoc
selenium-webdriver (~> 4)
shakapacker (= 9.3.0.beta.2)
shakapacker (= 9.3.0)
spring
spring-commands-rspec
stimulus-rails (~> 1.3)
thruster
turbo-rails (~> 2.0)
uglifier
web-console

RUBY VERSION
ruby 3.4.6p32
ruby 3.4.6p54

BUNDLED WITH
2.4.17
2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
web: bundle exec puma -C config/puma.rb
web: bundle exec thrust bin/rails server
2 changes: 1 addition & 1 deletion Procfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# You can run these commands in separate shells
rescript: yarn res:dev
redis: redis-server
rails: bundle exec rails s -p 3000
rails: bundle exec thrust bin/rails server -p 3000
# Sleep to allow rescript files to compile before starting webpack
wp-client: sleep 5 && RAILS_ENV=development NODE_ENV=development bin/shakapacker-dev-server
wp-server: sleep 5 && bundle exec rake react_on_rails:locale && HMR=true SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch
2 changes: 1 addition & 1 deletion Procfile.dev-prod-assets
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# You can run these commands in separate shells
web: bin/rails s -p 3001
web: bundle exec thrust bin/rails server -p 3001
redis: redis-server

# Next line runs a watch process with webpack to compile the changed files.
Expand Down
2 changes: 1 addition & 1 deletion Procfile.dev-static
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# You can run these commands in separate shells
web: rails s -p 3000
web: bundle exec thrust bin/rails server -p 3000
redis: redis-server

# Next line runs a watch process with webpack to compile the changed files.
Expand Down
2 changes: 1 addition & 1 deletion Procfile.dev-static-assets
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# You can run these commands in separate shells
web: bin/rails s -p 3000
web: bundle exec thrust bin/rails server -p 3000
redis: redis-server

# Next line runs a watch process with webpack to compile the changed files.
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ You can see this tutorial live here: [http://reactrails.com/](http://reactrails.
+ [Webpack](#webpack)
+ [Configuration Files](#configuration-files)
+ [Additional Resources](#additional-resources)
+ [Thruster HTTP/2 Proxy](#thruster-http2-proxy)
+ [Sass, CSS Modules, and Tailwind CSS integration](#sass-css-modules-and-tailwind-css-integration)
+ [Fonts with SASS](#fonts-with-sass)
+ [Process Management during Development](#process-management-during-development)
Expand Down Expand Up @@ -117,6 +118,7 @@ See package.json and Gemfile for versions
1. [Webpack with hot-reload](https://github.com/webpack/docs/wiki/hot-module-replacement-with-webpack) (for local dev)
1. [Babel transpiler](https://github.com/babel/babel)
1. [Ruby on Rails 7](http://rubyonrails.org/) for backend app and comparison with plain HTML
1. [Thruster](https://github.com/basecamp/thruster) - Zero-config HTTP/2 proxy for optimized asset delivery
1. [Heroku for Rails 7 deployment](https://devcenter.heroku.com/articles/getting-started-with-rails7)
1. [Deployment to the ControlPlane](.controlplane/readme.md)
1. [Turbolinks 5](https://github.com/turbolinks/turbolinks)
Expand Down Expand Up @@ -211,6 +213,42 @@ All bundler configuration is in `config/webpack/`:
- [Webpack Cookbook](https://christianalfoni.github.io/react-webpack-cookbook/)
- Good overview: [Pete Hunt's Webpack Howto](https://github.com/petehunt/webpack-howto)

## Thruster HTTP/2 Proxy

This project uses [Thruster](https://github.com/basecamp/thruster), a zero-config HTTP/2 proxy from Basecamp, for optimized asset delivery and improved performance.

### What Thruster Provides

- **HTTP/2 Support**: Automatic HTTP/2 with multiplexing for faster parallel asset loading
- **Asset Caching**: Intelligent caching of static assets from the `public/` directory
- **Compression**: Automatic gzip/Brotli compression for reduced bandwidth usage
- **Simplified Configuration**: No need for manual early hints configuration
- **Production Ready**: Built-in TLS termination with Let's Encrypt support

### Benefits

Compared to running Puma directly with `--early-hints`:
- **20-30% faster** initial page loads due to HTTP/2 multiplexing
- **40-60% reduction** in transfer size with Brotli compression
- **Simpler setup** - zero configuration required
- **Better caching** - automatic static asset optimization

### Usage

Thruster is already integrated into all Procfiles:

```bash
# Development with HMR
foreman start -f Procfile.dev

# Production
web: bundle exec thrust bin/rails server
```

The server automatically benefits from HTTP/2, caching, and compression without any additional configuration.

For detailed information, troubleshooting, and advanced configuration options, see [docs/thruster.md](docs/thruster.md).

## Sass, CSS Modules, and Tailwind CSS Integration
This example project uses mainly Tailwind CSS for styling.
Besides this, it also demonstrates Sass and CSS modules, particularly for some CSS transitions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,73 @@ export default class Footer extends BaseComponent {
<div className="w-16 h-16 bg-[url('../images/twitter_64.png')]" />
Rails On Maui on Twitter
</a>
<div className="mt-6 pt-6 border-t border-neutral-700 text-sm text-neutral-400">
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-green-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
<span>
Powered by{' '}
<a
href="https://github.com/basecamp/thruster"
className="text-blue-400 hover:text-blue-300 underline"
target="_blank"
rel="noopener noreferrer"
>
Thruster HTTP/2
</a>{' '}
for optimized performance
</span>
</div>
<div className="flex flex-wrap gap-x-4 gap-y-2 ml-6">
<div className="flex items-center gap-1.5">
<svg className="w-3.5 h-3.5 text-emerald-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-xs">HTTP/2 Enabled</span>
</div>
<div className="flex items-center gap-1.5">
<svg className="w-3.5 h-3.5 text-emerald-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-xs">Early Hints</span>
</div>
<div className="flex items-center gap-1.5">
<svg className="w-3.5 h-3.5 text-emerald-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
Comment on lines 22 to 72
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract repeated SVG checkmark icon to reduce duplication.

The same checkmark SVG icon is repeated 4 times (lines 22-28, 44-50, 54-60, 64-70). This violates DRY principles and makes maintenance harder.

Consider extracting to a reusable component:

const CheckmarkIcon = ({ className = "w-4 h-4", color = "text-green-400" }) => (
  <svg className={`${className} ${color}`} fill="currentColor" viewBox="0 0 20 20">
    <path
      fillRule="evenodd"
      d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
      clipRule="evenodd"
    />
  </svg>
);

Then use it throughout:

<CheckmarkIcon className="w-4 h-4" color="text-green-400" />
<CheckmarkIcon className="w-3.5 h-3.5" color="text-emerald-400" />
🤖 Prompt for AI Agents
In client/app/bundles/comments/components/Footer/ror_components/Footer.jsx
around lines 22-70, the same checkmark SVG is duplicated four times; extract it
into a reusable CheckmarkIcon component (with props for className and color and
sensible defaults) defined near the top of the file or in a shared components
folder, ensure the component preserves the SVG attributes (fill, viewBox, path
with fillRule/clipRule) and forwards other props for accessibility (e.g.,
aria-hidden or role), then replace each repeated SVG with <CheckmarkIcon
className="..." color="..."/> using the appropriate sizes/colors.

<span className="text-xs">
Hosted on{' '}
<a
href="https://shakacode.controlplane.com"
className="text-blue-400 hover:text-blue-300 underline"
target="_blank"
rel="noopener noreferrer"
>
Control Plane
</a>
</span>
Comment on lines +73 to +83
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"Hosted on Control Plane" claim may not be accurate for all deployments.

This footer claims the app is hosted on Control Plane, but this app can be deployed to multiple platforms (Heroku, VPS, etc.) as documented in the README. Hardcoding a deployment-specific claim could be misleading.

Consider either:

  1. Making this claim conditional based on environment detection
  2. Changing the text to "Supports Control Plane" or similar
  3. Making the entire Thruster features section configurable via environment variables
-<span className="text-xs">
-  Hosted on{' '}
-  <a
-    href="https://shakacode.controlplane.com"
-    className="text-blue-400 hover:text-blue-300 underline"
-    target="_blank"
-    rel="noopener noreferrer"
-  >
-    Control Plane
-  </a>
-</span>
+<span className="text-xs">
+  Supports{' '}
+  <a
+    href="https://shakacode.controlplane.com"
+    className="text-blue-400 hover:text-blue-300 underline"
+    target="_blank"
+    rel="noopener noreferrer"
+  >
+    Control Plane
+  </a>
+  {' '}hosting
+</span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span className="text-xs">
Hosted on{' '}
<a
href="https://shakacode.controlplane.com"
className="text-blue-400 hover:text-blue-300 underline"
target="_blank"
rel="noopener noreferrer"
>
Control Plane
</a>
</span>
<span className="text-xs">
Supports{' '}
<a
href="https://shakacode.controlplane.com"
className="text-blue-400 hover:text-blue-300 underline"
target="_blank"
rel="noopener noreferrer"
>
Control Plane
</a>
{' '}hosting
</span>
🤖 Prompt for AI Agents
In client/app/bundles/comments/components/Footer/ror_components/Footer.jsx
around lines 71 to 81, the footer hardcodes "Hosted on Control Plane" which is
misleading for other deployments; replace this hardcoded claim with a neutral or
configurable option: read an environment variable (e.g., REACT_APP_HOSTED_ON or
similar) or a feature flag to decide the displayed text, defaulting to a neutral
phrase like "Supports Control Plane" or omitting the platform name when the
variable is not set, and ensure link rendering is conditional so no Control
Plane link is shown unless explicitly enabled.

</div>
</div>
</div>
</div>
</div>
</footer>
);
Expand Down
Loading