Files
emdash-blog-template/src/pages/index.astro
Kunthawat Greethong be63c72048 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>
2026-04-30 13:40:49 +07:00

524 lines
15 KiB
Plaintext

---
import { getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
import Base from "../layouts/Base.astro";
const { entry: homepage } = await getEmDashEntry("homepage", "main");
Astro.cache.set(homepage.cacheHint);
const d = homepage.data;
---
<Base title="EmDash CMS - Self-Hosted for Astro" description="Fully self-hosted CMS for Astro. No cloud required, fully local, with admin panel, authentication, and plugin system."
content={{ collection: "homepage", id: homepage.id, slug: "main" }}
>
<main class="landing">
<!-- Hero Section -->
<section class="hero">
<div class="hero-content">
<div class="badge">Open Source • 10k+ Stars</div>
<h1 class="hero-title" {...homepage.edit.hero_headline}>
{d.hero_headline.split('\n').map((line: string) => <><span>{line}</span><br /></>)}
</h1>
<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">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18M9 21V9"/>
</svg>
</span>
{d.hero_button_text}
</a>
<a href="https://github.com/emdash-cms/emdash" class="btn btn-secondary" target="_blank">
View on GitHub
</a>
</div>
</div>
<div class="hero-visual">
<div class="code-window">
<div class="code-header">
<span class="dot red"></span>
<span class="dot yellow"></span>
<span class="dot green"></span>
<span class="code-title">astro.config.mjs</span>
</div>
<pre class="code-content"><code><span class="keyword">import</span> emdash <span class="keyword">from</span> <span class="string">"emdash/astro"</span>;
<span class="keyword">import</span> &#123; betterSqlite &#125; <span class="keyword">from</span> <span class="string">"emdash/db"</span>;
<span class="keyword">export default</span> defineConfig(&#123;
<span class="property">integrations</span>: [
emdash(&#123;
<span class="property">database</span>: betterSqlite(&#123;
<span class="property">databasePath</span>: <span class="string">"./data.db"</span>
&#125;),
&#125;),
],
&#125;);</code></pre>
</div>
</div>
</section>
<!-- Features Section -->
<section class="features" id="features">
<div class="section-header">
<h2 class="section-title" {...homepage.edit.features_section_title}>{d.features_section_title}</h2>
<p class="section-subtitle" {...homepage.edit.features_section_subtitle}>{d.features_section_subtitle}</p>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
<path d="M2 17l10 5 10-5"/>
<path d="M2 12l10 5 10-5"/>
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_1_title}>{d.feature_1_title}</h3>
<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">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18M9 21V9"/>
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_2_title}>{d.feature_2_title}</h3>
<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">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_3_title}>{d.feature_3_title}</h3>
<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">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v6l4 2"/>
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_4_title}>{d.feature_4_title}</h3>
<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">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6"/>
<polyline points="8 6 2 12 8 18"/>
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_5_title}>{d.feature_5_title}</h3>
<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">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
</svg>
</div>
<h3 class="feature-title" {...homepage.edit.feature_6_title}>{d.feature_6_title}</h3>
<div class="feature-desc" {...homepage.edit.feature_6_description}><PortableText value={d.feature_6_description} /></div>
</div>
</div>
</section>
<!-- Comparison Section -->
<section class="comparison">
<div class="section-header">
<h2 class="section-title" {...homepage.edit.comparison_title}>{d.comparison_title}</h2>
<div class="section-subtitle" {...homepage.edit.comparison_subtitle}><PortableText value={d.comparison_subtitle} /></div>
</div>
<div class="comparison-table">
<div class="comparison-header">
<div class="comparison-cell header">Feature</div>
<div class="comparison-cell header">EmDash</div>
<div class="comparison-cell header">Tina CMS</div>
</div>
<div class="comparison-row">
<div class="comparison-cell label">Self-hosted</div>
<div class="comparison-cell emdash">Fully local (SQLite)</div>
<div class="comparison-cell tina">Needs Tina Cloud</div>
</div>
<div class="comparison-row">
<div class="comparison-cell label">Admin URL</div>
<div class="comparison-cell emdash">/_emdash/admin</div>
<div class="comparison-cell tina">/admin</div>
</div>
<div class="comparison-row">
<div class="comparison-cell label">Database</div>
<div class="comparison-cell emdash">SQLite, D1, PostgreSQL</div>
<div class="comparison-cell tina">Git-based</div>
</div>
<div class="comparison-row">
<div class="comparison-cell label">Setup</div>
<div class="comparison-cell emdash">Template-based</div>
<div class="comparison-cell tina">Manual config</div>
</div>
<div class="comparison-row">
<div class="comparison-cell label">Auth</div>
<div class="comparison-cell emdash">Passkey + OAuth</div>
<div class="comparison-cell tina">Git-based</div>
</div>
<div class="comparison-row">
<div class="comparison-cell label">Price</div>
<div class="comparison-cell emdash">Free (open source)</div>
<div class="comparison-cell tina">Free tier + paid plans</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="cta">
<div class="cta-content">
<h2 class="cta-title" {...homepage.edit.cta_title}>{d.cta_title}</h2>
<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>
</div>
</section>
</main>
</Base>
<style>
.landing {
--color-bg: #0a0a0f;
--color-surface: #14141f;
--color-surface-hover: #1a1a2e;
--color-border: #2a2a3e;
--color-text: #f0f0f5;
--color-text-secondary: #a0a0b0;
--color-muted: #6a6a80;
--color-accent: #6366f1;
--color-accent-hover: #818cf8;
--color-emdash: #10b981;
--color-tina: #f59e0b;
background: var(--color-bg);
color: var(--color-text);
min-height: 100vh;
}
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
max-width: 1200px;
margin: 0 auto;
padding: 6rem 2rem;
align-items: center;
}
.badge {
display: inline-block;
padding: 0.5rem 1rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 2rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
margin-bottom: 1.5rem;
}
.hero-title {
font-size: 3.5rem;
font-weight: 700;
line-height: 1.1;
margin-bottom: 1.5rem;
letter-spacing: -0.02em;
}
.accent {
color: var(--color-emdash);
}
.hero-subtitle {
font-size: 1.25rem;
line-height: 1.7;
color: var(--color-text-secondary);
margin-bottom: 2rem;
}
.hero-actions {
display: flex;
gap: 1rem;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.875rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
text-decoration: none;
transition: all 0.2s;
}
.btn-primary {
background: var(--color-emdash);
color: #0a0a0f;
}
.btn-primary:hover {
background: #059669;
}
.btn-secondary {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
}
.btn-secondary:hover {
background: var(--color-surface-hover);
}
.btn-large {
padding: 1rem 2rem;
font-size: 1.125rem;
}
.code-window {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
overflow: hidden;
}
.code-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: var(--color-surface-hover);
border-bottom: 1px solid var(--color-border);
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.dot.red { background: #ef4444; }
.dot.yellow { background: #eab308; }
.dot.green { background: #22c55e; }
.code-title {
margin-left: auto;
font-size: 0.75rem;
color: var(--color-muted);
}
.code-content {
padding: 1.5rem;
margin: 0;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.875rem;
line-height: 1.6;
overflow-x: auto;
}
.keyword { color: #c678dd; }
.string { color: #98c379; }
.property { color: #e5c07b; }
.features {
padding: 6rem 2rem;
max-width: 1200px;
margin: 0 auto;
}
.section-header {
text-align: center;
margin-bottom: 4rem;
}
.section-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
}
.section-subtitle {
font-size: 1.25rem;
color: var(--color-text-secondary);
}
.features-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
.feature-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
transition: all 0.2s;
}
.feature-card:hover {
border-color: var(--color-emdash);
transform: translateY(-4px);
}
.feature-icon {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--color-emdash), #059669);
border-radius: 0.75rem;
margin-bottom: 1.5rem;
color: white;
}
.feature-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.75rem;
}
.feature-desc {
color: var(--color-text-secondary);
line-height: 1.6;
}
.comparison {
padding: 6rem 2rem;
max-width: 1000px;
margin: 0 auto;
}
.comparison-table {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 1rem;
overflow: hidden;
}
.comparison-header {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr;
background: var(--color-surface-hover);
border-bottom: 1px solid var(--color-border);
}
.comparison-row {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr;
border-bottom: 1px solid var(--color-border);
}
.comparison-row:last-child {
border-bottom: none;
}
.comparison-cell {
padding: 1rem 1.5rem;
font-size: 0.9375rem;
}
.comparison-cell.header {
font-weight: 600;
color: var(--color-muted);
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
}
.comparison-cell.label {
color: var(--color-text-secondary);
}
.comparison-cell.emdash {
color: var(--color-emdash);
font-weight: 500;
}
.comparison-cell.tina {
color: var(--color-tina);
font-weight: 500;
}
.cta {
padding: 8rem 2rem;
text-align: center;
background: linear-gradient(180deg, var(--color-bg) 0%, var(--color-surface) 100%);
}
.cta-content {
max-width: 600px;
margin: 0 auto;
}
.cta-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
}
.cta-subtitle {
font-size: 1.25rem;
color: var(--color-text-secondary);
margin-bottom: 2rem;
}
@media (max-width: 900px) {
.hero {
grid-template-columns: 1fr;
padding: 4rem 2rem;
text-align: center;
}
.hero-actions {
justify-content: center;
}
.hero-title {
font-size: 2.5rem;
}
.features-grid {
grid-template-columns: repeat(2, 1fr);
}
.comparison-header,
.comparison-row {
grid-template-columns: 1fr 1fr;
}
.comparison-cell.header:last-child,
.comparison-cell:last-child {
display: none;
}
}
@media (max-width: 600px) {
.hero-title {
font-size: 2rem;
}
.hero-actions {
flex-direction: column;
}
.features-grid {
grid-template-columns: 1fr;
}
}
</style>