- Context
- Architecture
- Installation
3.1 First time process
3.2 Usual process - Commands
- Backend Tools
5.1 ApostropheCMS
5.1.1 Data models
5.1.2 Modules
5.1.3 Templates, macros, fragments
5.1.4 Distinction between back and front code
5.1.5 Custom components
5.1.6 Publication workflow, locales and translations
5.2 Docker
5.3 MongoDB - Default user
- SSL certificate
1 Context ⬆
This project contains backend and frontend for Public Domain Library.
The app enable creation of authors and their relative books, and download of files.
2 Architecture ⬆
ApostropheCMS was chosen on the backend because it is an open-source CMS, enabling user management, models definition by developers while content edited by non-technical users is displayed in rendered templates. It is using MongoDB as database.
Every part of the application is dockerized, with a healtcheck status.
3 Installation ⬆
First, you need to have docker and docker-compose installed and launched on your machine. Then, it is quite easy;
- run
git clone
- copy the file
.env.sample
and name it.env
(this file will contain passwords, and will be git-ignored, so don't try to commit it) - in this
.env
file, create a password forMONGO_INITDB_ROOT_PASSWORD
and another one forMONGO_INITDB_USER_PASSWORD
(choose whatever you want but do not use@
or:
because it will mess the DB connection up otherwise) - do not modifyMONGO_INITDB_ROOT_USERNAME
andMONGO_INITDB_USER_USERNAME
- still in this
.env
file, adjust APOS_MONGODB_URI the connection URI with the password added forMONGO_INITDB_USER_USERNAME
- run
npm run dev
ordocker compose up
3.1 First time process ⬆
The first time you run docker compose up
, all Docker images will be downloaded and built.
There is a dependency between the containers pdl-server
and pdl-db
: the DB needs to be started to
enable the server to start (otherwise, Apostrophe does not have access to Mongo and is stuck). However, on the first
run, some time is spent to create database users. Apostrophe tries to connect to Mongo during a certain period of time,
but on the first run, this timeout expires before Mongo is ready to accept connections. So, read the logs and wait for
the user app-admin
to be created. When it is done, the DB is ready. You can now kill the docker containers by
hitting Ctrl + c
in your terminal. And run again docker compose up
. This time, the DB will start quickly, enabling the server to start correctly.
3.2 Usual process ⬆
Run simply npm run dev
to start on development mode. The CMS part is accessible on http://localhost:3000
.
4 Commands ⬆
Run:
docker ps
for running instancesdocker stop container-name
ordocker kill
for stopping the containersdocker exec -it container-name sh
to log into a container
Mongo outputs a lot of logs, and while this can be necessary to read them sometimes, most of the time it is too much
useless information. Displaying its logs is possible though through docker logs pdl-db -f
.
5 Backend Tools ⬆
5.1 ApostropheCMS ⬆
As explained previously, Apostrophe is a framework with REST APIs. Therefore, no need to create CRUD routes, as it is handled by the framework.
5.1.1 Data model ⬆
Data models are defined by schemas in Apostrophe, also called piece types. These piece types are necessary to create pieces, or items in the database.
See https://v3.docs.apostrophecms.org/guide/pieces.html#pieces
5.1.2 Modules ⬆
Modules are the heart of a Apostrophe app. They handle REST routes:
- getAll ->
GET /api/v1/module-name/
- getOne ->
GET /api/v1/module-name/:id
- post ->
POST /api/v1/module-name/
- patch ->
PATCH /api/v1/module-name/:id
- put ->
PUT /api/v1/module-name/:id
- delete ->
DELETE /api/v1/module-name/:id
If you need a custom route, you could add 2 sorts of routes:
- through
apiRoutes
methods (https://v3.docs.apostrophecms.org/reference/module-api/module-overview.html#apiroutes-self): for exampleGET /api/v1/module-name/custom-route
with areq
object as parameter - through
routes
(https://v3.docs.apostrophecms.org/reference/module-api/module-overview.html#routes-self): it creates an Express route such as/api/v1/module-name/custom-route
but with areq
andres
objects
There is also a renderRoutes
function to feed Nunjucks templates with data returned by custom
functions (https://v3.docs.apostrophecms.org/reference/module-api/module-overview.html#renderroutes-self).
You can also "extend" existing routes or even functions(https://v3.docs.apostrophecms.org/reference/module-api/module-overview.html#the-extension-pattern).
A module is defined by its type, often it will be piece-type
to hold documents, and a schema with fields will be defined.
There are also options to configure.
Example of the "author" module:
module.exports = {
extend: '@apostrophecms/piece-type',
options: {
alias: 'author',
label: 'Author',
pluralLabel: 'Authors',
localized: true,
},
fields: { /* definition of different fields to display for the editor user in the UI */ },
methods() { /* methods and hooks */ }
}
To know more about the possible configurations of a module: https://v3.docs.apostrophecms.org/reference/module-api/module-overview.html#configuration-settings
5.1.3 Templates, macros, fragments ⬆
ApostropheCMS uses Nunjucks templates to render data by default.
layout.html
(apos/modules/@apostrophecms/page/views/layout.html) is the default template.
It is composed of several blocks that can be overridden. For example, the block main
in layout.html
is where pages will output their own content.
Simplified view of layout.html
:
{% block beforeMain %} {# the header is defined here, no need to override in most cases #} {% endblock %}
{% block main %}
{#
Usually, your page templates in the @apostrophecms/pages module will override
this block. It is safe to assume this is where your page-specific content
should go.
#}
{% endblock %}
{% block afterMain %} {# the footer is defined here, no need to override in most cases #} {% endblock %}
Then any page will extend layout.html
and override the main
block. Example:
{% extends "@apostrophecms/page:layout.html" %}
{% set fileBackground = apos.attachment.url(data.global.backgroundImage) %}
{% block main %}
<div class="t-background" style="background-image: url('{{ fileBackground }}');"></div>
<div class="t-places-categories">
{% component 'place:categories' %}
</div>
<div class="t-partnerships">
<div class="t-partnership">Logo</div>
<div class="t-partnership">Logo</div>
<div class="t-partnership">Logo</div>
</div>
{% endblock %}
It is also possible to include simple templates. When parameters are needed, macros come into place. With Apostrophe3, a new way of including functions has come: fragments. Examples can be found in the codebase. For more information, see https://v3.docs.apostrophecms.org/guide/templating.html
5.1.4 Distinction between back and front code ⬆
The backend code is always in index.js
file at the root of a module. It can be require
d from other files, but it will end up in this file.
The frontend code is always in the index.js
file of the src/ui/
folder of a module. It can be import
ed but there has to be index.js
at some point.
This file is then bundled by the automatic Webpack configuration contained in Apostrophe and is available in the browser.
5.1.5 Macros ⬆
If a component has a view
folder with a Nunjucks template, it means it can be included in another template.
For example, the locale-switcher component is imported in the layout template:
{% import "locale-switcher:index.html" as localeSwitcher with context %}
...
{{ localeSwitcher.display() }}
It uses a macro display
and outputs a list of available languages for the app user.
5.1.6 Publication workflow, locales and translations ⬆
Apostrophe3 has a publication workflow at the core. It gives the possibility to create draft documents and publish them later, or even give permissions to specific users to edit documents, and other users to publish them. About roles and permissions, see this documentation: https://v3.docs.apostrophecms.org/guide/users.html#user-roles
Languages are called "locales" in Apostrophe and they are defined in apos/modules/@apostrophecms/i18n/index.js
:
options: {
defaultLocale: 'en',
locales: {
fr: {
label: 'Français',
prefix: '/fr',
},
en: {
label: 'English',
prefix: '/en',
},
},
},
When creating a document in a module or a page, it is possible to "localize" in other defined locales, meaning it will be published (giving the "live" status). A localized document, when edited, is in draft. It has to be published to be public (when the user clicks on "Publish" in the document modal or programmatically). As a consequence, a document can be translated by a user who has the permission to edit.
However, labels in the UI or our frontend code cannot be translated through the Apostrophe "localization" feature. Therefore, there are translation files in apos/modules/@apostrophecms/i18n/i18n
. These are JSON files.
There are different ways to use translations coming from these JSON files:
- in Apostrophe labels, as in module definition for options or fields for example:
label: 'pdl:exampleTranslation',
- in the
req
object in Apostrophe methods:req.t('pdl:exampleTranslation')
(thet
function is provided by the modulei18n
, the one managing translations in locales) - in templates:
{{ __t('pdl:exampleTranslation') }}
(__t
is the same function provided byi18n
- Apostrophe injects it in templates through a helper )
By default, these translations are not available in js frontend code. If needed in a specific page, they can be injected through a getBrowserData
method.
Labels are defined in data.labels
in the backend:
getBrowserData(_super, req) {
const data = _super(req)
data.labels = {
exampleLabel: req.t('pdl:exampleTranslation'),
}
return data
}
getBrowserData
is an Apostrophe function that pushes JS objects to the frontend. Therefore, labels are accessible in apos.register.labels
(for this example) in frontend js code.
// in apos/modules/register-page/ui/src/index.js
const { exampleLabel } = apos.register.labels
5.2 Docker ⬆
Docker containers have a healthcheck
command to ensure they are running correctly. It is an ongoing process, checking
if the container replies on a regular basis.
You can check if a container is healthy by typing docker ps
5.3 MongoDB ⬆
The database process uses authentication. The image used for the dockerized Mongo accepts some environment variables:
- MONGO_INITDB_DATABASE
- MONGO_INITDB_ROOT_USERNAME
- MONGO_INITDB_ROOT_PASSWORD
With these, a root
user for all databases is created when the Mongo image for Docker is launched the first time. This
enables authentication.
The Docker image can run scripts at the first initialization.
For example, the script db/scripts/init/01.add-user.sh
is copied into the image through the Dockerfile in the
folder db
. It creates a user specifically for the library
database. That is why the .env
file is important, and
MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_USER_USERNAME should not be changed.
FROM mongo:6.0
COPY scripts/init/01.add-user.sh /docker-entrypoint-initdb.d/
If other scripts during the first init step are needed in the future, they should be placed in db/scripts/init/
and
copied the same way in the Dockerfile.
Outside of the Docker network, the port exposed is 27018 (in order not to mess with existing MongoDB in the local
machine). Therefore, you can connect to GUI tools such as MongoDB Compass or Robo3T through mongodb://localhost:27018
and indicate in the authentication settings the root
credentials.
To log into the container, you can run docker exec -it pdl-db bash
and then mongosh -u root
. Insert
the root
password when it is asked and you have access to the mongo shell.
6 Default user ⬆
By default, no user is created in ApostropheCMS. To create one, when docker images are up, run docker exec -it pdl-server sh
then inside the container, run node app @apostrophecms/user:add admin admin
. A prompt will ask for the password, enter one and exit. Go to http://localhost:3000/login and enter
admin` as user and the freshly created password to log in.
7 SSL certificate ⬆
If there is no previous certificate:
- log in with SSH on the production server
- disable port 443 in nginx/local.conf (only port 80 is necessary)
- restart nginx (docker compose up -d)
- run
docker compose run --rm pdl-certbot certonly --webroot --webroot-path /var/www/certbot/ --agree-tos --renew-by-default -d publicdomainlibrary.org -d www.publicdomainlibrary.org -m EMAIL_ADDRESS@TO_CONFIGURE
with an email address to be alerted when the SSL certificate is about to expire - once generated, the SSL certificate will be saved in the
certbot
folder on the server - enable port 443 in nginx/local.conf and start again nginx
docker compose run --rm pdl-certbot renew
docker restart pdl-reverse-proxy