Emdash source with visual editor image upload fix
Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
23
demos/plugins-demo/CHANGELOG.md
Normal file
23
demos/plugins-demo/CHANGELOG.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# emdash-plugins-demo
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`3c319ed`](https://github.com/emdash-cms/emdash/commit/3c319ed6411a595e6974a86bc58c2a308b91c214)]:
|
||||
- emdash@0.0.3
|
||||
- @emdash-cms/plugin-api-test@0.0.3
|
||||
- @emdash-cms/plugin-audit-log@0.0.3
|
||||
- @emdash-cms/plugin-embeds@0.0.3
|
||||
- @emdash-cms/plugin-webhook-notifier@0.0.3
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`b09bfd5`](https://github.com/emdash-cms/emdash/commit/b09bfd51cece5e88fe8314668a591ab11de36b4d)]:
|
||||
- emdash@0.0.2
|
||||
- @emdash-cms/plugin-api-test@0.0.2
|
||||
- @emdash-cms/plugin-audit-log@0.0.2
|
||||
- @emdash-cms/plugin-embeds@0.0.2
|
||||
- @emdash-cms/plugin-webhook-notifier@0.0.2
|
||||
70
demos/plugins-demo/README.md
Normal file
70
demos/plugins-demo/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# EmDash Plugins Demo
|
||||
|
||||
This demo showcases EmDash's plugin system with plugins that demonstrate the hook architecture.
|
||||
|
||||
## Plugins Included
|
||||
|
||||
### 1. Audit Log Plugin (`@emdash-cms/plugin-audit-log`)
|
||||
|
||||
Tracks all content changes for compliance.
|
||||
|
||||
- **Hooks:**
|
||||
- `content:beforeSave` (priority 1) - captures "before" state
|
||||
- `content:afterSave` (priority 200) - logs final state
|
||||
- `content:beforeDelete` (priority 200) - logs deletions
|
||||
- `media:afterUpload` (priority 200) - logs uploads
|
||||
- **Features:**
|
||||
- Create/update/delete tracking
|
||||
- Before/after state comparison
|
||||
- Admin history page
|
||||
|
||||
### 2. Webhook Notifier Plugin (`@emdash-cms/plugin-webhook-notifier`)
|
||||
|
||||
Posts JSON payloads to external webhook URLs on content/media events.
|
||||
|
||||
- **Hooks:** `content:afterSave`, `content:afterDelete`, `media:afterUpload` (priority 210)
|
||||
- **Features:**
|
||||
- Retry with exponential backoff
|
||||
- Admin-configurable settings (URL, secret token)
|
||||
- SSRF protection
|
||||
- Delivery tracking
|
||||
|
||||
### 3. Embeds Plugin (`@emdash-cms/plugin-embeds`)
|
||||
|
||||
Provides Portable Text block types for embedding external content.
|
||||
|
||||
- **Features:**
|
||||
- YouTube, Vimeo, Twitter/X, Bluesky, Mastodon embeds
|
||||
- Link previews (Open Graph)
|
||||
- GitHub Gist embeds
|
||||
|
||||
### 4. API Test Plugin (`@emdash-cms/plugin-api-test`)
|
||||
|
||||
Exercises all v2 plugin APIs for testing.
|
||||
|
||||
- **Features:**
|
||||
- Routes for each plugin API (kv, storage, content, media, http)
|
||||
- Combined `test/all` route
|
||||
|
||||
## Running the Demo
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Seed sample content
|
||||
pnpm seed
|
||||
|
||||
# Start development server
|
||||
pnpm dev
|
||||
|
||||
# Open browser
|
||||
open http://localhost:4321
|
||||
```
|
||||
|
||||
## Testing Plugin Hooks
|
||||
|
||||
1. Open the admin at `http://localhost:4321/_emdash/admin`
|
||||
2. Create a new post with a title like "Hello World! Testing Plugins"
|
||||
3. Watch the console output to see hooks executing:
|
||||
- `[audit-log] + create content/posts/post-xxx`
|
||||
44
demos/plugins-demo/astro.config.mjs
Normal file
44
demos/plugins-demo/astro.config.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
import node from "@astrojs/node";
|
||||
import react from "@astrojs/react";
|
||||
import { apiTestPlugin } from "@emdash-cms/plugin-api-test";
|
||||
import { auditLogPlugin } from "@emdash-cms/plugin-audit-log";
|
||||
import { embedsPlugin } from "@emdash-cms/plugin-embeds";
|
||||
import { webhookNotifierPlugin } from "@emdash-cms/plugin-webhook-notifier";
|
||||
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({
|
||||
// SQLite database for demo
|
||||
database: sqlite({ url: "file:./data.db" }),
|
||||
|
||||
// Register plugins - order matters for hook execution!
|
||||
plugins: [
|
||||
// 1. Audit log runs last (priority 200) to capture final state
|
||||
// Settings (retention, data changes, excluded collections) are
|
||||
// configured at runtime via the admin UI, not constructor options.
|
||||
auditLogPlugin(),
|
||||
|
||||
// 2. Webhook notifier sends events to external URLs
|
||||
// Demonstrates: network:fetch:any, apiRoutes, settings.secret(),
|
||||
// hook dependencies, errorPolicy: "continue"
|
||||
// Webhook URL, collections, and actions are configured via admin settings.
|
||||
webhookNotifierPlugin(),
|
||||
|
||||
// 3. Embeds plugin for YouTube, Vimeo, Twitter, etc.
|
||||
// Components are auto-registered with PortableText
|
||||
embedsPlugin(),
|
||||
|
||||
// 4. API Test plugin - exercises all v2 APIs
|
||||
apiTestPlugin(),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
39
demos/plugins-demo/emdash-env.d.ts
vendored
Normal file
39
demos/plugins-demo/emdash-env.d.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
// Generated by EmDash on dev server start
|
||||
// Do not edit manually
|
||||
|
||||
/// <reference types="emdash/locals" />
|
||||
|
||||
import type { ContentBylineCredit, PortableTextBlock } from "emdash";
|
||||
|
||||
export interface Page {
|
||||
id: string;
|
||||
slug: string | null;
|
||||
status: string;
|
||||
title: string;
|
||||
content?: PortableTextBlock[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
publishedAt: Date | null;
|
||||
bylines?: ContentBylineCredit[];
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
id: string;
|
||||
slug: string | null;
|
||||
status: string;
|
||||
title: string;
|
||||
featured_image?: { id: string; src?: string; alt?: string; width?: number; height?: number };
|
||||
content?: PortableTextBlock[];
|
||||
excerpt?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
publishedAt: Date | null;
|
||||
bylines?: ContentBylineCredit[];
|
||||
}
|
||||
|
||||
declare module "emdash" {
|
||||
interface EmDashCollections {
|
||||
pages: Page;
|
||||
posts: Post;
|
||||
}
|
||||
}
|
||||
34
demos/plugins-demo/package.json
Normal file
34
demos/plugins-demo/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "emdash-plugins-demo",
|
||||
"version": "0.0.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"start": "node ./dist/server/entry.mjs",
|
||||
"seed": "emdash seed",
|
||||
"typecheck": "astro check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "catalog:",
|
||||
"@astrojs/react": "catalog:",
|
||||
"@emdash-cms/plugin-audit-log": "workspace:*",
|
||||
"@emdash-cms/plugin-api-test": "workspace:*",
|
||||
"@emdash-cms/plugin-webhook-notifier": "workspace:*",
|
||||
"@emdash-cms/plugin-embeds": "workspace:*",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-router": "catalog:",
|
||||
"astro": "catalog:",
|
||||
"better-sqlite3": "catalog:",
|
||||
"emdash": "workspace:*",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"peerDependencies": {},
|
||||
"optionalDependencies": {}
|
||||
}
|
||||
13
demos/plugins-demo/src/live.config.ts
Normal file
13
demos/plugins-demo/src/live.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* EmDash Live Content Collections
|
||||
*
|
||||
* Defines the _emdash collection that handles all content types from the database.
|
||||
* Query specific types using getEmDashCollection() and getEmDashEntry().
|
||||
*/
|
||||
|
||||
import { defineLiveCollection } from "astro:content";
|
||||
import { emdashLoader } from "emdash/runtime";
|
||||
|
||||
export const collections = {
|
||||
_emdash: defineLiveCollection({ loader: emdashLoader() }),
|
||||
};
|
||||
182
demos/plugins-demo/src/pages/index.astro
Normal file
182
demos/plugins-demo/src/pages/index.astro
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
// Demo homepage showcasing plugin functionality
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>EmDash Plugins Demo</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1a1a1a;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.subtitle {
|
||||
color: #666;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
.plugin-list {
|
||||
list-style: none;
|
||||
}
|
||||
.plugin {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.plugin h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #2563eb;
|
||||
}
|
||||
.plugin p {
|
||||
color: #555;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.plugin code {
|
||||
background: #e2e8f0;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.hooks {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.hooks strong {
|
||||
color: #333;
|
||||
}
|
||||
.cta {
|
||||
display: inline-block;
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.cta:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
.order {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>EmDash Plugins Demo</h1>
|
||||
<p class="subtitle">
|
||||
Demonstrating the plugin hook system with realistic plugins
|
||||
</p>
|
||||
|
||||
<h2>Active Plugins</h2>
|
||||
<ul class="plugin-list">
|
||||
<li class="plugin">
|
||||
<h3>Auto-Slug <span class="order">Priority 10</span></h3>
|
||||
<p>
|
||||
Automatically generates URL-friendly slugs from titles. Handles unicode,
|
||||
removes special characters, and ensures clean URLs.
|
||||
</p>
|
||||
<div class="hooks">
|
||||
<strong>Hook:</strong> <code>content:beforeSave</code>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="plugin">
|
||||
<h3>SEO Demo <span class="order">Priority 50</span></h3>
|
||||
<p>
|
||||
Validates SEO fields (meta title, description) against length limits.
|
||||
Provides admin UI for SEO settings and dashboard widget.
|
||||
</p>
|
||||
<div class="hooks">
|
||||
<strong>Hook:</strong> <code>content:beforeSave</code>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="plugin">
|
||||
<h3>Reading Time <span class="order">Priority 80</span></h3>
|
||||
<p>
|
||||
Calculates reading time based on word count and images. Parses Portable
|
||||
Text content and stores result in the content data.
|
||||
</p>
|
||||
<div class="hooks">
|
||||
<strong>Hook:</strong> <code>content:beforeSave</code>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="plugin">
|
||||
<h3>Audit Log <span class="order">Priority 200</span></h3>
|
||||
<p>
|
||||
Tracks all content changes for compliance and debugging. Logs create,
|
||||
update, delete operations with timestamps.
|
||||
</p>
|
||||
<div class="hooks">
|
||||
<strong>Hooks:</strong>
|
||||
<code>content:beforeSave</code>,
|
||||
<code>content:afterSave</code>,
|
||||
<code>content:beforeDelete</code>,
|
||||
<code>media:afterUpload</code>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="plugin">
|
||||
<h3>Image Optimizer <span class="order">Priority 10</span></h3>
|
||||
<p>
|
||||
Validates image uploads (file type, size limits). Sanitizes filenames
|
||||
and adds timestamps for uniqueness.
|
||||
</p>
|
||||
<div class="hooks">
|
||||
<strong>Hooks:</strong>
|
||||
<code>media:beforeUpload</code>,
|
||||
<code>media:afterUpload</code>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Hook Execution Order</h2>
|
||||
<p>
|
||||
When saving content, hooks execute in priority order (lower numbers first):
|
||||
</p>
|
||||
<ol style="margin: 1rem 0 1rem 1.5rem;">
|
||||
<li><strong>Auto-Slug (10)</strong> - Generates slug from title</li>
|
||||
<li><strong>Audit Log (1)</strong> - Captures "before" state for comparison</li>
|
||||
<li><strong>SEO (50)</strong> - Validates SEO field lengths</li>
|
||||
<li><strong>Reading Time (80)</strong> - Calculates reading time</li>
|
||||
<li><strong>Audit Log (200)</strong> - Logs the final saved state</li>
|
||||
</ol>
|
||||
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
<a href="/_emdash/admin" class="cta">Open Admin Panel</a>
|
||||
<a href="/posts" class="cta" style="background: #059669;">View Posts</a>
|
||||
<a href="/test-embeds" class="cta" style="background: #7c3aed;">Test Embeds</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
176
demos/plugins-demo/src/pages/posts/[slug].astro
Normal file
176
demos/plugins-demo/src/pages/posts/[slug].astro
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
/**
|
||||
* Individual post page with PortableText rendering
|
||||
*
|
||||
* This demonstrates the embeds plugin auto-registering components
|
||||
* for YouTube, Vimeo, etc. with the PortableText renderer.
|
||||
*/
|
||||
import { getEmDashEntry, decodeSlug } from "emdash";
|
||||
import { PortableText } from "emdash/ui";
|
||||
import { embedComponents } from "@emdash-cms/plugin-embeds/astro";
|
||||
|
||||
const slug = decodeSlug(Astro.params.slug);
|
||||
|
||||
const { entry: post } = slug
|
||||
? await getEmDashEntry("posts", slug)
|
||||
: { entry: null };
|
||||
|
||||
if (!post) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
|
||||
const title = post.data.title;
|
||||
const content = post.data.content;
|
||||
const metaDescription = post.data.excerpt;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>{title || "Post"} - EmDash Plugins Demo</title>
|
||||
{metaDescription && <meta name="description" content={metaDescription} />}
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1a1a1a;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.meta {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.back {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
/* Content styles */
|
||||
.content {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.content h1 {
|
||||
font-size: 1.75rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.content h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.content h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.content p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.content ul,
|
||||
.content ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.content li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.content blockquote {
|
||||
border-left: 4px solid #e5e7eb;
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
color: #4b5563;
|
||||
font-style: italic;
|
||||
}
|
||||
.content pre {
|
||||
background: #f3f4f6;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.content code {
|
||||
background: #f3f4f6;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
.content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.content a {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
/* Embed styles */
|
||||
.content iframe {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 3rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
footer a {
|
||||
color: #2563eb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/posts" class="back">← Back to posts</a>
|
||||
|
||||
<article>
|
||||
<h1>{title || "Untitled"}</h1>
|
||||
<div class="meta">
|
||||
{post.data.status === "draft" && <span>Draft</span>}
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
{
|
||||
Array.isArray(content) && content.length > 0 ? (
|
||||
<PortableText
|
||||
value={content}
|
||||
components={{ type: embedComponents }}
|
||||
/>
|
||||
) : typeof content === "string" && content ? (
|
||||
<p>{content}</p>
|
||||
) : (
|
||||
<p style="color: #6b7280; font-style: italic;">No content yet.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<footer>
|
||||
<a href={`/_emdash/admin/content/posts/${post.id}`}>Edit in Admin</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
113
demos/plugins-demo/src/pages/posts/index.astro
Normal file
113
demos/plugins-demo/src/pages/posts/index.astro
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
/**
|
||||
* Posts listing page
|
||||
*/
|
||||
import { getEmDashCollection } from "emdash";
|
||||
|
||||
const { entries: posts } = await getEmDashCollection("posts");
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Posts - EmDash Plugins Demo</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1a1a1a;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.back {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
color: #2563eb;
|
||||
}
|
||||
.post-list {
|
||||
list-style: none;
|
||||
}
|
||||
.post-item {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
.post-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.post-title {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.post-title a {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
}
|
||||
.post-title a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.post-meta {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
.empty {
|
||||
color: #6b7280;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/" class="back">← Back to home</a>
|
||||
<h1>Posts</h1>
|
||||
|
||||
{
|
||||
posts.length === 0 ? (
|
||||
<div class="empty">
|
||||
<p>No posts yet.</p>
|
||||
<p>
|
||||
<a href="/_emdash/admin/content/posts/new">
|
||||
Create your first post
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<ul class="post-list">
|
||||
{posts.map((post) => (
|
||||
<li class="post-item">
|
||||
<h2 class="post-title">
|
||||
<a href={`/posts/${post.data.slug || post.id}`}>
|
||||
{post.data.title || "Untitled"}
|
||||
</a>
|
||||
</h2>
|
||||
<div class="post-meta">
|
||||
{post.data.status === "draft" && <span>Draft</span>}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
<footer
|
||||
style="margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #e5e7eb; font-size: 0.875rem;"
|
||||
>
|
||||
<a href="/_emdash/admin" style="color: #2563eb;">Open Admin</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
129
demos/plugins-demo/src/pages/test-embeds.astro
Normal file
129
demos/plugins-demo/src/pages/test-embeds.astro
Normal file
@@ -0,0 +1,129 @@
|
||||
---
|
||||
/**
|
||||
* Test page for embed components
|
||||
*
|
||||
* This page renders hardcoded Portable Text with embed blocks.
|
||||
*/
|
||||
import { PortableText } from "emdash/ui";
|
||||
import { embedComponents } from "@emdash-cms/plugin-embeds/astro";
|
||||
|
||||
// Sample Portable Text content with various embed types
|
||||
const testContent = [
|
||||
{
|
||||
_type: "block",
|
||||
_key: "intro",
|
||||
style: "normal",
|
||||
children: [
|
||||
{
|
||||
_type: "span",
|
||||
text: "This page tests the auto-registered embed components. If you see the embeds below, the virtual module system is working!",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_type: "block",
|
||||
_key: "h1",
|
||||
style: "h2",
|
||||
children: [{ _type: "span", text: "YouTube Embed" }],
|
||||
},
|
||||
{
|
||||
_type: "youtube",
|
||||
_key: "yt1",
|
||||
id: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
},
|
||||
{
|
||||
_type: "block",
|
||||
_key: "h2",
|
||||
style: "h2",
|
||||
children: [{ _type: "span", text: "Vimeo Embed" }],
|
||||
},
|
||||
{
|
||||
_type: "vimeo",
|
||||
_key: "vim1",
|
||||
id: "https://vimeo.com/76979871",
|
||||
},
|
||||
{
|
||||
_type: "block",
|
||||
_key: "h3",
|
||||
style: "h2",
|
||||
children: [{ _type: "span", text: "Link Preview" }],
|
||||
},
|
||||
{
|
||||
_type: "linkPreview",
|
||||
_key: "lp1",
|
||||
id: "https://astro.build",
|
||||
},
|
||||
{
|
||||
_type: "block",
|
||||
_key: "outro",
|
||||
style: "normal",
|
||||
children: [
|
||||
{
|
||||
_type: "span",
|
||||
text: "If you see the embeds above rendered correctly, the plugin system is working! No manual component wiring was needed.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Test Embeds - EmDash Plugins Demo</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1a1a1a;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.content {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
a {
|
||||
color: #2563eb;
|
||||
}
|
||||
.back {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/" class="back">← Back to home</a>
|
||||
<h1>Embed Components Test</h1>
|
||||
<p>This page tests embed components from the embeds plugin.</p>
|
||||
|
||||
<div class="content">
|
||||
<PortableText
|
||||
value={testContent}
|
||||
components={{ type: embedComponents }}
|
||||
/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
7
demos/plugins-demo/tsconfig.json
Normal file
7
demos/plugins-demo/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": ["src/**/*", "astro.config.mjs", "emdash-env.d.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user