Einen benutzerdefinierten Serverless CMS erstellen: Teil 2

Avatar of John Polacek
John Polacek am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

In unserer vorherigen Proof-of-Concept-Demo haben wir ein rudimentäres Admin-Panel erstellt, um eine Webseite zu generieren, mit der Möglichkeit, etwas Text auf der Seite zu bearbeiten und den Seitentitel und die Beschreibung festzulegen. Für diese nächste Demo bauen wir auf unserem Beispiel auf und fügen Funktionen für Rich-Text-Bearbeitung und Bild-Uploads hinzu.

Artikelserie

  1. Einen benutzerdefinierten CMS für einen Serverless Static Site Generator erstellen + Repo
  2. Einen benutzerdefinierten Serverless CMS erstellen: Teil 2 (Sie sind hier!) + Repo

Rich-Text-Bearbeitung

TinyMCE ist der am weitesten verbreitete webbasierte Rich-Text-Editor, also nutzen wir ihn. Wir können ihn ziemlich einfach zu unserem Admin-Formular hinzufügen. Es gibt viele Konfigurationsoptionen für TinyMCE. Für diese Demo benötigen wir nur wenige.

tinymce.init({
  selector: '#calloutText',
  menubar: false,
  statusbar: false,
  toolbar: 'undo redo | styleselect | bold italic | link',
  plugins: 'autolink link'
});

Der Rich-Text-Editor wird seinen Inhalt als Markup kodieren, daher müssen wir die JSRender-Vorlage aktualisieren, um den Datenwert für calloutText als HTML auszugeben.

<div class="jumbotron">
  <div class="container">
  <h1 class="display-3">{{>calloutHeadline}}</h1>
  {{:calloutText}}
  ...

Bild-Uploads

Nun fügen wir unserem Jumbotron einen Bildhintergrund hinzu. Zuerst müssen wir ein neues Formularfeld hinzufügen, damit ein Administrator eine Datei zum Hochladen auswählen kann, und dann unseren Formular-Submit-Handler aktualisieren, um das Bild nach S3 hochzuladen.

Bei mehreren gleichzeitigen Uploads und Rückrufen können wir eine Hilfsmethode für Uploads erstellen und Deferred Objects verwenden, um unsere Ajax-Aufrufe gleichzeitig auszuführen.

$('body').on('submit','#form-admin',function(e) {

  e.preventDefault();

  var formData = {};
  var $formFields = $('#form-admin').find('input, textarea, select').not(':input[type=button], :input[type=submit], :input[type=reset]');

  $formFields.each(function() {
    formData[$(this).attr('name')] = $(this).val();
  });
 
  var jumbotronHTML = '<!DOCTYPE html>' +
    $.render.jumbotronTemplate(formData);
 
  var fileHTML = new File([jumbotronHTML], 'index.html', {type: "text/html", lastModified: new Date()});
 
  var fileJSON = new File([JSON.stringify(formData)], 'admin.json');
  
  var uploadHTML  = $.Deferred();
  var uploadJSON. = $.Deferred();
  var uploadImage = $.Deferred();
 
  upload({
    Key: 'index.html',
    Body: fileHTML,
    ACL: 'public-read',
    ContentDisposition: 'inline',
    ContentType: 'text/html'
  }, uploadHTML);

  upload({
    Key: 'admin/index.json',
    Body: fileJSON,
    ACL: 'public-read'
   }, uploadJSON);

  if ($('#calloutBackgroundImage').prop('files').length) {
    upload({
      Key: 'img/callout.jpg',
      Body: $('#calloutBackgroundImage').prop('files')[0],
      ACL: 'public-read'
    }, uploadImage);
  } else {
    uploadImage.resolve();
  } 
 
  $.when(uploadHTML, uploadImage, uploadJSON).then(function() {
    $('#form-admin').prepend('<p id="success">Update successful! View Website</p>');
  })
});

function upload(uploadData, deferred) {
  s3.upload(uploadData, function(err, data) {
    if (err) {
      return alert('There was an error: ', err.message);
      deferred.reject();
    } else {
      deferred.resolve();
    }
  });
}

Als Nächstes aktualisieren wir unser Jumbotron, um das Callout-Hintergrundbild anzuzeigen.

.jumbotron {
  background-image: url(../img/callout.jpg);
  background-repeat: no-repeat;
  background-attachment: fixed;
  background-position: center;
  background-size: cover;
}

Blog-Posts

Lassen Sie uns Rich-Text-Bearbeitung und Bild-Uploads zusammen verwenden, um Blog-Posts zu erstellen. Da wir viel mit Vorlagen arbeiten, können wir uns das Leben erleichtern, indem wir eine Hilfsfunktion schreiben, um JSX-Vorlagen automatisch zu registrieren.

