Files
kunthawat 2d1be52177 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
2026-05-03 10:44:54 +07:00

411 lines
15 KiB
Markdown

# WP Theme → EmDash Concept Mapping
## Template Hierarchy
| WP Template | Purpose | EmDash Equivalent |
| ------------------------- | ----------------------- | ------------------------------------------------------- |
| `index.php` | Fallback for everything | `src/pages/index.astro` |
| `front-page.php` | Static front page | `src/pages/index.astro` |
| `home.php` | Blog posts page | `src/pages/index.astro` or `src/pages/blog/index.astro` |
| `single.php` | Single post | `src/pages/posts/[slug].astro` |
| `single-{post_type}.php` | Custom post type single | `src/pages/{type}/[slug].astro` |
| `page.php` | Single page | `src/pages/pages/[slug].astro` |
| `page-{slug}.php` | Specific page template | `src/pages/pages/{slug}.astro` (static) |
| `archive.php` | Post archives | `src/pages/posts/index.astro` |
| `archive-{post_type}.php` | CPT archive | `src/pages/{type}/index.astro` |
| `category.php` | Category archive | `src/pages/categories/[slug].astro` |
| `tag.php` | Tag archive | `src/pages/tags/[slug].astro` |
| `author.php` | Author archive | `src/pages/authors/[slug].astro` |
| `date.php` | Date archive | `src/pages/archive/[year]/[month].astro` |
| `search.php` | Search results | `src/pages/search.astro` (use `search()` API) |
| `404.php` | Not found | `src/pages/404.astro` |
| `header.php` | Site header | Part of `src/layouts/Base.astro` |
| `footer.php` | Site footer | Part of `src/layouts/Base.astro` |
| `sidebar.php` | Sidebar widget area | Component: `src/components/Sidebar.astro` |
| `comments.php` | Comments template | Component or third-party (Giscus, etc.) |
## Template Parts
| WP Pattern | EmDash Pattern |
| ---------------------------------------------------------- | ---------------------------- |
| `get_template_part('content', 'post')` | `<PostCard />` component |
| `get_template_part('template-parts/header/site-branding')` | `<SiteBranding />` component |
| `template-parts/` directory | `src/components/` directory |
## Functions.php Registrations
### Navigation Menus
```php
// WordPress
register_nav_menus([
'primary' => 'Primary Menu',
'footer' => 'Footer Menu',
]);
```
EmDash has first-class menu support with automatic URL resolution:
```typescript
import { getMenu } from "emdash";
const primaryMenu = await getMenu("primary");
// Returns { id, name, label, items: MenuItem[] }
// Items have resolved URLs and support nesting
```
Menus are created via:
- Admin UI
- Seed files (JSON)
- WordPress import (automatic migration)
### Sidebars/Widget Areas
```php
// WordPress
register_sidebar([
'name' => 'Main Sidebar',
'id' => 'sidebar-1',
]);
```
EmDash has first-class widget area support:
```typescript
import { getWidgetArea } from "emdash";
const sidebar = await getWidgetArea("sidebar");
// Returns { id, name, label, widgets: Widget[] }
// Widgets can be content (Portable Text), menu, or component
```
Widget areas are created via:
- Admin UI
- Seed files (JSON)
- WordPress import (automatic migration)
### Theme Support
```php
// WordPress
add_theme_support('post-thumbnails');
add_theme_support('title-tag');
add_theme_support('custom-logo');
add_theme_support('post-formats');
```
EmDash equivalents:
- `post-thumbnails``featured_image` field on collections (automatic)
- `title-tag` → Astro handles `<title>` in layout
- `custom-logo``getSiteSetting("logo")` returns `{ mediaId, alt, url }`
- `post-formats` → Field on collection (select type)
### Custom Post Types
```php
// WordPress
register_post_type('portfolio', [...]);
```
EmDash: Create collection via admin UI or API. The collection will be created during content import if it doesn't exist.
## Template Tags → EmDash
### Content Retrieval
| WP Function | EmDash Equivalent |
| ----------------------------- | ------------------------------------------------- |
| `have_posts()` / `the_post()` | `getEmDashCollection()` |
| `get_post()` | `getEmDashEntry()` |
| `the_title()` | `post.data.title` |
| `the_content()` | `<PortableText value={post.data.content} />` |
| `the_excerpt()` | `post.data.excerpt` |
| `the_permalink()` | `/posts/${post.id}` or `/posts/${post.data.slug}` |
| `the_post_thumbnail()` | `post.data.featured_image` |
| `get_the_date()` | `post.data.publishedAt` |
| `get_the_author()` | `post.data.byline?.displayName` |
| `get_the_category()` | `getEntryTerms(coll, id, "categories")` |
| `get_the_tags()` | `getEntryTerms(coll, id, "tags")` |
### Taxonomies
| WP Function | EmDash Equivalent |
| ---------------------------- | -------------------------------------------------- |
| `get_categories()` | `getTaxonomyTerms("categories")` |
| `get_tags()` | `getTaxonomyTerms("tags")` |
| `get_terms($taxonomy)` | `getTaxonomyTerms(taxonomy)` |
| `get_term($id, $taxonomy)` | `getTerm(taxonomy, slug)` |
| `get_term_by('slug', ...)` | `getTerm(taxonomy, slug)` |
| `get_the_terms($post, $tax)` | `getEntryTerms(collection, entryId, taxonomy)` |
| `wp_get_post_categories()` | `getEntryTerms(collection, entryId, "categories")` |
| `wp_get_post_tags()` | `getEntryTerms(collection, entryId, "tags")` |
| `get_category_link($cat)` | `/categories/${term.slug}` |
| `get_tag_link($tag)` | `/tags/${term.slug}` |
EmDash supports hierarchical taxonomies (like categories) and flat taxonomies (like tags):
### Site Info
| WP Function | EmDash Equivalent |
| ------------------------- | --------------------------------------- |
| `bloginfo('name')` | `getSiteSetting("title")` |
| `bloginfo('description')` | `getSiteSetting("tagline")` |
| `home_url()` | `Astro.site` or `import.meta.env.SITE` |
| `get_theme_mod()` | `getSiteSetting(key)` or plugin storage |
| `get_option()` | `getSiteSetting(key)` or plugin storage |
| `get_custom_logo()` | `getSiteSetting("logo")` returns URL |
### Conditional Tags
| WP Function | Astro Equivalent |
| ----------------- | ---------------------------------- |
| `is_home()` | `Astro.url.pathname === '/'` |
| `is_front_page()` | `Astro.url.pathname === '/'` |
| `is_single()` | Check route pattern |
| `is_page()` | Check route pattern |
| `is_archive()` | Check route pattern |
| `is_category()` | Check route pattern |
| `is_search()` | `Astro.url.pathname === '/search'` |
| `is_404()` | N/A (404.astro handles this) |
### Media
| WP Function | EmDash Equivalent |
| --------------------------- | -------------------------- |
| `wp_get_attachment_image()` | `<img src={media.url} />` |
| `wp_get_attachment_url()` | `media.url` |
| `the_post_thumbnail()` | `post.data.featured_image` |
### Navigation
| WP Function | EmDash Equivalent |
| ------------------------ | ------------------------------------- |
| `wp_nav_menu()` | `getMenu("menu-name")` + render items |
| `wp_list_pages()` | Query pages collection or use menu |
| `the_posts_navigation()` | Custom pagination component |
| `the_posts_pagination()` | Custom pagination component |
| `get_nav_menu_items()` | `getMenu("name").items` |
## Hooks → EmDash Events
WordPress hooks don't have direct equivalents. Most hook functionality becomes:
1. **Astro middleware** - For request/response modification
2. **EmDash plugin hooks** - For content lifecycle events
3. **Build-time logic** - In Astro config or components
| WP Hook | EmDash Approach |
| -------------------- | ---------------------------------------- |
| `wp_head` | Add to `<head>` in layout |
| `wp_footer` | Add before `</body>` in layout |
| `the_content` filter | PortableText components |
| `pre_get_posts` | Query filters in `getEmDashCollection()` |
| `save_post` | EmDash plugin hook: `content:beforeSave` |
## Asset Enqueueing
```php
// WordPress
wp_enqueue_style('theme-style', get_stylesheet_uri());
wp_enqueue_script('theme-script', get_template_directory_uri() . '/js/main.js');
```
Astro:
```astro
---
// In layout or component
import '../styles/main.css';
import '../scripts/main.js';
---
<link rel="stylesheet" href="/styles/main.css" />
<script src="/scripts/main.js"></script>
```
Or use Astro's built-in bundling:
```astro
<style>
/* Scoped styles */
</style>
<script>
// Client-side JS
</script>
```
## Shortcodes → Portable Text Blocks
```php
// WordPress shortcode
add_shortcode('gallery', function($atts) {
return '<div class="gallery">...</div>';
});
// Usage: [gallery ids="1,2,3"]
```
EmDash: Custom Portable Text block type + component:
```astro
---
// GalleryBlock.astro
const { ids } = Astro.props;
---
<div class="gallery">
<!-- Render images -->
</div>
```
```astro
<PortableText
value={content}
components={{ gallery: GalleryBlock }}
/>
```
## Widgets → Widget Areas
EmDash has first-class widget support with `getWidgetArea()`:
```typescript
import { getWidgetArea } from "emdash";
const sidebar = await getWidgetArea("sidebar");
sidebar?.widgets.forEach((widget) => {
// widget.type: "content" | "menu" | "component"
});
```
### Widget Types
| WP Widget | EmDash Widget Type | Notes |
| ------------ | ------------------------------- | ----------------------------- |
| Text/HTML | `content` | Portable Text (rich content) |
| Custom Menu | `menu` | References menu by name |
| Recent Posts | `component` `core:recent-posts` | Built-in component with props |
| Categories | `component` `core:categories` | Built-in component |
| Tag Cloud | `component` `core:tag-cloud` | Built-in component |
| Search | `<LiveSearch />` component | Use `emdash/ui` LiveSearch |
| Archives | `component` `core:archives` | Built-in component |
### Core Widget Components
| Component ID | Props |
| ------------------- | ------------------------ |
| `core:recent-posts` | `limit`, `collection` |
| `core:categories` | `taxonomy`, `showCounts` |
| `core:tag-cloud` | `taxonomy`, `limit` |
| `core:search` | `placeholder` |
| `core:archives` | `collection`, `format` |
## Search
WordPress search maps to EmDash's FTS5-based search system:
```php
// WordPress search form
get_search_form();
// WordPress search query
$results = new WP_Query(['s' => 'hello world']);
```
EmDash:
```typescript
import { search } from "emdash";
import LiveSearch from "emdash/ui/search";
// Programmatic search
const results = await search("hello world", {
collections: ["posts", "pages"],
limit: 20,
});
// Or use the LiveSearch component
<LiveSearch placeholder="Search..." />
```
### Search Page Pattern
```astro
---
// src/pages/search.astro
import { search } from "emdash";
import Base from "../layouts/Base.astro";
const query = Astro.url.searchParams.get("q") || "";
const results = query ? await search(query, { limit: 20 }) : { results: [] };
---
<Base title={`Search: ${query}`}>
<h1>Search Results for "{query}"</h1>
{results.results.length === 0 ? (
<p>No results found.</p>
) : (
<ul>
{results.results.map(r => (
<li>
<a href={`/${r.collection}/${r.slug}`}>{r.title}</a>
<p set:html={r.snippet} />
</li>
))}
</ul>
)}
</Base>
```
### Search Features
| WordPress | EmDash |
| ---------------------------- | ------------------------------- |
| Basic keyword search | FTS5 with Porter stemming |
| Search all public post types | Per-collection search enable |
| `s` query parameter | `q` query parameter |
| Relevance sorting | BM25 ranking with field weights |
| Search widget | `<LiveSearch />` component |
**Note:** Search must be enabled per-collection in admin. Mark fields as "Searchable" to include them in the index.
## Reusable Blocks → Sections
WordPress reusable blocks (`wp_block` post type) map to EmDash sections:
```php
// WordPress - creating a reusable block
// Done via Gutenberg editor, saved as wp_block post type
```
EmDash:
```typescript
import { getSection, getSections } from "emdash";
// Get a specific section
const cta = await getSection("newsletter-cta");
// List sections by category
const heroes = await getSections({ category: "heroes" });
```
### Inserting Sections
In WordPress, you insert reusable blocks from the block inserter. In EmDash, editors use the `/section` slash command in the rich text editor.
### Section Sources
| Source | Origin |
| -------- | --------------------------------------- |
| `theme` | Defined in seed file (theme patterns) |
| `user` | Created by editors in admin |
| `import` | Imported from WordPress reusable blocks |
### Migration
WordPress `wp_block` posts are automatically imported as sections:
- Content converted from Gutenberg to Portable Text
- Placed in "Imported" category
- Source set to `"import"`