# Blog/CMS

The Content Management System (CMS) provides a basic blogging platform. The CMS provides a set of basic features:

* Minimal data structure - one table for Posts, one table for Files
* Simple publishing process - draft fields are copied over published fields
* Rich content is supported via the Content Editor [component](https://petal.build/components/content-editor) (integrated with [Editor.js)](https://editorjs.io/)
* File browser allows the user to upload and select files
* Admin console to manage blog posts
* Basic UI to show published posts&#x20;

### Data Entry

To create a blog post, go to the admin console:

```
/admin/posts
```

Here you'll see the list of posts:

<figure><img src="https://2964479083-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fjybnmx3gX5MSuPHDwIJB%2Fuploads%2FRDF4CaHk3uIHRmAOOEHu%2FXnapper-2024-12-06-15.38.23.png?alt=media&#x26;token=bc0840e5-6721-4315-ae75-8c2470e45a06" alt=""><figcaption><p>Posts admin console</p></figcaption></figure>

To create a post, click the "New Post" button:

<figure><img src="https://2964479083-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fjybnmx3gX5MSuPHDwIJB%2Fuploads%2Fb9GwOHGLqvq5R2oslRbY%2FXnapper-2024-12-06-16.16.26.png?alt=media&#x26;token=491aad78-ddaa-4b73-8efa-394b88136465" alt=""><figcaption><p>New Post modal</p></figcaption></figure>

Fill in the fields (only Title is required) and click on "Save and Continue". Here you'll see the main data entry screen:

<figure><img src="https://2964479083-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fjybnmx3gX5MSuPHDwIJB%2Fuploads%2FWgUTUCCBjcbZCkAIxFZO%2FXnapper-2024-12-06-15.48.18.png?alt=media&#x26;token=744d72c5-e0c7-4bcd-849e-2ba6251e6d2e" alt=""><figcaption><p>Main data entry screen for a post</p></figcaption></figure>

You can click the cover image to bring up the [file browser](#file-browser). The main data entry starts at the section that says, "Start typing...". This part of the document is using the [Content Editor](#content-editor).

{% hint style="info" %}
While on the edit page, any changes made to the document will be auto-saved!
{% endhint %}

### Publishing Process

Once your happy with your edits, click on "Save and Finish". This will bring you to a preview of the draft:

<figure><img src="https://2964479083-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fjybnmx3gX5MSuPHDwIJB%2Fuploads%2FflHuwgIeDhMsyJsTbIrG%2FXnapper-2024-12-06-15.55.08.png?alt=media&#x26;token=29962790-7285-4742-8588-7bf1d1eb385c" alt=""><figcaption><p>Preview of the current draft</p></figcaption></figure>

To make this post publically available, click on the "Publish" button:

<figure><img src="https://2964479083-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fjybnmx3gX5MSuPHDwIJB%2Fuploads%2FZ0o3YFG841swFPFIvYhf%2FXnapper-2024-12-06-16.15.05.png?alt=media&#x26;token=73d5b425-1a43-42ec-bd48-f2a1ef4a173f" alt=""><figcaption><p>Publish modal</p></figcaption></figure>

To complete the process, select a "Go Live" date/time and hit the second "Publish" button!

{% hint style="info" %}
The Go Live date/time is based on UTC. Currently there is no support for user/timezone configuration in Petal Pro. If the time of day matters, you'll need to convert it to UTC before filling out this form.
{% endhint %}

Draft data will be copied over published fields. For more details on what those fields are and what else happens, see the [Data Structure](#data-structure) section. Assuming the post is live, it will be available in the Blog.

### Un-publishing

Once a post has been published, you can bring up the Publish modal again:

<figure><img src="https://2964479083-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fjybnmx3gX5MSuPHDwIJB%2Fuploads%2Fo7Wj4Ng6D4XbcfafUPNf%2FXnapper-2024-12-06-16.12.33.png?alt=media&#x26;token=5946121a-deaa-4f50-bafe-5b1c0aaf96de" alt=""><figcaption><p>Remove button is enabled for published post</p></figcaption></figure>

To un-publish, click the "Remove" button. This won't affect data entry, but it will ensure that the post is no longer publically available via the Blog.

### Content Editor

The Content Editor [component](https://petal.build/components/content-editor) is based on [Editor.js](https://editorjs.io/). You can use it to create paragraphs, lists, tables, insert images and even insert embeds.

pic

Editor.js is a block-style editor that that generates a json document as output. This json is captured and stored in the `Post` data structure (see [Data Structure](#data-structure) below for more details). Rendering is taken care of by the Content Editor [component](https://petal.build/components/content-editor), via the `.content` and `.pretty_content` function components.

### Pre-installed Editor.js Plug-ins

Editor.js has it's own eco-system - functionality is provided via plug-ins. The following plug-ins are installed by default:

* [Header](https://github.com/editor-js/header) (`h1` to `h6`)
* [Quote](https://github.com/editor-js/quote) (similar to a markdown quote)
* [Marker](https://github.com/editor-js/marker) (a tool for highlighting sections of text)
* [InlineCode](https://github.com/editor-js/inline-code) (so you can display inline text as code)
* [Code](https://github.com/editor-js/code) (so you can show a code block)
* [Delimiter](https://github.com/editor-js/delimiter) (break up paragraphs with a line-based delimeter)
* [List](https://github.com/editor-js/list) (ordered and unordered lists)
* [SimpleImage](https://github.com/editor-js/simple-image) (paste in a url to an image and it will render)
* [Table](https://github.com/editor-js/table) (UI for creating a HTML table)
* [Warning](https://github.com/editor-js/warning) (displays an alert, similar to the next section)
* [Embeds](https://github.com/editor-js/embed) (e.g. paste a YouTube link to create an embedded iframe)

{% hint style="info" %}
The SimpleImage plug-in will only work for urls that are configured in the Content Security Policy. See the [Data Structure](#data-structure) section for more information on the Content Security Policy
{% endhint %}

Finally, we've created a plug-in that integrates with the file browser, called `PetalImage`

### Adding Plug-ins

There are many core and community-based plug-ins available for Editor.js and there's nothing stopping you from using them! Petal Pro uses `npm` for installation. Documentation for installing plug-ins can be found [here](https://editorjs.io/getting-started/#tools-installation).

The only requirement is that the Content Editor component is updated so that the renderer can output data from the new plug-in. Thankfully, this is extremely easy to do. In fact, here's a tutorial on how you can create your own Editor.js plug-in and adjust the Content Editor:

{% content-ref url="../guides/content-editor-adding-your-own-plug-in" %}
[content-editor-adding-your-own-plug-in](https://docs.petal.build/petal-pro-documentation/v2.2.0/guides/content-editor-adding-your-own-plug-in)
{% endcontent-ref %}

### Data Structure

Here's what a `Post` looks like:

```elixir
schema "posts" do
  # Draft fields (used for editing)
  field :category, :string
  field :title, :string
  field :slug, :string
  field :cover, :string
  field :cover_caption, :string
  field :summary, :string
  field :content, :string
  field :duration, :string

  # Published fields
  field :published_category, :string
  field :published_title, :string
  field :published_slug, :string
  field :published_cover, :string
  field :published_cover_caption, :string
  field :published_summary, :string
  field :published_content, :string
  field :published_duration, :string

  # The last time this post was published
  field :last_published, :naive_datetime
  
  # Date/time when post is publically available. 
  # `nil` means "private" or "not published"
  field :go_live, :utc_datetime

  belongs_to :author, PetalPro.Accounts.User

  timestamps(type: :utc_datetime)
end
```

Blog posts belong to a user. However, the default behaviour is that only admins have access to the console.

As a general rule, the draft fields are used in the admin console. The published fields are only used in the public facing Blog.

At the time of saving a draft, if the `:title` has changed, then it is used to generate `:slug`. The `:slug` includes the `:id` as a postfix (base64 encoded). So if the title is, "Hello Fred", then the slug will look similar to `hello-fred-OwAw1rxK`. This way it won't matter if the `:title` changes, pasting an old `:slug` into the browser will still result in loading the correct Blog post.

When publishing a post, the draft fields are copied over the published fields. In addition `:last_published` is set to `DateTime.now_utc` and `:go_live` is based on the user's data entry.

The output of the Content Editor component (i.e. Editor.js json [data structure](https://editorjs.io/base-concepts/#what-is-clean-data)) is stored in the `content` field (and published to the `published_content` field).

The `cover` field is a url to an image (and by extension `published_cover` is too). This can be any url, but you'll need to make sure the domain is configured in the Content Security Policy. To find out how to configure the Content Security Policy (or disable it), check out:

{% content-ref url="deployment" %}
[deployment](https://docs.petal.build/petal-pro-documentation/v2.2.0/fundamentals/deployment)
{% endcontent-ref %}

Though the `cover` image can point to any url, in reality the domain will probably be defined by the file browser. See the [File Browser](#file-browser) section (below) for more information.

Here's what the `File` data structure looks like:

```elixir
schema "files" do
  field :url, :string
  field :name, :string
  
  # Files aren't removed, they're archived
  field :archived, :boolean, default: false

  belongs_to :author, PetalPro.Accounts.User

  timestamps(type: :utc_datetime)
end
```

Again, a file belongs to a user, but by default only admins have access to the console.

A file points to a url and has a name. Files are intended to be single use - you can add a file, name it and then you're done. If you want something different, you add a new file. If you want to replace a file, archive the old file and add a new one. This behaviour is supported by the File Browser. The main benefit of this design is that you can control what you see, but limit the chance of accidentally deleting a file that's in use.

### File Browser

The file browser provides a means to upload and select images:

<figure><img src="https://2964479083-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fjybnmx3gX5MSuPHDwIJB%2Fuploads%2FmQ694wJF9TmONlgnl0l3%2FXnapper-2024-12-06-16.06.20.png?alt=media&#x26;token=639ccbed-d781-4b71-b1bf-5a3b74ce7592" alt=""><figcaption><p>Upload or select image</p></figcaption></figure>

{% hint style="info" %}
NB - the file browser is currently targetted at images (i.e. files that are easy to preview). Other file types (such as pdf documents) could be supported but will require more work to enable. If you're interested in a feature like this, please consider adding it to the Petal Pro [roadmap](https://petal.build/pro/roadmap).
{% endhint %}

Based on LiveView [Uploads](https://hexdocs.pm/phoenix_live_view/uploads.html) the file browser defaults to storing files on the local server:

```elixir
defmodule PetalProWeb.AdminFilesLive.FormComponent do
  @moduledoc false
  use PetalProWeb, :live_component

  alias PetalPro.Files
  alias PetalPro.Files.File
  alias PetalProWeb.FileUploadComponents

  @upload_provider PetalPro.FileUploads.Local
  # @upload_provider PetalPro.FileUploads.Cloudinary
  # @upload_provider PetalPro.FileUploads.S3

  @impl true
  def update(assigns, socket) do
    socket =
      socket
      |> assign(assigns)
      |> assign_new(:form, fn -> %File{} |> Files.change_file() |> to_form() end)
      |> assign(:uploaded_files, [])
      |> allow_upload(:new_file,
        # SETUP_TODO: Uncomment the line below if using an external provider (Cloudinary or S3)
        # external: &@upload_provider.presign_upload/2,
        accept: ~w(.jpg .jpeg .png .gif .svg .webp),
        max_entries: 1
      )

    {:ok, socket}
  end
  
  # ...
end
```

As you can see, this can be configured to work with an external uploader instead. You can use one of the built-in providers (Cloudinary or S3).

In the case of a local server upload, Cloudinary or Amazon S3 - the Content Security Policy has been pre-configured and should work out of the box.
