minutework 0.1.56 → 0.1.57

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.
@@ -73,6 +73,25 @@ When the task involves event-driven behavior or reacting to external/reference
73
73
  datasets, read `skills/event-bus/SKILL.md` and
74
74
  `skills/cross-server-subscriptions/SKILL.md`.
75
75
 
76
+ ## Authored Workspace Overlays
77
+
78
+ Everything under `skills/` plus `CLAUDE.md` and `AGENTS.md` is **managed**:
79
+ `minutework workspace sync-assets` regenerates it from the MinuteWork baseline.
80
+ This workspace may also carry **authored overlays** that capture intent specific
81
+ to this build and are **never** overwritten by sync:
82
+
83
+ - `CLAUDE.workspace.md` / `AGENTS.workspace.md` — authored guidance. Read the one
84
+ your engine uses (`CLAUDE.workspace.md` in Claude Code; `AGENTS.workspace.md`
85
+ in Codex/Cursor/other agents). They typically mirror each other.
86
+ - `skills.workspace/<name>/SKILL.md` — authored skills. Browse this directory the
87
+ same way you browse `skills/`, and read a `SKILL.md` when its topic matches.
88
+
89
+ When an authored overlay and the managed baseline disagree for this workspace,
90
+ the authored overlay wins — it was written for this specific product. Treat the
91
+ overlays as your own to edit; do not move authored content into the managed
92
+ `CLAUDE.md`, `AGENTS.md`, or `skills/` files, because the next
93
+ `minutework workspace sync-assets` would discard it.
94
+
76
95
  ## Capability Gaps
77
96
 
78
97
  When a request exposes missing shared MinuteWork substrate, read
@@ -54,6 +54,8 @@ Generated-workspace-first guidance should live here, especially:
54
54
  - `vuilder-workspace-architecture/SKILL.md`
55
55
  - `vuilder-public-site-authoring/SKILL.md`
56
56
  - `vuilder-mode-b-hosted-and-spawn/SKILL.md`
57
+ - `visualize-context/SKILL.md`
58
+ - `landing-page-assets/SKILL.md`
57
59
  - `workspace-guidance-refresh/SKILL.md`
58
60
  - `shell-architecture/SKILL.md`