$('script[type="text/x-jsrender"]').each(function() {
  $.templates($(this).attr('id'), '#'+$(this).attr('id'));
});

Wir können verschiedene Bereiche der Website verwalten, in diesem Fall einen Blog, indem wir unserer Admin-Seite Navigation mit einem Navigationsleisten-Vorlagen-Teil hinzufügen.

<script type="text/x-jsrender" id="adminNav">
  <nav class="navbar navbar-light rounded bg-faded my-4">
    <div class="navbar-collapse" id="navbarNav">
      <ul class="nav navbar-nav d-flex flex-row">
        <li class="nav-item pl-2 pr-3 mr-1 border-right">
          <a class="nav-link text-primary" href="#adminIndex">Admin</a>
        </li>
        <li class="nav-item px-2{{if active=='adminIndex'}} active{{/if}}">
          <a class="nav-link" href="#adminIndex">Home {{if active=='adminIndex'}}<span class="sr-only">(current)</span>{{/if}}</a>
        </li>
        <li class="nav-item px-2{{if active=='adminBlog'}} active{{/if}}">
          <a class="nav-link" href="#adminBlog">Blog {{if active=='adminBlog'}}<span class="sr-only">(current)</span>{{/if}}</a>
        ...

Aktualisieren Sie als Nächstes unsere bestehende Admin-Seite mit der Navigationsleiste und einer neuen ID.

<script type="text/x-jsrender" id="adminHome">
  {{include tmpl='adminNav' /}}
  <form class="py-2" id="form-admin">
    <h3 class="py-2">Site Info</h3>
    ...

Um unserer Admin-Ansicht Navigation hinzuzufügen, können wir bei Klick auf die Navigationsschaltflächen einen Event-Handler hinzufügen, der die zugehörigen Daten lädt und die entsprechende Vorlage rendert.

Wir werden den Rich-Text-Editor verwenden, während wir verschiedene Bereiche der Website bearbeiten. Daher ermöglicht uns das Erstellen einer weiteren Hilfsfunktion, den Editor mit unterschiedlichen Einstellungen einfach zu konfigurieren.

$('body').on('click','.nav-link', function(e) {
  e.preventDefault();
  loadPage($(this).attr('href').slice(1));
});

function loadPage(pageId) {
  adminData = {};
$.getJSON(pageId+'.json', function( data ) {
    adminData = data;
  }).always(function() {
    $('.container').html($.render[pageId]($.extend(adminData,{active:pageId})));
    initRichTextEditor();
  });
}

function initRichTextEditor(settings) {
  tinymce.init($.extend({
    selector:'textarea',
    menubar: false,
    statusbar: false,
    toolbar: 'undo redo | styleselect | bold italic | link',
    plugins: 'autolink link',
    init_instance_callback : function(editor) {
      $('.mce-notification-warning').remove();
    }
  }, settings ? settings : {}));
}

Erstellen Sie einen neuen Admin-Bereich für die Verwaltung des Blogs mit einer Schaltfläche zum Erstellen eines neuen Posts.

<script type="text/x-jsrender" id="adminBlog">
  {{include tmpl='adminNav' /}}
  <div id="blogPosts">
    <h3 class="py-2">Blog Posts</h3>
    <button id="newPostButton" class="btn btn-primary">+ New Post</button>
    ...

Außerdem benötigen wir ein Formular zum Schreiben dieser Posts. Beachten Sie, dass wir ein verstecktes Dateieingabefeld einschließen, das wir verwenden werden, damit der Rich-Text-Editor Bilder hochladen kann.

<script type="text/x-jsrender" id="editBlogPost">
  <form class="py-2" id="form-blog">
    {{if postTitle}}
      <h3 class="py-2">Edit Blog Post</h3>
    {{else}}
      <h3 class="py-2">New Blog Post</h3>
    {{/if}}
    <div class="form-group">
        <label for="postTitle">Title</label>
        <input type="text" value="{{>postTitle}}" class="form-control" id="postTitle" name="postTitle" />
    </div>
    <div class="form-group pb-2">
      <textarea class="form-control" id="postContent" name="postContent" rows="12">{{>postContent}}</textarea>
    </div>
    <div class="hidden-xs-up">
      <input type="file" id="imageUploadFile" />
    </div>
    <div class="text-xs-right">
      <button class="btn btn-link">Cancel</button>
      <button type="submit" class="btn btn-primary">Save</button>
    </div>  
  </form>
</script>

Die Ermöglichung der Bearbeitung mehrerer Seiten der Website durch den Administrator erfordert eine andere Strukturierung der Seitengenerierung. Jedes Mal, wenn Änderungen am Seitentitel und den Informationen vorgenommen werden, müssen wir diese sowohl auf die Homepage als auch auf den Blog übertragen.

