Zurück zur Übersicht

Hello World: OpenIntegration Tutorial

Unser Tutorial für alle die einen Weg finden wollen, um eigene Integrationen auf ihrem OpenPaper zu zeigen

Mit diesem kurzen Tutorial möchten wir euch zeigen wie einfach es ist, eine eigenständige OpenIntegration-Erweiterung zu programmieren, um diese dann in der paperlesspaper-App als Integrations-Plugin zu nutzen.

Alles was ihr hierzu braucht sind ein paar Kenntnisse in HTML/CSS und im Umgang mit Docker, sowie einen Server oder eine Cloud-Instanz beim Anbieter eurer Wahl. Wichtig ist nur dass der Server über irgendeine Domain und mit gültigem SSL-Zertifikat aus dem Internet erreichbar ist. Für echte Tests empfehlen wir natürlich die OpenPaper 7 oder OpenPaper L Bilderrahmen. :)

Na dann, fangen wir an!

Bildschirmfoto 2026-04-27 um 09

Hier also unsere Grundstruktur. Wir benötigen ein Dockerfile um unsere Integration später in einen Docker-Container zu packen. Da es für eine OpenIntegration notwendig ist HTML-Dateien und config.json per Webserver an die Paperlesspaper-App zurückzugeben, haben wir uns für NGINX als Webserver entschieden - daher auch die nginx.conf. Das von uns erstellte Docker-Image wird also auf nginx:alpine basieren.

Im /src Ordner befinden sich die beiden für eine OpenIntegration mindestens notwendigen Dateien: config.json und index.html

In der config.json können die Metadaten für eure OpenIntegration angegeben werden: Name, Beschreibung und Versionsnummer. Zusätzlich werden an dieser Stelle auch die Einstellungen konfiguriert, welche der Benutzer eurer Integration später selbst festlegen kann. In unserem Beispiel ist dies nur ein Textfeld zur Namenseingabe.

Hier unsere

json
{
  "name": "This one is a basic Hello-World example, which greets you with your name",
  "version": "1.0.0",
  "description": "A simple plugin that displays a greeting message based on the user's name.",
  "nativeSettings": {
    "orientation": "portrait"
  },
  "formSchema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "The name of the user to greet.",
        "default": "OpenPaper-Lover"
      }
    }
  },
  "renderPage": "./index.html"
}

Unter nativeSettings könnt ihr Einstellungen für den Bilderrahmen übergeben, beispielsweise das eure Integration nur im Portrait Modus läuft. Der formSchema-Teil definiert unser Texteingabefeld. Wichtig ist die Angabe der renderPage das ist der relative Pfad innerhalb eurer Server-Domain, an dem die Datei liegt, welche das Bild für den Bilderrahmen zurückgibt. Wir geben uns hier mit einem einfachen Texteingabefeld mit dem Namen: "name" zufrieden. Wer mehr wissen möchte welche Möglichkeiten für Eingabefelder es gibt kann hier die Typendefinition der Schnittstelle anschauen.

Für unsere index.html benötigen wir nur folgendes HTML-Grundgerüst:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Openintegration: Hello World</title>
  </head>
  <body>
    <div id="website-has-loading-element" class="hidden-marker"></div>
    <main class="screen">
      <section>
        <h2 class="subtitle">Hello</h2>
        <h1 class="title" id="name">World</h1>
        <h4 id="clock">00:00</h4>
      </section>
    </main>
  </body>
</html>

Hier passiert nicht viel, außer das Hello World angezeigt wird. Der <div id="website-has-loading-element" class="hidden-marker"></div> zeigt unserem Rendering-Server dass die Webseite erst dann vollständig geladen ist, wenn ein div mit id="website-has-loaded" auf der Seite zu finden ist.

Solltet ihr asynchrone Aufrufe oder große Grafiken in eurer Integration verwenden, kann dies notwendig sein. Unser Rendering-Server bricht sonst nach wenigen Sekunden selbständig ab, und rendert nur was bis dahin geladen wurde. Ihr könnt euch das ganze ja mal im Browser anschauen, dann wisst ihr in etwa wie es auf eurem OpenPaper am Ende aussehen würde.

Das ganze sollte aktuell nun in etwa so aussehen:

Das erste html wird auf dem Rahmen angezeigtdesign-htmlDas erste html wird auf dem Rahmen angezeigt

Da wir das ein wenig hübscher haben wollen, müssen wir im HTM-Header etwas CSS hinzufügen:

html
<style>
      :root {
        color-scheme: light dark;
      }
      * {
        box-sizing: border-box;
      }

      html,
      body {
        width: 100%;
        height: 100%;
        margin: 0;
        padding: 0;
        font-family:
          ui-sans-serif,
          system-ui,
          -apple-system,
          BlinkMacSystemFont,
          "Segoe UI",
          sans-serif;
        background: #fff;
        color: #000;
      }

      .screen {
        width: 100vw;
        height: 100vh;
        padding: 1.3em;
        display: flex;
        align-items: center;
        justify-content: center;
      }

      .title {
        font-size: clamp(1rem, 1.4vw + 0.8rem, 2rem);
        font-weight: 600;
        text-align: center;
        margin: 0 0 0.2rem 0;
      }

      .subtitle {
        font-size: clamp(0.9rem, 0.9vw + 0.6rem, 1.3rem);
        opacity: 0.75;
        text-align: center;
        margin: 0 0 0.2rem 0;
      }

      #clock {
        font-size: clamp(0.5rem, 0.5vw + 0.2rem, 0.8rem);
        opacity: 0.25;
        text-align: center;
        margin: 5rem 0 0.2rem 0;
      }

      .hidden-marker {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
      }
    </style>

