@invisibleloop/pulse 0.20.11 → 0.20.20
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/package.json +6 -5
- package/public/.pulse-ui-version +1 -1
- package/scripts/build.js +26 -0
- package/scripts/release-version.js +17 -8
- package/scripts/strip-server.test.js +60 -0
- package/src/agent/checklist.md +8 -12
- package/src/agent/commands/pulse-start.md +2 -0
- package/src/agent/commands/verify.md +30 -8
- package/src/agent/guide-components.md +7 -25
- package/src/agent/guide-routing.md +0 -12
- package/src/agent/guide-server.md +102 -1
- package/src/agent/guide-spec.md +8 -1
- package/src/agent/guide-styles.md +23 -0
- package/src/agent/identity.md +9 -0
- package/src/agent/workflow.md +4 -23
- package/src/cli/check-bundles.js +113 -0
- package/src/cli/check-bundles.test.js +137 -0
- package/src/cli/cli.test.js +105 -0
- package/src/cli/dev.js +48 -3
- package/src/cli/diagnose.js +105 -0
- package/src/cli/diagnose.test.js +124 -0
- package/src/cli/discover.js +9 -2
- package/src/cli/index.js +171 -9
- package/src/cli/process-utils.js +57 -0
- package/src/cli/process-utils.test.js +114 -0
- package/src/cli/review.js +391 -0
- package/src/cli/review.test.js +143 -0
- package/src/cli/scaffold.js +9 -4
- package/src/cli/stamp.js +102 -0
- package/src/cli/stamp.test.js +149 -0
- package/src/cli/start.js +15 -2
- package/src/cli/validate.js +184 -0
- package/src/cli/validate.test.js +153 -0
- package/src/mcp/mcp.test.js +261 -30
- package/src/mcp/server.js +207 -515
- package/src/mcp/validate-worker.js +18 -1
- package/src/runtime/index.js +144 -11
- package/src/runtime/runtime.test.js +199 -0
- package/src/runtime/ssr.js +13 -6
- package/src/runtime/ssr.test.js +31 -0
- package/src/server/index.js +162 -9
- package/src/server/server.test.js +225 -2
- package/src/spec/schema.js +1 -1
- package/src/ui/index.js +0 -5
- package/src/ui/ui.test.js +0 -75
- package/types/ui.d.ts +0 -21
- package/.claude/commands/build-page.md +0 -63
- package/.claude/commands/new-doc-page.md +0 -45
- package/.claude/commands/pulse-start.md +0 -11
- package/.claude/commands/pulse-stop.md +0 -5
- package/.claude/commands/verify.md +0 -118
- package/.claude/pulse-checklist.md +0 -189
- package/.github/workflows/ci.yml +0 -25
- package/.github/workflows/publish.yml +0 -45
- package/.pulse/load-reports/home/1773432711417.json +0 -22
- package/.pulse-verified +0 -1
- package/AGENTS.md +0 -697
- package/CLAUDE.md +0 -697
- package/README.md +0 -95
- package/benchmark/public/dist/counter.boot-PO6SNSJZ.js +0 -8
- package/benchmark/public/dist/home.boot-YK6VAEQI.js +0 -6
- package/benchmark/public/dist/manifest.json +0 -5
- package/benchmark/public/dist/runtime-5Y7ESAIB.js +0 -2
- package/benchmark/public/dist/runtime-KO4BHUQ3.js +0 -49
- package/benchmark/results/2026-03-27-09-20.json +0 -188
- package/benchmark/results/2026-03-27-09-21.json +0 -188
- package/benchmark/results/2026-03-27-09-29.json +0 -188
- package/benchmark/results/2026-03-27-10-14.json +0 -188
- package/benchmark/results/2026-03-27-10-18.json +0 -232
- package/benchmark/src/pages/counter.js +0 -37
- package/benchmark/src/pages/home.js +0 -21
- package/docs/.claude/commands/pulse-build.md +0 -7
- package/docs/.claude/commands/pulse-dev.md +0 -7
- package/docs/.claude/commands/pulse-start.md +0 -9
- package/docs/.claude/commands/pulse-stop.md +0 -5
- package/docs/.claude/commands/verify.md +0 -84
- package/docs/.claude/coverage-check.js +0 -196
- package/docs/.claude/pulse-checklist.md +0 -189
- package/docs/public/.pulse-ui-version +0 -1
- package/docs/public/andypixel.css +0 -237
- package/docs/public/bookworms.css +0 -422
- package/docs/public/coutumes.css +0 -561
- package/docs/public/docs.css +0 -2388
- package/docs/public/favicon.svg +0 -4
- package/docs/public/home-brut.css +0 -982
- package/docs/public/images/bookworms/hero-books.jpg +0 -0
- package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/docs/public/images/journey/178.jpg +0 -0
- package/docs/public/images/journey/29.jpg +0 -0
- package/docs/public/images/retrowheels/hero.jpg +0 -0
- package/docs/public/images/shop/devbook-pro.jpg +0 -0
- package/docs/public/images/suzukibento/1015.jpg +0 -0
- package/docs/public/images/suzukibento/1025.jpg +0 -0
- package/docs/public/images/suzukibento/1043.jpg +0 -0
- package/docs/public/images/suzukibento/1059.jpg +0 -0
- package/docs/public/images/suzukibento/1062.jpg +0 -0
- package/docs/public/images/suzukibento/1084.jpg +0 -0
- package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/docs/public/img/heic1501a.jpg +0 -0
- package/docs/public/img/placeholder-landscape.svg +0 -18
- package/docs/public/img/placeholder-square.svg +0 -14
- package/docs/public/img/placeholder-tall.svg +0 -22
- package/docs/public/journey.css +0 -528
- package/docs/public/menu.js +0 -327
- package/docs/public/pulse-ui.css +0 -3028
- package/docs/public/pulse-ui.js +0 -257
- package/docs/public/retrowheels.css +0 -425
- package/docs/public/shop.css +0 -269
- package/docs/public/sparkys.css +0 -382
- package/docs/public/suzukibento.css +0 -517
- package/docs/public/swish.css +0 -394
- package/docs/public/theme.css +0 -52
- package/docs/public/themes/lumio.css +0 -50
- package/docs/public/themes/observatory.css +0 -104
- package/docs/server.js +0 -161
- package/docs/src/lib/component-page.js +0 -49
- package/docs/src/lib/highlight.js +0 -2
- package/docs/src/lib/layout.js +0 -190
- package/docs/src/lib/metrics-store.js +0 -6
- package/docs/src/lib/nav.js +0 -171
- package/docs/src/lib/stats.js +0 -81
- package/docs/src/pages/accessibility.js +0 -157
- package/docs/src/pages/actions.js +0 -194
- package/docs/src/pages/auth.js +0 -176
- package/docs/src/pages/caching.js +0 -95
- package/docs/src/pages/components/accordion.js +0 -48
- package/docs/src/pages/components/alert.js +0 -35
- package/docs/src/pages/components/app-badge.js +0 -41
- package/docs/src/pages/components/avatar.js +0 -35
- package/docs/src/pages/components/badge.js +0 -36
- package/docs/src/pages/components/breadcrumbs.js +0 -94
- package/docs/src/pages/components/button.js +0 -84
- package/docs/src/pages/components/card.js +0 -225
- package/docs/src/pages/components/charts.js +0 -283
- package/docs/src/pages/components/checkbox.js +0 -129
- package/docs/src/pages/components/cluster.js +0 -47
- package/docs/src/pages/components/container.js +0 -40
- package/docs/src/pages/components/cta.js +0 -54
- package/docs/src/pages/components/divider.js +0 -37
- package/docs/src/pages/components/empty.js +0 -36
- package/docs/src/pages/components/feature.js +0 -60
- package/docs/src/pages/components/fieldset.js +0 -65
- package/docs/src/pages/components/fileupload.js +0 -128
- package/docs/src/pages/components/footer.js +0 -58
- package/docs/src/pages/components/grid.js +0 -169
- package/docs/src/pages/components/heading.js +0 -107
- package/docs/src/pages/components/hero.js +0 -123
- package/docs/src/pages/components/icons.js +0 -410
- package/docs/src/pages/components/image.js +0 -71
- package/docs/src/pages/components/input.js +0 -51
- package/docs/src/pages/components/media.js +0 -51
- package/docs/src/pages/components/modal.js +0 -111
- package/docs/src/pages/components/nav.js +0 -136
- package/docs/src/pages/components/phone-frame.js +0 -74
- package/docs/src/pages/components/progress.js +0 -102
- package/docs/src/pages/components/prose.js +0 -111
- package/docs/src/pages/components/pullquote.js +0 -71
- package/docs/src/pages/components/radio.js +0 -194
- package/docs/src/pages/components/rating.js +0 -106
- package/docs/src/pages/components/search.js +0 -61
- package/docs/src/pages/components/section.js +0 -98
- package/docs/src/pages/components/segmented.js +0 -121
- package/docs/src/pages/components/select.js +0 -45
- package/docs/src/pages/components/slider.js +0 -115
- package/docs/src/pages/components/spinner.js +0 -73
- package/docs/src/pages/components/stack.js +0 -48
- package/docs/src/pages/components/stat.js +0 -55
- package/docs/src/pages/components/stepper.js +0 -66
- package/docs/src/pages/components/table.js +0 -45
- package/docs/src/pages/components/textarea.js +0 -31
- package/docs/src/pages/components/toggle.js +0 -84
- package/docs/src/pages/components/tooltip.js +0 -48
- package/docs/src/pages/components.js +0 -200
- package/docs/src/pages/config.js +0 -193
- package/docs/src/pages/constraints.js +0 -100
- package/docs/src/pages/deploy.js +0 -233
- package/docs/src/pages/error-pages.js +0 -99
- package/docs/src/pages/extending.js +0 -198
- package/docs/src/pages/faq.js +0 -99
- package/docs/src/pages/forms.js +0 -129
- package/docs/src/pages/getting-started.js +0 -147
- package/docs/src/pages/guard.js +0 -120
- package/docs/src/pages/home.js +0 -350
- package/docs/src/pages/how-it-works.js +0 -244
- package/docs/src/pages/hydration.js +0 -108
- package/docs/src/pages/images.js +0 -120
- package/docs/src/pages/markdown.js +0 -156
- package/docs/src/pages/meta.js +0 -193
- package/docs/src/pages/mutations.js +0 -107
- package/docs/src/pages/navigation.js +0 -85
- package/docs/src/pages/performance.js +0 -157
- package/docs/src/pages/persist.js +0 -89
- package/docs/src/pages/project-structure.js +0 -91
- package/docs/src/pages/prompt-examples.js +0 -186
- package/docs/src/pages/raw-responses.js +0 -122
- package/docs/src/pages/routing.js +0 -120
- package/docs/src/pages/server-api.js +0 -291
- package/docs/src/pages/server-data.js +0 -203
- package/docs/src/pages/slash-commands.js +0 -55
- package/docs/src/pages/spec.js +0 -207
- package/docs/src/pages/state.js +0 -102
- package/docs/src/pages/store.js +0 -203
- package/docs/src/pages/streaming.js +0 -107
- package/docs/src/pages/stripe.js +0 -194
- package/docs/src/pages/supabase.js +0 -324
- package/docs/src/pages/templates/andypixel.js +0 -260
- package/docs/src/pages/templates/blog-post.js +0 -226
- package/docs/src/pages/templates/bookworms.js +0 -227
- package/docs/src/pages/templates/coutumes.js +0 -237
- package/docs/src/pages/templates/journey.js +0 -186
- package/docs/src/pages/templates/mobile-app.js +0 -371
- package/docs/src/pages/templates/retrowheels.js +0 -355
- package/docs/src/pages/templates/shop.js +0 -268
- package/docs/src/pages/templates/sparkys.js +0 -264
- package/docs/src/pages/templates/suzukibento.js +0 -200
- package/docs/src/pages/templates/swish.js +0 -313
- package/docs/src/pages/templates.js +0 -534
- package/docs/src/pages/testing.js +0 -199
- package/docs/src/pages/validation.js +0 -138
- package/examples/.claude/commands/pulse-build.md +0 -7
- package/examples/.claude/commands/pulse-dev.md +0 -7
- package/examples/.claude/commands/pulse-start.md +0 -9
- package/examples/.claude/commands/pulse-stop.md +0 -5
- package/examples/.claude/commands/verify.md +0 -84
- package/examples/.claude/coverage-check.js +0 -196
- package/examples/.claude/pulse-checklist.md +0 -189
- package/examples/andypixel.css +0 -237
- package/examples/andypixel.js +0 -258
- package/examples/bookworms.css +0 -422
- package/examples/bookworms.js +0 -225
- package/examples/contact.js +0 -166
- package/examples/counter.js +0 -94
- package/examples/dev.server.js +0 -110
- package/examples/examples.test.js +0 -394
- package/examples/journey.css +0 -528
- package/examples/journey.js +0 -186
- package/examples/pricing.js +0 -262
- package/examples/products.js +0 -190
- package/examples/public/.pulse-ui-version +0 -1
- package/examples/public/pulse-ui.css +0 -3028
- package/examples/public/pulse-ui.js +0 -257
- package/examples/quiz.js +0 -208
- package/examples/retrowheels.css +0 -425
- package/examples/retrowheels.js +0 -353
- package/examples/shared.js +0 -84
- package/examples/suzukibento.css +0 -513
- package/examples/suzukibento.js +0 -200
- package/examples/swish.css +0 -394
- package/examples/swish.js +0 -311
- package/examples/todos.js +0 -162
- package/src/ui/fieldset.js +0 -37
- package/src/ui/phone-frame.js +0 -40
- package/src/ui/search.js +0 -78
- package/src/ui/segmented.js +0 -50
- package/src/ui/tooltip.js +0 -28
|
@@ -1,108 +0,0 @@
|
|
|
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('/hydration')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/hydration',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Hydration — Pulse Docs',
|
|
12
|
-
description: 'How Pulse hydrates server-rendered HTML on the client — automatic hydration, mount(), and production bundles.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
state: {},
|
|
16
|
-
view: () => renderLayout({
|
|
17
|
-
currentHref: '/hydration',
|
|
18
|
-
prev,
|
|
19
|
-
next,
|
|
20
|
-
content: `
|
|
21
|
-
${h1('Hydration')}
|
|
22
|
-
${lead('Hydration in Pulse is automatic. Specs with <code>mutations</code>, <code>actions</code>, or <code>persist</code> are hydrated automatically — Pulse binds events to the server-rendered HTML without re-rendering it. Purely server-rendered specs get zero JavaScript sent to the browser.')}
|
|
23
|
-
|
|
24
|
-
${section('enabling', 'How hydration works')}
|
|
25
|
-
<p>Pass spec files as <code>URL</code> objects to <code>createServer</code>. Pulse detects whether each spec needs client-side interactivity and, if so, derives the browser-importable path automatically — no <code>hydrate</code> field needed in the spec:</p>
|
|
26
|
-
${codeBlock(highlight(`// server.js
|
|
27
|
-
await createServer(
|
|
28
|
-
[
|
|
29
|
-
new URL('./src/pages/counter.js', import.meta.url), // has mutations → auto-hydrated
|
|
30
|
-
new URL('./src/pages/about.js', import.meta.url), // no mutations → zero JS
|
|
31
|
-
],
|
|
32
|
-
{ port: 3000, root: new URL('.', import.meta.url) }
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
// src/pages/counter.js — no hydrate field needed
|
|
36
|
-
export default {
|
|
37
|
-
route: '/counter',
|
|
38
|
-
state: { count: 0 },
|
|
39
|
-
mutations: {
|
|
40
|
-
increment: (state) => ({ count: state.count + 1 }),
|
|
41
|
-
decrement: (state) => ({ count: state.count - 1 }),
|
|
42
|
-
},
|
|
43
|
-
view: (state) => \`
|
|
44
|
-
<div>
|
|
45
|
-
<button data-event="decrement">-</button>
|
|
46
|
-
<span>\${state.count}</span>
|
|
47
|
-
<button data-event="increment">+</button>
|
|
48
|
-
</div>
|
|
49
|
-
\`,
|
|
50
|
-
}`, 'js'))}
|
|
51
|
-
|
|
52
|
-
${section('dev-bootstrap', 'Development bootstrap')}
|
|
53
|
-
<p>In development Pulse emits an inline bootstrap script that imports the spec and runtime source files directly — no build step required:</p>
|
|
54
|
-
${codeBlock(highlight(`<script type="module">
|
|
55
|
-
import spec from '/src/pages/counter.js'
|
|
56
|
-
import { mount } from '/src/runtime/index.js'
|
|
57
|
-
import { initNavigation } from '/src/runtime/navigate.js'
|
|
58
|
-
mount(spec, root, window.__PULSE_SERVER__ || {}, { ssr: true })
|
|
59
|
-
initNavigation(root, mount)
|
|
60
|
-
</script>`, 'html'))}
|
|
61
|
-
<p>This imports the spec and runtime source files directly — no build step required for development.</p>
|
|
62
|
-
|
|
63
|
-
${section('production', 'Production bundles')}
|
|
64
|
-
<p>Run <code>npm run build</code> to generate production bundles. This creates content-hashed files in <code>public/dist/</code> and a <code>manifest.json</code> mapping spec hydrate paths to bundle paths.</p>
|
|
65
|
-
${codeBlock(highlight(`# Generated by npm run build
|
|
66
|
-
public/dist/
|
|
67
|
-
runtime-abc123.js # shared runtime (~3.8 kB brotli)
|
|
68
|
-
counter.boot-def456.js # per-page spec bundle (~0.4–0.9 kB brotli)
|
|
69
|
-
manifest.json # { '/src/pages/counter.js': '/dist/counter.boot-def456.js' }`, 'bash'))}
|
|
70
|
-
<p>When Pulse detects a manifest (via <code>staticDir</code> auto-detection or explicit <code>manifest</code> option), it resolves the auto-derived hydrate path to the bundle path and emits a single <code><script src></code> tag instead of the inline bootstrap:</p>
|
|
71
|
-
${codeBlock(highlight(`<script type="module" src="/dist/counter.boot-def456.js"></script>`, 'html'))}
|
|
72
|
-
|
|
73
|
-
${section('ssr-true', 'The { ssr: true } option')}
|
|
74
|
-
<p>The bootstrap script calls <code>mount(spec, root, serverState, { ssr: true })</code>. This tells the runtime to skip the initial re-render and bind event listeners to the existing DOM only.</p>
|
|
75
|
-
<p>This is what keeps LCP fast. The server-painted HTML is the LCP element. The JavaScript binds events without touching the DOM — no flash, no layout shift, no JS-rendered replacement.</p>
|
|
76
|
-
${callout('warning', 'Never set <code>{ ssr: false }</code> on a server-rendered page. It re-renders the entire DOM on mount, replacing the server-painted HTML — causing a visible flash and pushing LCP to 400–600ms.')}
|
|
77
|
-
|
|
78
|
-
${section('mount', 'mount()')}
|
|
79
|
-
<p>The <code>mount</code> function attaches the Pulse runtime to a DOM element:</p>
|
|
80
|
-
${codeBlock(highlight(`import { mount } from '@invisibleloop/pulse/runtime'
|
|
81
|
-
|
|
82
|
-
mount(
|
|
83
|
-
spec, // the page spec
|
|
84
|
-
rootEl, // the DOM element to mount into
|
|
85
|
-
serverState, // window.__PULSE_SERVER__ (server data from SSR)
|
|
86
|
-
{ ssr: true } // skip re-render on first mount
|
|
87
|
-
)`, 'js'))}
|
|
88
|
-
<p>After mount, all <code>data-event</code> and <code>data-action</code> attributes in the DOM are wired to the spec's mutations and actions. State updates trigger a full view re-render via <code>innerHTML</code> replacement.</p>
|
|
89
|
-
|
|
90
|
-
${section('no-hydrate', 'Pages without hydration')}
|
|
91
|
-
<p>Specs with no <code>mutations</code>, <code>actions</code>, or <code>persist</code> automatically get zero JavaScript — no runtime overhead, no hydration cost. This is the correct default for:</p>
|
|
92
|
-
<ul>
|
|
93
|
-
<li>Documentation pages</li>
|
|
94
|
-
<li>Marketing/landing pages</li>
|
|
95
|
-
<li>Blog posts and articles</li>
|
|
96
|
-
<li>Any page with no client-side interactivity</li>
|
|
97
|
-
</ul>
|
|
98
|
-
${callout('tip', 'Many pages that appear to need JavaScript can be handled server-side with <a href="/routing">routing</a> and <a href="/server-data">server data</a>. Keep specs free of mutations and actions wherever possible.')}
|
|
99
|
-
|
|
100
|
-
${section('server-state', 'Passing server state to the client')}
|
|
101
|
-
<p>Server data fetched via <code>server.data()</code> is serialised into the page HTML as <code>window.__PULSE_SERVER__</code>. The client runtime reads this on mount, making server data available to the view during client re-renders without an additional network request.</p>
|
|
102
|
-
${codeBlock(highlight(`// Emitted in the page HTML
|
|
103
|
-
<script id="__PULSE_SERVER__" type="application/json">
|
|
104
|
-
{"product":{"id":1,"name":"Widget","price":9.99}}
|
|
105
|
-
</script>`, 'html'))}
|
|
106
|
-
`,
|
|
107
|
-
}),
|
|
108
|
-
}
|
package/docs/src/pages/images.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
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('/images')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/images',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Images — Pulse Docs',
|
|
12
|
-
description: 'The img() and picture() helpers for optimised, CLS-free image markup.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderLayout({
|
|
16
|
-
currentHref: '/images',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
content: `
|
|
20
|
-
${h1('Images')}
|
|
21
|
-
${lead('The <code>img()</code> and <code>picture()</code> helpers generate image markup that prevents CLS by requiring dimensions and handles loading priority correctly. Pulse targets 0.00 CLS — these helpers enforce the attributes that make that possible.')}
|
|
22
|
-
|
|
23
|
-
${section('import', 'Importing')}
|
|
24
|
-
${codeBlock(highlight(`// In your page spec or component
|
|
25
|
-
import { img, picture } from '@invisibleloop/pulse/image'`, 'js'))}
|
|
26
|
-
|
|
27
|
-
${section('img', 'img(options)')}
|
|
28
|
-
<p>Generates an optimised <code><img></code> element:</p>
|
|
29
|
-
${codeBlock(highlight(`img({
|
|
30
|
-
src: '/images/hero.jpg',
|
|
31
|
-
alt: 'A hero image showing our product',
|
|
32
|
-
width: 1200,
|
|
33
|
-
height: 630,
|
|
34
|
-
priority: true, // → eager loading + high fetchpriority
|
|
35
|
-
})`, 'js'))}
|
|
36
|
-
<p>Output:</p>
|
|
37
|
-
${codeBlock(highlight(`<img src="/images/hero.jpg" alt="A hero image showing our product" width="1200" height="630" loading="eager" decoding="async" fetchpriority="high">`, 'html'))}
|
|
38
|
-
|
|
39
|
-
${section('img-options', 'img() options')}
|
|
40
|
-
${table(
|
|
41
|
-
['Option', 'Type', 'Required', 'Description'],
|
|
42
|
-
[
|
|
43
|
-
['<code>src</code>', '<code>string</code>', 'Yes', 'Image URL.'],
|
|
44
|
-
['<code>alt</code>', '<code>string</code>', 'Yes', 'Alt text. Required for accessibility. Use an empty string for decorative images.'],
|
|
45
|
-
['<code>width</code>', '<code>number</code>', 'Recommended', 'Intrinsic width in pixels. Prevents CLS by reserving layout space.'],
|
|
46
|
-
['<code>height</code>', '<code>number</code>', 'Recommended', 'Intrinsic height in pixels. Prevents CLS.'],
|
|
47
|
-
['<code>priority</code>', '<code>boolean</code>', 'No', 'If true: <code>loading="eager"</code> + <code>fetchpriority="high"</code>. Use for LCP images. Default: <code>false</code>.'],
|
|
48
|
-
['<code>class</code>', '<code>string</code>', 'No', 'CSS class applied to the <code><img></code> element.'],
|
|
49
|
-
]
|
|
50
|
-
)}
|
|
51
|
-
|
|
52
|
-
${callout('warning', '<code>width</code> and <code>height</code> are required to prevent Cumulative Layout Shift. Without them the browser cannot reserve layout space before the image loads. Pulse targets 0.00 CLS — omitting these attributes breaks that guarantee.')}
|
|
53
|
-
|
|
54
|
-
${section('picture', 'picture(options)')}
|
|
55
|
-
<p>Generates a <code><picture></code> element with modern format sources and a fallback <code><img></code>. Use this to serve AVIF or WebP to browsers that support them, with JPEG/PNG as the fallback:</p>
|
|
56
|
-
${codeBlock(highlight(`picture({
|
|
57
|
-
src: '/images/hero.jpg', // fallback
|
|
58
|
-
alt: 'Hero image',
|
|
59
|
-
width: 1200,
|
|
60
|
-
height: 630,
|
|
61
|
-
priority: true,
|
|
62
|
-
sources: [
|
|
63
|
-
{ src: '/images/hero.avif', type: 'image/avif' },
|
|
64
|
-
{ src: '/images/hero.webp', type: 'image/webp' },
|
|
65
|
-
],
|
|
66
|
-
})`, 'js'))}
|
|
67
|
-
<p>Output:</p>
|
|
68
|
-
${codeBlock(highlight(`<picture>
|
|
69
|
-
<source srcset="/images/hero.avif" type="image/avif">
|
|
70
|
-
<source srcset="/images/hero.webp" type="image/webp">
|
|
71
|
-
<img src="/images/hero.jpg" alt="Hero image" width="1200" height="630" loading="eager" decoding="async" fetchpriority="high">
|
|
72
|
-
</picture>`, 'html'))}
|
|
73
|
-
|
|
74
|
-
${section('picture-options', 'picture() options')}
|
|
75
|
-
${table(
|
|
76
|
-
['Option', 'Type', 'Description'],
|
|
77
|
-
[
|
|
78
|
-
['<code>src</code>', '<code>string</code>', 'Fallback image URL (JPEG/PNG for universal compatibility).'],
|
|
79
|
-
['<code>alt</code>', '<code>string</code>', 'Alt text — shared by the inner <code><img></code>.'],
|
|
80
|
-
['<code>width</code>', '<code>number</code>', 'Intrinsic width — applied to inner <code><img></code>.'],
|
|
81
|
-
['<code>height</code>', '<code>number</code>', 'Intrinsic height — applied to inner <code><img></code>.'],
|
|
82
|
-
['<code>priority</code>', '<code>boolean</code>', 'If true: eager loading + high priority.'],
|
|
83
|
-
['<code>class</code>', '<code>string</code>', 'CSS class on the inner <code><img></code>.'],
|
|
84
|
-
['<code>sources</code>', '<code>{src, type}[]</code>', 'Modern format sources in preference order (AVIF first, WebP second).'],
|
|
85
|
-
]
|
|
86
|
-
)}
|
|
87
|
-
|
|
88
|
-
${section('priority', 'When to use priority')}
|
|
89
|
-
<p>Set <code>priority: true</code> on the <strong>Largest Contentful Paint (LCP) element</strong> — typically the hero image above the fold. This tells the browser to load it with high priority, improving LCP.</p>
|
|
90
|
-
<p>Every other image should omit <code>priority</code> (defaults to lazy loading with <code>loading="lazy"</code>). Lazy images are not fetched until they approach the viewport — reducing initial page weight and speeding up load time.</p>
|
|
91
|
-
${callout('tip', 'Only one image per page should have <code>priority: true</code>. Using it on multiple images defeats the purpose — every image becomes "high priority" which is the same as no image being prioritised.')}
|
|
92
|
-
|
|
93
|
-
${section('in-view', 'Using in a view')}
|
|
94
|
-
${codeBlock(highlight(`import { img, picture } from '@invisibleloop/pulse/image'
|
|
95
|
-
|
|
96
|
-
export default {
|
|
97
|
-
route: '/blog/:slug',
|
|
98
|
-
server: {
|
|
99
|
-
data: async (ctx) => ({ post: await db.posts.findBySlug(ctx.params.slug) }),
|
|
100
|
-
},
|
|
101
|
-
view: (state, server) => \`
|
|
102
|
-
<article>
|
|
103
|
-
\${picture({
|
|
104
|
-
src: server.post.heroImage,
|
|
105
|
-
alt: server.post.heroAlt,
|
|
106
|
-
width: 1200,
|
|
107
|
-
height: 630,
|
|
108
|
-
priority: true,
|
|
109
|
-
sources: [
|
|
110
|
-
{ src: server.post.heroImage.replace('.jpg', '.avif'), type: 'image/avif' },
|
|
111
|
-
{ src: server.post.heroImage.replace('.jpg', '.webp'), type: 'image/webp' },
|
|
112
|
-
],
|
|
113
|
-
})}
|
|
114
|
-
<h1>\${server.post.title}</h1>
|
|
115
|
-
</article>
|
|
116
|
-
\`,
|
|
117
|
-
}`, 'js'))}
|
|
118
|
-
`,
|
|
119
|
-
}),
|
|
120
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { renderLayout, h1, lead, section, 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('/markdown')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/markdown',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Markdown — Pulse Docs',
|
|
12
|
-
description: 'Render markdown files as pages with syntax-highlighted code blocks, frontmatter, and the prose component.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderLayout({
|
|
16
|
-
currentHref: '/markdown',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
content: `
|
|
20
|
-
${h1('Markdown')}
|
|
21
|
-
${lead('Pulse includes a built-in markdown parser. Write content in <code>.md</code> files, load them with the <code>md()</code> helper, and render with the <code>prose</code> component. No external dependencies — zero browser bytes.')}
|
|
22
|
-
|
|
23
|
-
${section('basic', 'Basic usage')}
|
|
24
|
-
<p>Import <code>md</code> from <code>@invisibleloop/pulse/md</code> and call it with a file path. It returns an async server fetcher — use it in <code>server</code> and optionally in <code>meta</code>:</p>
|
|
25
|
-
${codeBlock(highlight(`import { md } from '@invisibleloop/pulse/md'
|
|
26
|
-
import { prose } from '@invisibleloop/pulse/ui'
|
|
27
|
-
|
|
28
|
-
const page = md('content/about.md')
|
|
29
|
-
|
|
30
|
-
export default {
|
|
31
|
-
route: '/about',
|
|
32
|
-
server: { page },
|
|
33
|
-
view: (state, { page }) => prose({ content: page.html }),
|
|
34
|
-
}`, 'js'))}
|
|
35
|
-
<p>The fetcher returns <code>{ html, frontmatter }</code>. The <code>html</code> is ready to pass directly to <code>prose()</code>. The <code>frontmatter</code> object contains the parsed key/value pairs from the <code>---</code> block at the top of the file.</p>
|
|
36
|
-
|
|
37
|
-
${section('frontmatter', 'Frontmatter')}
|
|
38
|
-
<p>Add a <code>---</code> block at the top of any <code>.md</code> file to define metadata. Use the values in <code>meta</code> for SEO:</p>
|
|
39
|
-
${codeBlock(highlight(`---
|
|
40
|
-
title: About Us
|
|
41
|
-
description: Learn about our company
|
|
42
|
-
date: 2024-01-15
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
# About Us
|
|
46
|
-
|
|
47
|
-
Welcome to our company…`, 'bash'))}
|
|
48
|
-
${codeBlock(highlight(`const page = md('content/about.md')
|
|
49
|
-
|
|
50
|
-
export default {
|
|
51
|
-
route: '/about',
|
|
52
|
-
meta: {
|
|
53
|
-
title: async (ctx) => (await page(ctx)).frontmatter.title,
|
|
54
|
-
description: async (ctx) => (await page(ctx)).frontmatter.description,
|
|
55
|
-
},
|
|
56
|
-
server: { page },
|
|
57
|
-
view: (state, { page }) => prose({ content: page.html }),
|
|
58
|
-
}`, 'js'))}
|
|
59
|
-
${callout('tip', 'Calling <code>page(ctx)</code> in both <code>meta</code> and <code>server</code> reads the file only once per request — the result is cached on <code>ctx._mdCache</code> automatically.')}
|
|
60
|
-
|
|
61
|
-
${section('dynamic', 'Dynamic routes')}
|
|
62
|
-
<p>Use <code>:param</code> placeholders in the path — they are resolved from <code>ctx.params</code> at request time:</p>
|
|
63
|
-
${codeBlock(highlight(`import { md } from '@invisibleloop/pulse/md'
|
|
64
|
-
import { prose } from '@invisibleloop/pulse/ui'
|
|
65
|
-
|
|
66
|
-
const post = md('content/blog/:slug.md')
|
|
67
|
-
|
|
68
|
-
export default {
|
|
69
|
-
route: '/blog/:slug',
|
|
70
|
-
meta: {
|
|
71
|
-
title: async (ctx) => (await post(ctx)).frontmatter.title,
|
|
72
|
-
description: async (ctx) => (await post(ctx)).frontmatter.description,
|
|
73
|
-
},
|
|
74
|
-
server: { post },
|
|
75
|
-
view: (state, { post }) => \`
|
|
76
|
-
<main id="main-content">
|
|
77
|
-
\${prose({ content: post.html })}
|
|
78
|
-
</main>
|
|
79
|
-
\`,
|
|
80
|
-
onViewError: (err) => \`
|
|
81
|
-
<main id="main-content">
|
|
82
|
-
<p>Post not found.</p>
|
|
83
|
-
</main>
|
|
84
|
-
\`,
|
|
85
|
-
}`, 'js'))}
|
|
86
|
-
${callout('note', 'If the file does not exist, the fetcher throws with <code>{ status: 404 }</code>. Define <code>onViewError</code> on dynamic-route pages to render a friendly not-found message instead of a 500.')}
|
|
87
|
-
|
|
88
|
-
${section('parseMd', 'parseMd()')}
|
|
89
|
-
<p>For cases where you already have a markdown string (from a database, API, or generated content), use <code>parseMd</code> directly instead of the file helper:</p>
|
|
90
|
-
${codeBlock(highlight(`import { parseMd } from '@invisibleloop/pulse/md'
|
|
91
|
-
import { prose } from '@invisibleloop/pulse/ui'
|
|
92
|
-
|
|
93
|
-
export default {
|
|
94
|
-
route: '/post/:id',
|
|
95
|
-
server: {
|
|
96
|
-
post: async (ctx) => {
|
|
97
|
-
const record = await db.posts.find(ctx.params.id)
|
|
98
|
-
const { html, frontmatter } = parseMd(record.body)
|
|
99
|
-
return { html, title: frontmatter.title ?? record.title }
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
view: (state, { post }) => prose({ content: post.html }),
|
|
103
|
-
}`, 'js'))}
|
|
104
|
-
|
|
105
|
-
${section('what-renders', 'What the parser renders')}
|
|
106
|
-
${table(
|
|
107
|
-
['Markdown', 'Output'],
|
|
108
|
-
[
|
|
109
|
-
['<code># Heading</code> through <code>###### Heading</code>', '<code><h1></code>–<code><h6></code> with auto <code>id</code> anchors'],
|
|
110
|
-
['<code>**bold**</code>, <code>*italic*</code>, <code>~~strike~~</code>', '<code><strong></code>, <code><em></code>, <code><del></code>'],
|
|
111
|
-
['<code>`inline code`</code>', '<code><code></code>'],
|
|
112
|
-
['Fenced code block with language tag', '<code><pre><code class="language-X"></code> with syntax highlighting'],
|
|
113
|
-
['<code>[text](url)</code>, <code></code>', '<code><a></code>, <code><img></code>'],
|
|
114
|
-
['<code>- item</code>, <code>1. item</code>', '<code><ul></code>, <code><ol></code> (nested lists supported)'],
|
|
115
|
-
['<code>> quote</code>', '<code><blockquote></code>'],
|
|
116
|
-
['GFM table (<code>| col | col |</code>)', '<code><table></code> with <code><thead></code> and <code><tbody></code>'],
|
|
117
|
-
['<code>---</code> (three or more)', '<code><hr></code>'],
|
|
118
|
-
]
|
|
119
|
-
)}
|
|
120
|
-
|
|
121
|
-
${section('syntax-highlighting', 'Syntax highlighting')}
|
|
122
|
-
<p>Fenced code blocks with a language tag are syntax-highlighted automatically — no client JS required. The highlighting runs on the server and emits <code><span class="tok-*"></code> elements. The colour tokens are defined in <code>pulse-ui.css</code> and respect the active theme.</p>
|
|
123
|
-
${codeBlock(highlight(`\`\`\`js
|
|
124
|
-
const greeting = 'Hello, world'
|
|
125
|
-
console.log(greeting)
|
|
126
|
-
\`\`\``, 'bash'))}
|
|
127
|
-
<p>Supported languages:</p>
|
|
128
|
-
${table(
|
|
129
|
-
['Tag(s)', 'Highlighter'],
|
|
130
|
-
[
|
|
131
|
-
['<code>js</code>, <code>javascript</code>, <code>ts</code>, <code>typescript</code>, <code>jsx</code>, <code>tsx</code>', 'JavaScript / TypeScript'],
|
|
132
|
-
['<code>html</code>, <code>xml</code>, <code>svg</code>, <code>vue</code>, <code>svelte</code>', 'HTML / XML'],
|
|
133
|
-
['<code>css</code>, <code>scss</code>, <code>less</code>', 'CSS'],
|
|
134
|
-
['<code>bash</code>, <code>sh</code>, <code>shell</code>, <code>zsh</code>', 'Bash / Shell'],
|
|
135
|
-
['<code>json</code>, <code>jsonc</code>', 'JSON'],
|
|
136
|
-
['Any other tag', 'Plain text (escaped, no colour tokens)'],
|
|
137
|
-
]
|
|
138
|
-
)}
|
|
139
|
-
|
|
140
|
-
${section('prose', 'The prose component')}
|
|
141
|
-
<p>The <code>prose</code> component is designed specifically for markdown output. It applies typographic styles to all descendant elements — headings, paragraphs, lists, blockquotes, code blocks, tables, images — without requiring any classes on the elements themselves:</p>
|
|
142
|
-
${codeBlock(highlight(`import { prose } from '@invisibleloop/pulse/ui'
|
|
143
|
-
|
|
144
|
-
// Wrap the html from parseMd or md()
|
|
145
|
-
prose({ content: page.html })
|
|
146
|
-
|
|
147
|
-
// Optional size modifier
|
|
148
|
-
prose({ content: page.html, size: 'lg' }) // 'sm' | 'base' | 'lg'`, 'js'))}
|
|
149
|
-
${callout('note', 'The <code>content</code> slot is injected as raw HTML — only pass server-side content that you trust. Never pass user-submitted HTML directly; sanitise it first.')}
|
|
150
|
-
|
|
151
|
-
${section('url-paths', 'Using URL paths')}
|
|
152
|
-
<p>Pass a <code>URL</code> object to resolve paths relative to the spec file rather than the process working directory:</p>
|
|
153
|
-
${codeBlock(highlight(`const page = md(new URL('./content/about.md', import.meta.url))`, 'js'))}
|
|
154
|
-
`,
|
|
155
|
-
}),
|
|
156
|
-
}
|
package/docs/src/pages/meta.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
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('/meta')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/meta',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Metadata & SEO — Pulse Docs',
|
|
12
|
-
description: 'How to configure page metadata, Open Graph tags, and structured data in Pulse.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderLayout({
|
|
16
|
-
currentHref: '/meta',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
content: `
|
|
20
|
-
${h1('Metadata & SEO')}
|
|
21
|
-
${lead('The <code>meta</code> field declares everything that appears in the <code><head></code> — title, description, stylesheets, Open Graph tags, and structured data. All metadata is rendered server-side, so crawlers and social media scrapers see the final HTML without executing JavaScript.')}
|
|
22
|
-
|
|
23
|
-
${section('basics', 'Basic metadata')}
|
|
24
|
-
${codeBlock(highlight(`export default {
|
|
25
|
-
route: '/about',
|
|
26
|
-
meta: {
|
|
27
|
-
title: 'About Us — Acme Corp',
|
|
28
|
-
description: 'Learn about the team behind Acme Corp.',
|
|
29
|
-
styles: ['/app.css'],
|
|
30
|
-
},
|
|
31
|
-
view: () => \`<h1>About Us</h1>\`,
|
|
32
|
-
}`, 'js'))}
|
|
33
|
-
<p>This generates:</p>
|
|
34
|
-
${codeBlock(highlight(`<title>About Us — Acme Corp</title>
|
|
35
|
-
<meta name="description" content="Learn about the team behind Acme Corp.">
|
|
36
|
-
<link rel="stylesheet" href="/app.css">`, 'html'))}
|
|
37
|
-
|
|
38
|
-
${section('all-fields', 'All meta fields')}
|
|
39
|
-
${table(
|
|
40
|
-
['Field', 'Type', 'Description'],
|
|
41
|
-
[
|
|
42
|
-
['<code>title</code>', '<code>string</code>', 'Page title — appears in the browser tab and search results.'],
|
|
43
|
-
['<code>description</code>', '<code>string</code>', 'Meta description — appears in search engine snippets. Keep under 160 characters.'],
|
|
44
|
-
['<code>styles</code>', '<code>string[]</code>', 'Array of stylesheet URLs — each emits a <code><link rel="stylesheet"></code> tag.'],
|
|
45
|
-
['<code>ogTitle</code>', '<code>string</code>', 'Open Graph title. If omitted, falls back to <code>title</code>.'],
|
|
46
|
-
['<code>ogImage</code>', '<code>string</code>', 'Open Graph image URL — shown when the page is shared on social media.'],
|
|
47
|
-
['<code>schema</code>', '<code>object</code>', 'JSON-LD structured data object — emitted as a <code><script type="application/ld+json"></code> tag.'],
|
|
48
|
-
['<code>canonical</code>', '<code>string | (ctx, serverState) => string</code>', 'Canonical URL — overrides the auto-derived canonical. Accepts a function for dynamic values.'],
|
|
49
|
-
]
|
|
50
|
-
)}
|
|
51
|
-
|
|
52
|
-
${section('open-graph', 'Open Graph')}
|
|
53
|
-
<p>Open Graph tags control how the page appears when shared on social media (Twitter/X, Facebook, LinkedIn, Slack, etc.):</p>
|
|
54
|
-
${codeBlock(highlight(`meta: {
|
|
55
|
-
title: 'My Product — Acme Corp',
|
|
56
|
-
description: 'The best product ever made.',
|
|
57
|
-
ogTitle: 'My Product', // shorter for social
|
|
58
|
-
ogImage: 'https://acme.com/og/my-product.jpg', // 1200×630 recommended
|
|
59
|
-
}`, 'js'))}
|
|
60
|
-
<p>Generated tags:</p>
|
|
61
|
-
${codeBlock(highlight(`<meta property="og:title" content="My Product">
|
|
62
|
-
<meta property="og:description" content="The best product ever made.">
|
|
63
|
-
<meta property="og:image" content="https://acme.com/og/my-product.jpg">
|
|
64
|
-
<meta name="twitter:card" content="summary_large_image">
|
|
65
|
-
<meta name="twitter:title" content="My Product">
|
|
66
|
-
<meta name="twitter:image" content="https://acme.com/og/my-product.jpg">`, 'html'))}
|
|
67
|
-
${callout('tip', 'Use an absolute URL for <code>ogImage</code> — social media crawlers need the full URL to fetch the image. Recommended size: 1200×630 pixels.')}
|
|
68
|
-
|
|
69
|
-
${section('structured-data', 'Structured data (ld+json)')}
|
|
70
|
-
<p>The <code>schema</code> field accepts a plain object conforming to <a href="https://schema.org" target="_blank" rel="noopener" aria-label="schema.org (opens in new tab)">schema.org</a> vocabulary. Pulse serialises it as a <code><script type="application/ld+json"></code> tag in the head:</p>
|
|
71
|
-
${codeBlock(highlight(`meta: {
|
|
72
|
-
title: 'How to make sourdough — My Blog',
|
|
73
|
-
schema: {
|
|
74
|
-
'@context': 'https://schema.org',
|
|
75
|
-
'@type': 'Article',
|
|
76
|
-
headline: 'How to make sourdough',
|
|
77
|
-
author: {
|
|
78
|
-
'@type': 'Person',
|
|
79
|
-
name: 'Jane Smith',
|
|
80
|
-
},
|
|
81
|
-
datePublished: '2025-01-15',
|
|
82
|
-
image: 'https://myblog.com/sourdough.jpg',
|
|
83
|
-
},
|
|
84
|
-
}`, 'js'))}
|
|
85
|
-
<p>Common schema types:</p>
|
|
86
|
-
${table(
|
|
87
|
-
['@type', 'Use for'],
|
|
88
|
-
[
|
|
89
|
-
['<code>WebSite</code>', 'The homepage or site root'],
|
|
90
|
-
['<code>WebPage</code>', 'General pages'],
|
|
91
|
-
['<code>Article</code>', 'Blog posts, news articles'],
|
|
92
|
-
['<code>Product</code>', 'E-commerce product pages'],
|
|
93
|
-
['<code>FAQPage</code>', 'FAQ pages (enables rich results in Google)'],
|
|
94
|
-
['<code>BreadcrumbList</code>', 'Breadcrumb navigation'],
|
|
95
|
-
['<code>Organization</code>', 'Company/brand information'],
|
|
96
|
-
]
|
|
97
|
-
)}
|
|
98
|
-
|
|
99
|
-
${section('canonical', 'Canonical URLs')}
|
|
100
|
-
<p>Pulse automatically derives a canonical URL from the request and emits it as a <code><link rel="canonical"></code> tag on every page. In most cases no configuration is needed.</p>
|
|
101
|
-
<p>Use <code>meta.canonical</code> to override — for example, on paginated pages or when content is accessible at more than one URL. A plain string is resolved once at startup:</p>
|
|
102
|
-
${codeBlock(highlight(`// Paginated blog — pages 2, 3, … all canonicalise to the first page
|
|
103
|
-
meta: {
|
|
104
|
-
title: 'Blog — Page 2',
|
|
105
|
-
canonical: 'https://mysite.com/blog',
|
|
106
|
-
}`, 'js'))}
|
|
107
|
-
<p>Pass a function to derive the canonical from the request context or server data. The function receives <code>(ctx, serverState)</code>:</p>
|
|
108
|
-
${codeBlock(highlight(`// Canonical from a URL param
|
|
109
|
-
meta: {
|
|
110
|
-
canonical: (ctx) => \`https://mysite.com/products/\${ctx.params.slug}\`,
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Canonical from a server fetcher result (e.g. canonical slug from a database lookup)
|
|
114
|
-
meta: {
|
|
115
|
-
canonical: (ctx, serverState) => \`https://mysite.com/products/\${serverState.product.slug}\`,
|
|
116
|
-
}`, 'js'))}
|
|
117
|
-
${callout('note', '<strong>Streaming caveat:</strong> when <code>stream: true</code> (the default), the <code><head></code> is written before server fetchers resolve, so <code>serverState</code> will be <code>null</code> for streaming responses. If your canonical depends on server data, set <code>stream: false</code> on that spec, or derive it from <code>ctx.params</code> instead.')}
|
|
118
|
-
|
|
119
|
-
${section('sitemap', 'Sitemap & robots.txt')}
|
|
120
|
-
<p>Pulse generates <code>/sitemap.xml</code> and <code>/robots.txt</code> from the registered routes — enable it with one option:</p>
|
|
121
|
-
${codeBlock(highlight(`await createServer(pages, {
|
|
122
|
-
sitemap: true, // or { origin: 'https://mysite.com' } to pin the origin
|
|
123
|
-
})`, 'js'))}
|
|
124
|
-
<p>Static page routes are included automatically. Excluded by default: the <code>route: '*'</code> not-found page, raw content specs, guarded pages (set <code>sitemap: true</code> on the spec to opt one in), and any page with <code>sitemap: false</code>.</p>
|
|
125
|
-
<p>Dynamic <code>:param</code> routes can't be enumerated automatically — give them a <code>sitemap</code> enumerator or they are skipped (the server logs a startup hint listing them):</p>
|
|
126
|
-
${codeBlock(highlight(`export default {
|
|
127
|
-
route: '/blog/:slug',
|
|
128
|
-
sitemap: async () => {
|
|
129
|
-
const posts = await db.posts.allPublished()
|
|
130
|
-
return posts.map(p => ({ path: \`/blog/\${p.slug}\`, lastmod: p.updatedAt }))
|
|
131
|
-
},
|
|
132
|
-
// entries are '/path' strings or { path, lastmod } objects
|
|
133
|
-
}`, 'js'))}
|
|
134
|
-
${table(
|
|
135
|
-
['Where', 'Value', 'Effect'],
|
|
136
|
-
[
|
|
137
|
-
['<code>createServer</code>', '<code>sitemap: true | { origin }</code>', 'Enables generation. Origin defaults to the request host; pin it for multi-domain setups.'],
|
|
138
|
-
['<code>createServer</code>', '<code>robots: false | string</code>', 'Disable robots.txt, or serve a custom string verbatim. Default auto-generates allow-all + the Sitemap line.'],
|
|
139
|
-
['spec', '<code>sitemap: false</code>', 'Exclude this page.'],
|
|
140
|
-
['spec', '<code>sitemap: true</code>', 'Include a guarded page that would be excluded by default.'],
|
|
141
|
-
['spec', '<code>sitemap: array | function</code>', 'Enumerate URLs — required for dynamic routes.'],
|
|
142
|
-
]
|
|
143
|
-
)}
|
|
144
|
-
${callout('note', 'A physical <code>sitemap.xml</code> or <code>robots.txt</code> in <code>staticDir</code> always wins over the generated one — drop a file in <code>public/</code> to take full manual control. For fully hand-built XML, see <a href="/raw-responses#sitemap">Raw Responses</a>.')}
|
|
145
|
-
|
|
146
|
-
${section('styles', 'Stylesheets')}
|
|
147
|
-
<p>The <code>styles</code> array accepts any number of stylesheet URLs. They are emitted as <code><link rel="stylesheet"></code> tags in the <code><head></code> in the order declared:</p>
|
|
148
|
-
${codeBlock(highlight(`meta: {
|
|
149
|
-
styles: [
|
|
150
|
-
'/app.css',
|
|
151
|
-
'/fonts.css',
|
|
152
|
-
'https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap',
|
|
153
|
-
],
|
|
154
|
-
}`, 'js'))}
|
|
155
|
-
${callout('note', 'For maximum performance, host your own fonts rather than using Google Fonts. External stylesheet requests add render-blocking latency and a DNS lookup.')}
|
|
156
|
-
|
|
157
|
-
${section('vibe', 'Personality presets (meta.vibe)')}
|
|
158
|
-
<p>Set <code>meta.vibe</code> to apply a personality preset that adjusts typography, border radius, and letter-spacing across all components on the page. The preset sets a <code>data-vibe</code> attribute on <code><body></code> — pair it with a <code>theme.css</code> for complete brand expression.</p>
|
|
159
|
-
${codeBlock(highlight(`export default {
|
|
160
|
-
route: '/landing',
|
|
161
|
-
meta: {
|
|
162
|
-
vibe: 'playful',
|
|
163
|
-
styles: ['/pulse-ui.css', '/theme.css'],
|
|
164
|
-
},
|
|
165
|
-
}`, 'js'))}
|
|
166
|
-
${table(
|
|
167
|
-
['Vibe', 'Character', 'Best for'],
|
|
168
|
-
[
|
|
169
|
-
['<code>warm</code>', 'Friendly, rounded (14px), humanist feel', 'Local businesses, wellness, food, services'],
|
|
170
|
-
['<code>editorial</code>', 'Serif display, zero radius, tight tracking', 'Publications, blogs, journalism, literary'],
|
|
171
|
-
['<code>playful</code>', 'Extra-large radius (22px), loose spacing', 'Consumer apps, kids, lifestyle, creators'],
|
|
172
|
-
['<code>minimal</code>', 'Ultra-low radius (2px), refined whitespace', 'Portfolios, luxury, developer tools'],
|
|
173
|
-
['<code>bold</code>', 'Tight tracking, strong presence', 'Agencies, startups, sports'],
|
|
174
|
-
['<code>brutalist</code>', 'Zero radius, oversized Arial Black / Impact headings', 'Art, fashion, counter-culture, zines'],
|
|
175
|
-
['<code>retro</code>', 'Slab serif, medium radius (8px), nostalgic geometry', 'Craft, breweries, vintage, heritage brands'],
|
|
176
|
-
['<code>corporate</code>', 'Conservative radius (4px), tight tracking, professional', 'B2B, finance, legal, consultancies'],
|
|
177
|
-
['<code>neon</code>', 'Monospace headline, futuristic, sharp edges', 'Gaming, crypto, dark-tech, cyberpunk'],
|
|
178
|
-
['<code>paper</code>', 'Organic radius (10px), serif body, tactile feel', 'Journals, bookshops, newsletters, diaries'],
|
|
179
|
-
]
|
|
180
|
-
)}
|
|
181
|
-
${callout('tip', 'Vibe only adjusts shape and type tokens — it does not set colours. Add a <code>theme.css</code> that defines <code>--ui-accent</code>, <code>--color-bg</code>, and other colour tokens to complete the personality.')}
|
|
182
|
-
|
|
183
|
-
${section('seo-tips', 'SEO tips')}
|
|
184
|
-
<ul>
|
|
185
|
-
<li>Write a unique <code>title</code> and <code>description</code> for every page — duplicate metadata prevents pages from competing in search results.</li>
|
|
186
|
-
<li>Keep descriptions under 160 characters — longer values are truncated.</li>
|
|
187
|
-
<li>Use structured data to qualify for Google rich results.</li>
|
|
188
|
-
<li>All metadata is in the server-rendered HTML — search engines and social scrapers do not need to execute JavaScript to read it.</li>
|
|
189
|
-
<li>Pulse targets 100/100 Lighthouse SEO out of the box. Run <code>/pulse-report</code> after new pages to confirm.</li>
|
|
190
|
-
</ul>
|
|
191
|
-
`,
|
|
192
|
-
}),
|
|
193
|
-
}
|