Zuerst erstellen wir Vorlagen-Teile für unsere Website-Navigation, die wir in jede der Seitenvorlagen einfügen können.

<script type="text/x-jsrender" id="siteNav">
  <nav class="navbar navbar-static-top navbar-dark bg-inverse">
    <a class="navbar-brand pr-2" href="#">{{>siteTitle}}</a>
    <ul class="nav navbar-nav">
      <li class="nav-item{{if active=='index'}} active{{/if}}">
        <a class="nav-link" href="{{>navPath}}index.html">Home {{if active=='index'}}<span class="sr-only">(current)</span>{{/if}}</a>
      </li>
      <li class="nav-item{{if active=='blog'}} active{{/if}}">
        <a class="nav-link" href="{{>navPath}}blog.html">Blog {{if active=='blog'}}<span class="sr-only">(current)</span>{{/if}}</a>
      ...
<body>
  {{include tmpl='siteNav' /}}
  ...

Wenn unser Administrator auf die Schaltfläche "Neuer Post" klickt, sollte ihm unser Bearbeitungsformular angezeigt werden. Wir können eine Funktion erstellen, die dies tut, und sie an ein Klickereignis auf der Schaltfläche anhängen.

Zusätzlich möchten wir in der Lage sein, Bilder zu unseren Blog-Posts hinzuzufügen. Um dies zu erreichen, müssen wir ein benutzerdefiniertes Bild-Upload-Fenster zu unserem Rich-Text-Editor hinzufügen, mit einigen Konfigurationseinstellungen.

function editPost(postData) {
  $('.container').append($.render.editBlogPost(postData));
  initRichTextEditor({
    toolbar: 'undo redo | styleselect | bold italic | bullist numlist | link addImage',
    setup: function(editor) {
      editor.addButton('addImage', {
        text: 'Add Image',
        icon: false,
        onclick: function() {
          // Open window
          editor.windowManager.open({
            title: 'Add Image',
            body: [{
              type: 'button',
              name: 'uploadImage',
              label: 'Select an image to upload',
              text: 'Browse',
              onclick: function(e) {
                $('#imageUploadFile').click();
              },
              onPostRender: function() {
                addImageButton = this;
              }
            }, {
              type: 'textbox',
              name: 'imageDescription',
              label: 'Image Description'
            }],
            buttons: [{
              text: 'Cancel',
              onclick: 'close'
            }, {
              text: 'OK',
              classes: 'widget btn primary first abs-layout-item',
              disabled: true,
              onclick: 'close',
              id: 'addImageButton'
            }]
          });
        }
      });
    }
  });
}

$('body').on('click', '#addImageButton', function() {
  if ($(this).hasClass('mce-disabled')) {
    alert('Please select an image');
  } else {
    var fileUploadData,
      extension = 'jpg',
      mimeType = $('#imageUploadFile')[0].files[0].type; // You can get the mime type
    if (mimeType.indexOf('png') != -1) {
      extension = 'png';
    }
    if (mimeType.indexOf('gif') != -1) {
      extension = 'gif';
    }
    var filepath = 'img/blog/' + ((new Date().getMonth()) + 1) + '/' + Date.now() + '.' + extension;

    upload({
      Key: filepath,
      Body: $('#imageUploadFile').prop('files')[0],
      ACL: 'public-read'
    }).done(function() {
      var bucketUrl = 'http://serverless-cms.s3-website-us-east-1.amazonaws.com/';
      tinyMCE.activeEditor.execCommand('mceInsertRawHTML', false, '<p><img src="' + bucketUrl + filepath + '" alt="' + $('.mce-textbox').val() + '" /></p>');
      $('#imageUploadFile').val();
      tinyMCE.activeEditor.windowManager.close();
    });
  }
});

Der obige Code platziert eine Schaltfläche Bild hinzufügen in den Steuerelementen des Rich-Text-Editors, die ein Modal öffnet, in dem der Administrator ein Bild zum Hochladen auswählen kann. Wenn der Administrator auf Durchsuchen klickt, haben wir einen Klick-Trigger auf das versteckte Dateieingabefeld im Bearbeitungsformular gelegt.

Sobald sie ein Bild zum Posten ausgewählt haben, lädt das Klicken auf OK zum Schließen des Fensters das Bild auch nach S3 hoch und fügt das Bild an der Cursorposition im Rich-Text-Editor ein.

Als Nächstes müssen wir den Blog-Post speichern. Dazu kombinieren wir unsere Formulardaten mit einer Vorlage, um HTML zu rendern und nach S3 hochzuladen. Es gibt eine Vorlage für den Blog und den einzelnen Post selbst. Wir müssen auch die Post-Daten speichern, damit der Administrator eine Liste der Posts sehen und Bearbeitungen vornehmen kann.