59
61
  - `runtime-capability-inventory/SKILL.md`
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: landing-page-assets
3
+ description: "Generate website-ready hero images, backgrounds, section visuals, textures, and OG assets, then wire selected images into public-site surfaces."
4
+ ---
5
+
6
+ # Landing Page Assets
7
+
8
+ Use this skill when the user asks for website-bound imagery, for example:
9
+
10
+ - "generate hero image"
11
+ - "background image"
12
+ - "section below it"
13
+ - "website asset"
14
+ - "landing page visual"
15
+ - "use this in the site"
16
+ - "OG image"
17
+ - "texture"
18
+
19
+ Default v1 output: generate an actual image, store it, add alt text, and wire
20
+ the selected asset into the target site/page/manifest when a target surface is
21
+ available.
22
+
23
+ ## Asset Roles
24
+
25
+ Choose one role before prompting:
26
+
27
+ - `hero_background`: wide atmospheric or metaphor image behind/around HTML text.
28
+ - `hero_visual`: primary product/domain visual next to or within the hero.
29
+ - `section_illustration`: visual supporting a feature/content section.
30
+ - `background_texture`: text-free repeatable or full-bleed texture/motif.
31
+ - `cta_visual`: closing section image that reinforces action.
32
+ - `og_image`: social preview image; text is allowed only when explicitly needed.
33
+
34
+ Strong rule: prefer HTML text over text baked into images. Only generate a
35
+ poster/graphic with text when the user explicitly asks for a graphic image or
36
+ OG card.
37
+
38
+ ## Workflow
39
+
40
+ 1. Infer the asset role and target section.
41
+ - Use the latest user request, current page JSON, section id, and prior
42
+ visual direction.
43
+ - If a target page/manifest is available, note where the asset should land.
44
+ - If the user only wants exploration, generate a preview artifact and do not
45
+ mutate the site.
46
+
47
+ 2. Preserve brand and layout inputs.
48
+ - Keep palette, typography mood, material style, photo/illustration style,
49
+ and metaphor constraints.
50
+ - For verticals like trucking, healthcare, legal, or finance, use grounded
51
+ domain visuals and avoid generic SaaS abstractions.
52
+
53
+ 3. Write an asset prompt.
54
+ - Include role, section purpose, subject, composition, palette, lighting,
55
+ texture, crop safety, responsive layout intent, and negative constraints.
56
+ - For backgrounds, explicitly request no text, no UI, no logos, and safe
57
+ empty space where page HTML will sit.
58
+ - For section images, avoid fake product screenshots unless the user asks
59
+ for an interface mockup.
60
+
61
+ 4. Generate through `media.generate_image`.
62
+ - `use_case`: `landing_page_asset`.
63
+ - Use 16:9 or 21:9 for hero/backgrounds, 4:3 for section visuals, and
64
+ 1200x630 for OG images.
65
+ - Attach generated outputs to the thread for preview.
66
+
67
+ 5. Promote the selected asset when it belongs in a public site.
68
+ - Use the public-dj media asset upsert endpoint/service to create or update
69
+ `MediaAsset` by `vertical + asset_key`.
70
+ - Store durable metadata: `asset_key`, `digest`, `url`, `alt_text`, `role`,
71
+ dimensions, mime type, and prompt summary.
72
+ - Do not store long-lived presigned URLs or storage secrets in manifest
73
+ metadata.
74
+
75
+ 6. Wire the page/manifest.
76
+ - Add or update manifest `payload.media[]` entries:
77
+
78
+ ```json
79
+ {
80
+ "asset_key": "fleet-hero-owner-operator",
81
+ "digest": "sha256:...",
82
+ "url": "/api/v1/media/fleetrun/fleet-hero-owner-operator/sha256:...",
83
+ "alt_text": "Owner-operator reviewing an insurance quote beside a semi truck",
84
+ "role": "hero_background",
85
+ "metadata": {
86
+ "width": 1600,
87
+ "height": 900,
88
+ "mime_type": "image/png"
89
+ }
90
+ }
91
+ ```
92
+
93
+ - In section payloads, reference images by `asset_key` when possible:
94
+
95
+ ```json
96
+ {
97
+ "type": "hero_with_cta",
98
+ "headline": "Instant quote. Instant answers.",
99
+ "image": {
100
+ "asset_key": "fleet-hero-owner-operator",
101
+ "role": "hero_background"
102
+ }
103
+ }
104
+ ```
105
+
106
+ - Supported section image fields for v1:
107
+ `hero_with_cta.image`, `feature_grid.items[].image`,
108
+ `content_markdown.image`, and `cta_banner.image`.
109
+ - For workspace-only generated assets, place selected files under the
110
+ site's public asset directory and reference them directly.
111
+
112
+ ## Prompt Patterns
113
+
114
+ Hero/background:
115
+
116
+ ```text
117
+ Create a website hero_background for [product/page] serving [audience].
118
+ Subject: [domain-specific scene or metaphor].
119
+ Composition: wide crop, responsive-safe, clear negative space for HTML text on [side].
120
+ Style: [brand style, palette, lighting, realism level].
121
+ No text, no UI, no logos, no fake brand marks.
122
+ Aspect ratio: 16:9 or 21:9.
123
+ ```
124
+
125
+ Section visual:
126
+
127
+ ```text
128
+ Create a section_illustration for the section "[section headline]".
129
+ Show [specific concept/user/workflow] in a grounded way.
130
+ Composition: uncluttered, works beside HTML text, no tiny labels.
131
+ Style: [brand cues].
132
+ Avoid generic SaaS dashboards unless a product UI is explicitly requested.
133
+ ```
134
+
135
+ OG image:
136
+
137
+ ```text
138
+ Create an og_image for [product/page].
139
+ Use the brand/product as the first-viewport signal.
140
+ Readable text only if explicitly requested; otherwise leave text to HTML/social metadata.
141
+ Aspect ratio: 1200x630.
142
+ ```
143
+
144
+ ## Guardrails
145
+
146
+ - Do not bake landing page body copy into imagery.
147
+ - Do not generate fake customer logos, certifications, insurance carrier logos,
148
+ DOT records, prices, or legal/compliance claims.
149
+ - Do not use dark, blurred, cropped, stock-like imagery when the asset needs to
150
+ communicate the actual product, buyer, or domain.
151
+ - Do not promote an asset into a site unless the user asked to use it or the
152
+ current task is site generation.
153
+ - Always provide alt text for promoted assets.
@@ -135,6 +135,9 @@ generated Builder workspace.
135
135
  action capability; preflight fail-closes on connection/secret availability, and the
