fix: convert homepage text fields to richtext for live editing

Change field types from "text" to "richtext" and wrap content in PortableText
components. This enables proper inline live editing instead of jumping to backend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kunthawat Greethong
2026-04-30 13:40:49 +07:00
parent 8094cec877
commit be63c72048
2 changed files with 177 additions and 33 deletions

View File

@@ -30,7 +30,7 @@
{
"slug": "hero_subtitle",
"label": "Hero Subtitle",
"type": "text"
"type": "richtext"
},
{
"slug": "hero_button_text",
@@ -65,7 +65,7 @@
{
"slug": "feature_1_description",
"label": "Feature 1 Description",
"type": "text"
"type": "richtext"
},
{
"slug": "feature_1_icon",
@@ -80,7 +80,7 @@
{
"slug": "feature_2_description",
"label": "Feature 2 Description",
"type": "text"
"type": "richtext"
},
{
"slug": "feature_2_icon",
@@ -95,7 +95,7 @@
{
"slug": "feature_3_description",
"label": "Feature 3 Description",
"type": "text"
"type": "richtext"
},
{
"slug": "feature_3_icon",
@@ -110,7 +110,7 @@
{
"slug": "feature_4_description",
"label": "Feature 4 Description",
"type": "text"
"type": "richtext"
},
{
"slug": "feature_4_icon",
@@ -125,7 +125,7 @@
{
"slug": "feature_5_description",
"label": "Feature 5 Description",
"type": "text"
"type": "richtext"
},
{
"slug": "feature_5_icon",
@@ -140,7 +140,7 @@
{
"slug": "feature_6_description",
"label": "Feature 6 Description",
"type": "text"
"type": "richtext"
},
{
"slug": "feature_6_icon",
@@ -155,7 +155,7 @@
{
"slug": "comparison_subtitle",
"label": "Comparison Section Subtitle",
"type": "text"
"type": "richtext"
},
{
"slug": "cta_title",
@@ -165,7 +165,7 @@
{
"slug": "cta_subtitle",
"label": "CTA Subtitle",
"type": "text"
"type": "richtext"
},
{
"slug": "cta_button_text",
@@ -180,12 +180,12 @@
{
"slug": "footer_tagline",
"label": "Footer Tagline",
"type": "text"
"type": "richtext"
},
{
"slug": "footer_about_text",
"label": "Footer About Text",
"type": "text"
"type": "richtext"
},
{
"slug": "footer_copyright",
@@ -490,7 +490,20 @@
"status": "published",
"data": {
"hero_headline": "The CMS that\nruns on your server",
"hero_subtitle": "EmDash is a full-stack TypeScript CMS built on Astro. No cloud account required, no external dependencies. Just a complete admin panel and your content.",
"hero_subtitle": [
{
"_type": "block",
"style": "normal",
"children": [
{
"_type": "span",
"text": "EmDash is a full-stack TypeScript CMS built on Astro. No cloud account required, no external dependencies. Just a complete admin panel and your content.",
"_key": "hero_subtitle-0-76099842902211"
}
],
"_key": "hero_subtitle-block-0-76099842902211"
}
],
"hero_button_text": "Open Admin Panel",
"hero_button_url": "/_emdash/admin",
"hero_image": {
@@ -501,31 +514,161 @@
"features_section_title": "Everything you need",
"features_section_subtitle": "A complete CMS without the vendor lock-in",
"feature_1_title": "Fully Self-Hosted",
"feature_1_description": "SQLite, D1, Turso, or PostgreSQL. Your data stays on your servers. No cloud account required.",
"feature_1_description": [
{
"_type": "block",
"style": "normal",
"children": [
{
"_type": "span",
"text": "SQLite, D1, Turso, or PostgreSQL. Your data stays on your servers. No cloud account required.",
"_key": "feature_1_description-0-3660002334634"
}
],
"_key": "feature_1_description-block-0-3660002334634"
}
],
"feature_1_icon": "M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5",
"feature_2_title": "Admin Panel",
"feature_2_description": "Visual schema builder, media library, navigation menus. Full admin at /_emdash/admin",
"feature_2_description": [
{
"_type": "block",
"style": "normal",
"children": [
{
"_type": "span",
"text": "Visual schema builder, media library, navigation menus. Full admin at /_emdash/admin",
"_key": "feature_2_description-0-10238165685399"
}
],
"_key": "feature_2_description-block-0-10238165685399"
}
],
"feature_2_icon": "M3 3h18v18H3zM3 9h18M9 21V9",
"feature_3_title": "Passkey Auth",
"feature_3_description": "WebAuthn passkey-first authentication with OAuth and magic link fallbacks. Role-based access control.",
"feature_3_description": [
{
"_type": "block",
"style": "normal",
"children": [
{
"_type": "span",
"text": "WebAuthn passkey-first authentication with OAuth and magic link fallbacks. Role-based access control.",
"_key": "feature_3_description-0-38648894518114"
}
],
"_key": "feature_3_description-block-0-38648894518114"
}
],
"feature_3_icon": "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z",
"feature_4_title": "Built-in MCP",
"feature_4_description": "Model Context Protocol server for AI tools. Claude and ChatGPT can interact with your site directly.",
"feature_4_description": [
{
"_type": "block",
"style": "normal",
"children": [
{
"_type": "span",
"text": "Model Context Protocol server for AI tools. Claude and ChatGPT can interact with your site directly.",
"_key": "feature_4_description-0-29611312411414"
}
],
"_key": "feature_4_description-block-0-29611312411414"
}
],
"feature_4_icon": "M12 6v6l4 2",
"feature_5_title": "Plugin System",
"feature_5_description": "Sandboxed plugins on Cloudflare Workers. Define capabilities, run safely in isolation.",
"feature_5_description": [
{
"_type": "block",
"style": "normal",
"children": [
{
"_type": "span",
"text": "Sandboxed plugins on Cloudflare Workers. Define capabilities, run safely in isolation.",
"_key": "feature_5_description-0-40926012290709"
}
],
"_key": "feature_5_description-block-0-40926012290709"
}
],
"feature_5_icon": "M16 18l6-6-6-6M8 6l-6 6 6 6",
"feature_6_title": "WordPress Import",
"feature_6_description": "Import posts, pages, media, and taxonomies from WXR exports, REST API, or WordPress.com.",
"feature_6_description": [
{
"_type": "block",
"style": "normal",
"children": [
{
"_type": "span",
"text": "Import posts, pages, media, and taxonomies from WXR exports, REST API, or WordPress.com.",
"_key": "feature_6_description-0-69308754770518"
}
],
"_key": "feature_6_description-block-0-69308754770518"
}
],
"feature_6_icon": "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8zM14 2v6h6M16 13H8M16 17H8M16 9H8",
"comparison_title": "EmDash vs Tina CMS",
"comparison_subtitle": "Both work with Astro, but differ in approach",
"comparison_subtitle": [
{
"_type": "block",
"style": "normal",
"children": [
{
"_type": "span",
"text": "Both work with Astro, but differ in approach",
"_key": "comparison_subtitle-0-26650886278406"
}
],
"_key": "comparison_subtitle-block-0-26650886278406"
}
],
"cta_title": "Ready to get started?",
"cta_subtitle": "Clone the template, run bootstrap, and you're ready to build.",
"cta_subtitle": [
{
"_type": "block",
"style": "normal",
"children": [
{
"_type": "span",
"text": "Clone the template, run bootstrap, and you're ready to build.",
"_key": "cta_subtitle-0-69221477697320"
}
],
"_key": "cta_subtitle-block-0-69221477697320"
}
],
"cta_button_text": "Try Admin Panel",
"cta_button_url": "/_emdash/admin",
"footer_tagline": "Thoughts on building for the web",
"footer_about_text": "A blog about software, design, and the occasional stray thought.",
"footer_tagline": [
{
"_type": "block",
"style": "normal",
"children": [
{
"_type": "span",
"text": "Thoughts on building for the web",
"_key": "footer_tagline-0-54139722085656"
}
],
"_key": "footer_tagline-block-0-54139722085656"
}
],
"footer_about_text": [
{
"_type": "block",
"style": "normal",
"children": [
{
"_type": "span",
"text": "A blog about software, design, and the occasional stray thought.",
"_key": "footer_about_text-0-19146283096269"
}
],
"_key": "footer_about_text-block-0-19146283096269"
}
],
"footer_copyright": "Powered by EmDash CMS"
}
}

View File

@@ -1,5 +1,6 @@
---
import { getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
import Base from "../layouts/Base.astro";
const { entry: homepage } = await getEmDashEntry("homepage", "main");
@@ -17,9 +18,9 @@ const d = homepage.data;
<h1 class="hero-title" {...homepage.edit.hero_headline}>
{d.hero_headline.split('\n').map((line: string) => <><span>{line}</span><br /></>)}
</h1>
<p class="hero-subtitle" {...homepage.edit.hero_subtitle}>
{d.hero_subtitle}
</p>
<div class="hero-subtitle" {...homepage.edit.hero_subtitle}>
<PortableText value={d.hero_subtitle} />
</div>
<div class="hero-actions">
<a href={d.hero_button_url} class="btn btn-primary" {...homepage.edit.hero_button_text}>
<span class="btn-icon">
@@ -75,7 +76,7 @@ const d = homepage.data;
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_1_title}>{d.feature_1_title}</h3>
<p class="feature-desc" {...homepage.edit.feature_1_description}>{d.feature_1_description}</p>
<div class="feature-desc" {...homepage.edit.feature_1_description}><PortableText value={d.feature_1_description} /></div>
</div>
<div class="feature-card">
<div class="feature-icon">
@@ -85,7 +86,7 @@ const d = homepage.data;
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_2_title}>{d.feature_2_title}</h3>
<p class="feature-desc" {...homepage.edit.feature_2_description}>{d.feature_2_description}</p>
<div class="feature-desc" {...homepage.edit.feature_2_description}><PortableText value={d.feature_2_description} /></div>
</div>
<div class="feature-card">
<div class="feature-icon">
@@ -94,7 +95,7 @@ const d = homepage.data;
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_3_title}>{d.feature_3_title}</h3>
<p class="feature-desc" {...homepage.edit.feature_3_description}>{d.feature_3_description}</p>
<div class="feature-desc" {...homepage.edit.feature_3_description}><PortableText value={d.feature_3_description} /></div>
</div>
<div class="feature-card">
<div class="feature-icon">
@@ -104,7 +105,7 @@ const d = homepage.data;
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_4_title}>{d.feature_4_title}</h3>
<p class="feature-desc" {...homepage.edit.feature_4_description}>{d.feature_4_description}</p>
<div class="feature-desc" {...homepage.edit.feature_4_description}><PortableText value={d.feature_4_description} /></div>
</div>
<div class="feature-card">
<div class="feature-icon">
@@ -114,7 +115,7 @@ const d = homepage.data;
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_5_title}>{d.feature_5_title}</h3>
<p class="feature-desc" {...homepage.edit.feature_5_description}>{d.feature_5_description}</p>
<div class="feature-desc" {...homepage.edit.feature_5_description}><PortableText value={d.feature_5_description} /></div>
</div>
<div class="feature-card">
<div class="feature-icon">
@@ -126,7 +127,7 @@ const d = homepage.data;
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_6_title}>{d.feature_6_title}</h3>
<p class="feature-desc" {...homepage.edit.feature_6_description}>{d.feature_6_description}</p>
<div class="feature-desc" {...homepage.edit.feature_6_description}><PortableText value={d.feature_6_description} /></div>
</div>
</div>
</section>
@@ -135,7 +136,7 @@ const d = homepage.data;
<section class="comparison">
<div class="section-header">
<h2 class="section-title" {...homepage.edit.comparison_title}>{d.comparison_title}</h2>
<p class="section-subtitle" {...homepage.edit.comparison_subtitle}>{d.comparison_subtitle}</p>
<div class="section-subtitle" {...homepage.edit.comparison_subtitle}><PortableText value={d.comparison_subtitle} /></div>
</div>
<div class="comparison-table">
<div class="comparison-header">
@@ -180,7 +181,7 @@ const d = homepage.data;
<section class="cta">
<div class="cta-content">
<h2 class="cta-title" {...homepage.edit.cta_title}>{d.cta_title}</h2>
<p class="cta-subtitle" {...homepage.edit.cta_subtitle}>{d.cta_subtitle}</p>
<div class="cta-subtitle" {...homepage.edit.cta_subtitle}><PortableText value={d.cta_subtitle} /></div>
<div class="cta-actions">
<a href={d.cta_button_url} class="btn btn-primary btn-large" {...homepage.edit.cta_button_text}>{d.cta_button_text}</a>
</div>