@invisibleloop/pulse 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/build-page.md +59 -0
- package/.claude/commands/new-doc-page.md +45 -0
- package/.claude/commands/verify.md +52 -0
- package/.claude/pulse-checklist.md +111 -0
- package/.claude/settings.local.json +102 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +41 -0
- package/.pulse/load-reports/home/1773432711417.json +22 -0
- package/CLAUDE.md +383 -0
- package/README.md +95 -0
- package/docs/.claude/pulse-checklist.md +111 -0
- package/docs/public/.pulse-ui-version +1 -0
- package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
- package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
- package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
- package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
- package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
- package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
- package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
- package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
- package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
- package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
- package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
- package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
- package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
- package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
- package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
- package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
- package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
- package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
- package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
- package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
- package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
- package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
- package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
- package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
- package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
- package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
- package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
- package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
- package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
- package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
- package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
- package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
- package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
- package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
- package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
- package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
- package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
- package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
- package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
- package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
- package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
- package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
- package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
- package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
- package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
- package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
- package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
- package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
- package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
- package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
- package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
- package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
- package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
- package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
- package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
- package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
- package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
- package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
- package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
- package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
- package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
- package/docs/public/dist/docs-8e3d4b5c.css +1 -0
- package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
- package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
- package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
- package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
- package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
- package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
- package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
- package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
- package/docs/public/dist/manifest.json +94 -0
- package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
- package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
- package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
- package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
- package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
- package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
- package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
- package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
- package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
- package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
- package/docs/public/dist/runtime-B73WLANC.js +1 -0
- package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
- package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
- package/docs/public/dist/runtime-QFURDKA2.js +5 -0
- package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
- package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
- package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
- package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
- package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
- package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
- package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
- package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
- package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
- package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
- package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
- package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
- package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
- package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
- package/docs/public/docs.css +2020 -0
- package/docs/public/menu.js +83 -0
- package/docs/public/pulse-ui.css +2739 -0
- package/docs/public/pulse-ui.js +236 -0
- package/docs/server.js +192 -0
- package/docs/src/lib/component-page.js +47 -0
- package/docs/src/lib/highlight.js +255 -0
- package/docs/src/lib/layout.js +131 -0
- package/docs/src/lib/metrics-store.js +6 -0
- package/docs/src/lib/nav.js +159 -0
- package/docs/src/lib/stats.js +81 -0
- package/docs/src/pages/accessibility.js +157 -0
- package/docs/src/pages/actions.js +191 -0
- package/docs/src/pages/auth.js +177 -0
- package/docs/src/pages/caching.js +95 -0
- package/docs/src/pages/components/accordion.js +48 -0
- package/docs/src/pages/components/alert.js +35 -0
- package/docs/src/pages/components/app-badge.js +41 -0
- package/docs/src/pages/components/avatar.js +35 -0
- package/docs/src/pages/components/badge.js +36 -0
- package/docs/src/pages/components/banner.js +45 -0
- package/docs/src/pages/components/breadcrumbs.js +94 -0
- package/docs/src/pages/components/button.js +84 -0
- package/docs/src/pages/components/card.js +225 -0
- package/docs/src/pages/components/carousel.js +72 -0
- package/docs/src/pages/components/charts.js +278 -0
- package/docs/src/pages/components/checkbox.js +129 -0
- package/docs/src/pages/components/cluster.js +47 -0
- package/docs/src/pages/components/code-window.js +57 -0
- package/docs/src/pages/components/container.js +40 -0
- package/docs/src/pages/components/cta.js +53 -0
- package/docs/src/pages/components/divider.js +37 -0
- package/docs/src/pages/components/empty.js +36 -0
- package/docs/src/pages/components/feature.js +60 -0
- package/docs/src/pages/components/fieldset.js +65 -0
- package/docs/src/pages/components/fileupload.js +127 -0
- package/docs/src/pages/components/footer.js +58 -0
- package/docs/src/pages/components/grid.js +165 -0
- package/docs/src/pages/components/heading.js +107 -0
- package/docs/src/pages/components/hero.js +65 -0
- package/docs/src/pages/components/icons.js +285 -0
- package/docs/src/pages/components/image.js +71 -0
- package/docs/src/pages/components/input.js +51 -0
- package/docs/src/pages/components/list.js +112 -0
- package/docs/src/pages/components/media.js +51 -0
- package/docs/src/pages/components/modal.js +111 -0
- package/docs/src/pages/components/nav.js +86 -0
- package/docs/src/pages/components/pricing.js +68 -0
- package/docs/src/pages/components/progress.js +102 -0
- package/docs/src/pages/components/prose.js +111 -0
- package/docs/src/pages/components/pullquote.js +71 -0
- package/docs/src/pages/components/radio.js +194 -0
- package/docs/src/pages/components/rating.js +106 -0
- package/docs/src/pages/components/search.js +61 -0
- package/docs/src/pages/components/section.js +59 -0
- package/docs/src/pages/components/segmented.js +121 -0
- package/docs/src/pages/components/select.js +45 -0
- package/docs/src/pages/components/slider.js +114 -0
- package/docs/src/pages/components/spinner.js +73 -0
- package/docs/src/pages/components/stack.js +48 -0
- package/docs/src/pages/components/stat.js +55 -0
- package/docs/src/pages/components/stepper.js +66 -0
- package/docs/src/pages/components/table.js +45 -0
- package/docs/src/pages/components/testimonial.js +49 -0
- package/docs/src/pages/components/textarea.js +31 -0
- package/docs/src/pages/components/timeline.js +227 -0
- package/docs/src/pages/components/toggle.js +84 -0
- package/docs/src/pages/components/tooltip.js +48 -0
- package/docs/src/pages/components.js +204 -0
- package/docs/src/pages/config.js +193 -0
- package/docs/src/pages/constraints.js +99 -0
- package/docs/src/pages/deploy.js +233 -0
- package/docs/src/pages/extending.js +198 -0
- package/docs/src/pages/faq.js +96 -0
- package/docs/src/pages/getting-started.js +106 -0
- package/docs/src/pages/guard.js +121 -0
- package/docs/src/pages/home.js +401 -0
- package/docs/src/pages/how-it-works.js +183 -0
- package/docs/src/pages/hydration.js +98 -0
- package/docs/src/pages/images.js +121 -0
- package/docs/src/pages/meta.js +120 -0
- package/docs/src/pages/mutations.js +106 -0
- package/docs/src/pages/navigation.js +85 -0
- package/docs/src/pages/performance.js +157 -0
- package/docs/src/pages/persist.js +88 -0
- package/docs/src/pages/project-structure.js +90 -0
- package/docs/src/pages/prompt-examples.js +186 -0
- package/docs/src/pages/raw-responses.js +124 -0
- package/docs/src/pages/routing.js +99 -0
- package/docs/src/pages/server-api.js +281 -0
- package/docs/src/pages/server-data.js +185 -0
- package/docs/src/pages/slash-commands.js +55 -0
- package/docs/src/pages/spec.js +207 -0
- package/docs/src/pages/state.js +101 -0
- package/docs/src/pages/store.js +181 -0
- package/docs/src/pages/streaming.js +108 -0
- package/docs/src/pages/stripe.js +193 -0
- package/docs/src/pages/supabase.js +323 -0
- package/docs/src/pages/testing.js +198 -0
- package/docs/src/pages/validation.js +138 -0
- package/examples/contact.js +166 -0
- package/examples/counter.js +94 -0
- package/examples/dev.server.js +91 -0
- package/examples/examples.test.js +394 -0
- package/examples/pricing.js +244 -0
- package/examples/products.js +191 -0
- package/examples/quiz.js +208 -0
- package/examples/shared.js +78 -0
- package/examples/todos.js +162 -0
- package/package.json +75 -0
- package/public/.pulse-ui-version +1 -0
- package/public/chippy-bird.css +246 -0
- package/public/examples/contact.css +119 -0
- package/public/examples/counter.css +79 -0
- package/public/examples/pricing.css +132 -0
- package/public/examples/products.css +100 -0
- package/public/examples/quiz.css +200 -0
- package/public/examples/todos.css +137 -0
- package/public/favicon.ico +0 -0
- package/public/log-dashboard.css +383 -0
- package/public/pulse-ui.css +2740 -0
- package/public/pulse-ui.js +236 -0
- package/public/pulse.css +149 -0
- package/scripts/build.js +411 -0
- package/src/agent/checklist.md +111 -0
- package/src/agent/coverage-check.js +66 -0
- package/src/agent/guide-components.md +274 -0
- package/src/agent/guide-examples.md +54 -0
- package/src/agent/guide-routing.md +36 -0
- package/src/agent/guide-server.md +258 -0
- package/src/agent/guide-spec.md +103 -0
- package/src/agent/guide-styles.md +191 -0
- package/src/agent/guide.md +979 -0
- package/src/agent/identity.md +106 -0
- package/src/agent/workflow.md +108 -0
- package/src/cli/cli.test.js +82 -0
- package/src/cli/dev.js +195 -0
- package/src/cli/discover.js +113 -0
- package/src/cli/index.js +361 -0
- package/src/cli/load-report.js +91 -0
- package/src/cli/load-runner.js +121 -0
- package/src/cli/report-server.js +723 -0
- package/src/cli/report.js +116 -0
- package/src/cli/scaffold.archive.js +1371 -0
- package/src/cli/scaffold.js +349 -0
- package/src/cli/start.js +74 -0
- package/src/html.js +19 -0
- package/src/mcp/server.js +884 -0
- package/src/mcp/validate-worker.js +110 -0
- package/src/runtime/image.js +74 -0
- package/src/runtime/image.test.js +111 -0
- package/src/runtime/index.js +621 -0
- package/src/runtime/navigate.js +146 -0
- package/src/runtime/runtime.test.js +773 -0
- package/src/runtime/ssr.js +464 -0
- package/src/runtime/ssr.test.js +421 -0
- package/src/runtime/store.js +92 -0
- package/src/runtime/toast.js +163 -0
- package/src/server/index.js +1386 -0
- package/src/server/server.test.js +1248 -0
- package/src/spec/schema.js +428 -0
- package/src/spec/schema.test.js +291 -0
- package/src/store/index.js +102 -0
- package/src/store/store.test.js +210 -0
- package/src/testing/html.js +283 -0
- package/src/testing/index.js +249 -0
- package/src/testing/testing.test.js +450 -0
- package/src/ui/accordion.js +28 -0
- package/src/ui/alert.js +43 -0
- package/src/ui/app-badge.js +48 -0
- package/src/ui/avatar.js +47 -0
- package/src/ui/badge.js +24 -0
- package/src/ui/banner.js +26 -0
- package/src/ui/breadcrumbs.js +38 -0
- package/src/ui/button.js +66 -0
- package/src/ui/card.js +34 -0
- package/src/ui/carousel.js +59 -0
- package/src/ui/charts.js +321 -0
- package/src/ui/checkbox.js +65 -0
- package/src/ui/cluster.js +44 -0
- package/src/ui/code-window.js +39 -0
- package/src/ui/container.js +24 -0
- package/src/ui/cta.js +37 -0
- package/src/ui/divider.js +29 -0
- package/src/ui/empty.js +33 -0
- package/src/ui/feature.js +33 -0
- package/src/ui/fieldset.js +37 -0
- package/src/ui/fileupload.js +89 -0
- package/src/ui/footer.js +38 -0
- package/src/ui/grid.js +36 -0
- package/src/ui/heading.js +45 -0
- package/src/ui/hero.js +37 -0
- package/src/ui/icons.js +161 -0
- package/src/ui/index.js +89 -0
- package/src/ui/input.js +74 -0
- package/src/ui/list.js +36 -0
- package/src/ui/media.js +44 -0
- package/src/ui/modal.js +80 -0
- package/src/ui/nav.js +61 -0
- package/src/ui/pricing.js +56 -0
- package/src/ui/progress.js +62 -0
- package/src/ui/prose.js +29 -0
- package/src/ui/pullquote.js +34 -0
- package/src/ui/radio.js +102 -0
- package/src/ui/rating.js +93 -0
- package/src/ui/search.js +77 -0
- package/src/ui/section.js +69 -0
- package/src/ui/segmented.js +50 -0
- package/src/ui/select.js +77 -0
- package/src/ui/slider.js +84 -0
- package/src/ui/spinner.js +34 -0
- package/src/ui/stack.js +36 -0
- package/src/ui/stat.js +52 -0
- package/src/ui/stepper.js +46 -0
- package/src/ui/switch.js +57 -0
- package/src/ui/table.js +45 -0
- package/src/ui/testimonial.js +48 -0
- package/src/ui/textarea.js +72 -0
- package/src/ui/timeline.js +72 -0
- package/src/ui/tooltip.js +28 -0
- package/src/ui/ui.test.js +1241 -0
- package/src/ui/uiimage.js +65 -0
- package/tsconfig.json +13 -0
- package/types/html.d.ts +17 -0
- package/types/image.d.ts +70 -0
- package/types/index.d.ts +7 -0
- package/types/navigate.d.ts +38 -0
- package/types/runtime.d.ts +63 -0
- package/types/schema.d.ts +243 -0
- package/types/server.d.ts +145 -0
- package/types/ssr.d.ts +110 -0
- package/types/testing.d.ts +154 -0
- package/types/ui.d.ts +704 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { renderLayout, h1, lead, section, codeBlock, table, callout } from '../lib/layout.js'
|
|
2
|
+
import { prevNext } from '../lib/nav.js'
|
|
3
|
+
import { highlight } from '../lib/highlight.js'
|
|
4
|
+
|
|
5
|
+
const { prev, next } = prevNext('/performance')
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
route: '/performance',
|
|
9
|
+
meta: {
|
|
10
|
+
title: 'Performance — Pulse Docs',
|
|
11
|
+
description: 'Performance targets, techniques, and built-in optimisations in Pulse.',
|
|
12
|
+
styles: ['/docs.css'],
|
|
13
|
+
},
|
|
14
|
+
state: {},
|
|
15
|
+
view: () => renderLayout({
|
|
16
|
+
currentHref: '/performance',
|
|
17
|
+
prev,
|
|
18
|
+
next,
|
|
19
|
+
content: `
|
|
20
|
+
${h1('Performance')}
|
|
21
|
+
${lead('Performance in Pulse is structural, not optional. Streaming SSR, immutable asset caching, brotli compression, and zero client JS by default are the baseline — not optimisations applied after the fact. A page that uses the framework correctly cannot score poorly.')}
|
|
22
|
+
|
|
23
|
+
${section('targets', 'Performance targets')}
|
|
24
|
+
<p>Every page served by Pulse should meet these targets on localhost with no throttling:</p>
|
|
25
|
+
${table(
|
|
26
|
+
['Metric', 'Target', 'How'],
|
|
27
|
+
[
|
|
28
|
+
['LCP', 'Fast', 'Streaming SSR sends HTML before server data resolves. Actual LCP depends on server location, CDN, and network conditions.'],
|
|
29
|
+
['CLS', '0.00', 'Always set <code>width</code> and <code>height</code> on images; framework never shifts layout'],
|
|
30
|
+
['Lighthouse Performance', '100', 'Compression, immutable caching, no render-blocking resources'],
|
|
31
|
+
['Lighthouse Accessibility', '100', 'Semantic HTML, proper alt text, sufficient contrast'],
|
|
32
|
+
['Lighthouse SEO', '100', 'Meta tags, structured data, canonical links'],
|
|
33
|
+
['Lighthouse Best Practices', '100', 'HTTPS, security headers, no deprecated APIs'],
|
|
34
|
+
]
|
|
35
|
+
)}
|
|
36
|
+
${callout('note', 'Run <code>mcp__chrome-devtools__lighthouse_audit</code> after every new page to verify all four scores are 100. Fix any failures before considering the task done.')}
|
|
37
|
+
|
|
38
|
+
${section('streaming-ssr', 'Streaming SSR')}
|
|
39
|
+
<p>Pulse uses Node.js streams for SSR. The server sends the <code><head></code> and page shell immediately, before any async data resolves. Browsers start downloading CSS and fonts while the server fetches data — so the user sees a styled shell within milliseconds.</p>
|
|
40
|
+
${codeBlock(highlight(`export default {
|
|
41
|
+
route: '/feed',
|
|
42
|
+
|
|
43
|
+
// Shell renders instantly — hero, nav, layout
|
|
44
|
+
// Deferred segments wait for data then stream in
|
|
45
|
+
stream: {
|
|
46
|
+
shell: ['header', 'hero'],
|
|
47
|
+
deferred: ['feed', 'sidebar'],
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
server: {
|
|
51
|
+
feed: async () => db.posts.getLatest(20), // slow
|
|
52
|
+
sidebar: async () => db.tags.getPopular(), // slow
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// view is a keyed object matching stream segments
|
|
56
|
+
view: {
|
|
57
|
+
header: (state) => \`<header>...</header>\`,
|
|
58
|
+
hero: (state) => \`<section class="hero">...</section>\`,
|
|
59
|
+
feed: (state, server) => server.feed.map(renderPost).join(''),
|
|
60
|
+
sidebar: (state, server) => renderSidebar(server.sidebar),
|
|
61
|
+
},
|
|
62
|
+
}`, 'js'))}
|
|
63
|
+
|
|
64
|
+
${section('compression', 'Automatic compression')}
|
|
65
|
+
<p>All responses are compressed automatically. Pulse negotiates the best available encoding from the <code>Accept-Encoding</code> header:</p>
|
|
66
|
+
${table(
|
|
67
|
+
['Encoding', 'Priority', 'Typical savings'],
|
|
68
|
+
[
|
|
69
|
+
['Brotli (<code>br</code>)', 'First choice', '20–30% smaller than gzip for text content'],
|
|
70
|
+
['gzip', 'Fallback', 'Widely supported, good compression'],
|
|
71
|
+
['None', 'Last resort', 'No compression (rare)'],
|
|
72
|
+
]
|
|
73
|
+
)}
|
|
74
|
+
<p>HTML, CSS, JavaScript, JSON, XML, and SVG responses are all compressed. Binary formats (images, fonts) are served as-is.</p>
|
|
75
|
+
|
|
76
|
+
${section('asset-caching', 'Immutable asset caching')}
|
|
77
|
+
<p>Production JS bundles include a content hash in their filename (<code>/dist/counter.boot-a1b2c3d4.js</code>). The server sends <code>Cache-Control: public, max-age=31536000, immutable</code> for these files — browsers cache them forever and never re-request them unless the hash changes.</p>
|
|
78
|
+
<p>When you deploy a new version, the hash changes, and users automatically get the new file. No cache-busting tricks needed.</p>
|
|
79
|
+
${callout('tip', 'Static assets in <code>public/</code> get <code>max-age=3600</code>. For rarely-updated images, consider versioning them by filename.')}
|
|
80
|
+
|
|
81
|
+
${section('zero-client-js', 'Zero client JS by default')}
|
|
82
|
+
<p>Pages with no <code>mutations</code>, <code>actions</code>, or <code>persist</code> send no JavaScript at all — the HTML is entirely self-contained. The Pulse CLI detects this automatically; you never need to opt out. This is appropriate for static content pages: marketing pages, blog posts, documentation.</p>
|
|
83
|
+
${codeBlock(highlight(`// No mutations, actions, or persist → zero JS sent to browser
|
|
84
|
+
export default {
|
|
85
|
+
route: '/about',
|
|
86
|
+
meta: { title: 'About', styles: ['/app.css'] },
|
|
87
|
+
state: {},
|
|
88
|
+
view: () => \`<main><h1>About us</h1></main>\`,
|
|
89
|
+
}`, 'js'))}
|
|
90
|
+
|
|
91
|
+
${section('js-bundle-splitting', 'JS bundle splitting and caching')}
|
|
92
|
+
<p>When you open the network tab you will see a single <code>[page].boot-[hash].js</code> file loading. What is inside that file depends on how many pages your app has:</p>
|
|
93
|
+
${table(
|
|
94
|
+
['App size', 'What the boot file contains', 'Size (brotli)'],
|
|
95
|
+
[
|
|
96
|
+
['Single page', 'Your spec + the full Pulse runtime bundled together', '~3.5 kB'],
|
|
97
|
+
['Multiple pages', 'Your spec only — runtime is in a separate <code>runtime-[hash].js</code> chunk', '~0.35–0.5 kB'],
|
|
98
|
+
]
|
|
99
|
+
)}
|
|
100
|
+
<p>With multiple pages, esbuild's code splitting extracts the Pulse runtime into a shared chunk because every page imports it. The browser downloads it once and caches it — subsequent page navigations only fetch the small per-page boot file.</p>
|
|
101
|
+
<p><strong>What you see in the network tab across navigations:</strong></p>
|
|
102
|
+
<ul>
|
|
103
|
+
<li><strong>First page visit</strong> — <code>runtime-[hash].js</code> (~3.1 kB) + <code>home.boot-[hash].js</code> (~0.35 kB)</li>
|
|
104
|
+
<li><strong>Navigate to another page</strong> — <code>contact.boot-[hash].js</code> (~0.47 kB) only. Runtime already cached.</li>
|
|
105
|
+
<li><strong>Return visit</strong> — nothing. Both files served from cache with <code>immutable</code> headers.</li>
|
|
106
|
+
</ul>
|
|
107
|
+
${callout('tip', 'The runtime hash only changes when the Pulse runtime itself is updated — not when your app changes. Deploying new pages or mutations does not bust the runtime cache for returning visitors.')}
|
|
108
|
+
|
|
109
|
+
${section('security-headers', 'Security headers')}
|
|
110
|
+
<p>Every response — including 404 and 500 errors — carries a full set of security headers automatically. There is no configuration step and no way to accidentally omit them:</p>
|
|
111
|
+
${table(
|
|
112
|
+
['Header', 'Value'],
|
|
113
|
+
[
|
|
114
|
+
['<code>X-Content-Type-Options</code>', '<code>nosniff</code>'],
|
|
115
|
+
['<code>X-Frame-Options</code>', '<code>DENY</code>'],
|
|
116
|
+
['<code>Referrer-Policy</code>', '<code>strict-origin-when-cross-origin</code>'],
|
|
117
|
+
['<code>Permissions-Policy</code>', '<code>camera=(), microphone=(), geolocation=()</code>'],
|
|
118
|
+
['<code>Cross-Origin-Opener-Policy</code>', '<code>same-origin</code>'],
|
|
119
|
+
['<code>Cross-Origin-Resource-Policy</code>', '<code>same-origin</code>'],
|
|
120
|
+
]
|
|
121
|
+
)}
|
|
122
|
+
<p>These headers are applied automatically — no configuration needed.</p>
|
|
123
|
+
|
|
124
|
+
${section('browser-support', 'Browser support')}
|
|
125
|
+
<p>Pulse ships modern JavaScript and CSS without transpilation or polyfills. The effective minimum is set by two features:</p>
|
|
126
|
+
${table(
|
|
127
|
+
['Constraint', 'Chrome', 'Firefox', 'Safari', 'Edge', 'Since'],
|
|
128
|
+
[
|
|
129
|
+
['<code>?.</code> optional chaining (JS)', '80', '74', '13.1', '80', 'Feb – Mar 2020'],
|
|
130
|
+
['<code>gap</code> on flexbox (CSS)', '84', '63', '14.1', '84', 'Aug 2020 – Apr 2021'],
|
|
131
|
+
]
|
|
132
|
+
)}
|
|
133
|
+
<p>In practice, <strong>Safari 14.1 (April 2021) is the combined floor</strong> — browsers released after that date support everything Pulse uses. This covers roughly 95%+ of global traffic.</p>
|
|
134
|
+
${callout('note', 'No explicit <code>target</code> is set in the esbuild config, so syntax ships as written. If you need to support older browsers, set <code>target</code> in <code>scripts/build.js</code> and esbuild will downcompile optional chaining and other modern syntax automatically.')}
|
|
135
|
+
|
|
136
|
+
${section('cls', 'Preventing layout shift (CLS)')}
|
|
137
|
+
<p>Pulse targets 0.00 CLS. Layout shift is caused by elements that change size or position after the initial paint. These rules prevent it:</p>
|
|
138
|
+
<ul>
|
|
139
|
+
<li>Always set <code>width</code> and <code>height</code> on images (use the <code>img()</code> helper)</li>
|
|
140
|
+
<li>Never inject content above existing content after page load</li>
|
|
141
|
+
<li>Use <code>aspect-ratio</code> CSS for embeds (videos, iframes)</li>
|
|
142
|
+
<li>Avoid loading web fonts that cause FOUT — use <code>font-display: swap</code> or system fonts</li>
|
|
143
|
+
</ul>
|
|
144
|
+
|
|
145
|
+
${section('lcp', 'Optimising LCP')}
|
|
146
|
+
<p>LCP (Largest Contentful Paint) is typically your hero image or largest heading. Tips:</p>
|
|
147
|
+
<ul>
|
|
148
|
+
<li>Use <code>priority: true</code> on the LCP image — sets <code>fetchpriority="high"</code> and <code>loading="eager"</code></li>
|
|
149
|
+
<li>Avoid blocking server fetches — use <code>stream</code> so the shell renders without waiting for data</li>
|
|
150
|
+
<li>Keep your hero HTML inline (SSR) — never rely on client JS to render the LCP element</li>
|
|
151
|
+
<li>Use modern image formats (AVIF, WebP) via the <code>picture()</code> helper</li>
|
|
152
|
+
</ul>
|
|
153
|
+
|
|
154
|
+
${callout('warning', 'Never render your LCP element (hero image, main heading) client-side. If it requires a client JS import or a dynamic import, it will not paint until JS executes — pushing LCP from <100ms to >500ms.')}
|
|
155
|
+
`,
|
|
156
|
+
}),
|
|
157
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { renderLayout, h1, lead, section, sub, codeBlock, callout, table } from '../lib/layout.js'
|
|
2
|
+
import { prevNext } from '../lib/nav.js'
|
|
3
|
+
import { highlight } from '../lib/highlight.js'
|
|
4
|
+
|
|
5
|
+
const { prev, next } = prevNext('/persist')
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
route: '/persist',
|
|
9
|
+
meta: {
|
|
10
|
+
title: 'Persist — Pulse Docs',
|
|
11
|
+
description: 'Persist client state across page refreshes using localStorage.',
|
|
12
|
+
styles: ['/docs.css'],
|
|
13
|
+
},
|
|
14
|
+
state: {},
|
|
15
|
+
view: () => renderLayout({
|
|
16
|
+
currentHref: '/persist',
|
|
17
|
+
prev,
|
|
18
|
+
next,
|
|
19
|
+
content: `
|
|
20
|
+
${h1('Persist')}
|
|
21
|
+
${lead('The <code>persist</code> field declares which state keys survive page refreshes. Everything else resets. The list is explicit — nothing is persisted unless it is named here, and sensitive data should never appear in it.')}
|
|
22
|
+
|
|
23
|
+
${section('declaring', 'Declaring persistence')}
|
|
24
|
+
<p><code>persist</code> is an array of dot-path strings. Each path points to a key in state that should be saved:</p>
|
|
25
|
+
${codeBlock(highlight(`export default {
|
|
26
|
+
route: '/settings',
|
|
27
|
+
state: {
|
|
28
|
+
theme: 'light',
|
|
29
|
+
fontSize: 16,
|
|
30
|
+
sidebarOpen: true,
|
|
31
|
+
user: {
|
|
32
|
+
name: '',
|
|
33
|
+
preferences: { notifications: true, newsletter: false },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
persist: [
|
|
37
|
+
'theme',
|
|
38
|
+
'fontSize',
|
|
39
|
+
'user.preferences',
|
|
40
|
+
],
|
|
41
|
+
// ...
|
|
42
|
+
}`, 'js'))}
|
|
43
|
+
<p>In this example, <code>theme</code>, <code>fontSize</code>, and the entire <code>user.preferences</code> sub-object are saved to <code>localStorage</code>. The <code>sidebarOpen</code> key and <code>user.name</code> are session-only — they reset on each visit.</p>
|
|
44
|
+
|
|
45
|
+
${section('how-it-works', 'How it works')}
|
|
46
|
+
<p>On every state update, Pulse serialises the persisted keys and writes them to <code>localStorage</code> under a key derived from the page route. On the next mount, persisted values are read back and merged over the spec's initial state before the view renders.</p>
|
|
47
|
+
|
|
48
|
+
${table(
|
|
49
|
+
['Step', 'What happens'],
|
|
50
|
+
[
|
|
51
|
+
['First visit', 'State initialised from <code>spec.state</code>. Nothing in storage yet.'],
|
|
52
|
+
['User interacts', 'Mutations update state. Persisted keys are written to <code>localStorage</code>.'],
|
|
53
|
+
['Page refresh / return visit', 'Persisted values loaded from storage and merged over initial state. View renders with restored state.'],
|
|
54
|
+
]
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
${section('dot-paths', 'Dot-path notation')}
|
|
58
|
+
<p>Persist keys use the same dot-path notation as <a href="/validation">validation</a>. Entire top-level keys or specific nested sub-objects can be persisted:</p>
|
|
59
|
+
${codeBlock(highlight(`persist: [
|
|
60
|
+
'theme', // top-level key
|
|
61
|
+
'user.preferences', // nested sub-object (entire object is saved)
|
|
62
|
+
'cart.items', // array of cart items
|
|
63
|
+
]`, 'js'))}
|
|
64
|
+
${callout('note', 'When persisting a nested path like <code>user.preferences</code>, the <em>entire value at that path</em> is saved and restored — not individual sub-keys within it.')}
|
|
65
|
+
|
|
66
|
+
${section('storage-key', 'Storage key')}
|
|
67
|
+
<p>Pulse stores persisted state under <code>pulse:[route]</code> in <code>localStorage</code>. For example, a spec with <code>route: \'/settings\'</code> uses the key <code>pulse:/settings</code>. This namespacing prevents collisions between pages.</p>
|
|
68
|
+
|
|
69
|
+
${section('ssr', 'SSR and persistence')}
|
|
70
|
+
<p>On the server, <code>localStorage</code> does not exist. The server always renders with the spec's initial state. Persisted values are applied on the client after hydration — before the first mutation, after mount.</p>
|
|
71
|
+
${callout('warning', 'If SSR and client state diverge significantly due to persisted values, a content flash may occur. Best practice: keep persisted state to preferences and settings that do not affect the main page content.')}
|
|
72
|
+
|
|
73
|
+
${section('clearing', 'Clearing persisted state')}
|
|
74
|
+
<p>To clear persisted state programmatically, remove the relevant key from <code>localStorage</code>:</p>
|
|
75
|
+
${codeBlock(highlight(`localStorage.removeItem('pulse:/settings')`, 'js'))}
|
|
76
|
+
<p>Or use a mutation that resets the persisted fields to their initial values — Pulse will save the reset values on the next update.</p>
|
|
77
|
+
|
|
78
|
+
${section('best-practices', 'Best practices')}
|
|
79
|
+
<ul>
|
|
80
|
+
<li>Persist preferences and settings (theme, language, layout choices)</li>
|
|
81
|
+
<li>Persist shopping cart contents or draft form data</li>
|
|
82
|
+
<li>Never persist sensitive data (tokens, passwords, PII) — <code>localStorage</code> is not secure storage</li>
|
|
83
|
+
<li>Never persist data that must be authoritative — use <a href="/server-data">server data</a> for anything that should be fresh from the server</li>
|
|
84
|
+
<li>Keep persisted payloads small — <code>localStorage</code> has a ~5 MB limit and blocks the main thread if abused</li>
|
|
85
|
+
</ul>
|
|
86
|
+
`,
|
|
87
|
+
}),
|
|
88
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { renderLayout, h1, lead, section, sub, codeBlock, callout, table } from '../lib/layout.js'
|
|
2
|
+
import { prevNext } from '../lib/nav.js'
|
|
3
|
+
import { highlight } from '../lib/highlight.js'
|
|
4
|
+
|
|
5
|
+
const { prev, next } = prevNext('/project-structure')
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
route: '/project-structure',
|
|
9
|
+
meta: {
|
|
10
|
+
title: 'Project Structure — Pulse Docs',
|
|
11
|
+
description: 'Recommended directory layout for a Pulse application.',
|
|
12
|
+
styles: ['/docs.css'],
|
|
13
|
+
},
|
|
14
|
+
state: {},
|
|
15
|
+
view: () => renderLayout({
|
|
16
|
+
currentHref: '/project-structure',
|
|
17
|
+
prev,
|
|
18
|
+
next,
|
|
19
|
+
content: `
|
|
20
|
+
${h1('Project Structure')}
|
|
21
|
+
${lead('Pulse enforces a single convention: one spec per page, one directory per concern. The structure is not a recommendation — it is how the framework discovers and registers your pages. There is nothing to configure because there is nothing to decide.')}
|
|
22
|
+
|
|
23
|
+
${section('layout', 'Layout')}
|
|
24
|
+
${codeBlock(highlight(`my-app/
|
|
25
|
+
├── package.json
|
|
26
|
+
├── public/ # static assets served directly
|
|
27
|
+
│ ├── app.css
|
|
28
|
+
│ └── dist/ # generated bundles (pulse build)
|
|
29
|
+
│ ├── manifest.json
|
|
30
|
+
│ ├── runtime-[hash].js
|
|
31
|
+
│ └── [name].boot-[hash].js
|
|
32
|
+
└── src/
|
|
33
|
+
├── pages/ # one file per page — auto-discovered
|
|
34
|
+
│ ├── home.js
|
|
35
|
+
│ ├── about.js
|
|
36
|
+
│ └── contact.js
|
|
37
|
+
├── lib/ # shared helpers
|
|
38
|
+
│ └── db.js
|
|
39
|
+
└── components/ # shared view fragments (optional)
|
|
40
|
+
└── card.js`, 'bash'))}
|
|
41
|
+
|
|
42
|
+
${section('pages', 'src/pages/')}
|
|
43
|
+
<p>Each file in <code>src/pages/</code> exports a single spec as the default export. <code>pulse dev</code> auto-discovers every file in this directory and registers it as a route. There is no route registry to maintain — the file is the registration.</p>
|
|
44
|
+
|
|
45
|
+
${callout('note', 'Routes are derived from filenames by default (<code>about.js</code> → <code>/about</code>). For dynamic segments, <code>route</code> is set explicitly in the spec.')}
|
|
46
|
+
|
|
47
|
+
${section('public', 'public/')}
|
|
48
|
+
<p>Static files served directly — CSS, fonts, images. Referenced via <code>meta.styles</code> in the spec. The <code>dist/</code> subdirectory is generated by <code>pulse build</code>. Its contents are content-hashed and must not be edited manually.</p>
|
|
49
|
+
|
|
50
|
+
${table(
|
|
51
|
+
['Path', 'Purpose'],
|
|
52
|
+
[
|
|
53
|
+
['<code>public/app.css</code>', 'Your global stylesheet — reference it in spec <code>meta.styles</code>'],
|
|
54
|
+
['<code>public/dist/</code>', 'Generated bundles from <code>npm run build</code> — do not edit manually'],
|
|
55
|
+
['<code>public/dist/manifest.json</code>', 'Maps spec hydrate paths to hashed bundle filenames'],
|
|
56
|
+
]
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
${section('lib', 'src/lib/')}
|
|
60
|
+
<p>Shared helpers used across multiple pages — database clients, API wrappers, utility functions. Imported directly into specs. Plain JavaScript modules with no framework coupling — they work identically in tests, scripts, or other contexts.</p>
|
|
61
|
+
|
|
62
|
+
${section('components', 'src/components/ (optional)')}
|
|
63
|
+
<p>Reusable view fragments. Since views are just functions that return HTML strings, a component is simply a function:</p>
|
|
64
|
+
|
|
65
|
+
${codeBlock(highlight(`// src/components/card.js
|
|
66
|
+
export function card({ title, body }) {
|
|
67
|
+
return \`
|
|
68
|
+
<div class="card">
|
|
69
|
+
<h2>\${title}</h2>
|
|
70
|
+
<p>\${body}</p>
|
|
71
|
+
</div>
|
|
72
|
+
\`
|
|
73
|
+
}`, 'js'), 'src/components/card.js')}
|
|
74
|
+
|
|
75
|
+
${codeBlock(highlight(`// src/pages/home.js
|
|
76
|
+
import { card } from '../components/card.js'
|
|
77
|
+
|
|
78
|
+
export default {
|
|
79
|
+
route: '/',
|
|
80
|
+
state: {},
|
|
81
|
+
view: () => card({ title: 'Hello', body: 'Welcome to Pulse' }),
|
|
82
|
+
}`, 'js'), 'src/pages/home.js')}
|
|
83
|
+
|
|
84
|
+
${section('one-file-per-page', 'One file per page')}
|
|
85
|
+
<p>Pulse enforces <strong>one spec per file</strong>. Every page is self-contained — state, view, mutations, actions, and validation in one place. This is not a style preference. It is how the framework eliminates the question of where things live.</p>
|
|
86
|
+
|
|
87
|
+
${callout('tip', 'For large pages, view fragments and helpers can be imported from other modules. The spec file remains the coordination point — the structure is always clear regardless of how the implementation is organised.')}
|
|
88
|
+
`,
|
|
89
|
+
}),
|
|
90
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { renderLayout, h1, lead } from '../lib/layout.js'
|
|
2
|
+
import { prevNext } from '../lib/nav.js'
|
|
3
|
+
|
|
4
|
+
const { prev, next } = prevNext('/prompt-examples')
|
|
5
|
+
|
|
6
|
+
function promptCard({ tag, prompt, produces }) {
|
|
7
|
+
return `
|
|
8
|
+
<div class="prompt-card">
|
|
9
|
+
<div class="prompt-tag">${tag}</div>
|
|
10
|
+
<blockquote class="prompt-text">${prompt}</blockquote>
|
|
11
|
+
<p class="prompt-produces">${produces}</p>
|
|
12
|
+
</div>`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function promptGroup(title, cards) {
|
|
16
|
+
return `
|
|
17
|
+
<div class="prompt-group">
|
|
18
|
+
<h3 class="prompt-group-title">${title}</h3>
|
|
19
|
+
<div class="prompt-grid">
|
|
20
|
+
${cards.map(promptCard).join('')}
|
|
21
|
+
</div>
|
|
22
|
+
</div>`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
route: '/prompt-examples',
|
|
27
|
+
meta: {
|
|
28
|
+
title: 'Prompt Examples — Pulse Docs',
|
|
29
|
+
description: 'Real prompts for building Pulse pages with your AI agent — from creating pages to authentication, performance audits, and integrations.',
|
|
30
|
+
styles: ['/docs.css'],
|
|
31
|
+
},
|
|
32
|
+
state: {},
|
|
33
|
+
view: () => renderLayout({
|
|
34
|
+
currentHref: '/prompt-examples',
|
|
35
|
+
prev,
|
|
36
|
+
next,
|
|
37
|
+
content: `
|
|
38
|
+
${h1('Prompt Examples')}
|
|
39
|
+
${lead('These prompts produce correct Pulse specs because the framework constrains the output. The agent works within a defined structure — there is no ambiguity about where state lives, how data is fetched, or how validation is wired.')}
|
|
40
|
+
|
|
41
|
+
${promptGroup('Creating pages', [
|
|
42
|
+
{
|
|
43
|
+
tag: 'New page',
|
|
44
|
+
prompt: 'Create a page at /about with a heading, a short paragraph about the company, and a link back to the home page.',
|
|
45
|
+
produces: 'A static page spec with <code>route</code>, <code>meta</code>, and a <code>view</code> returning the HTML.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
tag: 'Dynamic route',
|
|
49
|
+
prompt: 'Create a blog post page at /posts/:slug. It should fetch the post from the database by slug and display the title, published date, and body content. If no post is found, return a 404.',
|
|
50
|
+
produces: 'A spec with a parameterised <code>route</code>, a <code>server.data</code> fetcher using <code>ctx.params.slug</code>, and a 404 raw response if the post is missing.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
tag: 'Listing page',
|
|
54
|
+
prompt: 'Create a paginated list of articles at /articles. Read the ?page query parameter to fetch the right slice. Show 10 articles per page with previous and next links.',
|
|
55
|
+
produces: 'A spec with <code>server.data</code> reading <code>ctx.query.page</code>, pagination state, and prev/next links rendered in the view.',
|
|
56
|
+
},
|
|
57
|
+
])}
|
|
58
|
+
|
|
59
|
+
${promptGroup('State & interactions', [
|
|
60
|
+
{
|
|
61
|
+
tag: 'Live filter',
|
|
62
|
+
prompt: 'Add a search input to the /products page that filters the visible product list as the user types. Keep the full product list in server data and filter it in the view.',
|
|
63
|
+
produces: 'A mutation bound to <code>data-event="input:setQuery"</code> that updates a <code>query</code> state field, with the view filtering <code>server.data.products</code> against it.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
tag: 'Toggle',
|
|
67
|
+
prompt: 'Add a show/hide toggle to the FAQ page. Each question should expand its answer when clicked and collapse when clicked again.',
|
|
68
|
+
produces: 'An <code>openId</code> state field and a <code>toggle</code> mutation. The view renders answers conditionally based on whether their id matches <code>openId</code>.',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
tag: 'Tabs',
|
|
72
|
+
prompt: 'The /dashboard page has three tabs: Overview, Activity, and Settings. Clicking a tab should show its panel and hide the others without a page reload.',
|
|
73
|
+
produces: 'An <code>activeTab</code> state field, a <code>setTab</code> mutation, and tab panels rendered conditionally in the view.',
|
|
74
|
+
},
|
|
75
|
+
])}
|
|
76
|
+
|
|
77
|
+
${promptGroup('Forms & validation', [
|
|
78
|
+
{
|
|
79
|
+
tag: 'Contact form',
|
|
80
|
+
prompt: 'Add a contact form at /contact with name, email, and message fields. All three are required. Email must be a valid format. Show inline errors on failure. Show a success message on submission.',
|
|
81
|
+
produces: 'A spec with <code>validation</code> rules, an <code>actions.submit</code> with <code>validate: true</code>, and <code>onSuccess</code>/<code>onError</code> state transitions.',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
tag: 'Multi-step form',
|
|
85
|
+
prompt: 'Create a multi-step signup form at /signup with three steps: account details, personal info, and confirmation. Show a step indicator at the top. Only advance when the current step is valid.',
|
|
86
|
+
produces: 'A <code>step</code> state field, step-specific validation, a <code>next</code> action that validates before advancing, and a view that renders the active step panel.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
tag: 'Constraints',
|
|
90
|
+
prompt: 'The quantity input on the /cart page should never go below 1 or above 99. Enforce this on the server — not just in the UI.',
|
|
91
|
+
produces: 'A <code>constraints</code> block with <code>quantity: { min: 1, max: 99 }</code> that the framework enforces after every mutation, regardless of what the client sends.',
|
|
92
|
+
},
|
|
93
|
+
])}
|
|
94
|
+
|
|
95
|
+
${promptGroup('Server data & actions', [
|
|
96
|
+
{
|
|
97
|
+
tag: 'Dashboard data',
|
|
98
|
+
prompt: 'The /dashboard page should show the logged-in user\'s name and their last 5 orders. Fetch both in parallel.',
|
|
99
|
+
produces: 'A <code>server.data</code> fetcher that runs two queries concurrently with <code>Promise.all</code> and passes the results to the view as <code>server.data.user</code> and <code>server.data.orders</code>.',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
tag: 'Async action',
|
|
103
|
+
prompt: 'Add a Delete button to each row in the /users table. It should send a DELETE request to the API, remove the row on success, and show an error message on failure.',
|
|
104
|
+
produces: 'An action with <code>onStart</code> setting a loading flag, <code>run</code> making the API call, <code>onSuccess</code> removing the item from state, and <code>onError</code> setting an error message.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
tag: 'File upload',
|
|
108
|
+
prompt: 'Add an avatar upload form to /settings. The user picks a file, it uploads to storage, and the page shows the new avatar on success.',
|
|
109
|
+
produces: 'An action whose <code>run</code> reads the file from <code>FormData</code>, converts it to an <code>ArrayBuffer</code>, and uploads it to storage. <code>onSuccess</code> updates the avatar URL in state.',
|
|
110
|
+
},
|
|
111
|
+
])}
|
|
112
|
+
|
|
113
|
+
${promptGroup('Authentication', [
|
|
114
|
+
{
|
|
115
|
+
tag: 'Login page',
|
|
116
|
+
prompt: 'Create a login page at /login with email and password fields. On success, set an httpOnly session cookie and redirect to /dashboard. Show an error message if credentials are wrong.',
|
|
117
|
+
produces: 'A raw response action that verifies credentials, sets <code>Set-Cookie</code> headers with <code>httpOnly; SameSite=Strict</code>, and redirects. Failed logins return the form with an error state.',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
tag: 'Protected route',
|
|
121
|
+
prompt: 'Protect the /dashboard page so only logged-in users can access it. Redirect anyone without a valid session to /login.',
|
|
122
|
+
produces: 'A <code>guard</code> function that reads the session cookie, verifies it, and returns a redirect response if invalid.',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
tag: 'Logout',
|
|
126
|
+
prompt: 'Add a logout button to the nav. It should clear the session and redirect to /login.',
|
|
127
|
+
produces: 'A page spec at <code>/logout</code> using the raw response spec (<code>contentType</code> + <code>render</code>) that sets the session cookie to <code>maxAge: 0</code> and issues a 302 redirect.',
|
|
128
|
+
},
|
|
129
|
+
])}
|
|
130
|
+
|
|
131
|
+
${promptGroup('Streaming SSR', [
|
|
132
|
+
{
|
|
133
|
+
tag: 'Deferred content',
|
|
134
|
+
prompt: 'The /feed page is slow because it waits for the activity feed before painting anything. Make it stream — show the page chrome instantly and load the feed in the background.',
|
|
135
|
+
produces: 'A <code>stream</code> block with <code>shell: [\'header\']</code> and <code>deferred: [\'feed\']</code>. The view becomes a keyed object of named segment functions.',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
tag: 'Multiple segments',
|
|
139
|
+
prompt: 'The /home page has a hero, a trending section, and a recommendations section. The hero should appear instantly. The other two can load as data resolves.',
|
|
140
|
+
produces: 'Three view segments — <code>hero</code> in the shell, <code>trending</code> and <code>recommendations</code> deferred. Each fetches its own data independently.',
|
|
141
|
+
},
|
|
142
|
+
])}
|
|
143
|
+
|
|
144
|
+
${promptGroup('Performance & tooling', [
|
|
145
|
+
{
|
|
146
|
+
tag: 'Lighthouse audit',
|
|
147
|
+
prompt: 'Run a Lighthouse audit across all pages and show me the results.',
|
|
148
|
+
produces: 'The agent runs <code>/pulse-report</code>, which audits every registered route and opens the results dashboard.',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
tag: 'Score thresholds',
|
|
152
|
+
prompt: 'Make the audit fail if any page scores below 90 for performance or below 95 for accessibility.',
|
|
153
|
+
produces: 'A <code>lighthouse</code> block in <code>pulse.config.js</code> with <code>performance: 90</code> and <code>accessibility: 95</code>.',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
tag: 'Load test',
|
|
157
|
+
prompt: 'Load test the /api/data endpoint with 50 concurrent connections for 30 seconds and tell me the p99 latency.',
|
|
158
|
+
produces: 'The agent runs <code>/pulse-load</code> against that route with the specified parameters and reports the results.',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
tag: 'Environment config',
|
|
162
|
+
prompt: 'Set up a staging environment so I can run audits against https://staging.myapp.com instead of localhost.',
|
|
163
|
+
produces: 'An <code>environments.staging</code> block in <code>pulse.config.js</code> with the remote URL. The agent uses it when running <code>/pulse-report</code> or <code>/pulse-load</code>.',
|
|
164
|
+
},
|
|
165
|
+
])}
|
|
166
|
+
|
|
167
|
+
${promptGroup('Integrations', [
|
|
168
|
+
{
|
|
169
|
+
tag: 'Supabase query',
|
|
170
|
+
prompt: 'The /profile page should load the current user\'s row from the Supabase users table. It should only return data the logged-in user owns.',
|
|
171
|
+
produces: 'A <code>server.data</code> fetcher using the Supabase public client initialised with the user\'s access token, so Row Level Security applies automatically.',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
tag: 'Supabase auth',
|
|
175
|
+
prompt: 'Use Supabase for authentication. The login form should call Supabase\'s signInWithPassword and store the tokens in httpOnly cookies.',
|
|
176
|
+
produces: 'A login action that calls <code>supabase.auth.signInWithPassword</code>, reads the session tokens from the response, and sets them as <code>httpOnly; SameSite=Strict</code> cookies.',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
tag: 'Stripe checkout',
|
|
180
|
+
prompt: 'Add a checkout button to the /pricing page for the Pro plan. Clicking it should create a Stripe Checkout Session and redirect the user to the Stripe-hosted page.',
|
|
181
|
+
produces: 'An action whose <code>run</code> calls the Stripe API to create a session and returns the checkout URL. <code>onSuccess</code> triggers a redirect via state.',
|
|
182
|
+
},
|
|
183
|
+
])}
|
|
184
|
+
`,
|
|
185
|
+
}),
|
|
186
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { renderLayout, h1, lead, section, sub, codeBlock, callout, table } from '../lib/layout.js'
|
|
2
|
+
import { prevNext } from '../lib/nav.js'
|
|
3
|
+
import { highlight } from '../lib/highlight.js'
|
|
4
|
+
|
|
5
|
+
const { prev, next } = prevNext('/raw-responses')
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
route: '/raw-responses',
|
|
9
|
+
meta: {
|
|
10
|
+
title: 'Raw Responses — Pulse Docs',
|
|
11
|
+
description: 'Return non-HTML responses from Pulse specs — RSS, XML, JSON, and other content types.',
|
|
12
|
+
styles: ['/docs.css'],
|
|
13
|
+
},
|
|
14
|
+
state: {},
|
|
15
|
+
view: () => renderLayout({
|
|
16
|
+
currentHref: '/raw-responses',
|
|
17
|
+
prev,
|
|
18
|
+
next,
|
|
19
|
+
content: `
|
|
20
|
+
${h1('Raw Responses')}
|
|
21
|
+
${lead('Setting <code>contentType</code> switches a spec from the HTML pipeline to a raw response mode. The view returns the response body directly — no doctype, no hydration script. Security headers and compression still apply.')}
|
|
22
|
+
|
|
23
|
+
${section('basics', 'The basics')}
|
|
24
|
+
<p>Set <code>contentType</code> to any valid MIME type. When present, the normal HTML wrapper (doctype, head, body, hydration script) is skipped. The <code>render</code> function receives <code>(ctx, serverState)</code> and returns a string:</p>
|
|
25
|
+
${codeBlock(highlight(`export default {
|
|
26
|
+
route: '/feed.xml',
|
|
27
|
+
contentType: 'application/rss+xml',
|
|
28
|
+
state: {},
|
|
29
|
+
server: {
|
|
30
|
+
data: async () => ({
|
|
31
|
+
posts: await db.posts.latest(20),
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
view: (ctx, server) => \`<?xml version="1.0" encoding="UTF-8"?>
|
|
35
|
+
<rss version="2.0">
|
|
36
|
+
<channel>
|
|
37
|
+
<title>My Blog</title>
|
|
38
|
+
<link>https://example.com</link>
|
|
39
|
+
<description>Recent posts</description>
|
|
40
|
+
\${server.posts.map(post => \`
|
|
41
|
+
<item>
|
|
42
|
+
<title>\${esc(post.title)}</title>
|
|
43
|
+
<link>https://example.com/blog/\${post.slug}</link>
|
|
44
|
+
<pubDate>\${new Date(post.date).toUTCString()}</pubDate>
|
|
45
|
+
<description>\${esc(post.excerpt)}</description>
|
|
46
|
+
</item>
|
|
47
|
+
\`).join('')}
|
|
48
|
+
</channel>
|
|
49
|
+
</rss>\`,
|
|
50
|
+
}`, 'js'))}
|
|
51
|
+
|
|
52
|
+
${section('json', 'JSON API endpoints')}
|
|
53
|
+
${codeBlock(highlight(`export default {
|
|
54
|
+
route: '/api/products',
|
|
55
|
+
contentType: 'application/json',
|
|
56
|
+
state: {},
|
|
57
|
+
server: {
|
|
58
|
+
data: async (ctx) => ({
|
|
59
|
+
products: await db.products.list({
|
|
60
|
+
page: parseInt(ctx.query.page ?? '1', 10),
|
|
61
|
+
category: ctx.query.category,
|
|
62
|
+
}),
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
view: (ctx, server) => JSON.stringify({
|
|
66
|
+
products: server.products,
|
|
67
|
+
page: parseInt(ctx.query.page ?? '1', 10),
|
|
68
|
+
}),
|
|
69
|
+
}`, 'js'))}
|
|
70
|
+
|
|
71
|
+
${section('sitemap', 'XML sitemap')}
|
|
72
|
+
${codeBlock(highlight(`export default {
|
|
73
|
+
route: '/sitemap.xml',
|
|
74
|
+
contentType: 'application/xml',
|
|
75
|
+
serverTtl: 3600,
|
|
76
|
+
state: {},
|
|
77
|
+
server: {
|
|
78
|
+
data: async () => ({
|
|
79
|
+
pages: await db.pages.allPublished(),
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
view: (ctx, server) => \`<?xml version="1.0" encoding="UTF-8"?>
|
|
83
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
84
|
+
\${server.pages.map(p => \`
|
|
85
|
+
<url>
|
|
86
|
+
<loc>https://example.com\${p.path}</loc>
|
|
87
|
+
<lastmod>\${p.updatedAt.toISOString().slice(0, 10)}</lastmod>
|
|
88
|
+
</url>
|
|
89
|
+
\`).join('')}
|
|
90
|
+
</urlset>\`,
|
|
91
|
+
}`, 'js'))}
|
|
92
|
+
|
|
93
|
+
${section('view-signature', 'View function signature')}
|
|
94
|
+
<p>For raw responses, the <code>view</code> function signature is <code>(ctx, serverState)</code>, not <code>(state, serverState)</code>. The <code>ctx</code> argument is the request context object with <code>params</code>, <code>query</code>, <code>headers</code>, and <code>cookies</code>.</p>
|
|
95
|
+
${callout('note', 'There is no client state (<code>state</code>) for raw response specs — they are purely server-side. The <code>state: {}</code> field is still required for spec validation, but is not used.')}
|
|
96
|
+
|
|
97
|
+
${section('escaping', 'Escaping in XML and HTML')}
|
|
98
|
+
<p>Pulse does not auto-escape raw response bodies. When returning XML or HTML, escaping all user-supplied and dynamic content is required — unescaped output is an injection vulnerability:</p>
|
|
99
|
+
${codeBlock(highlight(`// Simple escape helper for XML/HTML contexts
|
|
100
|
+
function esc(str) {
|
|
101
|
+
return String(str)
|
|
102
|
+
.replace(/&/g, '&')
|
|
103
|
+
.replace(/</g, '<')
|
|
104
|
+
.replace(/>/g, '>')
|
|
105
|
+
.replace(/"/g, '"')
|
|
106
|
+
.replace(/'/g, ''')
|
|
107
|
+
}`, 'js'))}
|
|
108
|
+
${callout('warning', 'Never interpolate raw user data or database content into XML/HTML without escaping. This applies to raw response specs just as much as to HTML view functions.')}
|
|
109
|
+
|
|
110
|
+
${section('caching-raw', 'Caching raw responses')}
|
|
111
|
+
<p>Raw response specs support the same <code>serverTtl</code> and <code>cache</code> options as HTML specs. For feeds and sitemaps that update infrequently, a generous TTL dramatically reduces server load:</p>
|
|
112
|
+
${codeBlock(highlight(`export default {
|
|
113
|
+
route: '/feed.xml',
|
|
114
|
+
contentType: 'application/rss+xml',
|
|
115
|
+
serverTtl: 300, // cache data for 5 minutes
|
|
116
|
+
cache: {
|
|
117
|
+
public: true,
|
|
118
|
+
maxAge: 300,
|
|
119
|
+
},
|
|
120
|
+
// ...
|
|
121
|
+
}`, 'js'))}
|
|
122
|
+
`,
|
|
123
|
+
}),
|
|
124
|
+
}
|