136
136
  delivery row is idempotent on the ActionRun id with a SENT short-circuit. Not
137
137
  agent-exposed yet: exposure is a separate per-definition mw.core.agent_actions flip.
138
+ - `media.generate_image` -- Runtime media action capability wraps the existing
139
+ generate_image service, stores outputs in runtime_storage, and attaches
140
+ generated_image thread artifacts when thread context is present.
138
141
  - `consent.check_channel` -- Service seam consumed by other executors and sidecars as
139
142
  a preflight; not an agent-callable tool.
140
143
  - `discovery.request_proposal` -- Effectful; outside the default READ_RING, granted
@@ -217,6 +217,25 @@
217
217
  "approval_required": false,
218
218
  "interim_path": ""
219
219
  },
220
+ {
221
+ "primitive_id": "media.generate_image",
222
+ "family": "media",
223
+ "display_name": "Generate Image",
224
+ "description": "Generate image assets through the runtime media provider and store outputs as runtime files.",
225
+ "runtime_app": "runtime_media",
226
+ "dispatch_path": "",
227
+ "status": "active",
228
+ "requires_integration": false,
229
+ "requires_secret": true,
230
+ "metered": true,
231
+ "executor_ready": "live",
232
+ "executor_ref": "capability:media.generate_image",
233
+ "executor_notes": "Runtime media action capability wraps the existing generate_image service, stores outputs in runtime_storage, and attaches generated_image thread artifacts when thread context is present.",
234
+ "agent_exposable": true,
235
+ "agent_exposure_path": "app_pack_action",
236
+ "approval_required": false,
237
+ "interim_path": ""
238
+ },
220
239
  {
221
240
  "primitive_id": "consent.check_channel",
222
241
  "family": "consent",
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: visualize-context
3
+ description: "Generate strategy, product, system, PLG, and concept visuals from messy context, then attach generated images to the Builder thread."
4
+ ---
5
+
6
+ # Visualize Context
7
+
8
+ Use this skill when the user asks to visualize strategy/product context, for
9
+ example:
10
+
11
+ - "visualize this"
12
+ - "generate an image"
13
+ - "show the flow"
14
+ - "make this easier to see"
15
+ - "PLG loop"
16
+ - "system map"
17
+ - "architecture visual"
18
+ - "background metaphor"
19
+
20
+ Default v1 output: an actual generated image attached back to the thread as a
21
+ runtime-stored `generated_image` artifact. Do not stop at a prompt unless image
22
+ generation is unavailable.
23
+
24
+ ## Workflow
25
+
26
+ 1. Extract the decision context.
27
+ - Identify the product, audience, buyer, loop, system, or strategic argument.
28
+ - Preserve named entities, constraints, data flow, and any stated design style.
29
+ - Separate verified facts from interpretation. Do not invent traction,
30
+ customers, logos, certifications, or claims.
31
+
32
+ 2. Choose the visual type.
33
+ - `product_flow_map`: activation, value moment, retention, conversion.
34
+ - `plg_loop`: acquisition, free value, habit, share/expand, conversion.
35
+ - `system_diagram`: components, data movement, approvals, runtime boundaries.
36
+ - `strategy_map`: thesis, unfair advantage, risks, market timing, proof.
37
+ - `concept_visual`: metaphor image that makes an abstract idea graspable.
38
+ - `landing_hero_mockup`: first-screen marketing visual preview only. If the
39
+ user wants assets used in a site, switch to `landing-page-assets`.
40
+
41
+ 3. Write a structured image prompt.
42
+ - Put the core visual in the first sentence.
43
+ - Include layout, hierarchy, style, palette, target aspect ratio, and what
44
+ should be legible.
45
+ - Use labels sparingly. Prefer large, readable labels over dense tiny text.
46
+ - If the image is meant to explain a complex flow, ask for a clean diagram
47
+ composition, not a photorealistic scene.
48
+
49
+ 4. Generate through `media.generate_image`.
50
+ - Use `use_case`: `visualize_context`.
51
+ - Use 16:9 or 4:3 for strategy diagrams; 1:1 only when the user asks for a
52
+ square artifact.
53
+ - Set `request_source_ref` to the current thread/run context when available.
54
+ - Attach the generated output as a thread artifact. The action capability
55
+ does this automatically when thread context is present.
56
+
57
+ 5. Respond with the result.
58
+ - Explain the visual in one or two sentences only if needed.
59
+ - Do not claim the image proves anything. It is a visualization aid.
60
+
61
+ ## Prompt Contract
62
+
63
+ For visual prompts, include:
64
+
65
+ - `visual_type`
66
+ - `main_subject`
67
+ - `audience`
68
+ - `flow_or_components`
69
+ - `style_reference`
70
+ - `brand_constraints`
71
+ - `must_show`
72
+ - `must_avoid`
73
+ - `aspect_ratio`
74
+
75
+ Use this shape internally:
76
+
77
+ ```text
78
+ Create a [visual_type] showing [main_subject] for [audience].
79
+ Composition: [layout and hierarchy].
80
+ Content: [flow/components, 4-7 max].
81
+ Style: [brand/design cues].
82
+ Readable text: [short labels only].
83
+ Avoid: [fake logos, claims, tiny text, generic stock visuals].
84
+ Aspect ratio: [16:9 / 4:3 / square].
85
+ ```
86
+
87
+ ## Guardrails
88
+
89
+ - Do not invent fake customer names, fake logos, fake metrics, fake compliance,
90
+ or market claims.
91
+ - Do not create tiny unreadable UI text. Use simple labels and preserve detail
92
+ in the thread answer or source content.
93
+ - Do not bake long paragraphs into images.
94
+ - Do not use generic SaaS stock visuals when the user supplied a specific
95
+ domain, metaphor, or brand style.
96
+ - If the user asks for a website asset or says "use it in the site", switch to
97
+ `landing-page-assets` because that needs asset role, alt text, and promotion.
@@ -23,6 +23,12 @@ does not match the current MinuteWork CLI export.
23
23
  - `mcp/claude-desktop.sample.json`
24
24
  - It does not regenerate tenant code, sidecar code, schema files, or arbitrary
25
25
  user-owned files outside the managed export set.
26
+ - It never touches the authored overlays: `CLAUDE.workspace.md`,
27
+ `AGENTS.workspace.md`, and `skills.workspace/**`. These are deliberately
28
+ outside the managed manifest, so authored guidance and authored skills survive
29
+ every `sync-assets` (including `--force`) and are never reported as conflicts.
30
+ Keep workspace-specific guidance and skills in the overlays, not in the managed
31
+ `CLAUDE.md` / `AGENTS.md` / `skills/` files.
26
32
  - If the workspace uses an older exported skill set, prefer refreshing assets
27
33
  before concluding that a Builder capability or instruction is missing.
28
34
  - If the workspace lacks sandbox/tier guidance for `minutework sandbox create`,
@@ -53,6 +53,38 @@ a {
53
53
  max-width: 760px;
54
54
  }
55
55
 
56
+ .hero-with-media {
57
+ grid-template-columns: minmax(0, 0.95fr) minmax(280px, 0.85fr);
58
+ max-width: none;
59
+ align-items: center;
60
+ }
61
+
62
+ .hero-copy {
63
+ display: grid;
64
+ gap: 24px;
65
+ }
66
+
67
+ .hero-media,
68
+ .section-image,
69
+ .feature-image {
70
+ overflow: hidden;
71
+ border: 1px solid var(--line);
72
+ background: var(--surface);
73
+ }
74
+
75
+ .hero-media {
76
+ aspect-ratio: 4 / 3;
77
+ }
78
+
79
+ .hero-media img,
80
+ .section-image img,
81
+ .feature-image img {
82
+ display: block;
83
+ width: 100%;
84
+ height: 100%;
85
+ object-fit: cover;
86
+ }
87
+
56
88
  .eyebrow {
57
89
  margin: 0;
58
90
  color: var(--accent);
@@ -96,8 +128,71 @@ p {
96
128
  padding: 24px;
97
129
  }
98
130
 
131
+ .content-section,
132
+ .cta-banner {
133
+ display: grid;
134
+ grid-template-columns: minmax(0, 0.9fr) minmax(240px, 0.7fr);
135
+ gap: 32px;
136
+ align-items: center;
137
+ margin-top: 56px;
138
+ padding-top: 56px;
139
+ border-top: 1px solid var(--line);
140
+ }
141
+
142
+ .content-section h2,
143
+ .cta-banner h2 {
144
+ margin: 0;
145
+ font-size: clamp(2rem, 5vw, 4rem);
146
+ line-height: 1;
147
+ }
148
+
149
+ .section-image {
150
+ aspect-ratio: 4 / 3;
151
+ }
152
+
153
+ .feature-grid {
154
+ display: grid;
155
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
156
+ gap: 16px;
157
+ margin-top: 28px;
158
+ }
159
+
160
+ .feature-item {
161
+ border: 1px solid var(--line);
162
+ background: var(--surface);
163
+ padding: 18px;
164
+ }
165
+
166
+ .feature-item h3 {
167
+ margin: 14px 0 0;
168
+ font-size: 1rem;
169
+ }
170
+
171
+ .feature-item p {
172
+ margin-bottom: 0;
173
+ font-size: 0.95rem;
174
+ }
175
+
176
+ .feature-image {
177
+ aspect-ratio: 16 / 10;
178
+ }
179
+
99
180
  .site-footer {
100
181
  padding: 32px 0;
101
182
  border-top: 1px solid var(--line);
102
183
  color: var(--muted);
103
184
  }
185
+
186
+ @media (max-width: 760px) {
187
+ .site-header {
188
+ align-items: flex-start;
189
+ gap: 16px;
190
+ flex-direction: column;
191
+ }
192
+
193
+ .hero-with-media,
194
+ .content-section,
195
+ .cta-banner {
196
+ grid-template-columns: 1fr;
197
+ }
198
+ }
@@ -1,15 +1,206 @@
1
1
  import Link from "next/link";
2
2
 
3
3
  import { appRoutes } from "@/lib/routes";
4
- import { getHomePageRef, getSiteManifest } from "@/lib/public-dj.server";
4
+ import {
5
+ getHomePage,
6
+ getSiteManifest,
7
+ type PageSection,
8
+ type PublicSiteManifest,
9
+ type PublicSiteMediaAsset,
10
+ } from "@/lib/public-dj.server";
11
+
12
+ type UnknownRecord = Record<string, unknown>;
13
+
14
+ function asRecord(value: unknown): UnknownRecord | null {
15
+ return value && typeof value === "object" && !Array.isArray(value)
16
+ ? (value as UnknownRecord)
17
+ : null;
18
+ }
19
+
20
+ function asRecordArray(value: unknown): UnknownRecord[] {
21
+ return Array.isArray(value)
22
+ ? value.map(asRecord).filter((item): item is UnknownRecord => item !== null)
23
+ : [];
24
+ }
25
+
26
+ function textValue(value: unknown): string {
27
+ return typeof value === "string" ? value.trim() : "";
28
+ }
29
+
30
+ function firstText(...values: unknown[]): string {
31
+ for (const value of values) {
32
+ const text = textValue(value);
33
+ if (text) {
34
+ return text;
35
+ }
36
+ }
37
+ return "";
38
+ }
39
+
40
+ function mediaByAssetKey(
41
+ manifest: PublicSiteManifest,
42
+ assetKey: string,
43
+ ): PublicSiteMediaAsset | null {
44
+ return (
45
+ manifest.payload.media.find(
46
+ (item: PublicSiteMediaAsset) => item.asset_key === assetKey,
47
+ ) ?? null
48
+ );
49
+ }
50
+
51
+ function mediaByRole(
52
+ manifest: PublicSiteManifest,
53
+ roles: string[],
54
+ ): PublicSiteMediaAsset | null {
55
+ return (
56
+ manifest.payload.media.find((item: PublicSiteMediaAsset) =>
57
+ roles.includes(item.role),
58
+ ) ?? null
59
+ );
60
+ }
61
+
62
+ function mediaFromImageField(
63
+ manifest: PublicSiteManifest,
64
+ value: unknown,
65
+ ): PublicSiteMediaAsset | null {
66
+ const image = asRecord(value);
67
+ if (!image) {
68
+ return null;
69
+ }
70
+ const assetKey = firstText(image.asset_key, image.assetKey);
71
+ if (assetKey) {
72
+ return mediaByAssetKey(manifest, assetKey);
73
+ }
74
+ const url = firstText(image.url, image.src);
75
+ if (!url) {
76
+ return null;
77
+ }
78
+ return {
79
+ asset_key: firstText(image.asset_key, image.assetKey, `inline-${url}`),
80
+ digest: firstText(image.digest, "inline"),
81
+ url,
82
+ alt_text: firstText(image.alt_text, image.altText, image.alt),
83
+ role: firstText(image.role, "section_illustration"),
84
+ metadata: {},
85
+ };
86
+ }
87
+
88
+ function renderImage(asset: PublicSiteMediaAsset, className: string) {
89
+ return (
90
+ <div className={className}>
91
+ <img src={asset.url} alt={asset.alt_text || ""} loading="lazy" />
92
+ </div>
93
+ );
94
+ }
95
+
96
+ function sectionImage(
97
+ manifest: PublicSiteManifest,
98
+ section: UnknownRecord,
99
+ ): PublicSiteMediaAsset | null {
100
+ return (
101
+ mediaFromImageField(manifest, section.image) ??
102
+ mediaFromImageField(manifest, section.visual) ??
103
+ null
104
+ );
105
+ }
106
+
107
+ function renderSection(
108
+ section: PageSection,
109
+ manifest: PublicSiteManifest,
110
+ index: number,
111
+ ) {
112
+ const record = asRecord(section) ?? {};
113
+ const type = firstText(record.type, "section");
114
+ const headline = firstText(record.headline, record.title);
115
+ const body = firstText(record.body, record.subhead, record.summary, record.markdown);
116
+ const image = sectionImage(manifest, record);
117
+
118
+ if (type === "feature_grid") {
119
+ const items = asRecordArray(record.items);
120
+ return (
121
+ <section className="content-section" key={`${type}-${index}`}>
122
+ {image ? renderImage(image, "section-image") : null}
123
+ <div>
124
+ {headline ? <h2>{headline}</h2> : null}
125
+ {body ? <p>{body}</p> : null}
126
+ {items.length ? (
127
+ <div className="feature-grid">
128
+ {items.map((item, itemIndex) => {
129
+ const itemImage = sectionImage(manifest, item);
130
+ return (
131
+ <article className="feature-item" key={`${type}-${index}-${itemIndex}`}>
132
+ {itemImage ? renderImage(itemImage, "feature-image") : null}
133
+ <h3>{firstText(item.headline, item.title)}</h3>
134
+ <p>{firstText(item.body, item.summary)}</p>
135
+ </article>
136
+ );
137
+ })}
138
+ </div>
139
+ ) : null}
140
+ </div>
141
+ </section>
142
+ );
143
+ }
144
+
145
+ if (type === "cta_banner") {
146
+ const href = firstText(asRecord(record.cta)?.href) || appRoutes.onboarding();
147
+ const ctaLabel = firstText(asRecord(record.cta)?.label, "Start onboarding");
148
+ return (
149
+ <section className="cta-banner" key={`${type}-${index}`}>
150
+ <div>
151
+ {headline ? <h2>{headline}</h2> : null}
152
+ {body ? <p>{body}</p> : null}
153
+ <Link className="button" href={href}>
154
+ {ctaLabel}
155
+ </Link>
156
+ </div>
157
+ {image ? renderImage(image, "section-image") : null}
158
+ </section>
159
+ );
160
+ }
161
+
162
+ if (type === "content_markdown" || headline || body || image) {
163
+ return (
164
+ <section className="content-section" key={`${type}-${index}`}>
165
+ <div>
166
+ {headline ? <h2>{headline}</h2> : null}
167
+ {body ? <p>{body}</p> : null}
168
+ </div>
169
+ {image ? renderImage(image, "section-image") : null}
170
+ </section>
171
+ );
172
+ }
173
+
174
+ return null;
175
+ }
5
176
 
6
177
  export default async function HomePage() {
7
- const [manifest, homePage] = await Promise.all([getSiteManifest(), getHomePageRef()]);
8
- const headline = String(manifest.payload.metadata.headline ?? manifest.display_name);
9
- const summary = String(
10
- manifest.payload.metadata.summary ??
178
+ const [manifest, homePage] = await Promise.all([getSiteManifest(), getHomePage()]);
179
+ const sections: PageSection[] = homePage?.payload.sections ?? [];
180
+ const heroSection =
181
+ sections.find((section: PageSection) => firstText(section.type) === "hero_with_cta") ??
182
+ null;
183
+ const heroRecord = asRecord(heroSection) ?? {};
184
+ const heroAsset =
185
+ sectionImage(manifest, heroRecord) ??
186
+ mediaByRole(manifest, ["hero_background", "hero_visual"]);
187
+ const headline =
188
+ firstText(
189
+ heroRecord.headline,
190
+ manifest.payload.metadata.headline,
191
+ homePage?.title,
192
+ manifest.display_name,
193
+ ) || manifest.display_name;
194
+ const summary =
195
+ firstText(
196
+ heroRecord.subhead,
197
+ heroRecord.summary,
198
+ manifest.payload.metadata.summary,
11
199
  "A custom Vuilder public site rendered from pinned public-dj content.",
12
- );
200
+ ) || "A custom Vuilder public site rendered from pinned public-dj content.";
201
+ const cta = asRecord(heroRecord.cta);
202
+ const ctaLabel = firstText(cta?.label, "Start onboarding");
203
+ const ctaHref = firstText(cta?.href) || appRoutes.onboarding();
13
204
 
14
205
  return (
15
206
  <div className="site-shell">
@@ -22,14 +213,24 @@ export default async function HomePage() {
22
213
  </nav>
23
214
  </header>
24
215
  <main className="site-main">
25
- <section className="hero">
26
- <p className="eyebrow">{manifest.vertical_slug}</p>
27
- <h1>{headline}</h1>
28
- <p>{summary}</p>
29
- <Link className="button" href={appRoutes.onboarding()}>
30
- Start onboarding
31
- </Link>
216
+ <section className={heroAsset ? "hero hero-with-media" : "hero"}>
217
+ <div className="hero-copy">
218
+ <p className="eyebrow">{manifest.vertical_slug}</p>
219
+ <h1>{headline}</h1>
220
+ <p>{summary}</p>
221
+ <Link className="button" href={ctaHref}>
222
+ {ctaLabel}
223
+ </Link>
224
+ </div>
225
+ {heroAsset ? renderImage(heroAsset, "hero-media") : null}
32
226
  </section>
227
+ {sections
228
+ .filter(
229
+ (section: PageSection) => firstText(section.type) !== "hero_with_cta",
230
+ )
231
+ .map((section: PageSection, index: number) =>
232
+ renderSection(section, manifest, index),
233
+ )}
33
234
  <section className="panel">
34
235
  <p>
35
236
  Release manifest: {manifest.content_ref} / {manifest.digest}
@@ -19,6 +19,17 @@ const verticalPackageRefDigestSchema = refDigestSchema.extend({
19
19
  content_ref: marketplacePackageRefSchema,
20
20
  });
21
21
 
22
+ const mediaAssetSchema = z
23
+ .object({
24
+ asset_key: z.string().min(1),
25
+ digest: z.string().min(1),
26
+ url: z.string().min(1),
27
+ alt_text: z.string().default(""),
28
+ role: z.string().default(""),
29
+ metadata: z.record(z.unknown()).default({}),
30
+ })
31
+ .passthrough();
32
+
22
33
  const siteManifestSchema = z.object({
23
34
  content_ref: z.string(),
24
35
  digest: z.string(),
@@ -38,13 +49,36 @@ const siteManifestSchema = z.object({
38
49
  onboarding_flow: refDigestSchema.nullable().optional(),
39
50
  vertical_package: verticalPackageRefDigestSchema.nullable().optional(),
40
51
  navigation: z.array(z.unknown()).default([]),
41
- media: z.array(z.unknown()).default([]),
52
+ media: z.array(mediaAssetSchema).default([]),
42
53
  theme_tokens: z.record(z.unknown()).default({}),
43
54
  metadata: z.record(z.unknown()).default({}),
44
55
  }),
45
56
  });
46
57
 
47
58
  export type PublicSiteManifest = z.infer<typeof siteManifestSchema>;
59
+ export type PublicSiteMediaAsset = z.infer<typeof mediaAssetSchema>;
60
+ type ManifestPageRef = PublicSiteManifest["payload"]["pages"][number];
61
+
62
+ const sectionSchema = z.object({ type: z.string().optional() }).passthrough();
63
+
64
+ const marketingPageSchema = z.object({
65
+ content_ref: z.string(),
66
+ digest: z.string(),
67
+ content_kind: z.literal("marketing_page"),
68
+ vertical_key: z.string(),
69
+ vertical_slug: z.string(),
70
+ path: z.string(),
71
+ title: z.string(),
72
+ seo: z.record(z.unknown()).default({}),
73
+ payload: z
74
+ .object({
75
+ sections: z.array(sectionSchema).default([]),
76
+ })
77
+ .passthrough(),
78
+ });
79
+
80
+ export type MarketingPage = z.infer<typeof marketingPageSchema>;
81
+ export type PageSection = z.infer<typeof sectionSchema>;
48
82
 
49
83
  async function resolvePublicContent(contentRef: string, digest: string) {
50
84
  const url = new URL("/api/v1/content/resolve/", env.MW_PUBLIC_DJ_BASE_URL);
@@ -79,8 +113,20 @@ export const getSiteManifest = cache(async (): Promise<PublicSiteManifest> => {
79
113
  export async function getHomePageRef() {
80
114
  const manifest = await getSiteManifest();
81
115
  return (
82
- manifest.payload.pages.find((page) => page.path === "/") ??
116
+ manifest.payload.pages.find((page: ManifestPageRef) => page.path === "/") ??
83
117
  manifest.payload.pages[0] ??
84
118
  null
85
119
  );
86
120
  }
121
+
122
+ export const getHomePage = cache(async (): Promise<MarketingPage | null> => {
123
+ const homePageRef = await getHomePageRef();
124
+ if (!homePageRef) {
125
+ return null;
126
+ }
127
+ const payload = await resolvePublicContent(
128
+ homePageRef.content_ref,
129
+ homePageRef.digest,
130
+ );
131
+ return marketingPageSchema.parse(payload);
132
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minutework",
3
- "version": "0.1.56",
3
+ "version": "0.1.57",
4
4
  "description": "MinuteWork CLI for workspace scaffolding, local preview workflows, and hosted preview deploys.",
5
5
  "type": "module",
6
6
  "bin": {