Emdash source with visual editor image upload fix
Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
123
skills/wordpress-theme-to-emdash/scaffold/CHECKLIST.md
Normal file
123
skills/wordpress-theme-to-emdash/scaffold/CHECKLIST.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Theme Porting Checklist
|
||||
|
||||
Use this checklist to track progress when porting a WordPress theme to EmDash.
|
||||
|
||||
## Phase 1: Discovery & Reference Capture
|
||||
|
||||
- [ ] Theme source downloaded and unzipped
|
||||
- [ ] Demo site URL identified
|
||||
- [ ] Created `discovery/` folder structure:
|
||||
- [ ] `discovery/screenshots/`
|
||||
- [ ] `discovery/images/`
|
||||
- [ ] `discovery/notes.md`
|
||||
- [ ] Identified all page types in demo
|
||||
- [ ] Screenshots captured:
|
||||
- [ ] Homepage (`discovery/screenshots/homepage.png`)
|
||||
- [ ] Single post (`discovery/screenshots/single-post.png`)
|
||||
- [ ] Blog archive (`discovery/screenshots/archive.png`)
|
||||
- [ ] Category archive (`discovery/screenshots/category.png`)
|
||||
- [ ] Static page (`discovery/screenshots/page.png`)
|
||||
- [ ] 404 page (`discovery/screenshots/404.png`)
|
||||
- [ ] Sample images downloaded to `discovery/images/`
|
||||
- [ ] Design notes documented in `discovery/notes.md`:
|
||||
- [ ] Colors (background, text, primary, accent, borders)
|
||||
- [ ] Typography (font families, sizes, line heights)
|
||||
- [ ] Layout (content width, header height, sidebar position)
|
||||
- [ ] Special components to recreate
|
||||
|
||||
## Phase 2: Design Extraction
|
||||
|
||||
- [ ] CSS variables defined in `src/styles/global.css`:
|
||||
- [ ] Color palette (`--color-base`, `--color-contrast`, `--color-primary`, etc.)
|
||||
- [ ] Typography (`--font-body`, `--font-heading`, `--font-mono`)
|
||||
- [ ] Font sizes (`--text-sm` through `--text-5xl`)
|
||||
- [ ] Spacing scale (`--space-1` through `--space-24`)
|
||||
- [ ] Layout (`--content-width`, `--wide-width`, `--header-height`)
|
||||
- [ ] Fonts loading correctly (Google Fonts or local)
|
||||
- [ ] Color scheme matches original demo
|
||||
- [ ] Responsive breakpoints defined
|
||||
|
||||
## Phase 3: Template Conversion
|
||||
|
||||
- [ ] Base layout created (`src/layouts/Base.astro`)
|
||||
- [ ] Homepage (`src/pages/index.astro`)
|
||||
- [ ] Single post (`src/pages/posts/[slug].astro`)
|
||||
- [ ] Blog archive (`src/pages/posts/index.astro`)
|
||||
- [ ] Category archive (`src/pages/categories/[slug].astro`)
|
||||
- [ ] Tag archive (`src/pages/tags/[slug].astro`)
|
||||
- [ ] Static pages (`src/pages/pages/[slug].astro`)
|
||||
- [ ] 404 page (`src/pages/404.astro`)
|
||||
- [ ] Reusable components extracted (PostCard, etc.)
|
||||
|
||||
## Phase 4: Dynamic Features
|
||||
|
||||
- [ ] Site settings configured:
|
||||
- [ ] Title and tagline
|
||||
- [ ] Logo (if applicable)
|
||||
- [ ] Favicon
|
||||
- [ ] Navigation menus:
|
||||
- [ ] Primary menu
|
||||
- [ ] Footer menu (if applicable)
|
||||
- [ ] Mobile menu (if different)
|
||||
- [ ] Taxonomies:
|
||||
- [ ] Categories
|
||||
- [ ] Tags
|
||||
- [ ] Custom taxonomies (if any)
|
||||
- [ ] Widget areas (if applicable):
|
||||
- [ ] Sidebar
|
||||
- [ ] Footer widgets
|
||||
|
||||
## Phase 5: Create Seed File
|
||||
|
||||
- [ ] Seed file created (`.emdash/seed.json`)
|
||||
- [ ] Collections defined with all fields
|
||||
- [ ] Taxonomies defined with sample terms
|
||||
- [ ] Menus defined with items
|
||||
- [ ] Sample content created:
|
||||
- [ ] Posts (3-5 with varied content)
|
||||
- [ ] Pages (About, Contact, etc.)
|
||||
- [ ] Images use `$media` syntax with `discovery/images/` files
|
||||
- [ ] Seed validates: `emdash seed --validate`
|
||||
|
||||
## Phase 6: Verify & Iterate
|
||||
|
||||
- [ ] Seed applied successfully: `emdash seed`
|
||||
- [ ] Dev server running: `pnpm dev`
|
||||
- [ ] Output screenshots captured to `output/`:
|
||||
- [ ] Homepage
|
||||
- [ ] Single post
|
||||
- [ ] Blog archive
|
||||
- [ ] Category archive
|
||||
- [ ] Static page
|
||||
- [ ] 404 page
|
||||
- [ ] Visual comparison completed for each page
|
||||
- [ ] Differences identified and fixed
|
||||
- [ ] Production build succeeds: `pnpm build`
|
||||
|
||||
## License Compliance
|
||||
|
||||
- [ ] `README.md` credits original theme
|
||||
|
||||
If the original theme is GPL-licensed:
|
||||
|
||||
- [ ] `LICENSE` file added (GPL-2.0 text)
|
||||
- [ ] `package.json` has `"license": "GPL-2.0-or-later"`
|
||||
|
||||
## Final Review
|
||||
|
||||
- [ ] All pages render without errors
|
||||
- [ ] Mobile responsive design works
|
||||
- [ ] Navigation works on all pages
|
||||
- [ ] Images load correctly
|
||||
- [ ] Typography matches design intent
|
||||
- [ ] Colors match design intent
|
||||
- [ ] No console errors in browser
|
||||
|
||||
## No Hard-Coded Content
|
||||
|
||||
- [ ] Site title uses `settings.title`, not hard-coded string
|
||||
- [ ] Site tagline uses `settings.tagline`, not hard-coded string
|
||||
- [ ] Logo uses `settings.logo`, not hard-coded path
|
||||
- [ ] Navigation uses `getMenu()`, not hard-coded `<a>` tags
|
||||
- [ ] Footer content uses site settings or widget areas
|
||||
- [ ] No placeholder text like "My Blog" or "Lorem ipsum" in templates
|
||||
101
skills/wordpress-theme-to-emdash/scaffold/README.md
Normal file
101
skills/wordpress-theme-to-emdash/scaffold/README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# EmDash Theme Scaffold
|
||||
|
||||
This is a minimal, working EmDash theme that demonstrates correct patterns for:
|
||||
|
||||
- **Site settings** - Use `getSiteSettings()` for title, tagline, logo - never hard-code
|
||||
- **Menus** - Use `getMenu()` for navigation - never hard-code links
|
||||
- **Image fields** - Always access `.src` and `.alt`, never the field directly
|
||||
- **Taxonomy terms** - Use `getEntryTerms()` without a db parameter
|
||||
- **PortableText** - Use the `<PortableText>` component from `emdash/ui`
|
||||
|
||||
## Critical: No Hard-Coded Content
|
||||
|
||||
The theme is a shell that displays CMS content. Never hard-code:
|
||||
|
||||
- Site title or tagline (use `settings.title`, `settings.tagline`)
|
||||
- Navigation links (use `getMenu("primary")`)
|
||||
- Logo or favicon (use `settings.logo`, `settings.favicon`)
|
||||
- Footer content (use site settings or widget areas)
|
||||
|
||||
## Usage
|
||||
|
||||
When porting a WordPress theme:
|
||||
|
||||
1. Copy this scaffold to your theme directory
|
||||
2. Run `pnpm install` from monorepo root
|
||||
3. Verify it builds: `pnpm --filter your-theme build`
|
||||
4. Use these templates as reference for correct API usage
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Image Fields
|
||||
|
||||
```astro
|
||||
{/* CORRECT - check .src exists */}
|
||||
{post.data.featured_image?.src && (
|
||||
<img
|
||||
src={post.data.featured_image.src}
|
||||
alt={post.data.featured_image.alt || post.data.title}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* WRONG - field is an object, not a string */}
|
||||
{post.data.featured_image && (
|
||||
<img src={post.data.featured_image} /> // Renders [object Object]
|
||||
)}
|
||||
```
|
||||
|
||||
### Taxonomy Terms
|
||||
|
||||
```astro
|
||||
{/* CORRECT - no db parameter */}
|
||||
const categories = await getEntryTerms("posts", post.id, "categories");
|
||||
|
||||
{/* WRONG - db is not a parameter */}
|
||||
const categories = await getEntryTerms("posts", post.id, "categories", db);
|
||||
```
|
||||
|
||||
### Seed File Images
|
||||
|
||||
```json
|
||||
{
|
||||
"featured_image": {
|
||||
"$media": {
|
||||
"url": "https://example.com/image.jpg",
|
||||
"alt": "Description",
|
||||
"filename": "image.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
At runtime, this becomes `{ src: "...", alt: "..." }`.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
scaffold/
|
||||
├── package.json # Working dependency versions
|
||||
├── astro.config.mjs # Minimal config
|
||||
├── tsconfig.json
|
||||
├── src/
|
||||
│ ├── env.d.ts
|
||||
│ ├── live.config.ts # Collection loader setup
|
||||
│ ├── styles/global.css # Minimal styles with comments
|
||||
│ ├── layouts/Base.astro # Header, footer, menus
|
||||
│ ├── components/
|
||||
│ │ └── PostCard.astro # Image field handling example
|
||||
│ └── pages/
|
||||
│ ├── index.astro
|
||||
│ ├── 404.astro
|
||||
│ ├── posts/
|
||||
│ │ ├── index.astro
|
||||
│ │ └── [slug].astro # Taxonomy terms example
|
||||
│ ├── pages/[slug].astro
|
||||
│ ├── categories/[slug].astro
|
||||
│ └── tags/[slug].astro
|
||||
├── public/
|
||||
│ └── favicon.svg
|
||||
└── .emdash/
|
||||
└── seed.json # All field types demonstrated
|
||||
```
|
||||
27
skills/wordpress-theme-to-emdash/scaffold/astro.config.mjs
Normal file
27
skills/wordpress-theme-to-emdash/scaffold/astro.config.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
// @ts-check
|
||||
import node from "@astrojs/node";
|
||||
import react from "@astrojs/react";
|
||||
import { defineConfig } from "astro/config";
|
||||
import emdash from "emdash/astro";
|
||||
import { sqlite } from "emdash/db";
|
||||
|
||||
export default defineConfig({
|
||||
output: "server",
|
||||
adapter: node({ mode: "standalone" }),
|
||||
integrations: [
|
||||
react(),
|
||||
emdash({
|
||||
database: sqlite({ url: "file:./data.db" }),
|
||||
}),
|
||||
],
|
||||
// Optional: Add custom fonts
|
||||
// experimental: {
|
||||
// fonts: [
|
||||
// {
|
||||
// provider: "google",
|
||||
// family: "Inter",
|
||||
// weights: [400, 500, 600, 700],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
});
|
||||
26
skills/wordpress-theme-to-emdash/scaffold/package.json
Normal file
26
skills/wordpress-theme-to-emdash/scaffold/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@emdash-cms/theme-scaffold",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"check": "astro check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^10.0.0-beta.0",
|
||||
"@astrojs/react": "^5.0.0-beta.1",
|
||||
"astro": "^6.0.0-beta.0",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"emdash": "workspace:*",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.6",
|
||||
"@types/node": "^24.10.9",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" fill="#0066cc" rx="6"/>
|
||||
<text x="16" y="22" font-family="system-ui, sans-serif" font-size="16" font-weight="600" fill="white" text-anchor="middle">E</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 257 B |
@@ -0,0 +1,52 @@
|
||||
---
|
||||
/**
|
||||
* PostCard Component
|
||||
*
|
||||
* Displays a post preview with optional featured image.
|
||||
*
|
||||
* IMPORTANT: Image fields are objects with { src, alt }, not strings!
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
href: string;
|
||||
date?: string;
|
||||
excerpt?: string;
|
||||
// Image fields from EmDash are always { src?: string, alt?: string }
|
||||
featuredImage?: {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const { title, href, date, excerpt, featuredImage } = Astro.props;
|
||||
|
||||
const formattedDate = date
|
||||
? new Date(date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: null;
|
||||
---
|
||||
|
||||
<article class="post-card">
|
||||
{/* Check featuredImage.src, not just featuredImage */}
|
||||
{
|
||||
featuredImage?.src && (
|
||||
<a href={href} class="post-card-image">
|
||||
<img src={featuredImage.src} alt={featuredImage.alt || title} loading="lazy" />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="post-card-content">
|
||||
<h2 class="post-card-title">
|
||||
<a href={href}>{title}</a>
|
||||
</h2>
|
||||
|
||||
{formattedDate && <p class="post-card-meta">{formattedDate}</p>}
|
||||
|
||||
{excerpt && <p class="post-card-excerpt">{excerpt}</p>}
|
||||
</div>
|
||||
</article>
|
||||
2
skills/wordpress-theme-to-emdash/scaffold/src/env.d.ts
vendored
Normal file
2
skills/wordpress-theme-to-emdash/scaffold/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
/**
|
||||
* Base Layout
|
||||
*
|
||||
* Main layout with header, footer, and navigation from EmDash menus.
|
||||
*/
|
||||
|
||||
import { getMenu, getSiteSettings } from "emdash";
|
||||
import "../styles/global.css";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
|
||||
// These APIs automatically get the database from the Astro context
|
||||
const settings = await getSiteSettings();
|
||||
const primaryMenu = await getMenu("primary");
|
||||
const footerMenu = await getMenu("footer");
|
||||
|
||||
const siteTitle = settings.title || "My Site";
|
||||
const pageTitle = title ? `${title} | ${siteTitle}` : siteTitle;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{pageTitle}</title>
|
||||
{description && <meta name="description" content={description} />}
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<a href="/" class="site-title">{siteTitle}</a>
|
||||
|
||||
{
|
||||
primaryMenu && primaryMenu.items.length > 0 && (
|
||||
<nav class="site-nav">
|
||||
{primaryMenu.items.map((item) => (
|
||||
<a href={item.url}>{item.label}</a>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
{
|
||||
footerMenu && footerMenu.items.length > 0 && (
|
||||
<nav class="footer-nav">
|
||||
{footerMenu.items.map((item) => (
|
||||
<a href={item.url}>{item.label}</a>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
<p class="copyright">© {new Date().getFullYear()} {siteTitle}</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
19
skills/wordpress-theme-to-emdash/scaffold/src/live.config.ts
Normal file
19
skills/wordpress-theme-to-emdash/scaffold/src/live.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* EmDash Live Config
|
||||
*
|
||||
* This file defines your content collections using EmDash's loader.
|
||||
* It replaces Astro's content collections for CMS-managed content.
|
||||
*/
|
||||
|
||||
import { defineCollection } from "astro:content";
|
||||
import { emdashLoader } from "emdash";
|
||||
|
||||
// Posts collection - loaded from EmDash CMS
|
||||
export const collections = {
|
||||
posts: defineCollection({
|
||||
loader: emdashLoader({ collection: "posts" }),
|
||||
}),
|
||||
pages: defineCollection({
|
||||
loader: emdashLoader({ collection: "pages" }),
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
/**
|
||||
* 404 Page
|
||||
*/
|
||||
|
||||
import Base from "../layouts/Base.astro";
|
||||
---
|
||||
|
||||
<Base title="Page Not Found">
|
||||
<div class="container">
|
||||
<div class="content-width" style="text-align: center; padding: 4rem 0;">
|
||||
<h1>404</h1>
|
||||
<p class="text-muted">The page you're looking for doesn't exist.</p>
|
||||
<p><a href="/">Go home</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
/**
|
||||
* Category Archive
|
||||
*
|
||||
* Demonstrates:
|
||||
* - getTerm for fetching taxonomy term details
|
||||
* - getEntriesByTerm for entries with a specific term
|
||||
*/
|
||||
|
||||
import { getTerm, getEntriesByTerm } from "emdash";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import PostCard from "../../components/PostCard.astro";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
const category = await getTerm("categories", slug!);
|
||||
const posts = await getEntriesByTerm("posts", "categories", slug!);
|
||||
|
||||
if (!category) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
---
|
||||
|
||||
<Base title={category.label}>
|
||||
<div class="container">
|
||||
<div class="content-width">
|
||||
<header class="archive-header">
|
||||
<h1>{category.label}</h1>
|
||||
</header>
|
||||
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<div class="posts-list">
|
||||
{posts.map((post) => (
|
||||
<PostCard
|
||||
title={post.data.title}
|
||||
href={`/posts/${post.data.slug || post.id}`}
|
||||
date={post.data.published_at}
|
||||
excerpt={post.data.excerpt}
|
||||
featuredImage={
|
||||
post.data.featured_image?.src
|
||||
? {
|
||||
src: post.data.featured_image.src,
|
||||
alt: post.data.featured_image.alt || post.data.title,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p class="text-muted">No posts in this category.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
/**
|
||||
* Homepage / Blog Index
|
||||
*
|
||||
* Demonstrates:
|
||||
* - getEmDashCollection for listing entries
|
||||
* - Passing image fields correctly to components
|
||||
*/
|
||||
|
||||
import { getEmDashCollection } from "emdash";
|
||||
import Base from "../layouts/Base.astro";
|
||||
import PostCard from "../components/PostCard.astro";
|
||||
|
||||
const { entries: posts } = await getEmDashCollection("posts", {
|
||||
status: "published",
|
||||
limit: 10,
|
||||
});
|
||||
---
|
||||
|
||||
<Base>
|
||||
<div class="container">
|
||||
<div class="content-width">
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<div class="posts-list">
|
||||
{posts.map((post) => (
|
||||
<PostCard
|
||||
title={post.data.title}
|
||||
href={`/posts/${post.data.slug || post.id}`}
|
||||
date={post.data.published_at}
|
||||
excerpt={post.data.excerpt}
|
||||
{
|
||||
/*
|
||||
IMPORTANT: featured_image is { src, alt }, not a string!
|
||||
Pass the whole object, or extract src/alt explicitly.
|
||||
*/ }
|
||||
featuredImage={
|
||||
post.data.featured_image?.src
|
||||
? {
|
||||
src: post.data.featured_image.src,
|
||||
alt: post.data.featured_image.alt || post.data.title,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p class="text-muted">No posts yet.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
/**
|
||||
* Single Page
|
||||
*/
|
||||
|
||||
import { getEmDashEntry } from "emdash";
|
||||
import { PortableText } from "emdash/ui";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
const { entry: page, error } = await getEmDashEntry("pages", slug!);
|
||||
|
||||
if (error) {
|
||||
return new Response("Server error", { status: 500 });
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
---
|
||||
|
||||
<Base title={page.data.title}>
|
||||
<article class="container">
|
||||
<div class="content-width">
|
||||
<header class="page-header">
|
||||
<h1>{page.data.title}</h1>
|
||||
</header>
|
||||
|
||||
<div class="prose">
|
||||
{page.data.content && <PortableText value={page.data.content} />}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Base>
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
/**
|
||||
* Single Post
|
||||
*
|
||||
* Demonstrates:
|
||||
* - getEmDashEntry for fetching a single entry
|
||||
* - getEntryTerms for taxonomy terms (NO db parameter!)
|
||||
* - PortableText component for rich content
|
||||
* - Proper image field access
|
||||
*/
|
||||
|
||||
import { getEmDashEntry, getEntryTerms } from "emdash";
|
||||
import { PortableText } from "emdash/ui";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
const { entry: post, error } = await getEmDashEntry("posts", slug!);
|
||||
|
||||
if (error) {
|
||||
return new Response("Server error", { status: 500 });
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
|
||||
// Get taxonomy terms - NOTE: no db parameter!
|
||||
const categories = await getEntryTerms("posts", post.id, "categories");
|
||||
const tags = await getEntryTerms("posts", post.id, "tags");
|
||||
|
||||
const formattedDate = post.data.published_at
|
||||
? new Date(post.data.published_at).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: null;
|
||||
---
|
||||
|
||||
<Base title={post.data.title} description={post.data.excerpt}>
|
||||
<article class="container">
|
||||
<div class="content-width">
|
||||
<header class="post-header">
|
||||
<h1>{post.data.title}</h1>
|
||||
|
||||
<div class="post-meta">
|
||||
{
|
||||
formattedDate && (
|
||||
<time datetime={post.data.published_at}>{formattedDate}</time>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
categories.length > 0 && (
|
||||
<span class="post-categories">
|
||||
in{" "}
|
||||
{categories.map((cat, i) => (
|
||||
<>
|
||||
{i > 0 && ", "}
|
||||
<a href={`/categories/${cat.slug}`}>{cat.label}</a>
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* IMPORTANT: Check .src, not just the field */}
|
||||
{
|
||||
post.data.featured_image?.src && (
|
||||
<div class="post-featured-image">
|
||||
<img
|
||||
src={post.data.featured_image.src}
|
||||
alt={post.data.featured_image.alt || post.data.title}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="prose">
|
||||
{post.data.content && <PortableText value={post.data.content} />}
|
||||
</div>
|
||||
|
||||
{
|
||||
tags.length > 0 && (
|
||||
<div class="post-tags">
|
||||
{tags.map((tag) => (
|
||||
<a href={`/tags/${tag.slug}`} class="tag">
|
||||
{tag.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
</Base>
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
/**
|
||||
* Posts Archive
|
||||
*/
|
||||
|
||||
import { getEmDashCollection } from "emdash";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import PostCard from "../../components/PostCard.astro";
|
||||
|
||||
const { entries: posts } = await getEmDashCollection("posts", {
|
||||
status: "published",
|
||||
});
|
||||
---
|
||||
|
||||
<Base title="Blog">
|
||||
<div class="container">
|
||||
<div class="content-width">
|
||||
<header class="archive-header">
|
||||
<h1>Blog</h1>
|
||||
</header>
|
||||
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<div class="posts-list">
|
||||
{posts.map((post) => (
|
||||
<PostCard
|
||||
title={post.data.title}
|
||||
href={`/posts/${post.data.slug || post.id}`}
|
||||
date={post.data.published_at}
|
||||
excerpt={post.data.excerpt}
|
||||
featuredImage={
|
||||
post.data.featured_image?.src
|
||||
? {
|
||||
src: post.data.featured_image.src,
|
||||
alt: post.data.featured_image.alt || post.data.title,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p class="text-muted">No posts found.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
/**
|
||||
* Tag Archive
|
||||
*/
|
||||
|
||||
import { getTerm, getEntriesByTerm } from "emdash";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import PostCard from "../../components/PostCard.astro";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
const tag = await getTerm("tags", slug!);
|
||||
const posts = await getEntriesByTerm("posts", "tags", slug!);
|
||||
|
||||
if (!tag) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
---
|
||||
|
||||
<Base title={`Tagged: ${tag.label}`}>
|
||||
<div class="container">
|
||||
<div class="content-width">
|
||||
<header class="archive-header">
|
||||
<p class="text-muted">Tagged</p>
|
||||
<h1>{tag.label}</h1>
|
||||
</header>
|
||||
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<div class="posts-list">
|
||||
{posts.map((post) => (
|
||||
<PostCard
|
||||
title={post.data.title}
|
||||
href={`/posts/${post.data.slug || post.id}`}
|
||||
date={post.data.published_at}
|
||||
excerpt={post.data.excerpt}
|
||||
featuredImage={
|
||||
post.data.featured_image?.src
|
||||
? {
|
||||
src: post.data.featured_image.src,
|
||||
alt: post.data.featured_image.alt || post.data.title,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p class="text-muted">No posts with this tag.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
274
skills/wordpress-theme-to-emdash/scaffold/src/styles/global.css
Normal file
274
skills/wordpress-theme-to-emdash/scaffold/src/styles/global.css
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Global Styles - Scaffold Theme
|
||||
*
|
||||
* Minimal styles for demonstration. Replace with your theme's design tokens.
|
||||
*/
|
||||
|
||||
/* Reset */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Variables - replace with your theme's tokens */
|
||||
:root {
|
||||
--color-text: #1a1a1a;
|
||||
--color-text-muted: #666;
|
||||
--color-bg: #fff;
|
||||
--color-border: #e5e5e5;
|
||||
--color-accent: #0066cc;
|
||||
|
||||
--font-body: system-ui, -apple-system, sans-serif;
|
||||
--font-mono: ui-monospace, monospace;
|
||||
|
||||
--space-sm: 0.5rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 2rem;
|
||||
--space-xl: 4rem;
|
||||
|
||||
--content-width: 40rem;
|
||||
--container-width: 60rem;
|
||||
}
|
||||
|
||||
/* Base */
|
||||
html {
|
||||
font-family: var(--font-body);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
padding: var(--space-xl) var(--space-md);
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
max-width: var(--container-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-md);
|
||||
}
|
||||
|
||||
.content-width {
|
||||
max-width: var(--content-width);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.site-header {
|
||||
padding: var(--space-md) 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.site-header .container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.site-nav {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.site-nav a {
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-nav a:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.site-footer {
|
||||
padding: var(--space-lg) 0;
|
||||
border-top: 1px solid var(--color-border);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.footer-nav a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.3;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
background: var(--color-border);
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Post Card */
|
||||
.post-card {
|
||||
margin-bottom: var(--space-lg);
|
||||
padding-bottom: var(--space-lg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.post-card:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.post-card-image {
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.post-card-image img {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.post-card-title {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.post-card-title a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-card-title a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.post-card-meta {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.post-card-excerpt {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Single Post/Page */
|
||||
.post-header,
|
||||
.page-header {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.post-featured-image {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.post-featured-image img {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Taxonomy terms */
|
||||
.post-categories a,
|
||||
.post-tags a {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.post-tags {
|
||||
margin-top: var(--space-lg);
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 0.875rem;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--color-border);
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.tag:hover {
|
||||
background: var(--color-text);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
/* Prose (PortableText content) */
|
||||
.prose > * + * {
|
||||
margin-top: var(--space-md);
|
||||
}
|
||||
|
||||
.prose h2,
|
||||
.prose h3,
|
||||
.prose h4 {
|
||||
margin-top: var(--space-lg);
|
||||
}
|
||||
|
||||
/* Archive header */
|
||||
.archive-header {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
/* Utility */
|
||||
.text-muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
8
skills/wordpress-theme-to-emdash/scaffold/tsconfig.json
Normal file
8
skills/wordpress-theme-to-emdash/scaffold/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": ["src/**/*", ".astro/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user