@inneropen/marvin-sdk 1.0.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.
- package/QUICKSTART.md +408 -0
- package/README.md +721 -0
- package/dist/index.cjs +859 -0
- package/dist/index.d.cts +430 -0
- package/dist/index.d.ts +430 -0
- package/dist/index.js +846 -0
- package/dist/types/index.cjs +18 -0
- package/dist/types/index.d.cts +120 -0
- package/dist/types/index.d.ts +120 -0
- package/dist/types/index.js +0 -0
- package/examples.md +961 -0
- package/package.json +73 -0
package/examples.md
ADDED
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
# Marvin Client Usage Examples
|
|
2
|
+
|
|
3
|
+
Real-world examples for using the Marvin publishing client in an Astro site.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Basic Setup](#basic-setup)
|
|
8
|
+
2. [Markdown Rendering](#markdown-rendering)
|
|
9
|
+
3. [Site Configuration](#site-configuration)
|
|
10
|
+
4. [Blog/News Pages](#blognews-pages)
|
|
11
|
+
5. [Project Portfolio](#project-portfolio)
|
|
12
|
+
6. [Dynamic Routes](#dynamic-routes)
|
|
13
|
+
7. [Collections](#collections)
|
|
14
|
+
8. [Resources](#resources)
|
|
15
|
+
9. [Assets & Images](#assets--images)
|
|
16
|
+
10. [Metadata & SEO](#metadata--seo)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Basic Setup
|
|
21
|
+
|
|
22
|
+
### Create the Marvin Client Instance
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// src/lib/marvin.ts
|
|
26
|
+
import { createMarvinClient } from './marvin-client';
|
|
27
|
+
|
|
28
|
+
// Create client from environment variables
|
|
29
|
+
export const marvin = createMarvinClient();
|
|
30
|
+
|
|
31
|
+
// Or pass config explicitly
|
|
32
|
+
export const marvinProd = createMarvinClient({
|
|
33
|
+
apiUrl: 'https://marvin.example.com',
|
|
34
|
+
siteClientToken: import.meta.env.MARVIN_SITE_CLIENT_TOKEN,
|
|
35
|
+
workspaceSlug: 'mash-burn',
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Environment Variables
|
|
40
|
+
|
|
41
|
+
```env
|
|
42
|
+
# .env
|
|
43
|
+
MARVIN_API_URL=https://marvin.mashburn.co
|
|
44
|
+
MARVIN_SITE_CLIENT_TOKEN=mbc_abc123xyz789
|
|
45
|
+
MARVIN_WORKSPACE_SLUG=mash-burn
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Markdown Rendering
|
|
51
|
+
|
|
52
|
+
**Critical**: Entry content is returned as **raw Markdown**, not HTML. You must parse it before rendering.
|
|
53
|
+
|
|
54
|
+
### Install Markdown Parser
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install marked
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Create a Helper Function
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// src/lib/markdown.ts
|
|
64
|
+
import { marked } from 'marked';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse Markdown to HTML
|
|
68
|
+
*/
|
|
69
|
+
export function renderMarkdown(markdown: string): string {
|
|
70
|
+
return marked.parse(markdown ?? '');
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Use in Astro Components
|
|
75
|
+
|
|
76
|
+
```astro
|
|
77
|
+
---
|
|
78
|
+
import { renderMarkdown } from '@/lib/markdown';
|
|
79
|
+
import { marvin } from '@/lib/marvin';
|
|
80
|
+
|
|
81
|
+
const entry = await marvin.getEntry('about');
|
|
82
|
+
const contentHtml = renderMarkdown(entry.contentMarkdown ?? '');
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
<article>
|
|
86
|
+
<h1>{entry.title}</h1>
|
|
87
|
+
<!-- Render parsed HTML -->
|
|
88
|
+
<div class="content" set:html={contentHtml} />
|
|
89
|
+
</article>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Common Mistake
|
|
93
|
+
|
|
94
|
+
**❌ WRONG** - This will render raw Markdown as text:
|
|
95
|
+
```astro
|
|
96
|
+
<div set:html={entry.contentMarkdown} />
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**✅ CORRECT** - Parse Markdown first:
|
|
100
|
+
```astro
|
|
101
|
+
---
|
|
102
|
+
import { marked } from 'marked';
|
|
103
|
+
const contentHtml = marked.parse(entry.contentMarkdown ?? '');
|
|
104
|
+
---
|
|
105
|
+
<div set:html={contentHtml} />
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Site Configuration
|
|
111
|
+
|
|
112
|
+
### Use Site Config in Layout
|
|
113
|
+
|
|
114
|
+
```astro
|
|
115
|
+
// src/layouts/BaseLayout.astro
|
|
116
|
+
---
|
|
117
|
+
import { marvin } from '@/lib/marvin';
|
|
118
|
+
|
|
119
|
+
const site = await marvin.getSite();
|
|
120
|
+
const { title = 'Mash & Burn Co.' } = Astro.props;
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
<!DOCTYPE html>
|
|
124
|
+
<html lang={site.locale || 'en'}>
|
|
125
|
+
<head>
|
|
126
|
+
<meta charset="UTF-8" />
|
|
127
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
128
|
+
<title>{title} | {site.title}</title>
|
|
129
|
+
<meta name="description" content={site.description} />
|
|
130
|
+
<link rel="icon" type="image/x-icon" href={site.favicon || '/favicon.ico'} />
|
|
131
|
+
<link rel="canonical" href={site.canonicalUrl} />
|
|
132
|
+
</head>
|
|
133
|
+
<body>
|
|
134
|
+
<header>
|
|
135
|
+
<a href="/">
|
|
136
|
+
{site.logo && <img src={site.logo} alt={site.title} />}
|
|
137
|
+
<h1>{site.title}</h1>
|
|
138
|
+
</a>
|
|
139
|
+
<p class="tagline">{site.tagline}</p>
|
|
140
|
+
</header>
|
|
141
|
+
|
|
142
|
+
<main>
|
|
143
|
+
<slot />
|
|
144
|
+
</main>
|
|
145
|
+
|
|
146
|
+
<footer>
|
|
147
|
+
<p>© {new Date().getFullYear()} {site.title}</p>
|
|
148
|
+
</footer>
|
|
149
|
+
</body>
|
|
150
|
+
</html>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Blog/News Pages
|
|
156
|
+
|
|
157
|
+
### Blog Index Page
|
|
158
|
+
|
|
159
|
+
```astro
|
|
160
|
+
// src/pages/blog/index.astro
|
|
161
|
+
---
|
|
162
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
163
|
+
import { marvin } from '@/lib/marvin';
|
|
164
|
+
|
|
165
|
+
// Fetch all blog posts
|
|
166
|
+
const posts = await marvin.getEntries({ entryType: 'blog' });
|
|
167
|
+
|
|
168
|
+
// Sort by published date, newest first
|
|
169
|
+
const sortedPosts = posts.sort((a, b) => {
|
|
170
|
+
const dateA = new Date(a.publishedAt || a.createdAt).getTime();
|
|
171
|
+
const dateB = new Date(b.publishedAt || b.createdAt).getTime();
|
|
172
|
+
return dateB - dateA;
|
|
173
|
+
});
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
<BaseLayout title="Blog">
|
|
177
|
+
<h1>Blog</h1>
|
|
178
|
+
|
|
179
|
+
<div class="posts">
|
|
180
|
+
{sortedPosts.map((post) => (
|
|
181
|
+
<article class="post-preview">
|
|
182
|
+
<h2>
|
|
183
|
+
<a href={`/blog/${post.slug}`}>{post.title}</a>
|
|
184
|
+
</h2>
|
|
185
|
+
|
|
186
|
+
<time datetime={post.publishedAt}>
|
|
187
|
+
{new Date(post.publishedAt).toLocaleDateString('en-US', {
|
|
188
|
+
year: 'numeric',
|
|
189
|
+
month: 'long',
|
|
190
|
+
day: 'numeric'
|
|
191
|
+
})}
|
|
192
|
+
</time>
|
|
193
|
+
|
|
194
|
+
{post.summary && <p>{post.summary}</p>}
|
|
195
|
+
|
|
196
|
+
<a href={`/blog/${post.slug}`} class="read-more">
|
|
197
|
+
Read more →
|
|
198
|
+
</a>
|
|
199
|
+
</article>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
</BaseLayout>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Individual Blog Post
|
|
206
|
+
|
|
207
|
+
```astro
|
|
208
|
+
// src/pages/blog/[slug].astro
|
|
209
|
+
---
|
|
210
|
+
import { marked } from 'marked';
|
|
211
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
212
|
+
import { marvin } from '@/lib/marvin';
|
|
213
|
+
|
|
214
|
+
export async function getStaticPaths() {
|
|
215
|
+
const posts = await marvin.getEntries({ entryType: 'blog' });
|
|
216
|
+
|
|
217
|
+
return posts.map((post) => ({
|
|
218
|
+
params: { slug: post.slug },
|
|
219
|
+
props: { post },
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const { post } = Astro.props;
|
|
224
|
+
|
|
225
|
+
// Parse Markdown to HTML
|
|
226
|
+
const contentHtml = marked.parse(post.contentMarkdown ?? '');
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
<BaseLayout title={post.title}>
|
|
230
|
+
<article class="blog-post">
|
|
231
|
+
<header>
|
|
232
|
+
<h1>{post.title}</h1>
|
|
233
|
+
|
|
234
|
+
<time datetime={post.publishedAt}>
|
|
235
|
+
{new Date(post.publishedAt).toLocaleDateString('en-US', {
|
|
236
|
+
year: 'numeric',
|
|
237
|
+
month: 'long',
|
|
238
|
+
day: 'numeric'
|
|
239
|
+
})}
|
|
240
|
+
</time>
|
|
241
|
+
</header>
|
|
242
|
+
|
|
243
|
+
{post.assets && post.assets.length > 0 && (
|
|
244
|
+
<img
|
|
245
|
+
src={post.assets[0].url}
|
|
246
|
+
alt={post.assets[0].altText || post.title}
|
|
247
|
+
class="featured-image"
|
|
248
|
+
/>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
<div class="content" set:html={contentHtml} />
|
|
252
|
+
|
|
253
|
+
{post.metadata?.tags && (
|
|
254
|
+
<div class="tags">
|
|
255
|
+
{post.metadata.tags.map((tag: string) => (
|
|
256
|
+
<span class="tag">{tag}</span>
|
|
257
|
+
))}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
</article>
|
|
261
|
+
</BaseLayout>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Project Portfolio
|
|
267
|
+
|
|
268
|
+
### Projects Gallery
|
|
269
|
+
|
|
270
|
+
```astro
|
|
271
|
+
// src/pages/projects/index.astro
|
|
272
|
+
---
|
|
273
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
274
|
+
import { marvin } from '@/lib/marvin';
|
|
275
|
+
|
|
276
|
+
// Get all projects
|
|
277
|
+
const projects = await marvin.getEntries({ entryType: 'project' });
|
|
278
|
+
|
|
279
|
+
// Or get projects from a collection
|
|
280
|
+
const featuredProjects = await marvin.getCollectionEntries('featured-projects');
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
<BaseLayout title="Projects">
|
|
284
|
+
<h1>Our Projects</h1>
|
|
285
|
+
|
|
286
|
+
<div class="projects-grid">
|
|
287
|
+
{projects.map((project) => (
|
|
288
|
+
<a href={`/projects/${project.slug}`} class="project-card">
|
|
289
|
+
{project.assets && project.assets[0] && (
|
|
290
|
+
<img
|
|
291
|
+
src={project.assets[0].url}
|
|
292
|
+
alt={project.assets[0].altText || project.title}
|
|
293
|
+
loading="lazy"
|
|
294
|
+
/>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
<h2>{project.title}</h2>
|
|
298
|
+
<p>{project.summary}</p>
|
|
299
|
+
|
|
300
|
+
{project.metadata?.year && (
|
|
301
|
+
<span class="year">{project.metadata.year}</span>
|
|
302
|
+
)}
|
|
303
|
+
</a>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
</BaseLayout>
|
|
307
|
+
|
|
308
|
+
<style>
|
|
309
|
+
.projects-grid {
|
|
310
|
+
display: grid;
|
|
311
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
312
|
+
gap: 2rem;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.project-card {
|
|
316
|
+
text-decoration: none;
|
|
317
|
+
border: 1px solid #ddd;
|
|
318
|
+
border-radius: 8px;
|
|
319
|
+
overflow: hidden;
|
|
320
|
+
transition: transform 0.2s;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.project-card:hover {
|
|
324
|
+
transform: translateY(-4px);
|
|
325
|
+
}
|
|
326
|
+
</style>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Project Detail Page
|
|
330
|
+
|
|
331
|
+
```astro
|
|
332
|
+
// src/pages/projects/[slug].astro
|
|
333
|
+
---
|
|
334
|
+
import { marked } from 'marked';
|
|
335
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
336
|
+
import { marvin } from '@/lib/marvin';
|
|
337
|
+
|
|
338
|
+
export async function getStaticPaths() {
|
|
339
|
+
const projects = await marvin.getEntries({ entryType: 'project' });
|
|
340
|
+
|
|
341
|
+
return projects.map((project) => ({
|
|
342
|
+
params: { slug: project.slug },
|
|
343
|
+
props: { project },
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const { project } = Astro.props;
|
|
348
|
+
const metadata = project.metadata || {};
|
|
349
|
+
|
|
350
|
+
// Parse Markdown to HTML
|
|
351
|
+
const contentHtml = marked.parse(project.contentMarkdown ?? '');
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
<BaseLayout title={project.title}>
|
|
355
|
+
<article class="project">
|
|
356
|
+
<header>
|
|
357
|
+
<h1>{project.title}</h1>
|
|
358
|
+
|
|
359
|
+
<div class="meta">
|
|
360
|
+
{metadata.client && <span>Client: {metadata.client}</span>}
|
|
361
|
+
{metadata.year && <span>Year: {metadata.year}</span>}
|
|
362
|
+
{metadata.category && <span>Category: {metadata.category}</span>}
|
|
363
|
+
</div>
|
|
364
|
+
</header>
|
|
365
|
+
|
|
366
|
+
{project.assets && project.assets.length > 0 && (
|
|
367
|
+
<div class="gallery">
|
|
368
|
+
{project.assets.map((asset) => (
|
|
369
|
+
<img
|
|
370
|
+
src={asset.url}
|
|
371
|
+
alt={asset.altText || project.title}
|
|
372
|
+
width={asset.width}
|
|
373
|
+
height={asset.height}
|
|
374
|
+
loading="lazy"
|
|
375
|
+
/>
|
|
376
|
+
))}
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
<div class="content" set:html={contentHtml} />
|
|
381
|
+
|
|
382
|
+
{metadata.technologies && (
|
|
383
|
+
<div class="technologies">
|
|
384
|
+
<h3>Technologies Used</h3>
|
|
385
|
+
<ul>
|
|
386
|
+
{metadata.technologies.map((tech: string) => (
|
|
387
|
+
<li>{tech}</li>
|
|
388
|
+
))}
|
|
389
|
+
</ul>
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
</article>
|
|
393
|
+
</BaseLayout>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Dynamic Routes
|
|
399
|
+
|
|
400
|
+
### Catch-All Route for Pages
|
|
401
|
+
|
|
402
|
+
```astro
|
|
403
|
+
// src/pages/[...slug].astro
|
|
404
|
+
---
|
|
405
|
+
import { marked } from 'marked';
|
|
406
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
407
|
+
import { marvin } from '@/lib/marvin';
|
|
408
|
+
|
|
409
|
+
export async function getStaticPaths() {
|
|
410
|
+
const pages = await marvin.getEntries({ entryType: 'page' });
|
|
411
|
+
|
|
412
|
+
return pages.map((page) => ({
|
|
413
|
+
params: { slug: page.slug },
|
|
414
|
+
props: { page },
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const { page } = Astro.props;
|
|
419
|
+
|
|
420
|
+
// Parse Markdown to HTML
|
|
421
|
+
const contentHtml = marked.parse(page.contentMarkdown ?? '');
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
<BaseLayout title={page.title}>
|
|
425
|
+
<article class="page">
|
|
426
|
+
<h1>{page.title}</h1>
|
|
427
|
+
<div class="content" set:html={contentHtml} />
|
|
428
|
+
</article>
|
|
429
|
+
</BaseLayout>
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Collections
|
|
435
|
+
|
|
436
|
+
### Featured Items Showcase
|
|
437
|
+
|
|
438
|
+
```astro
|
|
439
|
+
// src/pages/index.astro
|
|
440
|
+
---
|
|
441
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
442
|
+
import { marvin } from '@/lib/marvin';
|
|
443
|
+
|
|
444
|
+
const featuredProjects = await marvin.getCollectionEntries('featured');
|
|
445
|
+
const latestPosts = await marvin.getEntries({
|
|
446
|
+
entryType: 'blog',
|
|
447
|
+
limit: 3
|
|
448
|
+
});
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
<BaseLayout>
|
|
452
|
+
<section class="hero">
|
|
453
|
+
<h1>Welcome to Mash & Burn Co.</h1>
|
|
454
|
+
<p>Handcrafted with Purpose</p>
|
|
455
|
+
</section>
|
|
456
|
+
|
|
457
|
+
<section class="featured">
|
|
458
|
+
<h2>Featured Projects</h2>
|
|
459
|
+
<div class="grid">
|
|
460
|
+
{featuredProjects.map((project) => (
|
|
461
|
+
<a href={`/projects/${project.slug}`}>
|
|
462
|
+
{project.assets?.[0] && (
|
|
463
|
+
<img src={project.assets[0].url} alt={project.title} />
|
|
464
|
+
)}
|
|
465
|
+
<h3>{project.title}</h3>
|
|
466
|
+
</a>
|
|
467
|
+
))}
|
|
468
|
+
</div>
|
|
469
|
+
</section>
|
|
470
|
+
|
|
471
|
+
<section class="latest-posts">
|
|
472
|
+
<h2>Latest from the Blog</h2>
|
|
473
|
+
{latestPosts.map((post) => (
|
|
474
|
+
<article>
|
|
475
|
+
<h3><a href={`/blog/${post.slug}`}>{post.title}</a></h3>
|
|
476
|
+
<p>{post.summary}</p>
|
|
477
|
+
</article>
|
|
478
|
+
))}
|
|
479
|
+
</section>
|
|
480
|
+
</BaseLayout>
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Collection Landing Page
|
|
484
|
+
|
|
485
|
+
```astro
|
|
486
|
+
// src/pages/collections/[slug].astro
|
|
487
|
+
---
|
|
488
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
489
|
+
import { marvin } from '@/lib/marvin';
|
|
490
|
+
|
|
491
|
+
export async function getStaticPaths() {
|
|
492
|
+
const collections = await marvin.getCollections();
|
|
493
|
+
|
|
494
|
+
return collections.map((collection) => ({
|
|
495
|
+
params: { slug: collection.slug },
|
|
496
|
+
props: { collection },
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const { collection } = Astro.props;
|
|
501
|
+
const entries = await marvin.getCollectionEntries(collection.slug);
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
<BaseLayout title={collection.name}>
|
|
505
|
+
<header>
|
|
506
|
+
{collection.icon && <span class="icon">{collection.icon}</span>}
|
|
507
|
+
<h1>{collection.name}</h1>
|
|
508
|
+
{collection.description && <p>{collection.description}</p>}
|
|
509
|
+
</header>
|
|
510
|
+
|
|
511
|
+
<div class="entries">
|
|
512
|
+
{entries.map((entry) => (
|
|
513
|
+
<article>
|
|
514
|
+
<h2><a href={`/${entry.entryType?.slug}/${entry.slug}`}>{entry.title}</a></h2>
|
|
515
|
+
<p>{entry.summary}</p>
|
|
516
|
+
</article>
|
|
517
|
+
))}
|
|
518
|
+
</div>
|
|
519
|
+
</BaseLayout>
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Resources
|
|
525
|
+
|
|
526
|
+
Resources are reusable structured objects that entries can reference (fabrics, tools, suppliers, etc.).
|
|
527
|
+
|
|
528
|
+
### List Resources by Type
|
|
529
|
+
|
|
530
|
+
```astro
|
|
531
|
+
// src/pages/materials/fabrics.astro
|
|
532
|
+
---
|
|
533
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
534
|
+
import { marvin } from '@/lib/marvin';
|
|
535
|
+
|
|
536
|
+
const fabrics = await marvin.getResources({ resourceType: 'fabric' });
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
<BaseLayout title="Fabrics">
|
|
540
|
+
<h1>Our Fabrics</h1>
|
|
541
|
+
|
|
542
|
+
<div class="resources-grid">
|
|
543
|
+
{fabrics.map((fabric) => (
|
|
544
|
+
<article class="resource-card">
|
|
545
|
+
<h2>
|
|
546
|
+
<a href={`/materials/fabrics/${fabric.slug}`}>{fabric.name}</a>
|
|
547
|
+
</h2>
|
|
548
|
+
|
|
549
|
+
{fabric.description && <p>{fabric.description}</p>}
|
|
550
|
+
|
|
551
|
+
{fabric.metadata?.weight && (
|
|
552
|
+
<span class="meta">Weight: {fabric.metadata.weight}</span>
|
|
553
|
+
)}
|
|
554
|
+
|
|
555
|
+
{fabric.metadata?.composition && (
|
|
556
|
+
<span class="meta">Composition: {fabric.metadata.composition}</span>
|
|
557
|
+
)}
|
|
558
|
+
|
|
559
|
+
{fabric.url && (
|
|
560
|
+
<a href={fabric.url} target="_blank" rel="noopener">
|
|
561
|
+
View supplier →
|
|
562
|
+
</a>
|
|
563
|
+
)}
|
|
564
|
+
</article>
|
|
565
|
+
))}
|
|
566
|
+
</div>
|
|
567
|
+
</BaseLayout>
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Resource Detail Page with Related Entries
|
|
571
|
+
|
|
572
|
+
```astro
|
|
573
|
+
// src/pages/materials/fabrics/[slug].astro
|
|
574
|
+
---
|
|
575
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
576
|
+
import { marvin } from '@/lib/marvin';
|
|
577
|
+
|
|
578
|
+
export async function getStaticPaths() {
|
|
579
|
+
const fabrics = await marvin.getResources({ resourceType: 'fabric' });
|
|
580
|
+
|
|
581
|
+
return fabrics.map((fabric) => ({
|
|
582
|
+
params: { slug: fabric.slug },
|
|
583
|
+
props: { fabric },
|
|
584
|
+
}));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const { fabric } = Astro.props;
|
|
588
|
+
|
|
589
|
+
// Get entries that use this fabric
|
|
590
|
+
const relatedEntries = await marvin.getResourceEntries(fabric.slug);
|
|
591
|
+
const metadata = fabric.metadata || {};
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
<BaseLayout title={fabric.name}>
|
|
595
|
+
<article class="resource-detail">
|
|
596
|
+
<header>
|
|
597
|
+
<h1>{fabric.name}</h1>
|
|
598
|
+
{fabric.resourceType && (
|
|
599
|
+
<span class="type">{fabric.resourceType}</span>
|
|
600
|
+
)}
|
|
601
|
+
</header>
|
|
602
|
+
|
|
603
|
+
{fabric.description && (
|
|
604
|
+
<p class="description">{fabric.description}</p>
|
|
605
|
+
)}
|
|
606
|
+
|
|
607
|
+
<dl class="metadata">
|
|
608
|
+
{fabric.externalId && (
|
|
609
|
+
<>
|
|
610
|
+
<dt>ID</dt>
|
|
611
|
+
<dd>{fabric.externalId}</dd>
|
|
612
|
+
</>
|
|
613
|
+
)}
|
|
614
|
+
|
|
615
|
+
{metadata.weight && (
|
|
616
|
+
<>
|
|
617
|
+
<dt>Weight</dt>
|
|
618
|
+
<dd>{metadata.weight}</dd>
|
|
619
|
+
</>
|
|
620
|
+
)}
|
|
621
|
+
|
|
622
|
+
{metadata.composition && (
|
|
623
|
+
<>
|
|
624
|
+
<dt>Composition</dt>
|
|
625
|
+
<dd>{metadata.composition}</dd>
|
|
626
|
+
</>
|
|
627
|
+
)}
|
|
628
|
+
|
|
629
|
+
{metadata.color && (
|
|
630
|
+
<>
|
|
631
|
+
<dt>Color</dt>
|
|
632
|
+
<dd>{metadata.color}</dd>
|
|
633
|
+
</>
|
|
634
|
+
)}
|
|
635
|
+
|
|
636
|
+
{fabric.url && (
|
|
637
|
+
<>
|
|
638
|
+
<dt>Supplier</dt>
|
|
639
|
+
<dd><a href={fabric.url} target="_blank" rel="noopener">{fabric.url}</a></dd>
|
|
640
|
+
</>
|
|
641
|
+
)}
|
|
642
|
+
</dl>
|
|
643
|
+
|
|
644
|
+
{relatedEntries.length > 0 && (
|
|
645
|
+
<section class="related-entries">
|
|
646
|
+
<h2>Used In</h2>
|
|
647
|
+
<div class="entries">
|
|
648
|
+
{relatedEntries.map((entry) => (
|
|
649
|
+
<article>
|
|
650
|
+
<h3>
|
|
651
|
+
<a href={`/${entry.entryType?.slug}/${entry.slug}`}>
|
|
652
|
+
{entry.title}
|
|
653
|
+
</a>
|
|
654
|
+
</h3>
|
|
655
|
+
{entry.summary && <p>{entry.summary}</p>}
|
|
656
|
+
</article>
|
|
657
|
+
))}
|
|
658
|
+
</div>
|
|
659
|
+
</section>
|
|
660
|
+
)}
|
|
661
|
+
</article>
|
|
662
|
+
</BaseLayout>
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Display Resources in Entry Pages
|
|
666
|
+
|
|
667
|
+
```astro
|
|
668
|
+
// src/pages/projects/[slug].astro
|
|
669
|
+
---
|
|
670
|
+
import { marked } from 'marked';
|
|
671
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
672
|
+
import { marvin } from '@/lib/marvin';
|
|
673
|
+
|
|
674
|
+
export async function getStaticPaths() {
|
|
675
|
+
const projects = await marvin.getEntries({ entryType: 'project' });
|
|
676
|
+
|
|
677
|
+
return projects.map((project) => ({
|
|
678
|
+
params: { slug: project.slug },
|
|
679
|
+
props: { project },
|
|
680
|
+
}));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const { project } = Astro.props;
|
|
684
|
+
const contentHtml = marked.parse(project.contentMarkdown ?? '');
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
<BaseLayout title={project.title}>
|
|
688
|
+
<article class="project">
|
|
689
|
+
<h1>{project.title}</h1>
|
|
690
|
+
<div class="content" set:html={contentHtml} />
|
|
691
|
+
|
|
692
|
+
{/* Display linked resources */}
|
|
693
|
+
{project.resources && project.resources.length > 0 && (
|
|
694
|
+
<section class="materials">
|
|
695
|
+
<h2>Materials Used</h2>
|
|
696
|
+
<div class="resources">
|
|
697
|
+
{project.resources.map((resource) => (
|
|
698
|
+
<a href={`/materials/${resource.resourceType}/${resource.slug}`} class="resource-tag">
|
|
699
|
+
{resource.name}
|
|
700
|
+
{resource.metadata?.weight && (
|
|
701
|
+
<span class="meta">({resource.metadata.weight})</span>
|
|
702
|
+
)}
|
|
703
|
+
</a>
|
|
704
|
+
))}
|
|
705
|
+
</div>
|
|
706
|
+
</section>
|
|
707
|
+
)}
|
|
708
|
+
</article>
|
|
709
|
+
</BaseLayout>
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### Resource Categories Index
|
|
713
|
+
|
|
714
|
+
```astro
|
|
715
|
+
// src/pages/materials/index.astro
|
|
716
|
+
---
|
|
717
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
718
|
+
import { marvin } from '@/lib/marvin';
|
|
719
|
+
|
|
720
|
+
const resources = await marvin.getResources();
|
|
721
|
+
|
|
722
|
+
// Group resources by type
|
|
723
|
+
const byType = resources.reduce((acc, resource) => {
|
|
724
|
+
const type = resource.resourceType || 'other';
|
|
725
|
+
if (!acc[type]) acc[type] = [];
|
|
726
|
+
acc[type].push(resource);
|
|
727
|
+
return acc;
|
|
728
|
+
}, {} as Record<string, typeof resources>);
|
|
729
|
+
|
|
730
|
+
const types = Object.keys(byType).sort();
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
<BaseLayout title="Materials & Resources">
|
|
734
|
+
<h1>Materials & Resources</h1>
|
|
735
|
+
|
|
736
|
+
{types.map((type) => (
|
|
737
|
+
<section class="resource-category">
|
|
738
|
+
<h2>{type.charAt(0).toUpperCase() + type.slice(1)}</h2>
|
|
739
|
+
|
|
740
|
+
<ul class="resource-list">
|
|
741
|
+
{byType[type].map((resource) => (
|
|
742
|
+
<li>
|
|
743
|
+
<a href={`/materials/${type}/${resource.slug}`}>
|
|
744
|
+
{resource.name}
|
|
745
|
+
</a>
|
|
746
|
+
{resource.description && (
|
|
747
|
+
<span class="description">{resource.description}</span>
|
|
748
|
+
)}
|
|
749
|
+
</li>
|
|
750
|
+
))}
|
|
751
|
+
</ul>
|
|
752
|
+
</section>
|
|
753
|
+
))}
|
|
754
|
+
</BaseLayout>
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
## Assets & Images
|
|
760
|
+
|
|
761
|
+
### Image Gallery
|
|
762
|
+
|
|
763
|
+
```astro
|
|
764
|
+
// src/pages/gallery.astro
|
|
765
|
+
---
|
|
766
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
767
|
+
import { marvin } from '@/lib/marvin';
|
|
768
|
+
|
|
769
|
+
const images = await marvin.getAssets({ type: 'image' });
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
<BaseLayout title="Gallery">
|
|
773
|
+
<h1>Gallery</h1>
|
|
774
|
+
|
|
775
|
+
<div class="gallery">
|
|
776
|
+
{images.map((image) => (
|
|
777
|
+
<figure>
|
|
778
|
+
<img
|
|
779
|
+
src={image.url}
|
|
780
|
+
alt={image.altText || image.name}
|
|
781
|
+
width={image.width}
|
|
782
|
+
height={image.height}
|
|
783
|
+
loading="lazy"
|
|
784
|
+
/>
|
|
785
|
+
{image.description && (
|
|
786
|
+
<figcaption>{image.description}</figcaption>
|
|
787
|
+
)}
|
|
788
|
+
</figure>
|
|
789
|
+
))}
|
|
790
|
+
</div>
|
|
791
|
+
</BaseLayout>
|
|
792
|
+
|
|
793
|
+
<style>
|
|
794
|
+
.gallery {
|
|
795
|
+
display: grid;
|
|
796
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
797
|
+
gap: 1rem;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
figure {
|
|
801
|
+
margin: 0;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
img {
|
|
805
|
+
width: 100%;
|
|
806
|
+
height: auto;
|
|
807
|
+
object-fit: cover;
|
|
808
|
+
aspect-ratio: 1;
|
|
809
|
+
}
|
|
810
|
+
</style>
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
## Metadata & SEO
|
|
816
|
+
|
|
817
|
+
### SEO Component
|
|
818
|
+
|
|
819
|
+
```astro
|
|
820
|
+
// src/components/SEO.astro
|
|
821
|
+
---
|
|
822
|
+
import type { MarvinEntry } from '@/lib/marvin';
|
|
823
|
+
|
|
824
|
+
interface Props {
|
|
825
|
+
entry: MarvinEntry;
|
|
826
|
+
siteName: string;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const { entry, siteName } = Astro.props;
|
|
830
|
+
const canonicalUrl = `${import.meta.env.SITE}/${entry.slug}`;
|
|
831
|
+
const ogImage = entry.assets?.[0]?.url;
|
|
832
|
+
---
|
|
833
|
+
|
|
834
|
+
<head>
|
|
835
|
+
<title>{entry.title} | {siteName}</title>
|
|
836
|
+
<meta name="description" content={entry.summary || entry.description} />
|
|
837
|
+
<link rel="canonical" href={canonicalUrl} />
|
|
838
|
+
|
|
839
|
+
<!-- Open Graph -->
|
|
840
|
+
<meta property="og:type" content="article" />
|
|
841
|
+
<meta property="og:title" content={entry.title} />
|
|
842
|
+
<meta property="og:description" content={entry.summary || entry.description} />
|
|
843
|
+
<meta property="og:url" content={canonicalUrl} />
|
|
844
|
+
{ogImage && <meta property="og:image" content={ogImage} />}
|
|
845
|
+
|
|
846
|
+
<!-- Twitter Card -->
|
|
847
|
+
<meta name="twitter:card" content={ogImage ? 'summary_large_image' : 'summary'} />
|
|
848
|
+
<meta name="twitter:title" content={entry.title} />
|
|
849
|
+
<meta name="twitter:description" content={entry.summary || entry.description} />
|
|
850
|
+
{ogImage && <meta name="twitter:image" content={ogImage} />}
|
|
851
|
+
|
|
852
|
+
<!-- Article Meta -->
|
|
853
|
+
{entry.publishedAt && (
|
|
854
|
+
<meta property="article:published_time" content={entry.publishedAt} />
|
|
855
|
+
)}
|
|
856
|
+
{entry.metadata?.tags && (
|
|
857
|
+
entry.metadata.tags.map((tag: string) => (
|
|
858
|
+
<meta property="article:tag" content={tag} />
|
|
859
|
+
))
|
|
860
|
+
)}
|
|
861
|
+
</head>
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
### Usage in Page
|
|
865
|
+
|
|
866
|
+
```astro
|
|
867
|
+
// src/pages/blog/[slug].astro
|
|
868
|
+
---
|
|
869
|
+
import BaseLayout from '@/layouts/BaseLayout.astro';
|
|
870
|
+
import SEO from '@/components/SEO.astro';
|
|
871
|
+
import { marvin } from '@/lib/marvin';
|
|
872
|
+
|
|
873
|
+
export async function getStaticPaths() {
|
|
874
|
+
const posts = await marvin.getEntries({ entryType: 'blog' });
|
|
875
|
+
return posts.map((post) => ({
|
|
876
|
+
params: { slug: post.slug },
|
|
877
|
+
props: { post },
|
|
878
|
+
}));
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const { post } = Astro.props;
|
|
882
|
+
const site = await marvin.getSite();
|
|
883
|
+
---
|
|
884
|
+
|
|
885
|
+
<html>
|
|
886
|
+
<SEO entry={post} siteName={site.title} />
|
|
887
|
+
<body>
|
|
888
|
+
<!-- page content -->
|
|
889
|
+
</body>
|
|
890
|
+
</html>
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
---
|
|
894
|
+
|
|
895
|
+
## Advanced Examples
|
|
896
|
+
|
|
897
|
+
### Pagination
|
|
898
|
+
|
|
899
|
+
```astro
|
|
900
|
+
// src/pages/blog/[...page].astro
|
|
901
|
+
---
|
|
902
|
+
import type { GetStaticPaths } from 'astro';
|
|
903
|
+
import { marvin } from '@/lib/marvin';
|
|
904
|
+
|
|
905
|
+
export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
|
|
906
|
+
const posts = await marvin.getEntries({ entryType: 'blog' });
|
|
907
|
+
|
|
908
|
+
return paginate(posts, { pageSize: 10 });
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const { page } = Astro.props;
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
<div class="posts">
|
|
915
|
+
{page.data.map((post) => (
|
|
916
|
+
<article>
|
|
917
|
+
<h2><a href={`/blog/${post.slug}`}>{post.title}</a></h2>
|
|
918
|
+
</article>
|
|
919
|
+
))}
|
|
920
|
+
</div>
|
|
921
|
+
|
|
922
|
+
<nav class="pagination">
|
|
923
|
+
{page.url.prev && <a href={page.url.prev}>← Previous</a>}
|
|
924
|
+
<span>Page {page.currentPage} of {page.lastPage}</span>
|
|
925
|
+
{page.url.next && <a href={page.url.next}>Next →</a>}
|
|
926
|
+
</nav>
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
### Sitemap Generation
|
|
930
|
+
|
|
931
|
+
```ts
|
|
932
|
+
// src/pages/sitemap.xml.ts
|
|
933
|
+
import type { APIRoute } from 'astro';
|
|
934
|
+
import { marvin } from '@/lib/marvin';
|
|
935
|
+
|
|
936
|
+
export const GET: APIRoute = async () => {
|
|
937
|
+
const [pages, posts, projects] = await Promise.all([
|
|
938
|
+
marvin.getEntries({ entryType: 'page' }),
|
|
939
|
+
marvin.getEntries({ entryType: 'blog' }),
|
|
940
|
+
marvin.getEntries({ entryType: 'project' }),
|
|
941
|
+
]);
|
|
942
|
+
|
|
943
|
+
const entries = [...pages, ...posts, ...projects];
|
|
944
|
+
const baseUrl = import.meta.env.SITE;
|
|
945
|
+
|
|
946
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
947
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
948
|
+
${entries.map((entry) => `
|
|
949
|
+
<url>
|
|
950
|
+
<loc>${baseUrl}/${entry.slug}</loc>
|
|
951
|
+
<lastmod>${entry.updatedAt}</lastmod>
|
|
952
|
+
</url>`).join('')}
|
|
953
|
+
</urlset>`;
|
|
954
|
+
|
|
955
|
+
return new Response(xml, {
|
|
956
|
+
headers: {
|
|
957
|
+
'Content-Type': 'application/xml',
|
|
958
|
+
},
|
|
959
|
+
});
|
|
960
|
+
};
|
|
961
|
+
```
|