Schon besser, oder?

Jetzt mit Formatierungformatted-design-1Jetzt mit Formatierung

Alles was wir nun noch brauchen, ist ein wenig Javascript. Wir wollen nämlich Hello "Name aus den Einstellungen" anzeigen, und nicht nur "World". Außerdem fügen wir noch klein die Uhrzeit des Renderings ein, das ist ganz sinnvoll, um zu schauen ob sich das Bild nach Anpassungen im Code tatsächlich bereits aktualisiert hat. Folgenden Code_block benötigt ihr also am Ende (vor dem schließenden Body-Tag) eures Quelltextes:

html
<script>
    // live clock
    const clockElement = document.getElementById("clock");
    const now = new Date();
    const hh = String(now.getHours()).padStart(2, "0");
    const mm = String(now.getMinutes()).padStart(2, "0");
    clockElement.textContent = `${hh}:${mm}`;

    // mark page loaded
    const markLoaded = function () {
      const markerLoaded = document.createElement("div");
      markerLoaded.id = "website-has-loaded";
      document.body.appendChild(markerLoaded);
    };


    // Handling Settings Page Updates
    window.addEventListener("message", (event) => {
      const data = event.data;
      const payload = data.data || {};
      if (data.type === "INIT" && data.cmd === "message") {
        if (payload.meta?.pluginSettings?.name) {
          document.getElementById("name").textContent =
            payload.meta?.pluginSettings?.name;
        }
        markLoaded();
      }
    });
  </script>

Wie in unserer Dokumentation beschrieben, fügen wir einen Event-Listener zur Seite hinzu, welcher auf das message-Event lauscht. Da hier einige Nachrichten zwischen App und Integration ausgetauscht werden, interessieren wir uns nur für die Nachricht wenn diese den type="INIT" und cmd=="message" hat. Das von uns vorher in der config.json angelegt Feld für den Namen (name) finden wir im empfangenen JS-Objekt unter payload.meta?.pluginSettings?.name; . Den enthaltenen String schrieben wir nun in unsere HTML-Headline mit der ID name.

Um der rendering Engine mitzuteilen, dass wir nun fertig sind mit dem Laden und anzeigen von Daten, wird die Funktion markLoaded aufgerufen. Diese fügt ans Ende des Dokumentes einen div mit der id website-had-loaded an. Fertig!

Wenn ihr eure Integration nun auf einem Docker-Host verfügbar machen wollt, könnt ihr einfach folgendes Dockerfile nutzen:

dockerfile
FROM nginx:alpine
# Remove default nginx config and content
RUN rm /etc/nginx/conf.d/default.conf && \
    rm -rf /usr/share/nginx/html/*
# Copy optimized nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# Copy static files
COPY src/ /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Wie ihr seht kopieren wir eine nginx.conf ins Dockerfile. Diese ist ebenfalls recht simpel. Hier muss nur darauf geachtet werden dass die CORS-Header richtig gesetzt werden:

plaintext
worker_processes auto;
events {
    worker_connections 1024;
}
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    server {
        listen 80;
        server_name _;
        add_header Access-Control-Allow-Origin "*" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type, Accept, Origin, X-Requested-With" always;
        root /usr/share/nginx/html;
        index index.html;
        location / {
            if ($request_method = OPTIONS) {
                return 204;
            }
            try_files $uri $uri/ =404;
        }
    }
}

Die Access-Control-Allow-Origin muss so gesetzt werden, dass mindestens web.paperlesspaper.de, api.paperlesspaper.de, capacitor://localhost (für iOS) und http://localhost (für Android) erlaubt sind. Wir erlauben im Beispiel der Einfachheit halber alle URLs.


Wie fügt ihr eure Integration nun in der paperlesspaper-App hinzu? Ganz einfach:

  • Ihr geht in der App in die Bibliothek und klickt auf "Neues Bild"
  • Aus der Liste wählt ihr "Integrations Plugin"
Bildschirmfoto 2026-05-04 um 11

In der erscheinenden Einstellungsseite müsst ihr nun einen Link zu eurem Config-Manifest (config.json eintragen).

Für unsere Beispieldomain helloworld.paperlesspaper.de wäre dies:

Bildschirmfoto 2026-05-04 um 12

Nach Eingabe der Integrationskonfigurations-URL und einem klick auf "Laden" erscheint das Eingabefeld name. Hier könnt ihr nun den gewünschten Namen eintragen, welcher dann auf dem OpenPaper angezeigt werden soll.

Nach einem Klick auf "Weiter" und der Auswahl des gewünschten Rahmens im nächsten Dialog ist eure Integration aktiv. Die gewünschte Anzeige wird nach dem nächsten Aktualisierungsintervall auf dem OpenPaper angezeigt.

That's it! Den kompletten Quelltext findet ihr im Openintegration-HelloWorld-Github-Repository.

Viel Spaß damit! Fragen und Anmerkungen gern an support@paperlesspaper.de