first commit
This commit is contained in:
84
skills/wordpress-theme-to-emdash/SKILL.md
Normal file
84
skills/wordpress-theme-to-emdash/SKILL.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: wordpress-theme-to-emdash
|
||||
description: Port WordPress themes to EmDash CMS. Use when asked to convert, migrate, or port a WordPress theme to EmDash, or when creating an EmDash site that should match an existing WordPress site's design. Handles design extraction, template conversion, and EmDash-specific features like menus, taxonomies, and widgets.
|
||||
---
|
||||
|
||||
# WordPress Theme to EmDash
|
||||
|
||||
Port WordPress themes to EmDash in six phases. **Read the phase file before starting each phase.**
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Copy scaffold first** - Start every theme by copying `scaffold/` from this skill
|
||||
2. **Take screenshots of demo** - Identify the demo URL and capture all page types using agent-browser before starting work
|
||||
3. **No hard-coded content** - Use `getSiteSettings()` for title/tagline, `getMenu()` for navigation
|
||||
4. **Server-rendered pages** - Never use `getStaticPaths()` for EmDash content
|
||||
5. **Astro 6** - Use `ClientRouter` not `ViewTransitions`, Zod 4 syntax, Node 22+
|
||||
6. **Use emdash Image component** - For all images, import Image from "emdash/ui"
|
||||
|
||||
## Phases
|
||||
|
||||
| Phase | File | Summary |
|
||||
| ----- | ----------------------- | ----------------------------------------------- |
|
||||
| 1 | `phases/1-discovery.md` | Download theme, screenshot demo, capture images |
|
||||
| 2 | `phases/2-design.md` | Extract CSS variables, fonts, colors |
|
||||
| 3 | `phases/3-templates.md` | Convert PHP templates to Astro |
|
||||
| 4 | `phases/4-dynamic.md` | Site settings, menus, taxonomies, widgets |
|
||||
| 5 | `phases/5-seed.md` | Create seed file with demo content |
|
||||
| 6 | `phases/6-verify.md` | Screenshot, compare, iterate, build |
|
||||
|
||||
## Checklist
|
||||
|
||||
### Setup
|
||||
|
||||
- [ ] Copy `scaffold/` to new theme directory. Unless otherwise specified by the user, make this a subdirectory of `themes/` and name it after the WordPress theme (e.g., `themes/twentytwentyfour/`).
|
||||
- [ ] Rename folder, update `package.json`
|
||||
- [ ] Verify build: `pnpm build`
|
||||
|
||||
### Phase 1: Discovery
|
||||
|
||||
- [ ] Theme source downloaded
|
||||
- [ ] Demo site identified
|
||||
- [ ] `discovery/` folder created with `screenshots/`, `images/`, `notes.md`
|
||||
- [ ] All page types screenshotted
|
||||
- [ ] Sample images downloaded
|
||||
|
||||
### Phase 2: Design
|
||||
|
||||
- [ ] CSS variables in `global.css`
|
||||
- [ ] Fonts loading
|
||||
- [ ] Colors match demo
|
||||
|
||||
### Phase 3: Templates
|
||||
|
||||
- [ ] Homepage, single post, archive, category, tag, page, 404
|
||||
- [ ] Components extracted (PostCard, etc.)
|
||||
|
||||
### Phase 4: Dynamic
|
||||
|
||||
- [ ] Site settings (title, tagline, logo from CMS)
|
||||
- [ ] Navigation menus (from CMS, not hard-coded)
|
||||
- [ ] Taxonomies
|
||||
- [ ] Widget areas (if applicable)
|
||||
|
||||
### Phase 5: Seed
|
||||
|
||||
- [ ] Seed file created with demo images
|
||||
- [ ] Validates: `emdash seed --validate`
|
||||
|
||||
### Phase 6: Verify
|
||||
|
||||
- [ ] Seed applied
|
||||
- [ ] Output screenshots captured
|
||||
- [ ] Visual comparison done
|
||||
- [ ] Build succeeds: `pnpm build`
|
||||
- [ ] LICENSE file downloaded (GPL-2.0 in most cases)
|
||||
- [ ] README credits original theme
|
||||
|
||||
## Reference Documents
|
||||
|
||||
- `references/astro-essentials.md` - Astro 6 patterns
|
||||
- `references/template-patterns.md` - PHP → Astro conversion
|
||||
- `references/concept-mapping.md` - WP → EmDash concepts
|
||||
- `references/emdash-api.md` - Full API reference
|
||||
- `references/design-extraction.md` - CSS extraction techniques
|
||||
149
skills/wordpress-theme-to-emdash/phases/1-discovery.md
Normal file
149
skills/wordpress-theme-to-emdash/phases/1-discovery.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Phase 1: Discovery & Reference Capture
|
||||
|
||||
Before writing any code, gather comprehensive reference materials from the demo site.
|
||||
|
||||
## 1.0 Create Discovery Folder
|
||||
|
||||
Create a `discovery/` folder in your theme directory to store all reference materials:
|
||||
|
||||
```
|
||||
discovery/
|
||||
├── screenshots/ # Reference screenshots from demo site
|
||||
│ ├── homepage.png
|
||||
│ ├── single-post.png
|
||||
│ ├── archive.png
|
||||
│ ├── category.png
|
||||
│ ├── page.png
|
||||
│ └── 404.png
|
||||
├── images/ # Sample images downloaded for seed content
|
||||
│ ├── featured-1.jpg
|
||||
│ ├── featured-2.jpg
|
||||
│ └── hero.jpg
|
||||
└── notes.md # Design decisions and observations
|
||||
```
|
||||
|
||||
The `notes.md` file should capture:
|
||||
|
||||
- Color values extracted from the demo
|
||||
- Font families and sizes observed
|
||||
- Layout patterns (header style, sidebar position, footer columns)
|
||||
- Special components or interactions to recreate
|
||||
- Anything that might be forgotten between sessions
|
||||
|
||||
## 1.1 Identify All Page Types
|
||||
|
||||
Identify the URL of the demo site for the WordPress theme you are converting. For wordpress.org themes, this is usually wp-themes.com/theme-name/. For other themes, use the "Live Preview" link. This may show it inside a frame; if so, ignore the frame and focus on the theme's actual content.
|
||||
|
||||
Use the agent-browser to explore the demo site to find every distinct page type:
|
||||
|
||||
- **Homepage** - Often has unique layout (hero, featured posts, etc.)
|
||||
- **Blog/Archive** - Post listing page
|
||||
- **Single Post** - Individual blog post with content
|
||||
- **Page** - Static page (About, Contact, etc.)
|
||||
- **Category/Tag Archive** - Taxonomy listing pages
|
||||
- **Search Results** - If the theme has custom search styling
|
||||
- **404 Page** - Error page styling
|
||||
|
||||
Use agent-browser to navigate the demo and discover pages:
|
||||
|
||||
```bash
|
||||
agent-browser open https://demo-site.com
|
||||
# Click around to find different page types
|
||||
# Check the navigation menu for page links
|
||||
# Look for "View all posts" or category links
|
||||
```
|
||||
|
||||
## 1.2 Screenshot All Page Types
|
||||
|
||||
Capture full-page screenshots of each page type to `discovery/screenshots/`:
|
||||
|
||||
```bash
|
||||
# Homepage
|
||||
agent-browser open https://demo-site.com
|
||||
agent-browser screenshot discovery/screenshots/homepage.png --full
|
||||
|
||||
# Single post (find a post with featured image and good content)
|
||||
agent-browser open https://demo-site.com/sample-post/
|
||||
agent-browser screenshot discovery/screenshots/single-post.png --full
|
||||
|
||||
# Blog archive
|
||||
agent-browser open https://demo-site.com/blog/
|
||||
agent-browser screenshot discovery/screenshots/archive.png --full
|
||||
|
||||
# Category page
|
||||
agent-browser open https://demo-site.com/category/news/
|
||||
agent-browser screenshot discovery/screenshots/category.png --full
|
||||
|
||||
# Static page
|
||||
agent-browser open https://demo-site.com/about/
|
||||
agent-browser screenshot discovery/screenshots/page.png --full
|
||||
|
||||
# 404 page
|
||||
agent-browser open https://demo-site.com/nonexistent-page-xyz/
|
||||
agent-browser screenshot discovery/screenshots/404.png --full
|
||||
```
|
||||
|
||||
## 1.3 Download Sample Images
|
||||
|
||||
If the theme is open source (GPL), download sample images from the demo to `discovery/images/`. This ensures visual consistency when comparing.
|
||||
|
||||
```bash
|
||||
# Find featured images in demo posts
|
||||
agent-browser eval "Array.from(document.querySelectorAll('article img')).map(i => i.src)"
|
||||
|
||||
# Download images for seed content
|
||||
curl -o discovery/images/featured-1.jpg "https://demo-site.com/wp-content/uploads/photo1.jpg"
|
||||
curl -o discovery/images/featured-2.jpg "https://demo-site.com/wp-content/uploads/photo2.jpg"
|
||||
```
|
||||
|
||||
For premium themes or when images aren't freely available, use Unsplash images that match the demo's visual style (same aspect ratios, similar subjects).
|
||||
|
||||
## 1.4 Document Page Structure
|
||||
|
||||
For each page type, document observations in `discovery/notes.md`:
|
||||
|
||||
- Header style (sticky? transparent? logo position?)
|
||||
- Sidebar presence and position
|
||||
- Footer layout (columns? widgets?)
|
||||
- Special components (hero sections, CTAs, etc.)
|
||||
- Color values (use browser DevTools color picker)
|
||||
- Font families and sizes
|
||||
- Spacing patterns
|
||||
|
||||
This inventory guides which templates and components you need to build, and preserves details that might be forgotten between sessions.
|
||||
|
||||
## Theme Source Discovery
|
||||
|
||||
### WordPress.org Themes
|
||||
|
||||
For themes on wordpress.org (e.g., `https://wordpress.org/themes/theme-name/`):
|
||||
|
||||
1. **Demo/Preview**: Click "Preview" button or visit `https://wp-themes.com/theme-name/`
|
||||
2. **Source Download**: The "Download" button provides a ZIP, or use:
|
||||
```bash
|
||||
curl -O https://downloads.wordpress.org/theme/theme-name.zip
|
||||
unzip theme-name.zip
|
||||
```
|
||||
3. **Theme Info**: The page includes author, version, tags, and description
|
||||
|
||||
### GitHub-Hosted Themes
|
||||
|
||||
1. **Source**: Clone or download the repository
|
||||
2. **Demo**: Check README for demo URL, or look for `Demo:` in theme description
|
||||
3. **Documentation**: Usually in README or `/docs` folder
|
||||
|
||||
### ThemeForest / Premium Themes
|
||||
|
||||
1. **Demo**: Use the "Live Preview" button on the product page
|
||||
2. **Source**: Requires purchase - ask the user to provide the unzipped theme files
|
||||
3. **Documentation**: Usually included in the download or linked from the product page
|
||||
|
||||
### Auto-Discovery
|
||||
|
||||
When given only a theme URL or name, derive URLs yourself:
|
||||
|
||||
1. Fetch the listing page to extract demo URL, download URL, and theme info
|
||||
2. Download the source (if freely available)
|
||||
3. Open the demo in agent-browser
|
||||
|
||||
Don't ask the user for URLs you can derive yourself.
|
||||
122
skills/wordpress-theme-to-emdash/phases/2-design.md
Normal file
122
skills/wordpress-theme-to-emdash/phases/2-design.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Phase 2: Design Extraction
|
||||
|
||||
Extract design tokens from the WordPress theme source and live demo.
|
||||
|
||||
## 2.1 Analyze the Live Site
|
||||
|
||||
Use `agent-browser` to extract computed styles:
|
||||
|
||||
```bash
|
||||
agent-browser eval "(() => {
|
||||
const body = getComputedStyle(document.body);
|
||||
const header = document.querySelector('header, .site-header');
|
||||
return JSON.stringify({
|
||||
body: {
|
||||
fontFamily: body.fontFamily,
|
||||
fontSize: body.fontSize,
|
||||
color: body.color,
|
||||
background: body.backgroundColor,
|
||||
},
|
||||
header: header ? {
|
||||
background: getComputedStyle(header).backgroundColor,
|
||||
height: getComputedStyle(header).height,
|
||||
} : null,
|
||||
}, null, 2);
|
||||
})()"
|
||||
```
|
||||
|
||||
## 2.2 Extract Design Tokens
|
||||
|
||||
Read the theme's CSS files. Look for:
|
||||
|
||||
```
|
||||
style.css # Main stylesheet (has theme header)
|
||||
assets/css/ # Additional stylesheets
|
||||
theme.json # Block themes (WP 5.9+) - structured design tokens
|
||||
```
|
||||
|
||||
### CSS Variable Mapping
|
||||
|
||||
| WP Pattern | EmDash Variable |
|
||||
| ---------------- | ------------------ |
|
||||
| Body font family | `--font-body` |
|
||||
| Heading font | `--font-heading` |
|
||||
| Primary color | `--color-primary` |
|
||||
| Background | `--color-base` |
|
||||
| Text color | `--color-contrast` |
|
||||
| Content width | `--content-width` |
|
||||
|
||||
### Block Theme (theme.json)
|
||||
|
||||
Block themes store design tokens in `theme.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": {
|
||||
"color": {
|
||||
"palette": [{ "slug": "primary", "color": "#0073aa", "name": "Primary" }]
|
||||
},
|
||||
"typography": {
|
||||
"fontFamilies": [{ "fontFamily": "'Open Sans', sans-serif", "slug": "body" }]
|
||||
},
|
||||
"layout": {
|
||||
"contentSize": "650px",
|
||||
"wideSize": "1200px"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2.3 Create Base Layout
|
||||
|
||||
Create `src/layouts/Base.astro` with:
|
||||
|
||||
- Extracted CSS variables in `:root`
|
||||
- Header/footer structure matching WP theme
|
||||
- Font loading (Google Fonts or local)
|
||||
- Responsive breakpoints
|
||||
|
||||
### CSS Variables Template
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Colors */
|
||||
--color-base: #ffffff;
|
||||
--color-contrast: #1a1a1a;
|
||||
--color-primary: #0073aa;
|
||||
--color-accent: #ff6b35;
|
||||
--color-muted: #6b7280;
|
||||
--color-border: #e5e7eb;
|
||||
|
||||
/* Typography */
|
||||
--font-body: system-ui, sans-serif;
|
||||
--font-heading: Georgia, serif;
|
||||
|
||||
/* Font sizes */
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
--text-3xl: 1.875rem;
|
||||
--text-4xl: 2.25rem;
|
||||
--text-5xl: clamp(2.5rem, 5vw, 3rem);
|
||||
|
||||
/* Spacing */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-4: 1rem;
|
||||
--space-6: 1.5rem;
|
||||
--space-8: 2rem;
|
||||
--space-12: 3rem;
|
||||
--space-16: 4rem;
|
||||
--space-24: 6rem;
|
||||
|
||||
/* Layout */
|
||||
--content-width: 720px;
|
||||
--wide-width: 1200px;
|
||||
--header-height: 80px;
|
||||
}
|
||||
```
|
||||
|
||||
See `references/design-extraction.md` for detailed extraction techniques.
|
||||
114
skills/wordpress-theme-to-emdash/phases/3-templates.md
Normal file
114
skills/wordpress-theme-to-emdash/phases/3-templates.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Phase 3: Template Conversion
|
||||
|
||||
Convert WordPress PHP templates to Astro components.
|
||||
|
||||
## 3.1 Analyze Theme Structure
|
||||
|
||||
Read `functions.php` to identify:
|
||||
|
||||
- `register_nav_menu()` → EmDash menus
|
||||
- `register_sidebar()` → EmDash widget areas
|
||||
- `add_theme_support()` → Features (thumbnails, formats, etc.)
|
||||
- `register_post_type()` → Collections
|
||||
- `register_taxonomy()` → EmDash taxonomy defs
|
||||
- `add_shortcode()` → Portable Text blocks
|
||||
|
||||
## 3.2 Template Mapping
|
||||
|
||||
| WP Template | Astro Route |
|
||||
| -------------- | ----------------------------------- |
|
||||
| `index.php` | `src/pages/index.astro` |
|
||||
| `single.php` | `src/pages/posts/[slug].astro` |
|
||||
| `page.php` | `src/pages/pages/[slug].astro` |
|
||||
| `archive.php` | `src/pages/posts/index.astro` |
|
||||
| `category.php` | `src/pages/categories/[slug].astro` |
|
||||
| `tag.php` | `src/pages/tags/[slug].astro` |
|
||||
| `search.php` | `src/pages/search.astro` |
|
||||
| `404.php` | `src/pages/404.astro` |
|
||||
| `header.php` | Component in layout |
|
||||
| `footer.php` | Component in layout |
|
||||
|
||||
## 3.3 Convert Templates
|
||||
|
||||
### The Loop → getEmDashCollection
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php while (have_posts()) : the_post(); ?>
|
||||
<h2><?php the_title(); ?></h2>
|
||||
<?php endwhile; ?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro/EmDash
|
||||
import { getEmDashCollection } from "emdash";
|
||||
const { entries: posts } = await getEmDashCollection("posts");
|
||||
---
|
||||
{posts.map(post => <h2>{post.data.title}</h2>)}
|
||||
```
|
||||
|
||||
### Single Post → getEmDashEntry
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php the_content(); ?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro/EmDash
|
||||
import { getEmDashEntry } from "emdash";
|
||||
import { PortableText } from "emdash/ui";
|
||||
const { entry: post } = await getEmDashEntry("posts", Astro.params.slug);
|
||||
---
|
||||
{post && <PortableText value={post.data.content} />}
|
||||
```
|
||||
|
||||
## 3.4 Page Templates
|
||||
|
||||
WordPress themes often register page templates (Full Width, Sidebar, Landing Page, etc.). In EmDash, this is a `select` field on the pages collection:
|
||||
|
||||
1. Add a `template` select field to the pages collection with the theme's template names as options (e.g. "Default", "Full Width", "Landing Page")
|
||||
2. Create an Astro layout component for each template in `src/layouts/`
|
||||
3. Map the field value to a layout component in the page route:
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/pages/pages/[slug].astro
|
||||
import { getEmDashEntry } from "emdash";
|
||||
import PageDefault from "../../layouts/PageDefault.astro";
|
||||
import PageFullWidth from "../../layouts/PageFullWidth.astro";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
const { entry: page } = await getEmDashEntry("pages", slug!);
|
||||
if (!page) return Astro.redirect("/404");
|
||||
|
||||
const layouts = {
|
||||
"Default": PageDefault,
|
||||
"Full Width": PageFullWidth,
|
||||
};
|
||||
const Layout = layouts[page.data.template as keyof typeof layouts] ?? PageDefault;
|
||||
---
|
||||
<Layout page={page} />
|
||||
```
|
||||
|
||||
Use human-readable option names (matching what the WP theme displayed) since these appear in the admin dropdown.
|
||||
|
||||
## Important: Server-Rendered Pages
|
||||
|
||||
**Never use `getStaticPaths()` or `export const prerender = true` for EmDash content pages.** Content changes at runtime, so pages must be server-rendered.
|
||||
|
||||
```astro
|
||||
---
|
||||
// CORRECT - server-rendered
|
||||
const { slug } = Astro.params;
|
||||
const { entry: post } = await getEmDashEntry("posts", slug!);
|
||||
|
||||
if (!post) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
---
|
||||
```
|
||||
|
||||
See `references/template-patterns.md` for more conversion patterns.
|
||||
147
skills/wordpress-theme-to-emdash/phases/4-dynamic.md
Normal file
147
skills/wordpress-theme-to-emdash/phases/4-dynamic.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Phase 4: Dynamic Features
|
||||
|
||||
Implement CMS-driven features: site settings, menus, taxonomies, and widgets.
|
||||
|
||||
## 4.1 Site Settings
|
||||
|
||||
Map WordPress customizer values to EmDash site settings:
|
||||
|
||||
| WP Customizer Setting | EmDash Site Setting |
|
||||
| --------------------- | --------------------- |
|
||||
| Site Title | `title` |
|
||||
| Tagline | `tagline` |
|
||||
| Site Icon | `favicon` |
|
||||
| Custom Logo | `logo` |
|
||||
| Posts per page | `postsPerPage` |
|
||||
| Date format | `dateFormat` |
|
||||
|
||||
```astro
|
||||
---
|
||||
import { getSiteSettings } from "emdash";
|
||||
const settings = await getSiteSettings();
|
||||
---
|
||||
<header>
|
||||
{settings.logo ? (
|
||||
<img src={settings.logo.url} alt={settings.title} />
|
||||
) : (
|
||||
<span class="site-title">{settings.title}</span>
|
||||
)}
|
||||
{settings.tagline && <p class="tagline">{settings.tagline}</p>}
|
||||
</header>
|
||||
```
|
||||
|
||||
## 4.2 Navigation Menus
|
||||
|
||||
Identify menus in `functions.php`:
|
||||
|
||||
```php
|
||||
register_nav_menus([
|
||||
'primary' => 'Primary Navigation',
|
||||
'footer' => 'Footer Links',
|
||||
]);
|
||||
```
|
||||
|
||||
Use in templates:
|
||||
|
||||
```astro
|
||||
---
|
||||
import { getMenu } from "emdash";
|
||||
const primaryNav = await getMenu("primary");
|
||||
---
|
||||
<nav class="primary-nav">
|
||||
{primaryNav && (
|
||||
<ul>
|
||||
{primaryNav.items.map(item => (
|
||||
<li>
|
||||
<a href={item.url} aria-current={Astro.url.pathname === item.url ? 'page' : undefined}>
|
||||
{item.label}
|
||||
</a>
|
||||
{item.children.length > 0 && (
|
||||
<ul class="submenu">
|
||||
{item.children.map(child => (
|
||||
<li><a href={child.url}>{child.label}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</nav>
|
||||
```
|
||||
|
||||
## 4.3 Taxonomies
|
||||
|
||||
Identify taxonomies in theme:
|
||||
|
||||
```php
|
||||
register_taxonomy('genre', 'book', [
|
||||
'label' => 'Genres',
|
||||
'hierarchical' => true,
|
||||
]);
|
||||
```
|
||||
|
||||
Use in templates:
|
||||
|
||||
```astro
|
||||
---
|
||||
import { getTaxonomyTerms, getEntryTerms, getEntriesByTerm } from "emdash";
|
||||
|
||||
// Get all terms
|
||||
const genres = await getTaxonomyTerms("genre");
|
||||
|
||||
// Get terms for a specific entry
|
||||
const bookGenres = await getEntryTerms("books", book.id, "genre");
|
||||
|
||||
// Get entries by term
|
||||
const fictionBooks = await getEntriesByTerm("books", "genre", "fiction");
|
||||
---
|
||||
```
|
||||
|
||||
## 4.4 Widget Areas
|
||||
|
||||
Identify sidebars in theme:
|
||||
|
||||
```php
|
||||
register_sidebar([
|
||||
'name' => 'Main Sidebar',
|
||||
'id' => 'sidebar-1',
|
||||
]);
|
||||
```
|
||||
|
||||
Use in templates:
|
||||
|
||||
```astro
|
||||
---
|
||||
import { getWidgetArea } from "emdash";
|
||||
import { PortableText } from "emdash/ui";
|
||||
|
||||
const sidebar = await getWidgetArea("sidebar");
|
||||
---
|
||||
{sidebar && sidebar.widgets.length > 0 && (
|
||||
<aside class="sidebar">
|
||||
{sidebar.widgets.map(widget => (
|
||||
<div class="widget">
|
||||
{widget.title && <h3>{widget.title}</h3>}
|
||||
{widget.type === "content" && <PortableText value={widget.content} />}
|
||||
</div>
|
||||
))}
|
||||
</aside>
|
||||
)}
|
||||
```
|
||||
|
||||
## 4.5 Widget Components
|
||||
|
||||
Map WP widgets to Astro components:
|
||||
|
||||
| WP Widget | EmDash Component |
|
||||
| ---------------- | ------------------- |
|
||||
| Recent Posts | `core:recent-posts` |
|
||||
| Categories | `core:categories` |
|
||||
| Tag Cloud | `core:tags` |
|
||||
| Search | `core:search` |
|
||||
| Archives | `core:archives` |
|
||||
| Text/Custom HTML | `type: 'content'` |
|
||||
| Navigation Menu | `type: 'menu'` |
|
||||
|
||||
See `references/emdash-api.md` for full API reference.
|
||||
206
skills/wordpress-theme-to-emdash/phases/5-seed.md
Normal file
206
skills/wordpress-theme-to-emdash/phases/5-seed.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Phase 5: Create Seed File
|
||||
|
||||
Combine all theme features into a seed file with sample content.
|
||||
|
||||
## 5.1 Image Strategy
|
||||
|
||||
**Use the same images you downloaded in Phase 1** for visual consistency.
|
||||
|
||||
1. **Open source themes (GPL)**: Use exact images from the demo
|
||||
2. **Premium themes**: Use Unsplash images matching the demo's style
|
||||
3. **Local images**: Reference with `file:./` prefix:
|
||||
```json
|
||||
"featured_image": {
|
||||
"$media": {
|
||||
"url": "file:./discovery/images/hero.jpg",
|
||||
"alt": "Hero image"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5.2 Validate Before Applying
|
||||
|
||||
```bash
|
||||
# Validate without applying
|
||||
emdash seed --validate
|
||||
```
|
||||
|
||||
The validator catches common mistakes:
|
||||
|
||||
| Check | Error |
|
||||
| ---------------------------- | ------------------------- |
|
||||
| Image using raw URL | "must use $media syntax" |
|
||||
| Reference using raw ID | "must use $ref:id syntax" |
|
||||
| PortableText not an array | "expected array" |
|
||||
| PortableText missing `_type` | "missing required \_type" |
|
||||
|
||||
### Common Fixes
|
||||
|
||||
```json
|
||||
// WRONG - raw URL
|
||||
"featured_image": "https://example.com/photo.jpg"
|
||||
|
||||
// CORRECT - $media syntax
|
||||
"featured_image": {
|
||||
"$media": {
|
||||
"url": "https://example.com/photo.jpg",
|
||||
"alt": "Description"
|
||||
}
|
||||
}
|
||||
|
||||
// WRONG - unknown byline reference
|
||||
"bylines": [{ "byline": "author-1" }]
|
||||
|
||||
// CORRECT - define root bylines[] and reference byline IDs
|
||||
"bylines": [{ "byline": "byline-author-1" }]
|
||||
```
|
||||
|
||||
## 5.3 Seed File Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://emdashcms.com/seed.schema.json",
|
||||
"version": "1",
|
||||
"meta": {
|
||||
"name": "Theme Name",
|
||||
"description": "Ported from WordPress theme"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Site Title",
|
||||
"tagline": "Site tagline"
|
||||
},
|
||||
|
||||
"collections": [
|
||||
{
|
||||
"slug": "posts",
|
||||
"label": "Posts",
|
||||
"fields": [
|
||||
{ "slug": "title", "type": "string", "required": true },
|
||||
{ "slug": "content", "type": "portableText" },
|
||||
{ "slug": "featured_image", "type": "image" }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"taxonomies": [
|
||||
{
|
||||
"name": "categories",
|
||||
"label": "Categories",
|
||||
"hierarchical": true,
|
||||
"collections": ["posts"],
|
||||
"terms": [{ "slug": "news", "label": "News" }]
|
||||
}
|
||||
],
|
||||
|
||||
"bylines": [
|
||||
{
|
||||
"id": "byline-author-1",
|
||||
"slug": "theme-author",
|
||||
"displayName": "Theme Author"
|
||||
}
|
||||
],
|
||||
|
||||
"menus": [
|
||||
{
|
||||
"name": "primary",
|
||||
"label": "Primary Navigation",
|
||||
"items": [
|
||||
{ "type": "custom", "label": "Home", "url": "/" },
|
||||
{ "type": "custom", "label": "Blog", "url": "/posts" }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"content": {
|
||||
"posts": [
|
||||
{
|
||||
"id": "post-1",
|
||||
"slug": "hello-world",
|
||||
"status": "published",
|
||||
"bylines": [{ "byline": "byline-author-1" }],
|
||||
"data": {
|
||||
"title": "Hello World",
|
||||
"content": [{ "_type": "block", "children": [{ "text": "Welcome!" }] }],
|
||||
"featured_image": {
|
||||
"$media": {
|
||||
"url": "file:./discovery/images/featured-1.jpg",
|
||||
"alt": "Featured image"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5.4 Adding Sections (Reusable Blocks)
|
||||
|
||||
If the theme has reusable block patterns, add them as sections:
|
||||
|
||||
```json
|
||||
{
|
||||
"sections": [
|
||||
{
|
||||
"slug": "hero-centered",
|
||||
"title": "Centered Hero",
|
||||
"description": "Full-width hero with centered heading and CTA button",
|
||||
"keywords": ["hero", "banner", "header", "landing"],
|
||||
"content": [
|
||||
{
|
||||
"_type": "block",
|
||||
"style": "h1",
|
||||
"children": [{ "_type": "span", "text": "Welcome to Our Site" }]
|
||||
},
|
||||
{
|
||||
"_type": "block",
|
||||
"children": [{ "_type": "span", "text": "Your compelling tagline goes here." }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "newsletter-cta",
|
||||
"title": "Newsletter Signup",
|
||||
"keywords": ["newsletter", "subscribe", "email", "signup"],
|
||||
"content": [
|
||||
{
|
||||
"_type": "block",
|
||||
"style": "h3",
|
||||
"children": [{ "_type": "span", "text": "Subscribe to our newsletter" }]
|
||||
},
|
||||
{
|
||||
"_type": "block",
|
||||
"children": [
|
||||
{ "_type": "span", "text": "Get the latest updates delivered to your inbox." }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Editors can insert these sections using the `/section` slash command in the rich text editor.
|
||||
|
||||
## 5.5 Add Redirects for Legacy WordPress URLs
|
||||
|
||||
Include redirects in the seed when the WordPress theme used different URL structures.
|
||||
|
||||
```json
|
||||
{
|
||||
"redirects": [
|
||||
{ "source": "/?p=123", "destination": "/hello-world" },
|
||||
{ "source": "/2024/01/hello-world", "destination": "/hello-world", "type": 301 },
|
||||
{ "source": "/category/news", "destination": "/categories/news" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `source` and `destination` must be local paths (start with `/`)
|
||||
- Supported `type` values are `301`, `302`, `307`, `308`
|
||||
- Redirects are idempotent during seeding (existing `source` entries are skipped)
|
||||
|
||||
See `references/emdash-api.md` for full seed file schema.
|
||||
97
skills/wordpress-theme-to-emdash/phases/6-verify.md
Normal file
97
skills/wordpress-theme-to-emdash/phases/6-verify.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Phase 6: Verify & Iterate
|
||||
|
||||
Seed content, run the dev server, compare screenshots, and iterate until pages match.
|
||||
|
||||
## 6.1 Apply the Seed
|
||||
|
||||
```bash
|
||||
# Validate first
|
||||
emdash seed --validate
|
||||
|
||||
# Apply seed with content
|
||||
emdash seed
|
||||
```
|
||||
|
||||
## 6.2 Start Dev Server
|
||||
|
||||
Kill any existing server first:
|
||||
|
||||
```bash
|
||||
lsof -ti:4321 | xargs kill -9 2>/dev/null || true
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 6.3 Screenshot Each Page Type
|
||||
|
||||
Screenshot every page type you captured in Phase 1:
|
||||
|
||||
```bash
|
||||
# Homepage
|
||||
agent-browser open http://localhost:4321
|
||||
agent-browser screenshot output/homepage.png --full
|
||||
|
||||
# Single post
|
||||
agent-browser open http://localhost:4321/posts/hello-world
|
||||
agent-browser screenshot output/single-post.png --full
|
||||
|
||||
# Blog archive
|
||||
agent-browser open http://localhost:4321/posts
|
||||
agent-browser screenshot output/archive.png --full
|
||||
|
||||
# Category page
|
||||
agent-browser open http://localhost:4321/categories/news
|
||||
agent-browser screenshot output/category.png --full
|
||||
|
||||
# Static page
|
||||
agent-browser open http://localhost:4321/pages/about
|
||||
agent-browser screenshot output/page.png --full
|
||||
|
||||
# 404 page
|
||||
agent-browser open http://localhost:4321/nonexistent
|
||||
agent-browser screenshot output/404.png --full
|
||||
```
|
||||
|
||||
## 6.4 Compare & Iterate
|
||||
|
||||
Compare each screenshot pair:
|
||||
|
||||
| Page Type | Reference | Output |
|
||||
| ----------- | --------------------------------------- | ------------------------ |
|
||||
| Homepage | `discovery/screenshots/homepage.png` | `output/homepage.png` |
|
||||
| Single Post | `discovery/screenshots/single-post.png` | `output/single-post.png` |
|
||||
| Archive | `discovery/screenshots/archive.png` | `output/archive.png` |
|
||||
| Category | `discovery/screenshots/category.png` | `output/category.png` |
|
||||
| Page | `discovery/screenshots/page.png` | `output/page.png` |
|
||||
| 404 | `discovery/screenshots/404.png` | `output/404.png` |
|
||||
|
||||
For each page, identify differences and fix:
|
||||
|
||||
1. **Layout** - CSS grid/flexbox, content width, spacing
|
||||
2. **Typography** - Font family, sizes, line height
|
||||
3. **Colors** - Background, text, links, borders
|
||||
4. **Components** - Headers, footers, cards, buttons
|
||||
5. **Responsive** - Check mobile viewport too
|
||||
|
||||
Re-screenshot after each round of fixes.
|
||||
|
||||
**Don't aim for pixel-perfect** - aim for "same design language."
|
||||
|
||||
## 6.5 Final Build Test
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## License Compliance
|
||||
|
||||
WordPress themes are GPL-licensed. Every ported theme needs:
|
||||
|
||||
1. **LICENSE** - GPL-2.0 text (download with curl, don't output directly):
|
||||
|
||||
```bash
|
||||
curl -o LICENSE https://raw.githubusercontent.com/spdx/license-list-data/main/text/GPL-2.0-or-later.txt
|
||||
```
|
||||
|
||||
2. **README.md** - Credits to original theme
|
||||
|
||||
3. **package.json** - `"license": "GPL-2.0-or-later"`
|
||||
1136
skills/wordpress-theme-to-emdash/references/astro-essentials.md
Normal file
1136
skills/wordpress-theme-to-emdash/references/astro-essentials.md
Normal file
File diff suppressed because it is too large
Load Diff
410
skills/wordpress-theme-to-emdash/references/concept-mapping.md
Normal file
410
skills/wordpress-theme-to-emdash/references/concept-mapping.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# 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"`
|
||||
396
skills/wordpress-theme-to-emdash/references/design-extraction.md
Normal file
396
skills/wordpress-theme-to-emdash/references/design-extraction.md
Normal file
@@ -0,0 +1,396 @@
|
||||
# Design Extraction Guide
|
||||
|
||||
Extract design tokens from WordPress themes for use in EmDash.
|
||||
|
||||
## CSS Variable Extraction
|
||||
|
||||
### Finding Design Tokens in Classic Themes
|
||||
|
||||
Look in these files (in order of priority):
|
||||
|
||||
1. **`style.css`** - Main stylesheet, often has custom properties
|
||||
2. **`assets/css/custom-properties.css`** - Some themes separate variables
|
||||
3. **`inc/customizer.php`** - Default values for customizer options
|
||||
4. **`functions.php`** - Inline styles with defaults
|
||||
|
||||
Common patterns to search for:
|
||||
|
||||
```css
|
||||
/* Root variables */
|
||||
:root {
|
||||
--primary-color: #0073aa;
|
||||
--font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
/* Theme-specific prefixes */
|
||||
--theme-name-color-primary
|
||||
--wp--preset--color--primary
|
||||
```
|
||||
|
||||
### Finding Design Tokens in Block Themes (theme.json)
|
||||
|
||||
Block themes (WordPress 5.9+) store design tokens in `theme.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": {
|
||||
"color": {
|
||||
"palette": [
|
||||
{ "slug": "primary", "color": "#0073aa", "name": "Primary" },
|
||||
{ "slug": "secondary", "color": "#23282d", "name": "Secondary" }
|
||||
]
|
||||
},
|
||||
"typography": {
|
||||
"fontFamilies": [
|
||||
{
|
||||
"fontFamily": "'Open Sans', sans-serif",
|
||||
"slug": "body",
|
||||
"name": "Body"
|
||||
}
|
||||
],
|
||||
"fontSizes": [{ "size": "1rem", "slug": "medium", "name": "Medium" }]
|
||||
},
|
||||
"spacing": {
|
||||
"units": ["px", "em", "rem", "%"],
|
||||
"spacingSizes": [{ "size": "1rem", "slug": "20", "name": "Small" }]
|
||||
},
|
||||
"layout": {
|
||||
"contentSize": "650px",
|
||||
"wideSize": "1200px"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Convert to EmDash CSS variables:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Colors from theme.json palette */
|
||||
--color-primary: #0073aa;
|
||||
--color-secondary: #23282d;
|
||||
|
||||
/* Typography */
|
||||
--font-body: "Open Sans", sans-serif;
|
||||
--font-size-medium: 1rem;
|
||||
|
||||
/* Layout */
|
||||
--content-width: 650px;
|
||||
--wide-width: 1200px;
|
||||
|
||||
/* Spacing */
|
||||
--space-20: 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
## Color Extraction
|
||||
|
||||
### From Live Site
|
||||
|
||||
Use browser DevTools or automation:
|
||||
|
||||
1. **Background colors**: `document.body.style.backgroundColor`
|
||||
2. **Text colors**: Inspect body, headings, links
|
||||
3. **Accent colors**: Buttons, links, highlights
|
||||
|
||||
Common elements to check:
|
||||
|
||||
- Body background and text
|
||||
- Header/footer backgrounds
|
||||
- Link colors (normal, hover, visited)
|
||||
- Button colors (primary, secondary)
|
||||
- Border colors
|
||||
- Selection highlight
|
||||
|
||||
### Common Color Mapping
|
||||
|
||||
| WP Pattern | EmDash Variable |
|
||||
| ---------------- | ------------------- |
|
||||
| Background | `--color-base` |
|
||||
| Text | `--color-contrast` |
|
||||
| Primary brand | `--color-primary` |
|
||||
| Secondary brand | `--color-secondary` |
|
||||
| Accent/highlight | `--color-accent-1` |
|
||||
| Muted text | `--color-muted` |
|
||||
| Border | `--color-border` |
|
||||
| Error | `--color-error` |
|
||||
| Success | `--color-success` |
|
||||
|
||||
## Typography Extraction
|
||||
|
||||
### Font Families
|
||||
|
||||
Check for:
|
||||
|
||||
1. Google Fonts in `<head>` - `fonts.googleapis.com`
|
||||
2. `@font-face` declarations in CSS
|
||||
3. Font files in `assets/fonts/` or `fonts/`
|
||||
4. Customizer settings for typography
|
||||
|
||||
Extract the stack:
|
||||
|
||||
```css
|
||||
/* WP theme might have */
|
||||
font-family: "Playfair Display", Georgia, serif;
|
||||
|
||||
/* Convert to EmDash */
|
||||
:root {
|
||||
--font-heading: "Playfair Display", Georgia, serif;
|
||||
}
|
||||
```
|
||||
|
||||
### Font Sizes
|
||||
|
||||
Common patterns:
|
||||
|
||||
```css
|
||||
/* WP theme */
|
||||
body {
|
||||
font-size: 18px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
/* EmDash with clamp for responsiveness */
|
||||
:root {
|
||||
--font-size-base: clamp(1rem, 0.5vw + 0.9rem, 1.125rem);
|
||||
--font-size-xl: clamp(1.75rem, 1vw + 1.5rem, 2rem);
|
||||
--font-size-xxl: clamp(2.15rem, 2vw + 1.5rem, 3rem);
|
||||
}
|
||||
```
|
||||
|
||||
### Line Height and Spacing
|
||||
|
||||
```css
|
||||
/* Extract these values */
|
||||
line-height: 1.6;
|
||||
letter-spacing: -0.01em;
|
||||
```
|
||||
|
||||
## Spacing System
|
||||
|
||||
### Identify the Scale
|
||||
|
||||
Look for consistent spacing values:
|
||||
|
||||
```css
|
||||
/* Common WordPress patterns */
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
gap: 2rem;
|
||||
```
|
||||
|
||||
Create a scale:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--space-10: 0.25rem; /* 4px */
|
||||
--space-20: 0.5rem; /* 8px */
|
||||
--space-30: 1rem; /* 16px */
|
||||
--space-40: 1.5rem; /* 24px */
|
||||
--space-50: 2rem; /* 32px */
|
||||
--space-60: 3rem; /* 48px */
|
||||
--space-70: 4rem; /* 64px */
|
||||
--space-80: 6rem; /* 96px */
|
||||
}
|
||||
```
|
||||
|
||||
## Layout Extraction
|
||||
|
||||
### Content Width
|
||||
|
||||
Find in:
|
||||
|
||||
- `.container`, `.wrapper`, `.site-content` max-width
|
||||
- `theme.json` layout.contentSize
|
||||
- Customizer settings
|
||||
|
||||
```css
|
||||
/* WP theme */
|
||||
.container {
|
||||
max-width: 1140px;
|
||||
}
|
||||
.content-area {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
/* EmDash */
|
||||
:root {
|
||||
--content-width: 720px;
|
||||
--wide-width: 1140px;
|
||||
}
|
||||
```
|
||||
|
||||
### Breakpoints
|
||||
|
||||
Common WordPress breakpoints:
|
||||
|
||||
```css
|
||||
/* Find in theme CSS */
|
||||
@media (max-width: 1200px) {
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
}
|
||||
@media (max-width: 576px) {
|
||||
}
|
||||
```
|
||||
|
||||
Document for use in Astro:
|
||||
|
||||
```css
|
||||
/* EmDash breakpoints */
|
||||
@media (max-width: 1024px) {
|
||||
/* Tablet landscape */
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
/* Tablet portrait */
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
/* Mobile */
|
||||
}
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Header Pattern
|
||||
|
||||
Identify header structure:
|
||||
|
||||
- Logo position (left, center)
|
||||
- Navigation style (horizontal, hamburger)
|
||||
- Background (solid, transparent, sticky)
|
||||
|
||||
```css
|
||||
/* Extract key values */
|
||||
.site-header {
|
||||
height: 80px;
|
||||
background: #ffffff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
```
|
||||
|
||||
### Card Pattern
|
||||
|
||||
```css
|
||||
/* WP card styles */
|
||||
.post-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* EmDash equivalent */
|
||||
:root {
|
||||
--radius-card: 8px;
|
||||
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
```
|
||||
|
||||
### Button Pattern
|
||||
|
||||
```css
|
||||
/* WP button */
|
||||
.button,
|
||||
.wp-block-button__link {
|
||||
padding: 12px 24px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* EmDash */
|
||||
.btn {
|
||||
padding: var(--space-30) var(--space-40);
|
||||
border-radius: var(--radius-button);
|
||||
font-weight: 600;
|
||||
}
|
||||
```
|
||||
|
||||
## Automated Extraction Script
|
||||
|
||||
For complex themes, consider a script approach:
|
||||
|
||||
```javascript
|
||||
// Run in browser console on live WP site
|
||||
const styles = getComputedStyle(document.body);
|
||||
const tokens = {
|
||||
colors: {
|
||||
background: styles.backgroundColor,
|
||||
text: styles.color,
|
||||
},
|
||||
typography: {
|
||||
fontFamily: styles.fontFamily,
|
||||
fontSize: styles.fontSize,
|
||||
lineHeight: styles.lineHeight,
|
||||
},
|
||||
};
|
||||
|
||||
// Check header
|
||||
const header = document.querySelector("header, .site-header");
|
||||
if (header) {
|
||||
const headerStyles = getComputedStyle(header);
|
||||
tokens.header = {
|
||||
background: headerStyles.backgroundColor,
|
||||
height: headerStyles.height,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(tokens, null, 2));
|
||||
```
|
||||
|
||||
## Output Template
|
||||
|
||||
Final CSS variables for EmDash Base.astro:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Colors */
|
||||
--color-base: #ffffff;
|
||||
--color-contrast: #1a1a1a;
|
||||
--color-primary: #0073aa;
|
||||
--color-secondary: #23282d;
|
||||
--color-muted: #757575;
|
||||
--color-border: #e0e0e0;
|
||||
|
||||
/* Typography */
|
||||
--font-body: "Open Sans", system-ui, sans-serif;
|
||||
--font-heading: "Playfair Display", Georgia, serif;
|
||||
--font-mono: "Fira Code", monospace;
|
||||
|
||||
/* Font sizes */
|
||||
--font-size-small: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-large: 1.125rem;
|
||||
--font-size-xl: 1.5rem;
|
||||
--font-size-xxl: 2rem;
|
||||
--font-size-xxxl: 3rem;
|
||||
|
||||
/* Spacing */
|
||||
--space-20: 0.5rem;
|
||||
--space-30: 1rem;
|
||||
--space-40: 1.5rem;
|
||||
--space-50: 2rem;
|
||||
--space-60: 3rem;
|
||||
--space-70: 4rem;
|
||||
--space-80: 6rem;
|
||||
|
||||
/* Layout */
|
||||
--content-width: 720px;
|
||||
--wide-width: 1200px;
|
||||
|
||||
/* Components */
|
||||
--radius-small: 4px;
|
||||
--radius-medium: 8px;
|
||||
--radius-large: 16px;
|
||||
--shadow-small: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
```
|
||||
516
skills/wordpress-theme-to-emdash/references/emdash-api.md
Normal file
516
skills/wordpress-theme-to-emdash/references/emdash-api.md
Normal file
@@ -0,0 +1,516 @@
|
||||
# EmDash API Reference
|
||||
|
||||
Quick reference for EmDash-specific APIs used when porting themes.
|
||||
|
||||
> **See also:** The `scaffold/` directory contains working examples of all these patterns. When in doubt, copy from there.
|
||||
|
||||
## Content Retrieval
|
||||
|
||||
EmDash's query functions follow Astro's [live content collections](https://docs.astro.build/en/reference/experimental-flags/live-content-collections/) pattern, returning structured results for graceful error handling.
|
||||
|
||||
### getEmDashCollection
|
||||
|
||||
Fetch multiple entries from a collection.
|
||||
|
||||
```typescript
|
||||
import { getEmDashCollection } from "emdash";
|
||||
|
||||
// Returns { entries, error }
|
||||
const { entries: posts } = await getEmDashCollection("posts");
|
||||
|
||||
// With filters
|
||||
const { entries: posts } = await getEmDashCollection("posts", {
|
||||
status: "published",
|
||||
limit: 10,
|
||||
where: { category: "news" },
|
||||
});
|
||||
```
|
||||
|
||||
### getEmDashEntry
|
||||
|
||||
Fetch a single entry by slug.
|
||||
|
||||
```typescript
|
||||
import { getEmDashEntry } from "emdash";
|
||||
|
||||
// Returns { entry, error, isPreview }
|
||||
const { entry: post } = await getEmDashEntry("posts", "hello-world");
|
||||
|
||||
if (!post) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
```
|
||||
|
||||
### Entry Shape
|
||||
|
||||
```typescript
|
||||
interface Entry {
|
||||
id: string;
|
||||
collection: string;
|
||||
data: {
|
||||
title: string;
|
||||
slug: string;
|
||||
content: PortableTextBlock[];
|
||||
featured_image?: ImageField; // { src, alt } - NOT a string!
|
||||
// ... custom fields
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Field Types at Runtime
|
||||
|
||||
**IMPORTANT:** Field types have specific runtime shapes. The most common mistake is treating image fields as strings.
|
||||
|
||||
### Image Fields
|
||||
|
||||
Image fields are **objects**, not strings:
|
||||
|
||||
```typescript
|
||||
interface ImageField {
|
||||
src: string; // The resolved URL
|
||||
alt?: string;
|
||||
}
|
||||
```
|
||||
|
||||
```astro
|
||||
{/* CORRECT */}
|
||||
{post.data.featured_image?.src && (
|
||||
<img
|
||||
src={post.data.featured_image.src}
|
||||
alt={post.data.featured_image.alt || post.data.title}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* WRONG - renders [object Object] */}
|
||||
<img src={post.data.featured_image} />
|
||||
```
|
||||
|
||||
### Reference Fields
|
||||
|
||||
In seed files use `"$ref:id"` format. At runtime they may be resolved objects or strings.
|
||||
|
||||
### PortableText Fields
|
||||
|
||||
Rich content is an array of blocks with `_type` property.
|
||||
|
||||
## Site Settings
|
||||
|
||||
### getSiteSettings
|
||||
|
||||
Get all site settings.
|
||||
|
||||
```typescript
|
||||
import { getSiteSettings } from "emdash";
|
||||
|
||||
const settings = await getSiteSettings();
|
||||
console.log(settings.title); // "My Site"
|
||||
console.log(settings.logo?.url); // Resolved media URL
|
||||
```
|
||||
|
||||
### getSiteSetting
|
||||
|
||||
Get a single setting.
|
||||
|
||||
```typescript
|
||||
import { getSiteSetting } from "emdash";
|
||||
|
||||
const title = await getSiteSetting("title");
|
||||
const logo = await getSiteSetting("logo");
|
||||
```
|
||||
|
||||
### Available Settings
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------ | ---------------- | ------------------------ |
|
||||
| `title` | `string` | Site name |
|
||||
| `tagline` | `string` | Site tagline/description |
|
||||
| `logo` | `MediaReference` | Site logo with URL |
|
||||
| `favicon` | `MediaReference` | Favicon with URL |
|
||||
| `social` | `SocialLinks` | Social media URLs |
|
||||
| `timezone` | `string` | Site timezone |
|
||||
| `dateFormat` | `string` | Date display format |
|
||||
|
||||
## Navigation Menus
|
||||
|
||||
### getMenu
|
||||
|
||||
Fetch a menu by name with resolved URLs.
|
||||
|
||||
```typescript
|
||||
import { getMenu } from "emdash";
|
||||
|
||||
const menu = await getMenu("primary");
|
||||
|
||||
if (menu) {
|
||||
console.log(menu.items); // MenuItem[]
|
||||
}
|
||||
```
|
||||
|
||||
### getMenus
|
||||
|
||||
Get all menus (names only).
|
||||
|
||||
```typescript
|
||||
import { getMenus } from "emdash";
|
||||
|
||||
const menus = await getMenus();
|
||||
// [{ id, name, label }]
|
||||
```
|
||||
|
||||
### MenuItem Shape
|
||||
|
||||
```typescript
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
url: string; // Resolved URL
|
||||
target?: string;
|
||||
children: MenuItem[];
|
||||
}
|
||||
```
|
||||
|
||||
### Rendering Menus
|
||||
|
||||
```astro
|
||||
---
|
||||
import { getMenu } from "emdash";
|
||||
|
||||
const primaryMenu = await getMenu("primary");
|
||||
---
|
||||
<nav>
|
||||
{primaryMenu?.items.map(item => (
|
||||
<a href={item.url}>{item.label}</a>
|
||||
))}
|
||||
</nav>
|
||||
```
|
||||
|
||||
## Taxonomies
|
||||
|
||||
### getTaxonomyTerms
|
||||
|
||||
Get all terms for a taxonomy.
|
||||
|
||||
```typescript
|
||||
import { getTaxonomyTerms } from "emdash";
|
||||
|
||||
const categories = await getTaxonomyTerms("categories");
|
||||
const tags = await getTaxonomyTerms("tags");
|
||||
```
|
||||
|
||||
### getTerm
|
||||
|
||||
Get a single term by slug.
|
||||
|
||||
```typescript
|
||||
import { getTerm } from "emdash";
|
||||
|
||||
const term = await getTerm("categories", "news");
|
||||
console.log(term?.label); // "News"
|
||||
console.log(term?.count); // Number of entries
|
||||
```
|
||||
|
||||
### getEntryTerms
|
||||
|
||||
Get terms assigned to a specific entry.
|
||||
|
||||
> **IMPORTANT:** This function does NOT take a `db` parameter.
|
||||
|
||||
```typescript
|
||||
import { getEntryTerms } from "emdash";
|
||||
|
||||
// Get all terms for an entry
|
||||
const terms = await getEntryTerms("posts", post.id);
|
||||
|
||||
// Get only categories
|
||||
const categories = await getEntryTerms("posts", post.id, "categories");
|
||||
```
|
||||
|
||||
### getEntriesByTerm
|
||||
|
||||
Get entries that have a specific term.
|
||||
|
||||
```typescript
|
||||
import { getEntriesByTerm } from "emdash";
|
||||
|
||||
const posts = await getEntriesByTerm("posts", "categories", "news");
|
||||
```
|
||||
|
||||
### TaxonomyTerm Shape
|
||||
|
||||
```typescript
|
||||
interface TaxonomyTerm {
|
||||
id: string;
|
||||
name: string; // Taxonomy name
|
||||
slug: string; // Term slug
|
||||
label: string; // Display label
|
||||
children: TaxonomyTerm[];
|
||||
count?: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Widget Areas
|
||||
|
||||
### getWidgetArea
|
||||
|
||||
Get a widget area by name.
|
||||
|
||||
```typescript
|
||||
import { getWidgetArea } from "emdash";
|
||||
|
||||
const sidebar = await getWidgetArea("sidebar");
|
||||
|
||||
if (sidebar) {
|
||||
console.log(sidebar.widgets); // Widget[]
|
||||
}
|
||||
```
|
||||
|
||||
### Widget Types
|
||||
|
||||
| Type | Description | Key Fields |
|
||||
| ----------- | -------------------- | ---------------------- |
|
||||
| `content` | Rich text (PT) | `content` |
|
||||
| `menu` | Navigation menu | `menuName` |
|
||||
| `component` | Registered component | `componentId`, `props` |
|
||||
|
||||
## Sections (Reusable Blocks)
|
||||
|
||||
Sections are reusable content blocks that editors can insert via `/section` slash command.
|
||||
|
||||
### getSection
|
||||
|
||||
Get a single section by slug.
|
||||
|
||||
```typescript
|
||||
import { getSection } from "emdash";
|
||||
|
||||
const cta = await getSection("newsletter-cta");
|
||||
// Returns { id, slug, title, content, keywords, source }
|
||||
```
|
||||
|
||||
### getSections
|
||||
|
||||
List sections with optional filters.
|
||||
|
||||
```typescript
|
||||
import { getSections } from "emdash";
|
||||
|
||||
// Get all sections
|
||||
const all = await getSections();
|
||||
|
||||
// Filter by source: "theme" | "user" | "import"
|
||||
const imported = await getSections({ source: "import" });
|
||||
```
|
||||
|
||||
### Section Sources
|
||||
|
||||
| Source | Description |
|
||||
| -------- | --------------------------------------- |
|
||||
| `theme` | Defined in seed file |
|
||||
| `user` | Created by editors in admin |
|
||||
| `import` | Imported from WordPress reusable blocks |
|
||||
|
||||
## Search
|
||||
|
||||
### search
|
||||
|
||||
Global search across collections.
|
||||
|
||||
```typescript
|
||||
import { search } from "emdash";
|
||||
|
||||
const results = await search("hello world", {
|
||||
collections: ["posts", "pages"], // Optional: limit to specific collections
|
||||
status: "published", // Optional: filter by status
|
||||
limit: 20, // Optional: max results
|
||||
});
|
||||
|
||||
// Returns { results: SearchResult[], total, nextCursor? }
|
||||
results.results.forEach((r) => {
|
||||
console.log(r.collection); // "posts"
|
||||
console.log(r.id); // Entry ID
|
||||
console.log(r.title); // Entry title
|
||||
console.log(r.slug); // Entry slug
|
||||
console.log(r.snippet); // HTML snippet with <mark> highlights
|
||||
console.log(r.score); // Relevance score
|
||||
});
|
||||
```
|
||||
|
||||
### LiveSearch Component
|
||||
|
||||
Ready-to-use search with instant results:
|
||||
|
||||
```astro
|
||||
---
|
||||
import LiveSearch from "emdash/ui/search";
|
||||
---
|
||||
|
||||
<LiveSearch
|
||||
placeholder="Search..."
|
||||
collections={["posts", "pages"]}
|
||||
/>
|
||||
```
|
||||
|
||||
Features:
|
||||
|
||||
- Debounced instant search
|
||||
- Prefix matching (automatic `*` suffix)
|
||||
- Porter stemming ("run" finds "running")
|
||||
- Result snippets with `<mark>` highlights
|
||||
|
||||
### Search Configuration
|
||||
|
||||
Search is enabled per-collection via admin UI:
|
||||
|
||||
1. Edit Content Type → check "Search" in Features
|
||||
2. Edit fields → check "Searchable" for text fields
|
||||
|
||||
Only collections with search enabled are indexed.
|
||||
|
||||
## Rendering Content
|
||||
|
||||
### PortableText Component
|
||||
|
||||
```astro
|
||||
---
|
||||
import { PortableText } from "emdash/ui";
|
||||
---
|
||||
|
||||
<PortableText value={post.data.content} />
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
### Seed Validation
|
||||
|
||||
Validate seed files before applying:
|
||||
|
||||
```bash
|
||||
# Validate default seed file (.emdash/seed.json)
|
||||
emdash seed --validate
|
||||
|
||||
# Validate a specific file
|
||||
emdash seed path/to/seed.json --validate
|
||||
```
|
||||
|
||||
Catches common mistakes:
|
||||
|
||||
- Image fields with raw URLs (should use `$media`)
|
||||
- Reference fields with raw IDs (should use `$ref:id`)
|
||||
- PortableText not an array or missing `_type`
|
||||
- Type mismatches (string vs number, etc.)
|
||||
|
||||
### Apply Seed
|
||||
|
||||
```bash
|
||||
# Apply seed with content
|
||||
emdash seed
|
||||
|
||||
# Apply seed without sample content
|
||||
emdash seed --no-content
|
||||
|
||||
# Specify database path
|
||||
emdash seed --database ./my-data.db
|
||||
```
|
||||
|
||||
### Export Seed
|
||||
|
||||
```bash
|
||||
# Export schema only
|
||||
emdash export-seed
|
||||
|
||||
# Export schema and all content
|
||||
emdash export-seed --with-content
|
||||
|
||||
# Export specific collections
|
||||
emdash export-seed --with-content=posts,pages
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### astro.config.mjs
|
||||
|
||||
```javascript
|
||||
import { defineConfig } from "astro/config";
|
||||
import emdash, { local } from "emdash/astro";
|
||||
import { sqlite } from "emdash/db";
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
emdash({
|
||||
database: sqlite({ url: "file:./data.db" }),
|
||||
storage: local({
|
||||
directory: "./uploads",
|
||||
baseUrl: "/_emdash/api/media/file",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### live.config.ts
|
||||
|
||||
```typescript
|
||||
// src/live.config.ts
|
||||
import { defineLiveCollection } from "astro:content";
|
||||
import { emdashLoader } from "emdash/runtime";
|
||||
|
||||
export const collections = {
|
||||
_emdash: defineLiveCollection({ loader: emdashLoader() }),
|
||||
};
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Homepage with Recent Posts
|
||||
|
||||
```astro
|
||||
---
|
||||
import { getEmDashCollection, getSiteSettings } from "emdash";
|
||||
import Base from "../layouts/Base.astro";
|
||||
|
||||
const settings = await getSiteSettings();
|
||||
const { entries: posts } = await getEmDashCollection("posts", { limit: 10 });
|
||||
---
|
||||
<Base title={settings.title}>
|
||||
{posts.map(post => (
|
||||
<article>
|
||||
<a href={`/posts/${post.data.slug}`}>{post.data.title}</a>
|
||||
</article>
|
||||
))}
|
||||
</Base>
|
||||
```
|
||||
|
||||
### Category Archive
|
||||
|
||||
```astro
|
||||
---
|
||||
import { getTerm, getEntriesByTerm } from "emdash";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
const category = await getTerm("categories", slug);
|
||||
const posts = await getEntriesByTerm("posts", "categories", slug);
|
||||
---
|
||||
<h1>{category?.label}</h1>
|
||||
{posts.map(post => (
|
||||
<a href={`/posts/${post.data.slug}`}>{post.data.title}</a>
|
||||
))}
|
||||
```
|
||||
|
||||
### Dynamic Navigation
|
||||
|
||||
```astro
|
||||
---
|
||||
import { getMenu, getSiteSettings } from "emdash";
|
||||
|
||||
const settings = await getSiteSettings();
|
||||
const primaryMenu = await getMenu("primary");
|
||||
---
|
||||
<header>
|
||||
<a href="/">{settings.title}</a>
|
||||
<nav>
|
||||
{primaryMenu?.items.map(item => (
|
||||
<a href={item.url}>{item.label}</a>
|
||||
))}
|
||||
</nav>
|
||||
</header>
|
||||
```
|
||||
839
skills/wordpress-theme-to-emdash/references/template-patterns.md
Normal file
839
skills/wordpress-theme-to-emdash/references/template-patterns.md
Normal file
@@ -0,0 +1,839 @@
|
||||
# PHP → Astro Template Patterns
|
||||
|
||||
Common WordPress PHP patterns and their Astro/EmDash equivalents.
|
||||
|
||||
## The Loop
|
||||
|
||||
### Basic Loop
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php if (have_posts()) : ?>
|
||||
<?php while (have_posts()) : the_post(); ?>
|
||||
<article>
|
||||
<h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
|
||||
<?php the_excerpt(); ?>
|
||||
</article>
|
||||
<?php endwhile; ?>
|
||||
<?php else : ?>
|
||||
<p>No posts found.</p>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro/EmDash
|
||||
import { getEmDashCollection } from "emdash";
|
||||
const { entries: posts } = await getEmDashCollection("posts");
|
||||
---
|
||||
{posts.length > 0 ? (
|
||||
posts.map(post => (
|
||||
<article>
|
||||
<h2><a href={`/posts/${post.id}`}>{post.data.title}</a></h2>
|
||||
<p>{post.data.excerpt}</p>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<p>No posts found.</p>
|
||||
)}
|
||||
```
|
||||
|
||||
### Custom Query
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
$args = [
|
||||
'post_type' => 'portfolio',
|
||||
'posts_per_page' => 6,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
];
|
||||
$query = new WP_Query($args);
|
||||
while ($query->have_posts()) : $query->the_post();
|
||||
// ...
|
||||
endwhile;
|
||||
wp_reset_postdata();
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro/EmDash
|
||||
import { getEmDashCollection } from "emdash";
|
||||
const { entries: items } = await getEmDashCollection("portfolio", {
|
||||
limit: 6,
|
||||
orderBy: { published_at: "desc" },
|
||||
});
|
||||
---
|
||||
{items.map(item => (
|
||||
// ...
|
||||
))}
|
||||
```
|
||||
|
||||
## Single Post/Page
|
||||
|
||||
### Basic Single
|
||||
|
||||
```php
|
||||
// WordPress single.php
|
||||
<?php get_header(); ?>
|
||||
<main>
|
||||
<?php while (have_posts()) : the_post(); ?>
|
||||
<article>
|
||||
<h1><?php the_title(); ?></h1>
|
||||
<div class="meta">
|
||||
<?php the_date(); ?> | <?php the_author(); ?>
|
||||
</div>
|
||||
<?php the_content(); ?>
|
||||
</article>
|
||||
<?php endwhile; ?>
|
||||
</main>
|
||||
<?php get_footer(); ?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro pages/posts/[slug].astro
|
||||
// NOTE: EmDash pages are always server-rendered (no getStaticPaths)
|
||||
import { getEmDashEntry } from "emdash";
|
||||
import { PortableText } from "emdash/ui";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
const { entry: post } = await getEmDashEntry("posts", slug!);
|
||||
|
||||
if (!post) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
---
|
||||
<Base title={post.data.title}>
|
||||
<main>
|
||||
<article>
|
||||
<h1>{post.data.title}</h1>
|
||||
<div class="meta">
|
||||
{post.data.publishedAt} | {post.data.byline?.displayName ?? "Unknown"}
|
||||
</div>
|
||||
<PortableText value={post.data.content} />
|
||||
</article>
|
||||
</main>
|
||||
</Base>
|
||||
```
|
||||
|
||||
## Featured Image
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php if (has_post_thumbnail()) : ?>
|
||||
<figure class="featured-image">
|
||||
<?php the_post_thumbnail('large'); ?>
|
||||
</figure>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro
|
||||
const { featured_image } = post.data;
|
||||
---
|
||||
{featured_image && (
|
||||
<figure class="featured-image">
|
||||
<img src={featured_image} alt={post.data.title} />
|
||||
</figure>
|
||||
)}
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
### Archive Pagination
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php
|
||||
the_posts_pagination([
|
||||
'prev_text' => '« Previous',
|
||||
'next_text' => 'Next »',
|
||||
]);
|
||||
?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro - using cursor pagination
|
||||
import { getEmDashCollection } from "emdash";
|
||||
const page = Astro.url.searchParams.get('page') || '1';
|
||||
const { entries, nextCursor, prevCursor } = await getEmDashCollection("posts", {
|
||||
limit: 10,
|
||||
cursor: Astro.url.searchParams.get('cursor'),
|
||||
});
|
||||
---
|
||||
<nav class="pagination">
|
||||
{prevCursor && <a href={`?cursor=${prevCursor}`}>« Previous</a>}
|
||||
{nextCursor && <a href={`?cursor=${nextCursor}`}>Next »</a>}
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Post Navigation (Prev/Next)
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php
|
||||
the_post_navigation([
|
||||
'prev_text' => '← %title',
|
||||
'next_text' => '%title →',
|
||||
]);
|
||||
?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro - requires fetching adjacent posts
|
||||
// This is more complex; typically done at query time
|
||||
// or by storing prev/next references
|
||||
---
|
||||
```
|
||||
|
||||
## Conditionals
|
||||
|
||||
### Check Post Type
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php if (is_singular('portfolio')) : ?>
|
||||
<!-- Portfolio-specific content -->
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro - handled by file-based routing
|
||||
// pages/portfolio/[slug].astro IS the portfolio single
|
||||
---
|
||||
```
|
||||
|
||||
### Check Page Template
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php if (is_page_template('templates/full-width.php')) : ?>
|
||||
<div class="full-width">
|
||||
<?php else : ?>
|
||||
<div class="with-sidebar">
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro - add a "template" select field to your pages collection
|
||||
// with options like "Default", "Full Width", etc.
|
||||
// Then in your page route, map templates to layout components:
|
||||
|
||||
import PageDefault from "../../layouts/PageDefault.astro";
|
||||
import PageFullWidth from "../../layouts/PageFullWidth.astro";
|
||||
|
||||
const layouts = {
|
||||
"Default": PageDefault,
|
||||
"Full Width": PageFullWidth,
|
||||
};
|
||||
|
||||
const Layout = layouts[page.data.template as keyof typeof layouts] ?? PageDefault;
|
||||
---
|
||||
<Layout page={page} />
|
||||
```
|
||||
|
||||
## Template Parts
|
||||
|
||||
### Include Template Part
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php get_template_part('template-parts/content', get_post_type()); ?>
|
||||
// Loads template-parts/content-{post_type}.php
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro - use components
|
||||
import PostCard from '../components/PostCard.astro';
|
||||
import PortfolioCard from '../components/PortfolioCard.astro';
|
||||
|
||||
const CardComponent = post.collection === 'portfolio' ? PortfolioCard : PostCard;
|
||||
---
|
||||
<CardComponent post={post} />
|
||||
```
|
||||
|
||||
### Reusable Card Component
|
||||
|
||||
```php
|
||||
// WordPress template-parts/content.php
|
||||
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
|
||||
<header class="entry-header">
|
||||
<?php the_title('<h2 class="entry-title"><a href="' . esc_url(get_permalink()) . '">', '</a></h2>'); ?>
|
||||
</header>
|
||||
<div class="entry-content">
|
||||
<?php the_excerpt(); ?>
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro components/PostCard.astro
|
||||
interface Props {
|
||||
post: {
|
||||
id: string;
|
||||
data: {
|
||||
title: string;
|
||||
excerpt?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
const { post } = Astro.props;
|
||||
---
|
||||
<article id={`post-${post.id}`} class="post">
|
||||
<header class="entry-header">
|
||||
<h2 class="entry-title">
|
||||
<a href={`/posts/${post.id}`}>{post.data.title}</a>
|
||||
</h2>
|
||||
</header>
|
||||
<div class="entry-content">
|
||||
<p>{post.data.excerpt}</p>
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
## Navigation Menus
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php
|
||||
wp_nav_menu([
|
||||
'theme_location' => 'primary',
|
||||
'container' => 'nav',
|
||||
'container_class' => 'primary-nav',
|
||||
]);
|
||||
?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro/EmDash - First-class menu support
|
||||
import { getMenu } from "emdash";
|
||||
|
||||
const primaryMenu = await getMenu("primary");
|
||||
---
|
||||
<nav class="primary-nav">
|
||||
{primaryMenu && (
|
||||
<ul>
|
||||
{primaryMenu.items.map(item => (
|
||||
<li class={item.cssClasses}>
|
||||
<a
|
||||
href={item.url}
|
||||
target={item.target}
|
||||
title={item.titleAttr}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
{/* Nested items for dropdowns */}
|
||||
{item.children.length > 0 && (
|
||||
<ul class="submenu">
|
||||
{item.children.map(child => (
|
||||
<li><a href={child.url}>{child.label}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Recursive Menu Component
|
||||
|
||||
```astro
|
||||
---
|
||||
// components/MenuItem.astro
|
||||
interface Props {
|
||||
item: {
|
||||
label: string;
|
||||
url: string;
|
||||
target?: string;
|
||||
cssClasses?: string;
|
||||
children: Props['item'][];
|
||||
};
|
||||
}
|
||||
const { item } = Astro.props;
|
||||
---
|
||||
<li class={item.cssClasses}>
|
||||
<a href={item.url} target={item.target}>{item.label}</a>
|
||||
{item.children.length > 0 && (
|
||||
<ul class="submenu">
|
||||
{item.children.map(child => (
|
||||
<Astro.self item={child} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
```
|
||||
|
||||
## Sidebars / Widget Areas
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php if (is_active_sidebar('sidebar-1')) : ?>
|
||||
<aside class="sidebar">
|
||||
<?php dynamic_sidebar('sidebar-1'); ?>
|
||||
</aside>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro/EmDash - First-class widget area support
|
||||
import { getWidgetArea, getMenu, getTaxonomyTerms, getEmDashCollection } from "emdash";
|
||||
import { PortableText } from "emdash/astro";
|
||||
|
||||
const sidebar = await getWidgetArea("sidebar");
|
||||
---
|
||||
{sidebar && (
|
||||
<aside class="sidebar">
|
||||
{sidebar.widgets.map(async (widget) => (
|
||||
<div class="widget">
|
||||
{widget.title && <h3 class="widget-title">{widget.title}</h3>}
|
||||
|
||||
{/* Content widget - rich text */}
|
||||
{widget.type === "content" && widget.content && (
|
||||
<PortableText value={widget.content} />
|
||||
)}
|
||||
|
||||
{/* Menu widget - displays a navigation menu */}
|
||||
{widget.type === "menu" && widget.menuName && (
|
||||
<MenuWidget menuName={widget.menuName} />
|
||||
)}
|
||||
|
||||
{/* Component widget - renders a registered component */}
|
||||
{widget.type === "component" && (
|
||||
<WidgetComponent
|
||||
componentId={widget.componentId}
|
||||
props={widget.componentProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</aside>
|
||||
)}
|
||||
```
|
||||
|
||||
### Widget Component Handler
|
||||
|
||||
```astro
|
||||
---
|
||||
// components/WidgetComponent.astro
|
||||
import { getTaxonomyTerms, getEmDashCollection } from "emdash";
|
||||
|
||||
interface Props {
|
||||
componentId: string;
|
||||
props?: Record<string, unknown>;
|
||||
}
|
||||
const { componentId, props = {} } = Astro.props;
|
||||
|
||||
// Handle core widget components
|
||||
let content = null;
|
||||
|
||||
if (componentId === "core:recent-posts") {
|
||||
const limit = (props.limit as number) || 5;
|
||||
const collection = (props.collection as string) || "posts";
|
||||
const { entries: posts } = await getEmDashCollection(collection, { limit });
|
||||
content = posts;
|
||||
}
|
||||
|
||||
if (componentId === "core:categories") {
|
||||
const taxonomy = (props.taxonomy as string) || "categories";
|
||||
content = await getTaxonomyTerms(taxonomy);
|
||||
}
|
||||
|
||||
if (componentId === "core:tag-cloud") {
|
||||
const taxonomy = (props.taxonomy as string) || "tags";
|
||||
content = await getTaxonomyTerms(taxonomy);
|
||||
}
|
||||
---
|
||||
{componentId === "core:recent-posts" && content && (
|
||||
<ul class="recent-posts">
|
||||
{content.map(post => (
|
||||
<li><a href={`/posts/${post.data.slug}`}>{post.data.title}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{componentId === "core:categories" && content && (
|
||||
<ul class="categories">
|
||||
{content.map(cat => (
|
||||
<li>
|
||||
<a href={`/categories/${cat.slug}`}>
|
||||
{cat.label}
|
||||
{props.showCounts && <span>({cat.count})</span>}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{componentId === "core:tag-cloud" && content && (
|
||||
<div class="tag-cloud">
|
||||
{content.map(tag => (
|
||||
<a href={`/tags/${tag.slug}`} class="tag">{tag.label}</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{componentId === "core:search" && (
|
||||
<form action="/search" method="get">
|
||||
<input
|
||||
type="search"
|
||||
name="q"
|
||||
placeholder={props.placeholder || "Search..."}
|
||||
/>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
)}
|
||||
```
|
||||
|
||||
## Taxonomy Archives
|
||||
|
||||
### Category Archive
|
||||
|
||||
```php
|
||||
// WordPress category.php
|
||||
<?php
|
||||
$category = get_queried_object();
|
||||
?>
|
||||
<h1><?php echo $category->name; ?></h1>
|
||||
<p><?php echo $category->description; ?></p>
|
||||
|
||||
<?php while (have_posts()) : the_post(); ?>
|
||||
<!-- post loop -->
|
||||
<?php endwhile; ?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro pages/categories/[slug].astro
|
||||
// NOTE: EmDash pages are always server-rendered (no getStaticPaths)
|
||||
import { getTerm, getEntriesByTerm } from "emdash";
|
||||
import Base from "../../layouts/Base.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}>
|
||||
<h1>{category.label}</h1>
|
||||
{category.description && <p>{category.description}</p>}
|
||||
|
||||
{posts.map(post => (
|
||||
<article>
|
||||
<a href={`/posts/${post.data.slug}`}>{post.data.title}</a>
|
||||
</article>
|
||||
))}
|
||||
</Base>
|
||||
```
|
||||
|
||||
### Tag Archive
|
||||
|
||||
```php
|
||||
// WordPress tag.php
|
||||
<?php
|
||||
$tag = get_queried_object();
|
||||
?>
|
||||
<h1>Posts tagged: <?php echo $tag->name; ?></h1>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro pages/tags/[slug].astro
|
||||
// NOTE: EmDash pages are always server-rendered (no getStaticPaths)
|
||||
import { getTerm, getEntriesByTerm } from "emdash";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
const tag = await getTerm("tags", slug!);
|
||||
const posts = await getEntriesByTerm("posts", "tags", slug!);
|
||||
|
||||
if (!tag) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
---
|
||||
<h1>Posts tagged: {tag.label}</h1>
|
||||
{posts.map(post => (
|
||||
<article>
|
||||
<a href={`/posts/${post.data.slug}`}>{post.data.title}</a>
|
||||
</article>
|
||||
))}
|
||||
```
|
||||
|
||||
### Display Post Terms
|
||||
|
||||
```php
|
||||
// WordPress - in single.php
|
||||
<?php
|
||||
$categories = get_the_category();
|
||||
$tags = get_the_tags();
|
||||
?>
|
||||
<div class="post-meta">
|
||||
<span>Categories:
|
||||
<?php foreach ($categories as $cat) : ?>
|
||||
<a href="<?php echo get_category_link($cat); ?>"><?php echo $cat->name; ?></a>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
<span>Tags:
|
||||
<?php the_tags('', ', '); ?>
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro - in post template
|
||||
import { getEntryTerms } from "emdash";
|
||||
|
||||
const categories = await getEntryTerms("posts", post.id, "categories");
|
||||
const tags = await getEntryTerms("posts", post.id, "tags");
|
||||
---
|
||||
<div class="post-meta">
|
||||
{categories.length > 0 && (
|
||||
<span>Categories:
|
||||
{categories.map((cat, i) => (
|
||||
<>
|
||||
{i > 0 && ", "}
|
||||
<a href={`/categories/${cat.slug}`}>{cat.label}</a>
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{tags.length > 0 && (
|
||||
<span>Tags:
|
||||
{tags.map((tag, i) => (
|
||||
<>
|
||||
{i > 0 && ", "}
|
||||
<a href={`/tags/${tag.slug}`}>{tag.label}</a>
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Hierarchical Category List
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php wp_list_categories(['hierarchical' => true]); ?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro - recursive category tree
|
||||
import { getTaxonomyTerms } from "emdash";
|
||||
|
||||
const categories = await getTaxonomyTerms("categories");
|
||||
|
||||
// Recursive component for nested categories
|
||||
function CategoryTree({ terms }) {
|
||||
return (
|
||||
<ul>
|
||||
{terms.map(term => (
|
||||
<li>
|
||||
<a href={`/categories/${term.slug}`}>
|
||||
{term.label} ({term.count})
|
||||
</a>
|
||||
{term.children?.length > 0 && (
|
||||
<CategoryTree terms={term.children} />
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
---
|
||||
<CategoryTree terms={categories} />
|
||||
```
|
||||
|
||||
## Site Settings
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php
|
||||
$site_name = get_bloginfo('name');
|
||||
$site_desc = get_bloginfo('description');
|
||||
$custom_logo_id = get_theme_mod('custom_logo');
|
||||
$logo_url = wp_get_attachment_image_url($custom_logo_id, 'full');
|
||||
?>
|
||||
<header>
|
||||
<?php if ($logo_url) : ?>
|
||||
<img src="<?php echo $logo_url; ?>" alt="<?php echo $site_name; ?>" />
|
||||
<?php endif; ?>
|
||||
<h1><?php echo $site_name; ?></h1>
|
||||
<p><?php echo $site_desc; ?></p>
|
||||
</header>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro - using EmDash site settings
|
||||
import { getSiteSettings } from "emdash";
|
||||
|
||||
const settings = await getSiteSettings();
|
||||
---
|
||||
<header>
|
||||
{settings.logo?.url && (
|
||||
<img src={settings.logo.url} alt={settings.logo.alt || settings.title} />
|
||||
)}
|
||||
<h1>{settings.title}</h1>
|
||||
{settings.tagline && <p>{settings.tagline}</p>}
|
||||
</header>
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php
|
||||
if (comments_open() || get_comments_number()) :
|
||||
comments_template();
|
||||
endif;
|
||||
?>
|
||||
```
|
||||
|
||||
EmDash doesn't include comments. Options:
|
||||
|
||||
1. **Giscus** - GitHub Discussions-based
|
||||
2. **Disqus** - Third-party
|
||||
3. **Custom** - Build with EmDash collections
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro with Giscus
|
||||
---
|
||||
<script src="https://giscus.app/client.js"
|
||||
data-repo="your/repo"
|
||||
data-repo-id="..."
|
||||
data-category="Comments"
|
||||
data-category-id="..."
|
||||
data-mapping="pathname"
|
||||
crossorigin="anonymous"
|
||||
async>
|
||||
</script>
|
||||
```
|
||||
|
||||
## Search
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php get_search_form(); ?>
|
||||
|
||||
// search.php
|
||||
<?php if (have_posts()) : ?>
|
||||
<h1>Search Results for: <?php the_search_query(); ?></h1>
|
||||
<?php while (have_posts()) : the_post(); ?>
|
||||
<!-- results -->
|
||||
<?php endwhile; ?>
|
||||
<?php else : ?>
|
||||
<p>No results found.</p>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro pages/search.astro
|
||||
import { getEmDashCollection } from "emdash";
|
||||
import Base from "../layouts/Base.astro";
|
||||
|
||||
const query = Astro.url.searchParams.get('q') || '';
|
||||
let results = [];
|
||||
|
||||
if (query) {
|
||||
// Note: Full-text search depends on EmDash implementation
|
||||
const { entries: posts } = await getEmDashCollection("posts");
|
||||
results = posts.filter(p =>
|
||||
p.data.title.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
}
|
||||
---
|
||||
<Base title={`Search: ${query}`}>
|
||||
<form action="/search" method="get">
|
||||
<input type="search" name="q" value={query} />
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
|
||||
{query && (
|
||||
<h1>Search Results for: {query}</h1>
|
||||
{results.length > 0 ? (
|
||||
results.map(post => (
|
||||
<!-- results -->
|
||||
))
|
||||
) : (
|
||||
<p>No results found.</p>
|
||||
)}
|
||||
)}
|
||||
</Base>
|
||||
```
|
||||
|
||||
## Custom Fields (ACF-style)
|
||||
|
||||
```php
|
||||
// WordPress with ACF
|
||||
<?php
|
||||
$subtitle = get_field('subtitle');
|
||||
$gallery = get_field('gallery');
|
||||
?>
|
||||
<h2><?php echo $subtitle; ?></h2>
|
||||
<div class="gallery">
|
||||
<?php foreach ($gallery as $image) : ?>
|
||||
<img src="<?php echo $image['url']; ?>" />
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro - fields are on post.data
|
||||
const { subtitle, gallery } = post.data;
|
||||
---
|
||||
<h2>{subtitle}</h2>
|
||||
<div class="gallery">
|
||||
{gallery?.map(image => (
|
||||
<img src={image.url} />
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Date Formatting
|
||||
|
||||
```php
|
||||
// WordPress
|
||||
<?php echo get_the_date('F j, Y'); ?> // January 23, 2025
|
||||
<?php echo human_time_diff(get_the_time('U'), current_time('timestamp')); ?> ago
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// Astro
|
||||
const date = post.data.publishedAt;
|
||||
const formatted = date?.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
// For relative time
|
||||
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
|
||||
const diff = date ? Date.now() - date.getTime() : 0;
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
const relative = rtf.format(-days, 'day');
|
||||
---
|
||||
<time datetime={date?.toISOString()}>{formatted}</time>
|
||||
<span>{relative}</span>
|
||||
```
|
||||
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": "@emdashcms/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