Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi
This commit is contained in:
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>
|
||||
```
|
||||
Reference in New Issue
Block a user