a13n
Build AI agents and run them on cloud.
Manifest-driven UI engine for complex structured Nostr content (Reviews, Articles, Events, Venues) with automatic event coordination, field-level visibility controls, plugin composition, and NIP-78 ma
Developer toolsTypeScript · HTMLGitHub @saintegoOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Manifest-driven UI engine for complex structured Nostr content (Reviews, Articles, Events, Venues) with automatic event coordination, field-level visibility controls, plugin composition, and NIP-78 manifest storage.
Taken from the repository README
Loading the full README.
Read the full README ↗🚧 ALPHA - API WILL CHANGE 🚧
This project is in active development. Expect breaking changes to the API, manifest format, and plugin system before v1.0.
An easy-to-use library for showing and creating Nostr posts on any web page, with a deeper plugin + manifest system for complex structured content.
Use it in plain HTML through the CDN bundle, drop in Web Components, use the React wrappers, or run it headless. Start with simple post creation and viewing, then scale up to manifest-driven structured content like reviews, articles, events, venues, and multi-event workflows.
nostr-post enables you to:
✅ Use it fast - Add Nostr post creation, viewing, and feeds to almost any site with Web Components or the CDN bundle
✅ Stay simple when you want - Works for basic kind 1 posts without needing manifests or custom plugins
✅ Define content once via JSON manifest (what fields, which plugin, where data lives)
✅ Works everywhere - Web Components, React, Next.js, or headless
✅ Automatic coordination - Forms split into multi-event bundles with NIP-78 linking
✅ Composable plugins - Media arrays, geohash locations, markdown editors, hashtags, ratings
✅ Field-level control - Visibility, read-only modes, default values, prefill, exclude fields
✅ NIP support - NIP-01, NIP-07 (signing), NIP-23 (articles), NIP-52 (geohash filtering), NIP-73 (identity tags), NIP-78 (manifest storage), NIP-92 (media metadata), NIP-98 (file upload auth), Blossom (media servers)
┌────────────────────────────────────┐
│ Your App (React, Vue, Next.js) │
└────────────────┬───────────────────┘
│
┌────────────▼──────────────┐
│ @nostr-post/react │ (Hooks + React Components)
│ + Plugin Packages │ (plugin-geo, plugin-media, etc.)
└────────────┬──────────────┘
│
┌────────────▼──────────────────┐
│ @nostr-post/web │ (Lit Web Components)
│ <nostr-post-composer> │
│ <nostr-post-view> │
│ <nostr-post-feed> │
└────────────┬──────────────────┘
│
┌────────────▼──────────────────┐
│ @nostr-post/posts │ (Publishing and reading posts,
│ publish, threads, feeds │ no UI: for any UI)
└────────────┬──────────────────┘
│
┌────────────▼──────────────────┐
│ @nostr-post/signer │ (NIP-07, Relay Publish/Fetch)
│ @nostr-post/plugins (Registry)│
└────────────┬──────────────────┘
│
┌────────────▼──────────────────┐
│ @nostr-post/core │ (Zero Dependencies)
│ Manifest validation │ Pure TypeScript Logic
│ Event coordination │
│ NIP-78 storage │
└───────────────────────────────┘
### Manifest fetching
- Manifest resolution and relay fetch logic lives in `@nostr-post/signer`.
- There is a single centralized, cached helper `fetchManifestByATag` that consumer
components (`view`, `composer`, `feed`) use to resolve an `a` tag (NIP-01 format
for NIP-78 manifests). The helper deduplicates in-flight requests and optionally
falls back to querying by `d` tag across authors (configurable via `fallbackToD`).
- `fetchManifestForEvent` finds the manifest a post or wiki page was made with: its `a` address,
else by its manifest label (see [MANIFEST.md](./MANIFEST.md#which-manifest-an-event-was-made-with)).
This keeps relay I/O in one place and avoids duplicate network requests.
| Package | Status | Purpose |
|---|---|---|
| @nostr-post/core | ✅ Complete | Manifest validation, event coordination, NIP-78 storage |
| @nostr-post/signer | ✅ Complete | NIP-07 signing, relay publish/fetch |
| @nostr-post/plugins | ✅ Complete | Plugin registry + types (extraTags, resolveFromTags) |
| @nostr-post/posts | ✅ Complete | Publishing and reading posts without a UI (bundles, threads, feeds) — README |
| @nostr-post/outbox | 🚧 In progress | Posts published later: drafts, timed posts, upload later — README |
| @nostr-post/web | ✅ Complete | Lit web components (composer, view, feed) |
| @nostr-post/react | ✅ Complete | React hooks + wrapper components |
| @nostr-post/cdn | ✅ Complete | Single-file CDN bundle (ESM + IIFE, no npm required) |
| plugin-stars | ✅ Complete | Star rating (1-5 scale, customizable) |
| plugin-geo | ✅ Complete | Geohash location picker with NIP-52 prefix tags |
| plugin-embed | ✅ Complete | Players for links: YouTube, Spotify, SoundCloud, Vimeo, Bandcamp |
| plugin-profile | ✅ Complete | People picker: profile search or npub, p tags with roles |
| plugin-date | ✅ Complete | Date / date-time input; NIP-52 date and day tags |
Define content structure once, deploy everywhere (all keys: MANIFEST.md):
const manifest: NostrPostManifest = {
id: "blog-v1",
version: "1.0.0",
publishFormats: [
{ id: "article", label: "Article", kinds: [30023], default: true },
], // Long-form articles
fields: [
{
id: "title",
type: "string",
uiPlugin: "textarea",
mapTo: { kind: 30023, target: "title" },
defaultValue: "Untitled Article",
},
{
id: "content",
type: "string",
uiPlugin: "markdown",
mapTo: { kind: 30023, target: "content" },
required: true,
},
],
};
Automatically splits data across event kinds and links them via NIP-78:
const result = coordinateEvents(manifest, formData, { pubkey });
// returns: { events: [Kind 30023, Kind 30078 (manifest)] }
Manifests can offer multiple user-facing publish formats, each activating a different set of kinds. This lets a composer keep one form while publishing, for example, either as a public Kind 1 note or as a structured NIP-78 event.
const manifest: NostrPostManifest = {
id: "review-v2",
version: "2.0.0",
publishFormats: [
{ id: "kind1", label: "Public note", kinds: [1], default: true },
{ id: "nip78", label: "Structured review", kinds: [30078] },
],
fields: [
{
id: "review",
type: "string",
uiPlugin: "textarea",
mapTo: [
{ kind: 1, target: "content" },
{ kind: 30078, target: "content", path: "review" },
],
},
],
};
If a field has multiple targets, mapBehavior controls how it publishes:
first-active (default): publish to the first mapping whose kind is activeall-active: publish to every active mapping for that fieldReady-to-use UI components via Web Components or React:
attachToattachToVisibility, defaults, prefill, readonly, exclusion, and field attachment:
{
id: "private-notes",
type: "string",
visibility: {
edit: "visible", // Author can edit
view: "hidden" // Viewers cannot see
},
defaultValue: "My notes...",
}
// attachTo: attach a plugin field to a target field's toolbar
// — auto-extracts data (hashtags, URLs) from the target field's content
// — renders the plugin's toggle button in the target field's toolbar
{
id: "tags",
type: "string",
uiPlugin: "hashtag",
attachTo: "content", // auto-extract #hashtags from 'content', show # button in its toolbar
mapTo: { kind: 1, target: "tag", tagName: "t" },
}
Plugins emit custom tags and reconstruct from tags:
// plugin-venue wraps plugin-geo, adds OSM search
// Emits: ["g", geohash], ["i", "osm:node:123"], ["location", address]
// Reconstructs from all 3 tags back into VenueData object
Built on standardized Nostr Improvement Proposals:
| NIP | Feature | Example |
|---|---|---|
| NIP-01 | Base protocol | All events build on this |
| NIP-07 | Browser extension signing | window.nostr.signEvent() |
| NIP-22 | Comments (kind 1111) | Comments on events other than notes |
| NIP-23 | Kind 30023 (articles) | Long-form content |
| NIP-52 | Geohash prefix tags | Relay-side location filtering |
| NIP-73 | External identity tags | Countries, profile URLs; OSM IDs in our own osm: namespace (conventions) |
| NIP-78 | Manifest storage | Kind 30078 structured data |
| NIP-98 | HTTP file server auth | nostr.build upload |
| NIP-92 | Media metadata (imeta) | Media plugin |
| Blossom | Media servers (BUD-01/02/03) | Media plugin upload |
The fastest way to get started — add one script tag:
<!DOCTYPE html>
<html>
<head>
<script
type="module"
src="https://saintego.github.io/nostr-post/nostr-post.js"
></script>
</head>
<body>
<h1>Create a Post</h1>
<nostr-post-composer auto-publish></nostr-post-composer>
</body>
</html>
Or use the IIFE build for older browsers:
<script src="https://saintego.github.io/nostr-post/nostr-post.iife.js"></script>
📦 What's included: All web components (<nostr-post-composer>, <nostr-post-view>, <nostr-post-feed>) + all 6 plugins are automatically registered. Full CDN docs →
<!DOCTYPE html>
<html>
<head>
<script type="module">
import "@nostr-post/web";
</script>
</head>
<body>
<nostr-post-composer></nostr-post-composer>
</body>
</html>
import { NostrPostComposer, useNostrAuth } from "@nostr-post/react";
export default function App() {
const { pubkey, login } = useNostrAuth();
return !pubkey ? (
<button onClick={login}>Login with Nostr</button>
) : (
<NostrPostComposer pubkey={pubkey} />
);
}
Detailed setup: QUICKSTART.md
import { coordinateEvents } from "@nostr-post/core/coordinator";
import { validateManifest, getFieldsByKind } from "@nostr-post/core/manifest";
import type { NostrPostManifest, PostField } from "@nostr-post/core/types";
Status: Complete
NIP-07 browser extension integration and relay communication.
import { NostrSigner } from "@nostr-post/signer";
const signer = new NostrSigner();
await signer.signEvent(event);
Status: Complete
Framework-agnostic plugin definitions and registry. Includes plugins for:
import { getPlugin } from "@nostr-post/plugins/registry";
const starsPlugin = getPlugin("stars");
Status: Complete
Universal Web Components that work in any framework or vanilla HTML.
<nostr-post-composer></nostr-post-composer>
<nostr-post-view></nostr-post-view>
<nostr-post-feed></nostr-post-feed>
Built-in plugin loading:
@nostr-post/web lazy-loads built-in plugin UI modules when a manifest uses them.media, hashtag, stars, geo, date, profile, embed, venue, markdown, list.Default behavior when no manifest is provided:
nostr-post-composer uses STANDARD_KIND1_POST_MANIFESTnostr-post-view uses STANDARD_KIND1_POST_MANIFESTThis means text, media (r tags), and hashtags (t tags) render out of the box for standard kind 1 posts.
<nostr-post-view> shows a note's text the way other Nostr clients do: links, images and videos
(by file extension or the event's imeta tags), #hashtags and nostr: references in place;
nostr:npub… / nostr:nprofile… mentions show the person's name once their profile loads.
Tag values the text already shows (r URLs, t hashtags) aren't repeated below it.
nostr-link-base (default https://njump.me/) sets where nostr: references link to.
<nostr-post-composer> extended reply context API:
reply-to-event-id, reply-to-pubkey, root-event-id, root-pubkey, root-address (an addressable root: the reply is a NIP-22 comment, kind 1111, naming it by address)show-reply-target to show current reply targeteditable-reply-target to allow editing reply target directly in the composer<nostr-post-composer> submit feedback: next to the submit button it shows the running step
(publishing linked pages such as a venue's wiki page, then signing and publishing the post, and
"waiting for your signer to approve" while a signature is pending), the
fields that failed validation (the first one is scrolled into view) and why a submit failed (also
dispatched as nostr-post-error). Publish hooks that take longer than 90 s fail the submit. After the
post is out, plugins' after-publish steps run (e.g. saving its venue to lists); one that fails is shown
next to the button but doesn't undo the post. A publish format with kinds: [] publishes no post, only
the plugins' steps ("Done!"). The text
comes from messages (ComposerMessages, defaults in DEFAULT_COMPOSER_MESSAGES), which apps can
override, e.g. for translations.
<nostr-post-view> metadata visibility defaults:
show-kind=false, show-tags=false)show-kind and/or show-tags<nostr-post-feed> interaction API:
comments-enabled (default true)reactions-enabled (default true)commentManifest (defaults to standard kind 1 manifest)reactionOptions (defaults to ['+', '❤️', '🔥', '😂'])<nostr-post-feed> supports direct Nostr filtering (authors, kinds, ids, since, until, limit, search) plus universal tag filters via tagFilters / filter-tags (for example #g geohash and #i OSM identity tags). It also supports advanced multi-filter REQ queries through the filters property, a layout hook (how posts are laid out) and trustedEditors. NIP-52 calendars are @nostr-post/calendar: <nostr-calendar-agenda> (a date window or a month, by day), RSVP, calendars and iCalendar.
Detailed examples: packages/cdn/README.md
Status: Complete
React hooks and components for Next.js/React apps.
import {
NostrPostComposer,
NostrPostFeed,
useNostrAuth,
} from "@nostr-post/react";
function App() {
const { pubkey, login } = useNostrAuth();
return <NostrPostComposer pubkey={pubkey} />;
}
@nostr-post/react wrappers expose the same interaction capabilities as web components:
NostrPostFeed: commentsEnabled, reactionsEnabled, commentManifest, reactionOptionsNostrPostView: showKind, showTags (technical metadata off by default in view)NostrPostComposer: reply-target properties via web-component attributes/propertiesuseNostrPublish() defaults to STANDARD_KIND1_POST_MANIFEST when manifest is omitted.
NostrPostFeed supports the same advanced filtering options as the web component:
import { NostrPostFeed } from "@nostr-post/react";
export function VenueFeed({ placeId }: { placeId: string }) {
return (
<NostrPostFeed
kinds={[1]}
limit={20}
filterTags={`#i:osm:node:${placeId},#g:u09tvw`}
/>
);
}
You can also use object/advanced forms:
<NostrPostFeed
tagFilters={{ "#i": [`osm:node:${placeId}`], "#g": ["u09tvw"] }}
since={Math.floor(Date.now() / 1000) - 60 * 60 * 24 * 7}
search="review"
/>
<NostrPostFeed
filters={[
{ kinds: [1], "#g": ["u09tvw"], limit: 20 },
{ kinds: [30023], "#t": ["review"], limit: 20 },
]}
/>
Status: Complete
Single-file CDN bundle with all components and plugins — no npm required.
Deployed at: https://saintego.github.io/nostr-post/
<!-- ESM (recommended) -->
<script
type="module"
src="https://saintego.github.io/nostr-post/nostr-post.js"
></script>
<!-- IIFE (classic) -->
<script src="https://saintego.github.io/nostr-post/nostr-post.iife.js"></script>
<!-- Use components -->
<nostr-post-composer auto-publish></nostr-post-composer>
Bundle size: ~267 KB raw / ~72 KB gzipped
Define your content structure once, use it everywhere.
Automatically split complex data across multiple Nostr event kinds:
Full TypeScript support with strict mode enabled.
No classes, no mutations, just pure functions and immutable data.
Core logic works in any JavaScript environment. UI layers available for Web Components and React.
# Clone the repository
git clone https://github.com/yourusername/nostr-post.git
cd nostr-post
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run type checking
pnpm typecheck
# Lint and format
pnpm lint:fix
pnpm format
nostr-post/
├── packages/
│ └── core/ ✅ Headless logic
│ ├── src/
│ │ ├── types.ts # Core type definitions
│ │ ├── manifest.ts # Manifest validation
│ │ └── coordinator.ts # Event coordination
│ └── package.json
├── DEVELOPMENT_GUIDE.md # Comprehensive dev guide
├── EXAMPLES.md # Usage examples
└── package.json # Monorepo root
metadata keyPlanned and finished work is tracked in GitHub Issues.
# Install dependencies
pnpm install
# Build all 15 packages (11 libraries + 3 examples + manifest creator)
pnpm -r build
# Watch mode for development
pnpm -r dev
# Type checking
pnpm typecheck
# Linting + formatting with Biome
pnpm lint
pnpm format
| Guide | Purpose |
|---|---|
| QUICKSTART.md | Setup for Web Components, React, Next.js |
| USAGE_GUIDE.md | Complete API reference for all packages |
| MANIFEST.md | Manifest reference: fields, plugin metadata, wiki entity manifests |
| ARCHITECTURE.md | Design decisions, NIP support, patterns |
| DEVELOPMENT_GUIDE.md | Contributing, technical standards |
| EXAMPLES.md | Real-world code examples |
| PLUGINS.md | Creating custom UI plugins |
| INTEGRATION.md | Using in other projects, Vercel deployment, venue review app example |
| PROJECT_SUMMARY.md | Technical implementation summary |
| NOSTR_CONVENTIONS.md | Our tags and formats beyond the NIPs, and what other clients see |
We follow strict coding standards:
See DEVELOPMENT_GUIDE.md for complete standards.
MIT
Built with ❤️ for the Nostr ecosystem
D| calendar | ✅ Complete | NIP-52 calendars: agenda/month, RSVP, calendars, iCalendar |
| plugin-venue | ✅ Complete | OSM venue search + NIP-73 identity tags, wraps geo |
| plugin-media | ✅ Complete | Multi-file upload: the user's Blossom servers, then nostr.build |
| plugin-markdown | ✅ Complete | Markdown editor with live preview |
| plugin-hashtag | ✅ Complete | Hashtag array input with optional auto-extraction via attachTo |
| plugin-reference | ✅ Complete | URL/nostr reference list with optional auto-extraction via attachTo |