a13n
AI 에이전트를 만들고 클라우드에서 실행합니다
복잡한 Nostr 콘텐츠(리뷰·기사·이벤트·장소)를 위한 매니페스트 기반 UI 엔진, 자동 이벤트 조정·필드 가시성·플러그인 구성 지원
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
개발 도구TypeScript · HTMLGitHub @saintego온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기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.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
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 |