Skip to content

Commit 9561e21

Browse files
justin808claude
andcommitted
Improve Early Hints tooling and documentation
- check_early_hints.js: Exit with error code 1 when no Early Hints found - Footer.jsx: Extract reusable SVG icon components to reduce duplication - chrome-mcp-server-setup.md: Add note about Playwright not capturing 103 responses, show CDP approach for detecting Early Hints - thruster.md: Add Shakapacker early_hints configuration example 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ed38906 commit 9561e21

File tree

5 files changed

+76
-34
lines changed

5 files changed

+76
-34
lines changed

check_early_hints.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ const req = http.get('http://localhost:9222/json', (res) => {
5757
// Look for Early Hints debug comments
5858
const earlyHintsMatch = html.match(/<!--[\s\S]*?Early Hints[\s\S]{0,500}?-->/g);
5959

60+
let earlyHintsFound = false;
61+
6062
if (earlyHintsMatch) {
63+
earlyHintsFound = true;
6164
console.log('🎉 Found Early Hints debug comments in HTML!\n');
6265
earlyHintsMatch.forEach(match => {
6366
console.log(match);
@@ -82,7 +85,7 @@ const req = http.get('http://localhost:9222/json', (res) => {
8285
}
8386

8487
ws.close();
85-
process.exit(0);
88+
process.exit(earlyHintsFound ? 0 : 1);
8689
}
8790
} catch (e) {
8891
// Ignore parse errors for other messages

check_early_hints.py

100644100755
File mode changed.

client/app/bundles/comments/components/Footer/ror_components/Footer.jsx

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,37 @@
11
import React from 'react';
22
import BaseComponent from 'libs/components/BaseComponent';
33

4+
// Reusable icon components to reduce SVG duplication
5+
const CheckmarkIcon = ({ className = 'w-4 h-4', color = 'text-green-400' }) => (
6+
<svg className={`${className} ${color}`} fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
7+
<path
8+
fillRule="evenodd"
9+
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"
10+
clipRule="evenodd"
11+
/>
12+
</svg>
13+
);
14+
15+
const CheckmarkCircleIcon = ({ className = 'w-4 h-4', color = 'text-green-400' }) => (
16+
<svg className={`${className} ${color}`} fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
17+
<path
18+
fillRule="evenodd"
19+
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"
20+
clipRule="evenodd"
21+
/>
22+
</svg>
23+
);
24+
25+
const InfoIcon = ({ className = 'w-4 h-4', color = 'text-yellow-400' }) => (
26+
<svg className={`${className} ${color}`} fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
27+
<path
28+
fillRule="evenodd"
29+
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
30+
clipRule="evenodd"
31+
/>
32+
</svg>
33+
);
34+
435
export default class Footer extends BaseComponent {
536
render() {
637
return (
@@ -19,13 +50,7 @@ export default class Footer extends BaseComponent {
1950
<div className="mt-6 pt-6 border-t border-neutral-700 text-sm text-neutral-400">
2051
<div className="flex flex-col gap-3">
2152
<div className="flex items-center gap-2">
22-
<svg className="w-4 h-4 text-green-400" fill="currentColor" viewBox="0 0 20 20">
23-
<path
24-
fillRule="evenodd"
25-
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"
26-
clipRule="evenodd"
27-
/>
28-
</svg>
53+
<CheckmarkCircleIcon className="w-4 h-4" color="text-green-400" />
2954
<span>
3055
Powered by{' '}
3156
<a
@@ -41,37 +66,19 @@ export default class Footer extends BaseComponent {
4166
</div>
4267
<div className="flex flex-wrap gap-x-4 gap-y-2 ml-6">
4368
<div className="flex items-center gap-1.5">
44-
<svg className="w-3.5 h-3.5 text-emerald-400" fill="currentColor" viewBox="0 0 20 20">
45-
<path
46-
fillRule="evenodd"
47-
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"
48-
clipRule="evenodd"
49-
/>
50-
</svg>
69+
<CheckmarkIcon className="w-3.5 h-3.5" color="text-emerald-400" />
5170
<span className="text-xs">HTTP/2 Enabled</span>
5271
</div>
5372
<div className="flex items-center gap-1.5">
54-
<svg className="w-3.5 h-3.5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
55-
<path
56-
fillRule="evenodd"
57-
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
58-
clipRule="evenodd"
59-
/>
60-
</svg>
73+
<InfoIcon className="w-3.5 h-3.5" color="text-yellow-400" />
6174
<span className="text-xs" title="Configured in Rails but stripped by Cloudflare CDN">
6275
Early Hints (Configured)
6376
</span>
6477
</div>
6578
<div className="flex items-center gap-1.5">
66-
<svg className="w-3.5 h-3.5 text-emerald-400" fill="currentColor" viewBox="0 0 20 20">
67-
<path
68-
fillRule="evenodd"
69-
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"
70-
clipRule="evenodd"
71-
/>
72-
</svg>
79+
<CheckmarkIcon className="w-3.5 h-3.5" color="text-emerald-400" />
7380
<span className="text-xs">
74-
Hosted on{' '}
81+
Supports{' '}
7582
<a
7683
href="https://shakacode.controlplane.com"
7784
className="text-blue-400 hover:text-blue-300 underline"
@@ -80,6 +87,7 @@ export default class Footer extends BaseComponent {
8087
>
8188
Control Plane
8289
</a>
90+
{' '}hosting
8391
</span>
8492
</div>
8593
</div>

docs/chrome-mcp-server-setup.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,12 @@ If the MCP server is too complex, you could also:
256256
```
257257

258258
2. **Create a test script:**
259+
260+
> **Note:** Playwright's `page.on('response')` does not capture HTTP 1xx informational
261+
> responses (status 103). The browser handles 103 Early Hints as preload hints without
262+
> exposing them as regular Response objects. To detect Early Hints programmatically,
263+
> you need to use the Chrome DevTools Protocol (CDP) directly.
264+
259265
```javascript
260266
// verify-early-hints.js
261267
const { chromium } = require('playwright');
@@ -265,12 +271,19 @@ If the MCP server is too complex, you could also:
265271
const context = await browser.newContext();
266272
const page = await context.newPage();
267273

268-
// Listen to all network responses
274+
// Use CDP to detect Early Hints (103 responses)
275+
const client = await context.newCDPSession(page);
276+
await client.send('Network.enable');
277+
278+
// Listen for Early Hints via CDP
279+
client.on('Network.responseReceivedEarlyHints', (params) => {
280+
console.log('✅ Early Hints detected!');
281+
console.log('Headers:', params.headers);
282+
});
283+
284+
// Regular responses (won't include 103)
269285
page.on('response', response => {
270286
console.log(`${response.status()} ${response.url()}`);
271-
if (response.status() === 103) {
272-
console.log('✅ Early Hints detected!');
273-
}
274287
});
275288

276289
await page.goto('https://rails-pdzxq1kxxwqg8.cpln.app/');

docs/thruster.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,24 @@ Thruster works seamlessly with Shakapacker for both Webpack and Rspack:
295295
- Manifest files are properly served
296296
- Hot Module Replacement (HMR) still works in development
297297

298+
#### Shakapacker Early Hints Configuration
299+
300+
To enable early hints in production, add the following to your `config/shakapacker.yml`:
301+
302+
```yaml
303+
# config/shakapacker.yml
304+
production:
305+
<<: *default
306+
# ... other settings ...
307+
308+
# Early hints configuration
309+
early_hints:
310+
enabled: true
311+
debug: true # Set to true to output debug info as HTML comments
312+
```
313+
314+
This allows Shakapacker to send Link headers for webpack assets, which Thruster can use to optimize asset loading via HTTP/2 server push or 103 Early Hints responses.
315+
298316
## Performance Expectations
299317

300318
Based on typical Rails applications with Thruster:

0 commit comments

Comments
 (0)