$('body').on('submit', '#form-blog', function(e) {
  e.preventDefault();
  var updateBlogPosts = $.Deferred();
  if ($(this).attr('data-post-id') === '') {
    postId = Date.now();
  } else {
    postId = $(this).attr('data-post-id');
  }
  if (!adminData.posts) {
    adminData.posts = [];
  }
  var postUrl = 'posts/' + ($('#title').val().toLowerCase().replace(/[^\w\s]/gi, '').replace(/\s/g, '-')) + '.html';
  var postTitle = $('#title').val();
  var postContent = tinyMCE.activeEditor.getContent({
    format: 'raw'
  });
  adminData.posts.push({
    url: postUrl,
    title: postTitle,
    excerpt: $(postContent)[0].innerText
  });
  var uploads = generateHTMLUploads('blog');
  uploads.push(generateAdminDataUpload());

  var postHTML = '<!DOCTYPE html>' + $.render['blogPostTemplate']($.extend(adminData, {
    active: 'blog',
    title: postTitle,
    content: postContent,
    navPath: '../'
  }));

  var fileHTML = new File([postHTML], postUrl, {
    type: "text/html",
    lastModified: new Date()
  });
  uploads.push(upload({
    Key: postUrl,
    Body: postHTML,
    ACL: 'public-read',
    ContentDisposition: 'inline',
    ContentType: 'text/html'
  }))
  $.when.apply($, uploads).then(function() {
    loadPage('adminBlog');
  });
});

Auf unserer Admin-Blog-Seite listen wir unsere veröffentlichten Posts auf.

<script type="text/x-jsrender" id="adminBlog">
  {{include tmpl='adminNav' /}}
  <div id="blogPosts">
    <h3 class="py-2">Blog Posts</h3>
    <button id="newPostButton" class="btn btn-primary my-1">+ New Post</button>
    {{if posts}}
      <div class="container p-0">
        <ul class="list-group d-inline-block">
          {{for posts}}
            <li class="list-group-item">
              <span class="pr-3">{{>title}}</span>
              <a href="../{{>url}}" target="_blank" class="pl-3 float-xs-right">view</a>
              <a href="#" data-id="{{:#getIndex()}}" data-url="{{>url}}" class="edit-post pl-3 float-xs-right">edit</a>
            </li>
          {{/for}}
        </ul>
      </div>
    {{/if}}
  </div>
</script>

Schließlich erweitern wir unseren Handler für "Neuer Post" so, dass er auch das Bearbeiten von Posts ermöglicht, indem er die Post-Daten in die Formularvorlage lädt.

$('body').on('click', '#newPostButton, .edit-post', function(e) {
  e.preventDefault();
  $('#blogPosts').remove();
  if ($(this).is('#newPostButton')) {
    editPost({});
  } else {
    var postId = $(this).attr('data-id');
    var postUrl = $(this).attr('data-url');
    $('<div />').load('../' + postUrl, function() {
      editPost({
        id: postId,
        title: $(this).find('h1').text(),
        content: $(this).find('#content').html()
      });
    });
  }
});

Nächste Schritte

Offensichtlich ist dies ein grundlegendes Beispiel und es fehlt viel wichtige Funktionalität, wie z. B. die Möglichkeit, Entwürfe von Posts zu haben, Posts zu löschen und Paginierung.

Wenn der Umfang der Website wächst, wird die clientseitige Generierung von HTML-Dateien in Batches mühsam und unzuverlässig. Wir können unsere Architektur jedoch serverlos halten, indem wir die Seitengenerierung an AWS Lambda auslagern und Microservices für die Aktualisierung von Website-Informationen und die Verwaltung von Blog-Posts erstellen.

Die Verwaltung unserer Website-Datenstruktur durch Aktualisierung von flachen JSON-Dateien, die auf S3 gespeichert sind, ist kostengünstig und eignet sich gut für die einfache Einrichtung von Backups und Wiederherstellungen. Für Projekte, die über einen einfachen Blog oder eine Marketingseite hinausgehen, ist es jedoch möglich, AWS Dynamo DB zum Speichern von Daten zu verwenden, was auch vom AWS SDK für JavaScript im Browser unterstützt wird.

Ein Blog ist nur ein Beispiel für etwas, das auf diese Weise erstellt werden kann. Der Aufstieg von Serverless Web Application Architectures (auch bekannt als Backend as a Service) ermöglicht es dem Frontend, sowohl die Benutzer- als auch die Autorenerfahrung zu steuern und Webprodukte und Inhalte von Anfang bis Ende zu gestalten.

Es ist eine aufregende Zeit, um Dinge im Web zu bauen.