Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi
This commit is contained in:
83
skills/wordpress-theme-to-emdash/SKILL.md
Normal file
83
skills/wordpress-theme-to-emdash/SKILL.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
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 at `.emdash/seed.json` (or wired up via `package.json#emdash.seed`)
|
||||
|
||||
### Phase 6: Verify
|
||||
|
||||
- [ ] Dev server applied seed cleanly on first request (no validation errors in logs)
|
||||
- [ ] 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.
|
||||
203
skills/wordpress-theme-to-emdash/phases/5-seed.md
Normal file
203
skills/wordpress-theme-to-emdash/phases/5-seed.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# 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 Validation
|
||||
|
||||
The seed is applied automatically on the first request when the database is empty (and no setup wizard run yet). Validation runs at apply time — errors show up in the dev server logs. Restart the dev server after fixing them.
|
||||
|
||||
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.
|
||||
92
skills/wordpress-theme-to-emdash/phases/6-verify.md
Normal file
92
skills/wordpress-theme-to-emdash/phases/6-verify.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Phase 6: Verify & Iterate
|
||||
|
||||
Seed content, run the dev server, compare screenshots, and iterate until pages match.
|
||||
|
||||
## 6.1 Start Dev Server
|
||||
|
||||
The seed is applied on the first request when the database is empty. If you've already run the dev server against an existing database, delete `data.db` first so the seed reapplies.
|
||||
|
||||
Kill any existing server first:
|
||||
|
||||
```bash
|
||||
lsof -ti:4321 | xargs kill -9 2>/dev/null || true
|
||||
rm -f data.db # only if you need a clean slate
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Watch the logs for any seed validation errors and fix them before continuing.
|
||||
|
||||
## 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);
|
||||
}
|
||||
```
|
||||
495
skills/wordpress-theme-to-emdash/references/emdash-api.md
Normal file
495
skills/wordpress-theme-to-emdash/references/emdash-api.md
Normal file
@@ -0,0 +1,495 @@
|
||||
# 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
|
||||
|
||||
### Apply Seed
|
||||
|
||||
The seed is inlined into the build and applied automatically on the first request when the database is empty and the setup wizard hasn't been completed. Validation runs at apply time — errors surface in the dev server logs. To re-seed during iteration, delete `data.db` and restart the dev server.
|
||||
|
||||
Common validation errors caught:
|
||||
|
||||
- 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.)
|
||||
|
||||
### 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>
|
||||
```
|
||||
122
skills/wordpress-theme-to-emdash/scaffold/CHECKLIST.md
Normal file
122
skills/wordpress-theme-to-emdash/scaffold/CHECKLIST.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# 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
|
||||
|
||||
## Phase 6: Verify & Iterate
|
||||
|
||||
- [ ] Dev server applied seed cleanly on first request (no validation errors in logs)
|
||||
- [ ] Dev server running: `pnpm dev`
|
||||
- [ ] Output screenshots captured to `output/`:
|
||||
- [ ] Homepage
|
||||
- [ ] Single post
|
||||
- [ ] Blog archive
|
||||
- [ ] Category archive
|
||||
- [ ] Static page
|
||||
- [ ] 404 page
|
||||
- [ ] Visual comparison completed for each page
|
||||
- [ ] Differences identified and fixed
|
||||
- [ ] Production build succeeds: `pnpm build`
|
||||
|
||||
## License Compliance
|
||||
|
||||
- [ ] `README.md` credits original theme
|
||||
|
||||
If the original theme is GPL-licensed:
|
||||
|
||||
- [ ] `LICENSE` file added (GPL-2.0 text)
|
||||
- [ ] `package.json` has `"license": "GPL-2.0-or-later"`
|
||||
|
||||
## Final Review
|
||||
|
||||
- [ ] All pages render without errors
|
||||
- [ ] Mobile responsive design works
|
||||
- [ ] Navigation works on all pages
|
||||
- [ ] Images load correctly
|
||||
- [ ] Typography matches design intent
|
||||
- [ ] Colors match design intent
|
||||
- [ ] No console errors in browser
|
||||
|
||||
## No Hard-Coded Content
|
||||
|
||||
- [ ] Site title uses `settings.title`, not hard-coded string
|
||||
- [ ] Site tagline uses `settings.tagline`, not hard-coded string
|
||||
- [ ] Logo uses `settings.logo`, not hard-coded path
|
||||
- [ ] Navigation uses `getMenu()`, not hard-coded `<a>` tags
|
||||
- [ ] Footer content uses site settings or widget areas
|
||||
- [ ] No placeholder text like "My Blog" or "Lorem ipsum" in templates
|
||||
101
skills/wordpress-theme-to-emdash/scaffold/README.md
Normal file
101
skills/wordpress-theme-to-emdash/scaffold/README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# EmDash Theme Scaffold
|
||||
|
||||
This is a minimal, working EmDash theme that demonstrates correct patterns for:
|
||||
|
||||
- **Site settings** - Use `getSiteSettings()` for title, tagline, logo - never hard-code
|
||||
- **Menus** - Use `getMenu()` for navigation - never hard-code links
|
||||
- **Image fields** - Always access `.src` and `.alt`, never the field directly
|
||||
- **Taxonomy terms** - Use `getEntryTerms()` without a db parameter
|
||||
- **PortableText** - Use the `<PortableText>` component from `emdash/ui`
|
||||
|
||||
## Critical: No Hard-Coded Content
|
||||
|
||||
The theme is a shell that displays CMS content. Never hard-code:
|
||||
|
||||
- Site title or tagline (use `settings.title`, `settings.tagline`)
|
||||
- Navigation links (use `getMenu("primary")`)
|
||||
- Logo or favicon (use `settings.logo`, `settings.favicon`)
|
||||
- Footer content (use site settings or widget areas)
|
||||
|
||||
## Usage
|
||||
|
||||
When porting a WordPress theme:
|
||||
|
||||
1. Copy this scaffold to your theme directory
|
||||
2. Run `pnpm install` from monorepo root
|
||||
3. Verify it builds: `pnpm --filter your-theme build`
|
||||
4. Use these templates as reference for correct API usage
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Image Fields
|
||||
|
||||
```astro
|
||||
{/* CORRECT - check .src exists */}
|
||||
{post.data.featured_image?.src && (
|
||||
<img
|
||||
src={post.data.featured_image.src}
|
||||
alt={post.data.featured_image.alt || post.data.title}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* WRONG - field is an object, not a string */}
|
||||
{post.data.featured_image && (
|
||||
<img src={post.data.featured_image} /> // Renders [object Object]
|
||||
)}
|
||||
```
|
||||
|
||||
### Taxonomy Terms
|
||||
|
||||
```astro
|
||||
{/* CORRECT - no db parameter */}
|
||||
const categories = await getEntryTerms("posts", post.id, "categories");
|
||||
|
||||
{/* WRONG - db is not a parameter */}
|
||||
const categories = await getEntryTerms("posts", post.id, "categories", db);
|
||||
```
|
||||
|
||||
### Seed File Images
|
||||
|
||||
```json
|
||||
{
|
||||
"featured_image": {
|
||||
"$media": {
|
||||
"url": "https://example.com/image.jpg",
|
||||
"alt": "Description",
|
||||
"filename": "image.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
At runtime, this becomes `{ src: "...", alt: "..." }`.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
scaffold/
|
||||
├── package.json # Working dependency versions
|
||||
├── astro.config.mjs # Minimal config
|
||||
├── tsconfig.json
|
||||
├── src/
|
||||
│ ├── env.d.ts
|
||||
│ ├── live.config.ts # Collection loader setup
|
||||
│ ├── styles/global.css # Minimal styles with comments
|
||||
│ ├── layouts/Base.astro # Header, footer, menus
|
||||
│ ├── components/
|
||||
│ │ └── PostCard.astro # Image field handling example
|
||||
│ └── pages/
|
||||
│ ├── index.astro
|
||||
│ ├── 404.astro
|
||||
│ ├── posts/
|
||||
│ │ ├── index.astro
|
||||
│ │ └── [slug].astro # Taxonomy terms example
|
||||
│ ├── pages/[slug].astro
|
||||
│ ├── categories/[slug].astro
|
||||
│ └── tags/[slug].astro
|
||||
├── public/
|
||||
│ └── favicon.svg
|
||||
└── .emdash/
|
||||
└── seed.json # All field types demonstrated
|
||||
```
|
||||
27
skills/wordpress-theme-to-emdash/scaffold/astro.config.mjs
Normal file
27
skills/wordpress-theme-to-emdash/scaffold/astro.config.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
// @ts-check
|
||||
import node from "@astrojs/node";
|
||||
import react from "@astrojs/react";
|
||||
import { defineConfig } from "astro/config";
|
||||
import emdash from "emdash/astro";
|
||||
import { sqlite } from "emdash/db";
|
||||
|
||||
export default defineConfig({
|
||||
output: "server",
|
||||
adapter: node({ mode: "standalone" }),
|
||||
integrations: [
|
||||
react(),
|
||||
emdash({
|
||||
database: sqlite({ url: "file:./data.db" }),
|
||||
}),
|
||||
],
|
||||
// Optional: Add custom fonts
|
||||
// experimental: {
|
||||
// fonts: [
|
||||
// {
|
||||
// provider: "google",
|
||||
// family: "Inter",
|
||||
// weights: [400, 500, 600, 700],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
});
|
||||
26
skills/wordpress-theme-to-emdash/scaffold/package.json
Normal file
26
skills/wordpress-theme-to-emdash/scaffold/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@emdash-cms/theme-scaffold",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"check": "astro check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^10.0.0-beta.0",
|
||||
"@astrojs/react": "^5.0.0-beta.1",
|
||||
"astro": "^6.0.0-beta.0",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"emdash": "workspace:*",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.6",
|
||||
"@types/node": "^24.10.9",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" fill="#0066cc" rx="6"/>
|
||||
<text x="16" y="22" font-family="system-ui, sans-serif" font-size="16" font-weight="600" fill="white" text-anchor="middle">E</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 257 B |
@@ -0,0 +1,52 @@
|
||||
---
|
||||
/**
|
||||
* PostCard Component
|
||||
*
|
||||
* Displays a post preview with optional featured image.
|
||||
*
|
||||
* IMPORTANT: Image fields are objects with { src, alt }, not strings!
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
href: string;
|
||||
date?: string;
|
||||
excerpt?: string;
|
||||
// Image fields from EmDash are always { src?: string, alt?: string }
|
||||
featuredImage?: {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const { title, href, date, excerpt, featuredImage } = Astro.props;
|
||||
|
||||
const formattedDate = date
|
||||
? new Date(date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: null;
|
||||
---
|
||||
|
||||
<article class="post-card">
|
||||
{/* Check featuredImage.src, not just featuredImage */}
|
||||
{
|
||||
featuredImage?.src && (
|
||||
<a href={href} class="post-card-image">
|
||||
<img src={featuredImage.src} alt={featuredImage.alt || title} loading="lazy" />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="post-card-content">
|
||||
<h2 class="post-card-title">
|
||||
<a href={href}>{title}</a>
|
||||
</h2>
|
||||
|
||||
{formattedDate && <p class="post-card-meta">{formattedDate}</p>}
|
||||
|
||||
{excerpt && <p class="post-card-excerpt">{excerpt}</p>}
|
||||
</div>
|
||||
</article>
|
||||
2
skills/wordpress-theme-to-emdash/scaffold/src/env.d.ts
vendored
Normal file
2
skills/wordpress-theme-to-emdash/scaffold/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
/**
|
||||
* Base Layout
|
||||
*
|
||||
* Main layout with header, footer, and navigation from EmDash menus.
|
||||
*/
|
||||
|
||||
import { getMenu, getSiteSettings } from "emdash";
|
||||
import "../styles/global.css";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
|
||||
// These APIs automatically get the database from the Astro context
|
||||
const settings = await getSiteSettings();
|
||||
const primaryMenu = await getMenu("primary");
|
||||
const footerMenu = await getMenu("footer");
|
||||
|
||||
const siteTitle = settings.title || "My Site";
|
||||
const pageTitle = title ? `${title} | ${siteTitle}` : siteTitle;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{pageTitle}</title>
|
||||
{description && <meta name="description" content={description} />}
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<a href="/" class="site-title">{siteTitle}</a>
|
||||
|
||||
{
|
||||
primaryMenu && primaryMenu.items.length > 0 && (
|
||||
<nav class="site-nav">
|
||||
{primaryMenu.items.map((item) => (
|
||||
<a href={item.url}>{item.label}</a>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
{
|
||||
footerMenu && footerMenu.items.length > 0 && (
|
||||
<nav class="footer-nav">
|
||||
{footerMenu.items.map((item) => (
|
||||
<a href={item.url}>{item.label}</a>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
<p class="copyright">© {new Date().getFullYear()} {siteTitle}</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
19
skills/wordpress-theme-to-emdash/scaffold/src/live.config.ts
Normal file
19
skills/wordpress-theme-to-emdash/scaffold/src/live.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* EmDash Live Config
|
||||
*
|
||||
* This file defines your content collections using EmDash's loader.
|
||||
* It replaces Astro's content collections for CMS-managed content.
|
||||
*/
|
||||
|
||||
import { defineCollection } from "astro:content";
|
||||
import { emdashLoader } from "emdash";
|
||||
|
||||
// Posts collection - loaded from EmDash CMS
|
||||
export const collections = {
|
||||
posts: defineCollection({
|
||||
loader: emdashLoader({ collection: "posts" }),
|
||||
}),
|
||||
pages: defineCollection({
|
||||
loader: emdashLoader({ collection: "pages" }),
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
/**
|
||||
* 404 Page
|
||||
*/
|
||||
|
||||
import Base from "../layouts/Base.astro";
|
||||
---
|
||||
|
||||
<Base title="Page Not Found">
|
||||
<div class="container">
|
||||
<div class="content-width" style="text-align: center; padding: 4rem 0;">
|
||||
<h1>404</h1>
|
||||
<p class="text-muted">The page you're looking for doesn't exist.</p>
|
||||
<p><a href="/">Go home</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
/**
|
||||
* Category Archive
|
||||
*
|
||||
* Demonstrates:
|
||||
* - getTerm for fetching taxonomy term details
|
||||
* - getEntriesByTerm for entries with a specific term
|
||||
*/
|
||||
|
||||
import { getTerm, getEntriesByTerm } from "emdash";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import PostCard from "../../components/PostCard.astro";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
const category = await getTerm("categories", slug!);
|
||||
const posts = await getEntriesByTerm("posts", "categories", slug!);
|
||||
|
||||
if (!category) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
---
|
||||
|
||||
<Base title={category.label}>
|
||||
<div class="container">
|
||||
<div class="content-width">
|
||||
<header class="archive-header">
|
||||
<h1>{category.label}</h1>
|
||||
</header>
|
||||
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<div class="posts-list">
|
||||
{posts.map((post) => (
|
||||
<PostCard
|
||||
title={post.data.title}
|
||||
href={`/posts/${post.data.slug || post.id}`}
|
||||
date={post.data.published_at}
|
||||
excerpt={post.data.excerpt}
|
||||
featuredImage={
|
||||
post.data.featured_image?.src
|
||||
? {
|
||||
src: post.data.featured_image.src,
|
||||
alt: post.data.featured_image.alt || post.data.title,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p class="text-muted">No posts in this category.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
/**
|
||||
* Homepage / Blog Index
|
||||
*
|
||||
* Demonstrates:
|
||||
* - getEmDashCollection for listing entries
|
||||
* - Passing image fields correctly to components
|
||||
*/
|
||||
|
||||
import { getEmDashCollection } from "emdash";
|
||||
import Base from "../layouts/Base.astro";
|
||||
import PostCard from "../components/PostCard.astro";
|
||||
|
||||
const { entries: posts } = await getEmDashCollection("posts", {
|
||||
status: "published",
|
||||
limit: 10,
|
||||
});
|
||||
---
|
||||
|
||||
<Base>
|
||||
<div class="container">
|
||||
<div class="content-width">
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<div class="posts-list">
|
||||
{posts.map((post) => (
|
||||
<PostCard
|
||||
title={post.data.title}
|
||||
href={`/posts/${post.data.slug || post.id}`}
|
||||
date={post.data.published_at}
|
||||
excerpt={post.data.excerpt}
|
||||
{
|
||||
/*
|
||||
IMPORTANT: featured_image is { src, alt }, not a string!
|
||||
Pass the whole object, or extract src/alt explicitly.
|
||||
*/ }
|
||||
featuredImage={
|
||||
post.data.featured_image?.src
|
||||
? {
|
||||
src: post.data.featured_image.src,
|
||||
alt: post.data.featured_image.alt || post.data.title,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p class="text-muted">No posts yet.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
/**
|
||||
* Single Page
|
||||
*/
|
||||
|
||||
import { getEmDashEntry } from "emdash";
|
||||
import { PortableText } from "emdash/ui";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
const { entry: page, error } = await getEmDashEntry("pages", slug!);
|
||||
|
||||
if (error) {
|
||||
return new Response("Server error", { status: 500 });
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
---
|
||||
|
||||
<Base title={page.data.title}>
|
||||
<article class="container">
|
||||
<div class="content-width">
|
||||
<header class="page-header">
|
||||
<h1>{page.data.title}</h1>
|
||||
</header>
|
||||
|
||||
<div class="prose">
|
||||
{page.data.content && <PortableText value={page.data.content} />}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Base>
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
/**
|
||||
* Single Post
|
||||
*
|
||||
* Demonstrates:
|
||||
* - getEmDashEntry for fetching a single entry
|
||||
* - getEntryTerms for taxonomy terms (NO db parameter!)
|
||||
* - PortableText component for rich content
|
||||
* - Proper image field access
|
||||
*/
|
||||
|
||||
import { getEmDashEntry, getEntryTerms } from "emdash";
|
||||
import { PortableText } from "emdash/ui";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
const { entry: post, error } = await getEmDashEntry("posts", slug!);
|
||||
|
||||
if (error) {
|
||||
return new Response("Server error", { status: 500 });
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
|
||||
// Get taxonomy terms - NOTE: no db parameter!
|
||||
const categories = await getEntryTerms("posts", post.id, "categories");
|
||||
const tags = await getEntryTerms("posts", post.id, "tags");
|
||||
|
||||
const formattedDate = post.data.published_at
|
||||
? new Date(post.data.published_at).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: null;
|
||||
---
|
||||
|
||||
<Base title={post.data.title} description={post.data.excerpt}>
|
||||
<article class="container">
|
||||
<div class="content-width">
|
||||
<header class="post-header">
|
||||
<h1>{post.data.title}</h1>
|
||||
|
||||
<div class="post-meta">
|
||||
{
|
||||
formattedDate && (
|
||||
<time datetime={post.data.published_at}>{formattedDate}</time>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
categories.length > 0 && (
|
||||
<span class="post-categories">
|
||||
in{" "}
|
||||
{categories.map((cat, i) => (
|
||||
<>
|
||||
{i > 0 && ", "}
|
||||
<a href={`/categories/${cat.slug}`}>{cat.label}</a>
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* IMPORTANT: Check .src, not just the field */}
|
||||
{
|
||||
post.data.featured_image?.src && (
|
||||
<div class="post-featured-image">
|
||||
<img
|
||||
src={post.data.featured_image.src}
|
||||
alt={post.data.featured_image.alt || post.data.title}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="prose">
|
||||
{post.data.content && <PortableText value={post.data.content} />}
|
||||
</div>
|
||||
|
||||
{
|
||||
tags.length > 0 && (
|
||||
<div class="post-tags">
|
||||
{tags.map((tag) => (
|
||||
<a href={`/tags/${tag.slug}`} class="tag">
|
||||
{tag.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
</Base>
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
/**
|
||||
* Posts Archive
|
||||
*/
|
||||
|
||||
import { getEmDashCollection } from "emdash";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import PostCard from "../../components/PostCard.astro";
|
||||
|
||||
const { entries: posts } = await getEmDashCollection("posts", {
|
||||
status: "published",
|
||||
});
|
||||
---
|
||||
|
||||
<Base title="Blog">
|
||||
<div class="container">
|
||||
<div class="content-width">
|
||||
<header class="archive-header">
|
||||
<h1>Blog</h1>
|
||||
</header>
|
||||
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<div class="posts-list">
|
||||
{posts.map((post) => (
|
||||
<PostCard
|
||||
title={post.data.title}
|
||||
href={`/posts/${post.data.slug || post.id}`}
|
||||
date={post.data.published_at}
|
||||
excerpt={post.data.excerpt}
|
||||
featuredImage={
|
||||
post.data.featured_image?.src
|
||||
? {
|
||||
src: post.data.featured_image.src,
|
||||
alt: post.data.featured_image.alt || post.data.title,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p class="text-muted">No posts found.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
/**
|
||||
* Tag Archive
|
||||
*/
|
||||
|
||||
import { getTerm, getEntriesByTerm } from "emdash";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import PostCard from "../../components/PostCard.astro";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
const tag = await getTerm("tags", slug!);
|
||||
const posts = await getEntriesByTerm("posts", "tags", slug!);
|
||||
|
||||
if (!tag) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
---
|
||||
|
||||
<Base title={`Tagged: ${tag.label}`}>
|
||||
<div class="container">
|
||||
<div class="content-width">
|
||||
<header class="archive-header">
|
||||
<p class="text-muted">Tagged</p>
|
||||
<h1>{tag.label}</h1>
|
||||
</header>
|
||||
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<div class="posts-list">
|
||||
{posts.map((post) => (
|
||||
<PostCard
|
||||
title={post.data.title}
|
||||
href={`/posts/${post.data.slug || post.id}`}
|
||||
date={post.data.published_at}
|
||||
excerpt={post.data.excerpt}
|
||||
featuredImage={
|
||||
post.data.featured_image?.src
|
||||
? {
|
||||
src: post.data.featured_image.src,
|
||||
alt: post.data.featured_image.alt || post.data.title,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p class="text-muted">No posts with this tag.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
274
skills/wordpress-theme-to-emdash/scaffold/src/styles/global.css
Normal file
274
skills/wordpress-theme-to-emdash/scaffold/src/styles/global.css
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Global Styles - Scaffold Theme
|
||||
*
|
||||
* Minimal styles for demonstration. Replace with your theme's design tokens.
|
||||
*/
|
||||
|
||||
/* Reset */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Variables - replace with your theme's tokens */
|
||||
:root {
|
||||
--color-text: #1a1a1a;
|
||||
--color-text-muted: #666;
|
||||
--color-bg: #fff;
|
||||
--color-border: #e5e5e5;
|
||||
--color-accent: #0066cc;
|
||||
|
||||
--font-body: system-ui, -apple-system, sans-serif;
|
||||
--font-mono: ui-monospace, monospace;
|
||||
|
||||
--space-sm: 0.5rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 2rem;
|
||||
--space-xl: 4rem;
|
||||
|
||||
--content-width: 40rem;
|
||||
--container-width: 60rem;
|
||||
}
|
||||
|
||||
/* Base */
|
||||
html {
|
||||
font-family: var(--font-body);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
padding: var(--space-xl) var(--space-md);
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
max-width: var(--container-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-md);
|
||||
}
|
||||
|
||||
.content-width {
|
||||
max-width: var(--content-width);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.site-header {
|
||||
padding: var(--space-md) 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.site-header .container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.site-nav {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.site-nav a {
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-nav a:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.site-footer {
|
||||
padding: var(--space-lg) 0;
|
||||
border-top: 1px solid var(--color-border);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.footer-nav a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.3;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
background: var(--color-border);
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Post Card */
|
||||
.post-card {
|
||||
margin-bottom: var(--space-lg);
|
||||
padding-bottom: var(--space-lg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.post-card:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.post-card-image {
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.post-card-image img {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.post-card-title {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.post-card-title a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-card-title a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.post-card-meta {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.post-card-excerpt {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Single Post/Page */
|
||||
.post-header,
|
||||
.page-header {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.post-featured-image {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.post-featured-image img {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Taxonomy terms */
|
||||
.post-categories a,
|
||||
.post-tags a {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.post-tags {
|
||||
margin-top: var(--space-lg);
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 0.875rem;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--color-border);
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.tag:hover {
|
||||
background: var(--color-text);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
/* Prose (PortableText content) */
|
||||
.prose > * + * {
|
||||
margin-top: var(--space-md);
|
||||
}
|
||||
|
||||
.prose h2,
|
||||
.prose h3,
|
||||
.prose h4 {
|
||||
margin-top: var(--space-lg);
|
||||
}
|
||||
|
||||
/* Archive header */
|
||||
.archive-header {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
/* Utility */
|
||||
.text-muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
8
skills/wordpress-theme-to-emdash/scaffold/tsconfig.json
Normal file
8
skills/wordpress-theme-to-emdash/scaffold/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": ["src/**/*", ".astro/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user