Basic widgets

On this page



DOCS








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:

  1. Express routes for each widget state
  2. Smart Widget component creation and signing
  3. Image generation for visual content
  4. 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

  1. Every endpoint must return a valid signed Nostr smart widget event (kind:30033)

    • Always use SMART_WIDGET.signEvent() and return the event
  2. Only publish the root widget to Nostr relays

    • Use SMART_WIDGET.publish() only for the entry-point widget
    • Only publish in production environments
  3. 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
  4. Generate dynamic images for data visualization

    • Use Puppeteer or Canvas to render visualizations
    • HTML templates provide flexibility for creating rich visuals
  5. Image handling options

    • Return images as base64 strings directly in the widget
    • Or upload to storage and reference by URL
  6. Component Organization

    • Use SWComponentsSet to organize widget elements
    • Follow the pattern: Image → Input (optional) → Buttons (up to 6)
  7. Request Parameters Handling

    • POST requests to widget endpoints may contain the following standard parameters:
      • input: Contains data submitted through input fields from the preceding widget
      • pubkey: Represents the Nostr public key of the end user interacting with the widget
      • aTag: Contains the canonical reference to the root widget in the format "30033:<author-pubkey>:<widget-identifier>" for maintaining widget hierarchy context

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 events
  • NODE_ENV: Set to 'production' when deploying
  • PROTOCOL and DOMAIN: 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

Quick tutorial

On this page