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", "slug": "hero_subtitle",
"label": "Hero Subtitle", "label": "Hero Subtitle",
"type": "text" "type": "richtext"
}, },
{ {
"slug": "hero_button_text", "slug": "hero_button_text",
@@ -65,7 +65,7 @@
{ {
"slug": "feature_1_description", "slug": "feature_1_description",
"label": "Feature 1 Description", "label": "Feature 1 Description",
"type": "text" "type": "richtext"
}, },
{ {
"slug": "feature_1_icon", "slug": "feature_1_icon",
@@ -80,7 +80,7 @@
{ {
"slug": "feature_2_description", "slug": "feature_2_description",
"label": "Feature 2 Description", "label": "Feature 2 Description",
"type": "text" "type": "richtext"
}, },
{ {
"slug": "feature_2_icon", "slug": "feature_2_icon",
@@ -95,7 +95,7 @@
{ {
"slug": "feature_3_description", "slug": "feature_3_description",
"label": "Feature 3 Description", "label": "Feature 3 Description",
"type": "text" "type": "richtext"
}, },
{ {
"slug": "feature_3_icon", "slug": "feature_3_icon",
@@ -110,7 +110,7 @@
{ {
"slug": "feature_4_description", "slug": "feature_4_description",
"label": "Feature 4 Description", "label": "Feature 4 Description",
"type": "text" "type": "richtext"
}, },
{ {
"slug": "feature_4_icon", "slug": "feature_4_icon",
@@ -125,7 +125,7 @@
{ {
"slug": "feature_5_description", "slug": "feature_5_description",
"label": "Feature 5 Description", "label": "Feature 5 Description",
"type": "text" "type": "richtext"
}, },
{ {
"slug": "feature_5_icon", "slug": "feature_5_icon",
@@ -140,7 +140,7 @@
{ {
"slug": "feature_6_description", "slug": "feature_6_description",
"label": "Feature 6 Description", "label": "Feature 6 Description",
"type": "text" "type": "richtext"
}, },
{ {
"slug": "feature_6_icon", "slug": "feature_6_icon",
@@ -155,7 +155,7 @@
{ {
"slug": "comparison_subtitle", "slug": "comparison_subtitle",
"label": "Comparison Section Subtitle", "label": "Comparison Section Subtitle",
"type": "text" "type": "richtext"
}, },
{ {
"slug": "cta_title", "slug": "cta_title",
@@ -165,7 +165,7 @@
{ {
"slug": "cta_subtitle", "slug": "cta_subtitle",
"label": "CTA Subtitle", "label": "CTA Subtitle",
"type": "text" "type": "richtext"
}, },
{ {
"slug": "cta_button_text", "slug": "cta_button_text",
@@ -180,12 +180,12 @@
{ {
"slug": "footer_tagline", "slug": "footer_tagline",
"label": "Footer Tagline", "label": "Footer Tagline",
"type": "text" "type": "richtext"
}, },
{ {
"slug": "footer_about_text", "slug": "footer_about_text",
"label": "Footer About Text", "label": "Footer About Text",
"type": "text" "type": "richtext"
}, },
{ {
"slug": "footer_copyright", "slug": "footer_copyright",
@@ -490,7 +490,20 @@
"status": "published", "status": "published",
"data": { "data": {
"hero_headline": "The CMS that\nruns on your server", "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_text": "Open Admin Panel",
"hero_button_url": "/_emdash/admin", "hero_button_url": "/_emdash/admin",
"hero_image": { "hero_image": {
@@ -501,31 +514,161 @@
"features_section_title": "Everything you need", "features_section_title": "Everything you need",
"features_section_subtitle": "A complete CMS without the vendor lock-in", "features_section_subtitle": "A complete CMS without the vendor lock-in",
"feature_1_title": "Fully Self-Hosted", "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_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_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_2_icon": "M3 3h18v18H3zM3 9h18M9 21V9",
"feature_3_title": "Passkey Auth", "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_3_icon": "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z",
"feature_4_title": "Built-in MCP", "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_4_icon": "M12 6v6l4 2",
"feature_5_title": "Plugin System", "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_5_icon": "M16 18l6-6-6-6M8 6l-6 6 6 6",
"feature_6_title": "WordPress Import", "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", "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_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_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_text": "Try Admin Panel",
"cta_button_url": "/_emdash/admin", "cta_button_url": "/_emdash/admin",
"footer_tagline": "Thoughts on building for the web", "footer_tagline": [
"footer_about_text": "A blog about software, design, and the occasional stray thought.", {
"_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" "footer_copyright": "Powered by EmDash CMS"
} }
} }

View File

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