DOCS
Introduction
What are Smart widgets?
Getting started
Build widgets
Basic widgets
Action/Tool widgets
SDK
Smart widget builder
Smart widget previewer
Smart widget handler
Useful links
Basic dynamic widgets
Search
/
Basic widgets
This guide shows how to build dynamic Nostr smart widgets using the smart-widget-builder package.
Basic Structure
A basic Smart Widget server needs:
- Express routes for each widget state
- Smart Widget component creation and signing
- Image generation for visual content
- Proper event response handling
Example
Here's a simple example of a "Weather Widget" that displays current weather based on user location:
const express = require("express");
const { SW, Button, Image, SWComponentsSet } = require("smart-widget-builder");
const router = express.Router();
const axios = require("axios");
const WeatherImage = require("../Painter/WeatherPainter");
// Root endpoint - Entry point for the smart widget
router.post("/", async (req, res) => {
try {
// Initialize Smart Widget instance
let SMART_WIDGET = new SW();
// Create welcome image and buttons
let SWImage = new Image("https://example.com/weather-widget-welcome.png");
// Button that posts to the /weather endpoint
let SWButton = new Button(
1,
"Check Weather 🌤️",
"post",
getMainURL() + "/weather"
);
// Create component set with image and button
let SWComp = new SWComponentsSet([SWImage, SWButton]);
// Unique identifier for this widget (important for root widget)
let identifier = "weather-widget-root-12345";
// Sign the event
let signedEvent = await SMART_WIDGET.signEvent(
SWComp,
"Weather Widget",
identifier
);
// Publish only in production (only for root widget)
let publishedEvent;
if (import.meta.env.NODE_ENV === "production") {
publishedEvent = await SMART_WIDGET.publish(
SWComp,
"Weather Widget",
identifier
);
}
// Return the signed event
res.send(publishedEvent ? publishedEvent.event : signedEvent.event);
} catch (err) {
console.log(err);
res.status(500).send({ message: "Server error" });
}
});
// Weather endpoint - Returns weather data based on location
router.post("/weather", async (req, res) => {
try {
// Extract parameters from request
const { input, pubkey, aTag } = req.body;
// Use input from previous widget or default
const location = input || "New York";
// Log user information (optional)
console.log(`Request from user: ${pubkey}`);
console.log(`Widget aTag: ${aTag}`);
// Initialize Smart Widget
let SMART_WIDGET = new SW();
// Fetch weather data from API
let weather = await axios.get(
`https://api.weatherapi.com/v1/current.json?key=YOUR_API_KEY&q=${location}`
);
// Generate a weather image using custom painter
let weatherImage = await WeatherImage({
location: weather.data.location.name,
temperature: weather.data.current.temp_c,
condition: weather.data.current.condition.text,
icon: weather.data.current.condition.icon
});
// Create image component with base64 encoded image
let SWImage = new Image(
`data:image/png;base64,${weatherImage.toString("base64")}`
);
// Button to check another location
let SWButton = new Button(
1,
"Check Another Location 🔄",
"post",
getMainURL() + "/weather"
);
// Create component set
let SWComp = new SWComponentsSet([SWImage, SWButton]);
// Sign the event (note: no identifier and no publishing)
let signed = await SMART_WIDGET.signEvent(SWComp, "Weather Widget");
// Return signed event
res.send(signed.event);
} catch (err) {
console.log(err);
res.status(500).send({ message: "Server error" });
}
});
module.exports = router;
Image Painter Example (WeatherPainter.js)
const { genImage } = require("../Helpers/Helper");
const getWeatherHtml = (data) => {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather Widget</title>
<style>
body, html {
margin: 0;
padding: 0;
font-family: 'Arial', sans-serif;
color: white;
}
* {
box-sizing: border-box;
}
</style>
</head>
<body>
<div style="width: 800px; min-height: 600px; background-color: #2c3e50;">
<div style="width: 100%; min-height: 600px; display: flex; justify-content: center; align-items: center; position: relative; overflow: hidden; padding: 3rem">
<div style="position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 120%; height: 120%; filter: blur(6px); background-size: cover; overflow: hidden; background-position: center; background-image: url(${data.icon}); z-index: 0;"></div>
<div style="width: 100%; position: relative; z-index: 2; display: flex; justify-content: center; flex-direction: column; gap: 20px; align-items: center; padding: 40px; border-radius: 30px; background-color: rgba(255, 255, 255, 0.9)">
<h1 style="font-size: 48px; text-align: center; margin: 0; color: #2c3e50">${data.location}</h1>
<h2 style="font-size: 72px; text-align: center; margin: 0; color: #3498db">${data.temperature}°C</h2>
<p style="font-size: 28px; text-align: center; margin: 0; color: #34495e">${data.condition}</p>
</div>
</div>
</div>
</body>
</html>
`;
};
/**
* Generate a weather image
* @param {*} data Weather data to be used in an HTML code to render dynamic images
* @returns an image buffer
*/
module.exports = async (data) => {
try {
let buffer = await genImage(getWeatherHtml(data));
return buffer;
} catch (error) {
return false;
}
};
Key Principles
-
Every endpoint must return a valid signed Nostr smart widget event (kind:30033)
- Always use
SMART_WIDGET.signEvent()and return the event
- Always use
-
Only publish the root widget to Nostr relays
- Use
SMART_WIDGET.publish()only for the entry-point widget - Only publish in production environments
- Use
-
Don't publish secondary widgets
- Child widgets (other endpoints) should only be signed, not published
- This prevents relay spam and maintains a cleaner event graph
-
Generate dynamic images for data visualization
- Use Puppeteer or Canvas to render visualizations
- HTML templates provide flexibility for creating rich visuals
-
Image handling options
- Return images as base64 strings directly in the widget
- Or upload to storage and reference by URL
-
Component Organization
- Use
SWComponentsSetto organize widget elements - Follow the pattern: Image → Input (optional) → Buttons (up to 6)
- Use
-
Request Parameters Handling
- POST requests to widget endpoints may contain the following standard parameters:
input: Contains data submitted through input fields from the preceding widgetpubkey: Represents the Nostr public key of the end user interacting with the widgetaTag: Contains the canonical reference to the root widget in the format"30033:<author-pubkey>:<widget-identifier>"for maintaining widget hierarchy context
- POST requests to widget endpoints may contain the following standard parameters:
Environment Setup
Create a .env file with your configuration:
NODE_ENV=development
SECRET_KEY=your_nostr_private_key_hex
PROTOCOL=https
DOMAIN=your-domain.com
Important environment variables:
SECRET_KEY: Your Nostr private key (hex format) for signing eventsNODE_ENV: Set to 'production' when deployingPROTOCOLandDOMAIN: Used to construct callback URLs in production
Advanced Features
For more complex widgets, you can:
- Add input fields for user data
- Include multiple buttons with different actions
- Chain multiple widget states together
- Integrate with external APIs for dynamic content
Notes
- Ensure all endpoints respond quickly (< 5 seconds)
- Keep image sizes reasonable for quick loading
- Test thoroughly in a Nostr client that supports Smart Widgets