A property valuation request feature delivered as an EmDash plugin + Astro integration pair. It demonstrates the route injection pattern described in emdash-lms-patterns.md.
| Piece | What it does |
|---|---|
/valuation page |
Server-rendered form injected by the Astro integration — no file in src/pages/ needed |
/_emdash/api/plugins/pwb-valuation/submit |
Public API route that stores form submissions |
/_emdash/api/plugins/pwb-valuation/list |
Admin-only JSON list of all submissions |
| Admin UI | Block Kit page under EmDash admin showing all valuation requests |
Both pieces must be registered in astro.config.mjs:
import { pwbValuationPlugin } from 'pwb-valuation'
import { pwbValuationIntegration } from 'pwb-valuation/integration'
export default defineConfig({
integrations: [
pwbValuationIntegration({
layout: './src/layouts/BaseLayout.astro',
pwbClientModule: './src/lib/pwb/client.js',
}),
emdash({
plugins: [..., pwbValuationPlugin()],
}),
],
})pwbValuationIntegration is an Astro integration — it injects the /valuation page route
and sets up the virtual modules that let the injected page import from the host site.
pwbValuationPlugin is an EmDash plugin — it registers the API routes and admin UI.
Both are needed. The integration alone gives you a page with no backend. The plugin alone gives you an API with no frontend.
| Option | Default | Description |
|---|---|---|
layout |
./src/layouts/BaseLayout.astro |
Path to the host's layout component (relative to project root) |
pwbClientModule |
./src/lib/pwb/client.js |
Path to the PWB client module (relative to project root) |
basePath |
'' |
URL prefix — set to '/en' for locale-prefixed routes |
The page (src/pages/valuation.astro inside the plugin package) cannot use relative imports
to the host site. Virtual modules bridge the gap — the Vite plugin in integration.js resolves
these at build time:
| Import | Resolves to |
|---|---|
virtual:pwb-valuation/layout |
Host's BaseLayout.astro |
virtual:pwb-valuation/pwb-client |
Host's client.js (re-exports createPwbClient) |
virtual:pwb-valuation/config |
{ basePath } serialised from integration options |
The page handles both GET (render form) and POST (forward JSON to the plugin's submit API
route, then redirect to ?sent=1).
POST /_emdash/api/plugins/pwb-valuation/submit
Public — no authentication required. EmDash pre-parses the request body into routeCtx.input;
do not call routeCtx.request.json() as the stream is already consumed.
| Field | Required | Description |
|---|---|---|
name |
Yes | Requester's full name |
email |
Yes | Requester's email address |
address |
Yes | Property address to value |
phone |
No | Contact phone number |
notes |
No | Free-text notes |
All fields are trimmed before validation. Whitespace-only strings are treated as empty.
| Status | Body | Meaning |
|---|---|---|
| 200 | { success: true, id: "..." } |
Stored successfully |
| 422 | { error: "..." } |
Missing required field |
Submissions are stored in the plugin's valuations collection, indexed by email, status,
and createdAt. Stored records have the shape:
{
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+1 555 000 0000",
"address": "123 Main St, Marbella",
"notes": "Three bed preferred",
"status": "new",
"createdAt": "2024-06-01T12:00:00.000Z"
}status is always "new" on creation. Future work could add a status-update route for the
admin to mark requests as reviewed or completed.
Route handlers and UI-block helpers are tested in
packages/plugins/pwb-valuation/src/sandbox-entry.test.js (19 tests).
Run them with:
npx vitest run packages/plugins/pwb-valuation/src/sandbox-entry.test.jsCoverage includes:
buildValuationRows— field mapping and missing-field fallbacksbuildListBlocks— empty state, table rendering, stats countssubmitroute — happy path, whitespace trimming, all required-field 422 cases, non-string inputlistroute — id spreading, empty resultadminroute — block structure, storage query parameters
packages/plugins/pwb-valuation/
├── package.json exports: . / /sandbox / /integration / /pages/valuation
├── src/
│ ├── index.js EmDash plugin descriptor factory
│ ├── integration.js Astro integration (injectRoute + virtual modules)
│ ├── sandbox-entry.js EmDash plugin runtime (routes + Block Kit admin)
│ ├── sandbox-entry.test.js Tests
│ └── pages/
│ └── valuation.astro Injected frontend page