loopwind 0.15.0 → 0.17.0

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.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Test for font files in SDK StyleConfig
3
+ * Run with: node test-font-files.mjs
4
+ */
5
+
6
+ import { defineTemplate, renderImage } from './dist/sdk/index.js';
7
+ import React from 'react';
8
+
9
+ const { createElement: h } = React;
10
+
11
+ console.log('šŸ“ Testing font files in StyleConfig...\n');
12
+
13
+ // Test with external URL (Google Fonts mirror)
14
+ const config = {
15
+ colors: {
16
+ text: '#000000',
17
+ },
18
+ fonts: {
19
+ sans: {
20
+ family: ['Inter', 'sans-serif'],
21
+ files: [
22
+ {
23
+ path: 'https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff',
24
+ weight: 400,
25
+ },
26
+ {
27
+ path: 'https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff',
28
+ weight: 700,
29
+ },
30
+ ],
31
+ },
32
+ },
33
+ };
34
+
35
+ const template = defineTemplate({
36
+ name: 'test-font-files',
37
+ size: { width: 600, height: 400 },
38
+ config,
39
+ render: ({ tw }) =>
40
+ h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-white p-8') },
41
+ h('h1', { style: tw('font-sans text-6xl font-bold text-text') }, 'Bold Text'),
42
+ h('p', { style: tw('font-sans text-3xl font-normal text-text') }, 'Normal Text')
43
+ ),
44
+ });
45
+
46
+ try {
47
+ console.log('ā³ Rendering with external font URLs...');
48
+ const svg = await renderImage(template, {}, { format: 'svg' });
49
+
50
+ console.log('āœ… SUCCESS: Font files loaded from URLs');
51
+ console.log(`āœ… Generated SVG: ${svg.length} bytes`);
52
+
53
+ // Also test PNG to ensure fonts render correctly
54
+ console.log('\nā³ Testing PNG rendering with fonts...');
55
+ const png = await renderImage(template, {}, { format: 'png' });
56
+
57
+ if (png[0] === 0x89 && png[1] === 0x50 && png[2] === 0x4e && png[3] === 0x47) {
58
+ console.log('āœ… SUCCESS: PNG generated with fonts');
59
+ console.log(`āœ… PNG size: ${png.length} bytes`);
60
+ } else {
61
+ console.error('āŒ FAIL: Invalid PNG signature');
62
+ process.exit(1);
63
+ }
64
+
65
+ console.log('\nšŸŽ‰ All font file tests passed!');
66
+ console.log('\n✨ Font files with URLs work correctly in SDK StyleConfig');
67
+
68
+ } catch (error) {
69
+ console.error('āŒ ERROR:', error.message);
70
+ console.error(error.stack);
71
+ process.exit(1);
72
+ }
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Integration tests for SDK StyleConfig
3
+ * Run with: node test-sdk-config.mjs
4
+ */
5
+
6
+ import { defineTemplate, renderImage, renderVideo } from './dist/sdk/index.js';
7
+ import React from 'react';
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const { createElement: h } = React;
14
+
15
+ // Test utilities
16
+ let testsPassed = 0;
17
+ let testsFailed = 0;
18
+
19
+ function assert(condition, message) {
20
+ if (!condition) {
21
+ console.error(`āŒ FAIL: ${message}`);
22
+ testsFailed++;
23
+ throw new Error(message);
24
+ } else {
25
+ console.log(`āœ… PASS: ${message}`);
26
+ testsPassed++;
27
+ }
28
+ }
29
+
30
+ async function test(name, fn) {
31
+ console.log(`\nšŸ“ Test: ${name}`);
32
+ try {
33
+ await fn();
34
+ } catch (error) {
35
+ console.error(`Error in test "${name}":`, error.message);
36
+ }
37
+ }
38
+
39
+ // Helper to check if SVG contains specific color
40
+ function svgContainsColor(svg, color) {
41
+ // Normalize color format (remove # if present)
42
+ const normalizedColor = color.replace('#', '');
43
+ return svg.includes(normalizedColor) || svg.includes(color);
44
+ }
45
+
46
+ // Test 1: Config in defineTemplate (image)
47
+ await test('Config in defineTemplate - Image', async () => {
48
+ const config = {
49
+ colors: {
50
+ primary: '#ff0000',
51
+ background: '#00ff00',
52
+ },
53
+ };
54
+
55
+ const template = defineTemplate({
56
+ name: 'test-config-template',
57
+ size: { width: 400, height: 400 },
58
+ config,
59
+ render: ({ tw, title }) =>
60
+ h('div', { style: tw('flex items-center justify-center w-full h-full bg-background') },
61
+ h('h1', { style: tw('text-4xl font-bold text-primary') }, title)
62
+ ),
63
+ });
64
+
65
+ assert(template.config, 'Template should have config');
66
+ assert(template.config.colors.primary === '#ff0000', 'Config colors should match');
67
+
68
+ const svg = await renderImage(template, { title: 'Test' }, { format: 'svg' });
69
+ const svgString = svg.toString();
70
+
71
+ assert(svgContainsColor(svgString, '#ff0000'), 'SVG should contain primary color');
72
+ assert(svgContainsColor(svgString, '#00ff00'), 'SVG should contain background color');
73
+ });
74
+
75
+ // Test 2: Config in renderImage options
76
+ await test('Config in renderImage options', async () => {
77
+ const template = defineTemplate({
78
+ name: 'test-render-config',
79
+ size: { width: 400, height: 400 },
80
+ render: ({ tw, title }) =>
81
+ h('div', { style: tw('flex items-center justify-center w-full h-full bg-secondary') },
82
+ h('h1', { style: tw('text-4xl font-bold text-accent') }, title)
83
+ ),
84
+ });
85
+
86
+ const svg = await renderImage(
87
+ template,
88
+ { title: 'Test' },
89
+ {
90
+ format: 'svg',
91
+ config: {
92
+ colors: {
93
+ accent: '#0000ff',
94
+ secondary: '#ffff00',
95
+ },
96
+ },
97
+ }
98
+ );
99
+
100
+ const svgString = svg.toString();
101
+ assert(svgContainsColor(svgString, '#0000ff'), 'SVG should contain accent color from options');
102
+ assert(svgContainsColor(svgString, '#ffff00'), 'SVG should contain secondary color from options');
103
+ });
104
+
105
+ // Test 3: Config override (options override template)
106
+ await test('Config override - Options override template', async () => {
107
+ const template = defineTemplate({
108
+ name: 'test-override',
109
+ size: { width: 400, height: 400 },
110
+ config: {
111
+ colors: {
112
+ brand: '#111111',
113
+ },
114
+ },
115
+ render: ({ tw }) =>
116
+ h('div', { style: tw('flex items-center justify-center w-full h-full bg-brand') },
117
+ h('h1', { style: tw('text-4xl font-bold') }, 'Override Test')
118
+ ),
119
+ });
120
+
121
+ const svg = await renderImage(
122
+ template,
123
+ {},
124
+ {
125
+ format: 'svg',
126
+ config: {
127
+ colors: {
128
+ brand: '#222222',
129
+ },
130
+ },
131
+ }
132
+ );
133
+
134
+ const svgString = svg.toString();
135
+ assert(svgContainsColor(svgString, '#222222'), 'SVG should contain overridden brand color');
136
+ assert(!svgContainsColor(svgString, '#111111'), 'SVG should NOT contain original template brand color');
137
+ });
138
+
139
+ // Test 4: PNG format with config
140
+ await test('PNG format with config', async () => {
141
+ const template = defineTemplate({
142
+ name: 'test-png',
143
+ size: { width: 200, height: 200 },
144
+ render: ({ tw }) =>
145
+ h('div', { style: tw('flex items-center justify-center w-full h-full bg-primary') },
146
+ h('h1', { style: tw('text-2xl font-bold text-white') }, 'PNG')
147
+ ),
148
+ });
149
+
150
+ const png = await renderImage(
151
+ template,
152
+ {},
153
+ {
154
+ format: 'png',
155
+ config: {
156
+ colors: {
157
+ primary: '#ff00ff',
158
+ },
159
+ },
160
+ }
161
+ );
162
+
163
+ assert(Buffer.isBuffer(png), 'PNG should be a Buffer');
164
+ assert(png.length > 0, 'PNG buffer should not be empty');
165
+ // PNG signature check
166
+ assert(png[0] === 0x89 && png[1] === 0x50 && png[2] === 0x4e && png[3] === 0x47, 'Should be valid PNG');
167
+ });
168
+
169
+ // Test 5: Video with config
170
+ await test('Video with config', async () => {
171
+ const template = defineTemplate({
172
+ name: 'test-video',
173
+ type: 'video',
174
+ size: { width: 400, height: 400 },
175
+ video: { fps: 30, duration: 0.5 },
176
+ config: {
177
+ colors: {
178
+ primary: '#ff6600',
179
+ },
180
+ },
181
+ render: ({ tw, progress }) => {
182
+ const opacity = progress;
183
+ return h('div', { style: tw('flex items-center justify-center w-full h-full bg-white') },
184
+ h('h1', { style: { ...tw('text-4xl font-bold text-primary'), opacity } }, 'Video')
185
+ );
186
+ },
187
+ });
188
+
189
+ const mp4 = await renderVideo(template, {});
190
+
191
+ assert(Buffer.isBuffer(mp4), 'MP4 should be a Buffer');
192
+ assert(mp4.length > 0, 'MP4 buffer should not be empty');
193
+ // MP4 signature check (ftyp)
194
+ const signature = mp4.toString('ascii', 4, 8);
195
+ assert(signature === 'ftyp', 'Should be valid MP4');
196
+ });
197
+
198
+ // Test 6: Video with config override
199
+ await test('Video with config override', async () => {
200
+ const template = defineTemplate({
201
+ name: 'test-video-override',
202
+ type: 'video',
203
+ size: { width: 400, height: 400 },
204
+ video: { fps: 30, duration: 0.5 },
205
+ config: {
206
+ colors: {
207
+ brand: '#333333',
208
+ },
209
+ },
210
+ render: ({ tw, progress }) =>
211
+ h('div', { style: tw('flex items-center justify-center w-full h-full bg-brand') },
212
+ h('h1', { style: { ...tw('text-4xl font-bold'), opacity: progress } }, 'Video')
213
+ ),
214
+ });
215
+
216
+ const mp4 = await renderVideo(template, {}, {
217
+ config: {
218
+ colors: {
219
+ brand: '#444444',
220
+ },
221
+ },
222
+ });
223
+
224
+ assert(Buffer.isBuffer(mp4), 'MP4 should be a Buffer');
225
+ assert(mp4.length > 0, 'MP4 buffer should not be empty');
226
+ });
227
+
228
+ // Test 7: Multiple custom colors
229
+ await test('Multiple custom colors', async () => {
230
+ const config = {
231
+ colors: {
232
+ brand: '#8b5cf6',
233
+ 'brand-light': '#a78bfa',
234
+ 'brand-dark': '#7c3aed',
235
+ accent: '#f59e0b',
236
+ success: '#10b981',
237
+ },
238
+ };
239
+
240
+ const template = defineTemplate({
241
+ name: 'test-multi-colors',
242
+ size: { width: 600, height: 400 },
243
+ config,
244
+ render: ({ tw }) =>
245
+ h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-brand-light p-8') },
246
+ h('h1', { style: tw('text-4xl font-bold text-brand') }, 'Brand'),
247
+ h('p', { style: tw('text-2xl text-brand-dark') }, 'Brand Dark'),
248
+ h('p', { style: tw('text-xl text-accent') }, 'Accent'),
249
+ h('p', { style: tw('text-lg text-success') }, 'Success')
250
+ ),
251
+ });
252
+
253
+ const svg = await renderImage(template, {}, { format: 'svg' });
254
+ const svgString = svg.toString();
255
+
256
+ assert(svgContainsColor(svgString, '#8b5cf6'), 'SVG should contain brand color');
257
+ assert(svgContainsColor(svgString, '#a78bfa'), 'SVG should contain brand-light color');
258
+ assert(svgContainsColor(svgString, '#7c3aed'), 'SVG should contain brand-dark color');
259
+ assert(svgContainsColor(svgString, '#f59e0b'), 'SVG should contain accent color');
260
+ assert(svgContainsColor(svgString, '#10b981'), 'SVG should contain success color');
261
+ });
262
+
263
+ // Test 8: Fonts in config
264
+ await test('Fonts in config', async () => {
265
+ const config = {
266
+ colors: {
267
+ text: '#000000',
268
+ },
269
+ fonts: {
270
+ sans: ['Inter', 'system-ui', 'sans-serif'],
271
+ mono: ['JetBrains Mono', 'monospace'],
272
+ },
273
+ };
274
+
275
+ const template = defineTemplate({
276
+ name: 'test-fonts',
277
+ size: { width: 400, height: 400 },
278
+ config,
279
+ render: ({ tw }) =>
280
+ h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-white') },
281
+ h('h1', { style: tw('font-sans text-4xl text-text') }, 'Sans Font'),
282
+ h('p', { style: tw('font-mono text-xl text-text') }, 'Mono Font')
283
+ ),
284
+ });
285
+
286
+ const svg = await renderImage(template, {}, { format: 'svg' });
287
+ const svgString = svg.toString();
288
+
289
+ assert(svg.length > 0, 'SVG should be generated with fonts');
290
+ // Fonts are embedded in the SVG rendering, no need to check for specific strings
291
+ });
292
+
293
+ // Test 9: Empty config (should work with defaults)
294
+ await test('Empty config - should work with defaults', async () => {
295
+ const template = defineTemplate({
296
+ name: 'test-no-config',
297
+ size: { width: 400, height: 400 },
298
+ render: ({ tw }) =>
299
+ h('div', { style: tw('flex items-center justify-center w-full h-full bg-white') },
300
+ h('h1', { style: tw('text-4xl font-bold text-black') }, 'No Config')
301
+ ),
302
+ });
303
+
304
+ const svg = await renderImage(template, {}, { format: 'svg' });
305
+ assert(svg.length > 0, 'SVG should be generated without config');
306
+ });
307
+
308
+ // Test 10: Partial config merge
309
+ await test('Partial config merge', async () => {
310
+ const template = defineTemplate({
311
+ name: 'test-partial-merge',
312
+ size: { width: 400, height: 400 },
313
+ config: {
314
+ colors: {
315
+ primary: '#111111',
316
+ secondary: '#222222',
317
+ },
318
+ },
319
+ render: ({ tw }) =>
320
+ h('div', { style: tw('flex items-center justify-center w-full h-full bg-secondary') },
321
+ h('h1', { style: tw('text-4xl font-bold text-primary') }, 'Merge')
322
+ ),
323
+ });
324
+
325
+ const svg = await renderImage(
326
+ template,
327
+ {},
328
+ {
329
+ format: 'svg',
330
+ config: {
331
+ colors: {
332
+ primary: '#999999', // Override only primary
333
+ },
334
+ },
335
+ }
336
+ );
337
+
338
+ const svgString = svg.toString();
339
+ assert(svgContainsColor(svgString, '#999999'), 'SVG should contain overridden primary color');
340
+ assert(svgContainsColor(svgString, '#222222'), 'SVG should retain original secondary color');
341
+ });
342
+
343
+ // Test 11: Font files from URLs
344
+ await test('Font files from URLs', async () => {
345
+ const config = {
346
+ colors: {
347
+ text: '#000000',
348
+ },
349
+ fonts: {
350
+ sans: {
351
+ family: ['Inter', 'sans-serif'],
352
+ files: [
353
+ {
354
+ path: 'https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff',
355
+ weight: 400,
356
+ },
357
+ {
358
+ path: 'https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff',
359
+ weight: 700,
360
+ },
361
+ ],
362
+ },
363
+ },
364
+ };
365
+
366
+ const template = defineTemplate({
367
+ name: 'test-font-files',
368
+ size: { width: 600, height: 400 },
369
+ config,
370
+ render: ({ tw }) =>
371
+ h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-white p-8') },
372
+ h('h1', { style: tw('font-sans text-6xl font-bold text-text') }, 'Bold Text'),
373
+ h('p', { style: tw('font-sans text-3xl font-normal text-text') }, 'Normal Text')
374
+ ),
375
+ });
376
+
377
+ const svg = await renderImage(template, {}, { format: 'svg' });
378
+ assert(svg.length > 0, 'SVG should be generated with font files from URLs');
379
+
380
+ const png = await renderImage(template, {}, { format: 'png' });
381
+ assert(Buffer.isBuffer(png), 'PNG should be a Buffer');
382
+ assert(png.length > 0, 'PNG buffer should not be empty');
383
+ assert(png[0] === 0x89 && png[1] === 0x50 && png[2] === 0x4e && png[3] === 0x47, 'Should be valid PNG with custom fonts');
384
+ });
385
+
386
+ // Summary
387
+ console.log('\n' + '='.repeat(50));
388
+ console.log(`āœ… Tests passed: ${testsPassed}`);
389
+ console.log(`āŒ Tests failed: ${testsFailed}`);
390
+ console.log('='.repeat(50));
391
+
392
+ if (testsFailed > 0) {
393
+ process.exit(1);
394
+ } else {
395
+ console.log('\nšŸŽ‰ All tests passed!');
396
+ process.exit(0);
397
+ }
package/website/README.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  This is the documentation website for loopwind, built with [Astro](https://astro.build/) and styled with [Tailwind CSS 4](https://tailwindcss.com/) using shadcn/ui design tokens.
4
4
 
5
+ ## Features
6
+
7
+ - **Dynamic OG Images**: Each page generates its own Open Graph image on-the-fly using the loopwind SDK
8
+ - **Dark Theme**: Beautiful dark mode optimized for readability
9
+ - **Responsive Design**: Works perfectly on desktop, tablet, and mobile
10
+ - **AI-Friendly**: Copy raw markdown for LLM context
11
+
5
12
  ## Development
6
13
 
7
14
  Install dependencies:
@@ -33,29 +40,19 @@ Preview the build:
33
40
  npm run preview
34
41
  ```
35
42
 
36
- ## Generating Demo Videos
43
+ ## Dynamic OG Images
37
44
 
38
- The site includes live video examples. To generate the demo video:
45
+ Each documentation page automatically gets a custom OG image generated using the loopwind SDK. The images are:
39
46
 
40
- ```bash
41
- # In the main loopwind directory
42
- loopwind render video-intro '{"title":"loopwind","subtitle":"Generate videos with React + Tailwind"}' --out website/public/demo-intro.mp4
43
- ```
47
+ - Generated at build time (pre-rendered)
48
+ - Cached indefinitely
49
+ - Customized per page with title and description
44
50
 
45
- Or create a template file and render it:
46
-
47
- ```bash
48
- cd website
49
- loopwind init
50
- loopwind add video-intro # or create a custom template
51
- loopwind render video-intro '{"title":"loopwind","subtitle":"Generate videos with React + Tailwind"}' --out public/demo-intro.mp4
52
- ```
53
-
54
- The `CodeVideoDemo` component expects videos in `/public/demo-intro.mp4`.
51
+ The OG image API route is at `src/pages/api/og/[...slug].ts`.
55
52
 
56
53
  ## Deployment
57
54
 
58
- ### Netlify
55
+ ### Netlify (Current)
59
56
 
60
57
  This site is configured to deploy on Netlify. The `netlify.toml` file in the root directory handles the configuration.
61
58
 
@@ -70,7 +67,7 @@ Just connect your GitHub repo to Netlify and it will automatically deploy!
70
67
 
71
68
  You can also deploy to:
72
69
  - **Vercel**: Connect repo, set build settings
73
- - **Cloudflare Pages**: Same as above
70
+ - **Cloudflare Pages**: Install `@astrojs/cloudflare` adapter
74
71
  - **GitHub Pages**: May need additional configuration
75
72
 
76
73
  ## Adding Documentation
@@ -89,6 +86,7 @@ description: Page description
89
86
  ---
90
87
  ```
91
88
  3. Add to navigation in `src/layouts/DocsLayout.astro`
89
+ 4. Add metadata to `src/pages/api/og/[...slug].ts` for OG images
92
90
 
93
91
  ## Styling
94
92
 
@@ -108,12 +106,14 @@ website/
108
106
  ā”œā”€ā”€ src/
109
107
  │ ā”œā”€ā”€ components/
110
108
  │ │ ā”œā”€ā”€ Hero.astro # Home page hero
109
+ │ │ ā”œā”€ā”€ Logo.astro # loopwind logo
111
110
  │ │ └── CodeVideoDemo.astro # Code + video side-by-side
112
111
  │ ā”œā”€ā”€ layouts/
113
112
  │ │ ā”œā”€ā”€ BaseLayout.astro # Base HTML with meta tags
114
113
  │ │ └── DocsLayout.astro # Docs with sidebar
115
114
  │ ā”œā”€ā”€ pages/
116
115
  │ │ ā”œā”€ā”€ index.mdx # Getting Started
116
+ │ │ ā”œā”€ā”€ getting-started.mdx # Installation guide
117
117
  │ │ ā”œā”€ā”€ templates.mdx # Installing Templates
118
118
  │ │ ā”œā”€ā”€ images.mdx # Image Rendering
119
119
  │ │ ā”œā”€ā”€ video.mdx # Video Rendering
@@ -121,9 +121,14 @@ website/
121
121
  │ │ ā”œā”€ā”€ helpers.mdx # QR & Composition
122
122
  │ │ ā”œā”€ā”€ styling.mdx # Tailwind & shadcn/ui
123
123
  │ │ ā”œā”€ā”€ fonts.mdx # Font Handling
124
+ │ │ ā”œā”€ā”€ config.mdx # loopwind.json reference
124
125
  │ │ ā”œā”€ā”€ agents.mdx # AI Agents & Cursor
125
126
  │ │ ā”œā”€ā”€ sdk.mdx # Programmatic SDK
126
- │ │ └── llm.txt.ts # Combined docs endpoint
127
+ │ │ ā”œā”€ā”€ preview.mdx # Preview Mode
128
+ │ │ ā”œā”€ā”€ llm.txt.ts # Combined docs endpoint
129
+ │ │ └── api/
130
+ │ │ ā”œā”€ā”€ og/[...slug].ts # Dynamic OG image generation
131
+ │ │ └── raw-markdown/[...path].ts # Raw markdown API
127
132
  │ └── styles/
128
133
  │ └── global.css # Tailwind 4 + design tokens
129
134
  ā”œā”€ā”€ public/
@@ -136,3 +141,18 @@ website/
136
141
  ā”œā”€ā”€ package.json
137
142
  └── tsconfig.json
138
143
  ```
144
+
145
+ ## Technologies
146
+
147
+ - **[Astro](https://astro.build/)** - Static site generator
148
+ - **[Tailwind CSS 4](https://tailwindcss.com/)** - Utility-first CSS
149
+ - **[MDX](https://mdxjs.com/)** - Markdown with JSX
150
+ - **[loopwind SDK](https://loopwind.dev/sdk)** - Dynamic OG image generation
151
+ - **[shadcn/ui](https://ui.shadcn.com/)** - Design system tokens
152
+
153
+ ## Performance
154
+
155
+ - Fast page loads with Astro's static generation
156
+ - Pre-rendered OG images for instant social previews
157
+ - Optimized fonts and assets
158
+ - Minimal JavaScript bundle
@@ -1,10 +1,13 @@
1
1
  import { defineConfig } from 'astro/config';
2
2
  import mdx from '@astrojs/mdx';
3
3
  import tailwindcss from '@tailwindcss/vite';
4
+ import cloudflare from '@astrojs/cloudflare';
4
5
 
5
6
  // https://astro.build/config
6
7
  export default defineConfig({
7
8
  site: 'https://loopwind.dev',
9
+ output: 'hybrid',
10
+ adapter: cloudflare(),
8
11
  integrations: [mdx()],
9
12
  vite: {
10
13
  plugins: [tailwindcss()],
@@ -7,17 +7,22 @@
7
7
  "start": "astro dev",
8
8
  "build": "astro check && astro build",
9
9
  "preview": "astro preview",
10
+ "deploy": "astro build && wrangler deploy",
11
+ "cf:deploy": "wrangler deploy",
10
12
  "astro": "astro"
11
13
  },
12
14
  "dependencies": {
13
15
  "@astrojs/check": "^0.9.3",
16
+ "@astrojs/cloudflare": "^11.1.0",
14
17
  "@astrojs/mdx": "^3.1.7",
15
18
  "astro": "^4.15.11",
19
+ "loopwind": "latest",
16
20
  "typescript": "^5.6.2"
17
21
  },
18
22
  "devDependencies": {
19
23
  "@tailwindcss/vite": "^4.0.0",
20
- "tailwindcss": "^4.0.0"
24
+ "tailwindcss": "^4.0.0",
25
+ "wrangler": "^3.86.1"
21
26
  }
22
27
  }
23
28
 
@@ -1,3 +1,4 @@
1
1
  # Place demo videos here
2
2
  # Generate with: loopwind render video-intro '{"title":"loopwind","subtitle":"Generate videos with React + Tailwind"}' --out public/demo-intro.mp4
3
3
 
4
+