Tillbaka till översikten

Hej världen: OpenIntegration-handledning

Vår handledning för alla som vill hitta ett sätt att visa sina egna integrationer på sitt OpenPaper

Med den här korta handledningen vill vi visa er hur enkelt det är att programmera ett fristående OpenIntegration-tillägg för att sedan använda det som ett integrations-plugin i paperlesspaper-appen.

Allt ni behöver för dettaär några kunskaper i HTML/CSS och i hantering av Docker, samt en server eller en molninstans hos valfri leverantör. Viktigt är bara att servern är nåbar från internet via någon domän och med ett giltigt SSL-certifikat. För verkliga tester rekommenderar vi förstås vår OpenPaper7 eller OpenPaperL.

Då sätter vi igång!

Bildschirmfoto 2026-04-27 um 09

Så här är vår grundstruktur. Vi behöver en Dockerfile för att senare paketera vår integration i en Docker-container. Eftersom en OpenIntegration behöver leverera HTML-filer och config.json via en webbserver till Paperlesspaper-appen, har vi valt NGINX som webbserver - därav även nginx.conf. Det Docker-image vi skapar kommer alltså att baseras på nginx:alpine.

I /src mappen finns de båda filer som minst krävs för en OpenIntegration: config.json och index.html

I den config.json kan metadata för er OpenIntegration anges: namn, beskrivning och versionsnummer. Dessutom konfigureras här de inställningar som användaren av er integration senare kan ange själv. I vårt exempel är det bara ett textfält för namninmatning.

Här är vår

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"
}

Under nativeSettings kan ni överföra inställningar för bildramen, till exempel att er integration bara körs i porträttläge. Den formSchema-delen definierar vårt textinmatningsfält. Viktigt är att ange renderPage det är den relativa sökvägen inom er serverdomän där filen ligger som returnerar bilden för bildramen. Vi nöjer oss här med ett enkelt textinmatningsfält med namnet: "name". Den som vill veta mer om vilka möjligheter som finns för inmatningsfält kan här titta på typdefinitionen för gränssnittet.

För vår index.html behöver vi bara följande grundläggande HTML-struktur:

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>

Här händer inte mycket, förutom att Hello World visas. Den <div id="website-has-loading-element" class="hidden-marker"></div> visar för vår renderingsserver att webbsidan först är fullständigt inladdad när en div med id="website-has-loaded" finns på sidan.

Om ni använder asynkrona anrop eller stora grafikfiler i er integration kan detta vara nödvändigt. Vår renderingsserver avbryter annars självmant efter några sekunder och renderar bara det som har laddats tills dess. Ni kan titta på allt i webbläsaren för att få en uppfattning om hur det skulle se ut på ert OpenPaper i slutändan.

Allt borde nu ungefär se ut så här:

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

Eftersom vi vill ha det lite snyggare måste vi lägga till lite CSS i HTML-huvudet:

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>

Lite bättre, eller hur?

Jetzt mit Formatierungformatted-design-1Jetzt mit Formatierung

Det enda vi behöver nu är lite JavaScript. Vi vill nämligen visa Hello "Namn från inställningarna", och inte bara "World". Dessutom lägger vi in tidpunkten för renderingen, vilket är bra för att se om bilden faktiskt har uppdaterats efter kodändringar. Följande kodblock behöver ni alltså i slutet (innan det stängande body-tagget) i er källkod:

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>

Som i vår dokumentation beskrivs, lägger vi till en eventlyssnare på sidan som lyssnar på message-eventet. Eftersom vissa meddelanden utbyts mellan appen och integrationen intresserar vi oss bara för meddelanden som har typen type="INIT" och cmd=="message" har. Det fält vi tidigare definierade i config.json finns det skapade fältet för namnet (name) i det mottagna JS-objektet under payload.meta?.pluginSettings?.name; . Den innehållna strängen skriver vi nu i vår HTML-rubrik med id name.

För att meddela rendering-motorn att vi är klara med att ladda och visa data anropas funktionen markLoaded . Denna lägger till i slutet av dokumentet en div med id website-had-loaded . Klart!

Om ni nu vill göra er integration tillgänglig på en Docker-host kan ni enkelt använda följande Dockerfile:

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;"]

Som ni ser kopierar vi en nginx.conf till Docker-image:en. Den är också ganska enkel. Här måste man bara se till att CORS-headers är korrekt inställda:

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;
        }
    }
}

Den Access-Control-Allow-Origin måste sättas så att åtminstone web.paperlesspaper.de, api.paperlesspaper.de, capacitor://localhost (för iOS) och http://localhost (för Android) är tillåtna. I vårt exempel tillåter vi av enkelhetsskäl alla URL:er.


Hur lägger ni nu till er integration i paperlesspaper-appen? Helt enkelt:

  • Gå i appen till biblioteket och klicka på "Ny bild"
  • Från listan väljer ni "Integrations-plugin"
Bildschirmfoto 2026-05-04 um 11

På den framkommande inställningssidan måste ni nu ange en länk till ert config-manifest (config.json ange).

För vår exempel-domän helloworld.paperlesspaper.de skulle detta vara:

Bildschirmfoto 2026-05-04 um 12

Efter inmatning av integrationskonfigurations-URL:en och ett klick på "Ladda" visas inmatningsfältet name. Här kan ni nu ange det önskade namnet som sedan ska visas på OpenPaper.

Efter ett klick på "Nästa" och val av önskad ram i nästa dialog är er integration aktiv. Den önskade visningen kommer att visas på OpenPaper efter nästa uppdateringsintervall.

Det var allt! Den kompletta källkoden hittar ni i Openintegration-HelloWorld-GitHub-repositoriet.

Ha så kul med det! Frågor och kommentarer till gärna support@paperlesspaper.de