A basic C++ website - a complete example


#include <ServerImplementation.h>
#include <InputParser.h>

int main(int argc, char ** argv)
{
    InputParser parser(argc, argv);

    Angelsen::Config config;
    config.updateFromJson(parser.getCmdOption("-config"));
    config.loadTemplate();

    Angelsen::runServer(config);
}

                              

https://github.com/stianang/angelsenCppWeb

A basic C++ website - a complete example

Many C++ developers would like to stick with what they know to create a basic website, without spending time to learn the latest trending javascript framework. Let's take a look at what it takes!

C++ is useful for at least the following types of websites:

  • As a personal homepage and blog
  • As simple a landing page for service/product
  • Hobby websites relying on C++ libraries, for example a data monitoring and control website

C++ is not suitable if you want the latest in responsive design, which relies on javascript, or larger websites. However, please note that it is possible to get a very dynamic feel using CSS only.

There are several approaches to creating a website in C++. For this blog post, I will focus on the method I think is the most plug and play. Some C++ web frameworks use widgets, but this makes it hard to use predefined HTML/CSS templates in my opinion (which are beatiful, and widely available on the web). It's easier to use exising html and css, and a template engine to replace variables and insert sub-html components.

See the source code for the demo here: https://github.com/stianang/angelsenCppWeb

TL;DR

A minimal example:


// Partly pseudo-code, to demonstrate a minimal example

int main(int argc, char ** argv)
{
    HttpServer server(8080);    // HTTP server

    server.resource["^/home$"]["GET"] = [](ResponsePtr response, RequestPtr) {  // Define a path
        std::string indexFile = readFile("path/to/web/index.html"); // Load HTML file

        TemplateEngine indexPage(indexFile); // Use template engine to set variables
        indexPage.setValue("title", "Welcome to website Hello World");
        indexPage.setValue("contactEmail", "my@email.com");

        response->write(indexPage.render()); // Return rendered page
    };

    server.start();
}

That's basically it (although not yet optimal)! For my website, I will split this up into convenient classes to add some additional features.

Required components

To create the website, we need some basic functionality:

  • An HTTP server: This allows us to receive HTTP requests and respond with data or the rendered webpage.
  • An HTML template engine: This allows us to insert custom variables into html pages
  • Page controllers: This allows us to apply logic to received request, interact with databases etc.

There are many available HTTP servers and HTML template engines on github. Pick your favorite. For this example, I've selected libraries with minimal dependencies to simplify the demonstration:

The HTTP server receives the requests, the page controller handles logic and combines one or more html files into the final webpage to send back to the client

Block diagram

A consultant homepage

I will use my own website as an example (this website), as it is running with a C++ backend

My requirement for the site was to have a simple presentation of myself, a contact form, and blog functionality.

First I scouted the web by googling 'html/css consultant template'. In the end I found this free template that met my requirements: https://templatemo.com/tm-509-hydro

You may say using template is 'cheating', but it is the industry standard when delivering web pages to let the client choose a template/theme, which is then modified. There are so many templates out there, so no need to do this from scratch

Once the template was selected, I started designing my mini-framework.

Tree structure

I split my code into 'common' elements, to be reused with future websites, and the concrete implementation of the consultant page.

  • - common
    • + Jinja2CppLight (Template engine)
    • + http (SimpleWebServer and some additional helpers)
    • + easyweb (utility classes)
  • - Angelsen
    • + src (main, page controllers)
    • + website (template, html)

Take a look at the github project to browse the structure.

I had the following features in mind when combining the libraries:

  • Easy to get started
  • Easy to use
  • Easy to scale
  • Easy to develop. Reload html files when refreshing the page, so that html pages can be modified without re-compiling

I did not focus on optimising. If your are a seasoned C++ developer please apply your optimizations were desired

WebFile class

For convenience, I created a simple class to load (and reload) html files when they are needed.

The key feature of the class is simply accessing file content as string, and loading:


std::string& WebFile::file()
{
    if (file_.empty())
    {
        read();
    }
    else if (reload_)
    {
        read();
    }

    return file_;
}

void WebFile::read()
{
    file_.clear();
    std::ifstream file(filePath_);

    if (file.is_open())
    {
      std::string line;

      while ( getline (file, line) )
      {
          file_ += line;
      }
      file.close();
    }
    else
    {
        std::cerr << "Failed to open file: " << filePath_ << std::endl;
    }
}

The basic html template

I modified the downloaded template so that the pages can have the same menu and footer, only replacing the body when loading different pages. Note that variables are inserted using {{variable}} tags.


<!DOCTYPE html>
<html lang="en">
<head>
    
    ... Head content
</head>
<body>

     
     ... Menu HTML content

     
     {{body}}

     
     ... Footer HTML content

</body>
</html>

Configuration

It is convenient to load config from a separate file, e.g. a json file. In addition to configuration, I store the main html template in the config class, for easy access from the page controllers. My intention is then to mainly depend on the config class below.



// Config.h

#include <json.hpp>
#include <WebFile.h>

using nlohmann::json;

namespace Angelsen
{
    struct Config
    {
        std::string rootDir{};
        std::string templateDir{};

        unsigned short port {8080};

        bool https {false};
        std::string crt{};
        std::string key{};

        struct SendInBlue
        {
            std::string url{};
            std::string apiKey{};
            std::string toEmail{};
            std::string toName{};
        } email;

        std::unique_ptr<EasyWeb::WebFile> template_;

        void updateFromJson(const std::string &configPath);
        void loadTemplate();
    };
}

// config.json

{
    "rootDir" : "/home/stian/EasyWeb/AngelsenSoftwareSystems",
    "templateDir" : "/home/stian/EasyWeb/AngelsenSoftwareSystems/website",
    "port": 8080,
    "https": false,
    "crt" : "server.crt",
    "key" : "server.key",
    "email" : {
        "url" : "api.sendinblue.com",
        "apiKey" : "mySecretApiKey",
        "toEmail" : "sangelsen@gmail.com",
        "toName" : "Stian Angelsen"
    }
}


Index page controller

The most basic page is the index page. It is simply a static page placed inside the body of the template

I chose to design the controllers as simply as possible. The task of the controllers is to take input and render the output.



// ControllerIndex.h

#include <WebFile.h>
#include <Config.h>

namespace Angelsen
{
    struct ControllerIndex
    {
        ControllerIndex(const Config& config);

        std::string get();

        const Config& config_;
        EasyWeb::WebFile index_;
    };
}

// index.cpp

#include "ControllerIndex.h"
#include <Jinja2CppLight.h>

Angelsen::ControllerIndex::ControllerIndex(const Config &config)
    : config_(config),
      index_(config.templateDir + "/index.html", true)
{

}

std::string Angelsen::ControllerIndex::get()
{
    Jinja2CppLight::Template t(config_.template_->file());
    t.setValue("body", index_.file());

    return t.render();
}


Contact confirmation controller

I will use the contact form to send myself an email when it is submitted. I use a third party email provider, which can send emails using a simple HTTP request. I will not go into the detail of the implementation, but you may look it up on the GitHub project if curious. The request requires me to send an http-version of the emails text as well as in plaintext format.



// ControllerContact.h

#include <WebFile.h>
#include <Config.h>

namespace Angelsen
{
    struct ControllerContact
    {
        ControllerContact(const Config& config);

        struct FormData
        {
            std::string name{};
            std::string email{};
            std::string number{};
            std::string subject{};
            std::string message{};
        };

        std::string get(const FormData &formData);
        bool sendEmail(const FormData& formData);

        const Config& config_;
        EasyWeb::WebFile confirmation_;
    };
}

// ControllerContact.cpp

#include "ControllerContact.h"
#include <Jinja2CppLight.h>
#include <EmailClientSendInBlue.h>
#include <sstream>

using namespace Angelsen;

ControllerContact::ControllerContact(const Config &config)
    : config_(config),
      confirmation_(config.templateDir + "/contact_confirmation.html", true)
{

}

std::string ControllerContact::get(const FormData &formData)
{
    Jinja2CppLight::Template confirmation(confirmation_.file());

    if (sendEmail(formData))
    {
        confirmation.setValue("headerTitle", "Thank you for your request!");
        confirmation.setValue("title", "Your request has been sent");
        confirmation.setValue("result", "We will contact you as soon as possible. For urgent request, please contact us on (+47) 90 07 98 93 or on sangelsen@gmail.com" );
    }
    else
    {
        confirmation.setValue("headerTitle", "Something went wrong");
        confirmation.setValue("title", "We were unable to process your request");
        confirmation.setValue("result", "Please verify all fields were entered correctly. For urgent request, please contact us on (+47) 90 07 98 93  or on sangelsen@gmail.com");
    }

    Jinja2CppLight::Template mainTemplate(config_.template_->file());
    mainTemplate.setValue("body", confirmation.render());

    return mainTemplate.render();
}


bool ControllerContact::sendEmail(const FormData &formData)
{
    std::stringstream html;
    html << "<h3>New message from your website!</h3>\n"
         << "<p>" << formData.message << "</p>\n"
         << "<h3>Info</h3>\n"
         << "<p>Name:    " << formData.name << "</p>\n"
         << "<p>Email:   " << formData.email << "</p>\n"
         << "<p>Number:  " << formData.number << "</p>\n"
         << "<p>Subject: " << formData.subject << "</p>\n";

    std::stringstream plain;
    plain << "New message from your website!\n\n"
          << formData.message << "\n\n"
          << "Info\n"
          << "Name:    " << formData.name << "\n"
          << "Email:   " << formData.email << "\n"
          << "Number:  " << formData.number << "\n"
          << "Subject: " << formData.subject << "\n";

    auto email = EmailClientSendInBlue(config_.email.url,
                                       config_.email.apiKey)
                    .to(config_.email.toEmail, config_.email.toName)
                    .from(formData.email, formData.name)
                    .subject(formData.subject)
                    .content(html.str(), plain.str());

    return email.send();
}

The controller takes form input, try to send the email, and then output if the sending was successful or not.

Blog detail controller

The controller shall be responsible to show any blog post, by using a blog-id as input. There are many oppertunities to expand the controller, such as storing the blog posts in a database. For now, I'll keep it simple, and just upload a new html file for every new blog post. Then I would manually have to update the index page to feature the latest blog posts. Expansion of this system is a good candiate for the next blog post.



// ControllerBlogPost.h

#include <WebFile.h>
#include <Config.h>

namespace Angelsen
{
    struct ControllerBlogPost
    {
        ControllerBlogPost(const Config& config);

        std::string get(const std::string &blogPath);

        const Config& config_;
    };
}

// ControllerBlogPost.cpp

#include "ControllerBlogPost.h"
#include <Jinja2CppLight.h>

using namespace Angelsen;

ControllerBlogPost::ControllerBlogPost(const Config &config)
    : config_(config)
{

}

std::string ControllerBlogPost::get(const std::string& blogPath)
{
    EasyWeb::WebFile blog(config_.templateDir + "/" + blogPath, false);

    Jinja2CppLight::Template t(config_.template_->file());
    t.setValue("body", blog.file());

    return t.render();
}

Each blog post is stored in a folder structure like: /blog/2019/aBlogPost.html, which will also be the path to access the blog post. The controller handles inserting the blog post into the main template.

Putting it all together, and setting up routing

I've designed the controllers so that they are not dependent on the http-server I intend to use. This will make it easier to replace the http server if desired. The routes could have been defined inside the controller classes, however, I've decided to define the routes in a separate implementation class. I've used the SimpleWebServer, which has satisfactory features. However, I prefer to switch between HTTP and HTTPS for local testing / production running. Unfortunately, this was a bit more inconvenient with this library. I had to create a template class to handle this. Some other http server library may handle this a bit better.

The SimpleWebServer included an example demonstrating loading of files from the server. I included this in my implementation, as it is required to load the necessary css and js files that the html pages refer to. I put this in a separate function for default setup.

The first part of the implementation is to launch either an HTTP server or HTTPS server based on the config settings:



#include <Config.h>
#include <HttpUtility.h>
#include <server_http.hpp>
#include <server_https.hpp>
#include <iostream>

namespace Angelsen
{
    template<typename SocketType>
    struct ServerImplementation
    {
        void run(const Config& config, SimpleWeb::Server<SocketType>& server);
        void defineDefaults(const Config& config, SimpleWeb::Server<SocketType>& server);
    };

    void runServer(const Config& config);
}


void Angelsen::runServer(const Config& config)
{
    using SimpleWeb::Server;
    using SimpleWeb::HTTP;
    using SimpleWeb::HTTPS;

    if (config.https)
    {
        Server<HTTPS> server(config.crt, config.key);
        server.config.port = config.port;

        std::cout << "Serving as HTTPS server on port: " << config.port << "\n";

        Angelsen::ServerImplementation<HTTPS> angelsen;
        angelsen.run(config, server);
    }
    else
    {
        Server<HTTP> server;
        server.config.port = config.port;

        std::cout << "Serving as HTTP server on port: " << config.port << "\n";

        Angelsen::ServerImplementation<HTTP> angelsen;
        angelsen.run(config, server);
    }
}

#include <ServerImplementation.tpp>

The run function includes creating the controllers and their respective routes. The function is not the most scalable, but it could be split into multiple files when expanding with more controllers. However, this was the expected tradeoff by separating the routing from the individual controllers.



// ServerImplementation.tpp

template<typename SocketType>
void ServerImplementation<SocketType>::run(const Config& config, SimpleWeb::Server<SocketType>& server)
{
    defineDefaults(config, server);

    using ResponsePtr = std::shared_ptr<SimpleWeb::Server<SocketType>::Response>;
    using RequestPtr = std::shared_ptr<SimpleWeb::Server<SocketType>::Request>;

    // Define controllers

    ControllerIndex    controllerIndex(config);
    ControllerBlogPost controllerBlogPost(config);
    ControllerContact  controllerContact(config);

    // Define routes

    server.resource["^/$"]["GET"] = [&controllerIndex](ResponsePtr response, RequestPtr) {
        response->write(controllerIndex.get());
    };

    server.resource["^/blog/(.*)/(.*html)$"]["GET"] = [&controllerBlogPost](ResponsePtr response, RequestPtr request) {
        response->write(controllerBlogPost.get(request->path));
    };

    server.resource["^/contact$"]["GET"] = [&controllerContact](ResponsePtr response, RequestPtr request) {
        json form = EasyWeb::HttpUtility::requestQueryToJson(request->parse_query_string());

        response->write(controllerContact.get({form["cf-name"].get<std::string>(),
                                               form["cf-email"].get<std::string>(),
                                               form["cf-number"].get<std::string>),
                                               form["cf-subject"].get<std::string>(),
                                               form["cf-message"].get<std::string>()}));
    };

    auto thread = std::thread([&server]()
    {
        std::cout << "Server started\n";
        server.start();
    });

    thread.join();
}

Even though it may look a bit verbose, it's actually quite simple. Basically, define the controllers, setup their routes, and start the server. Note that the Simple-Web-Server uses regex to match paths of incoming requests, which is convenient.

Running and testing

The server is now working, and it is possible to edit html files (such as this blog post) without recompiling. Using cmake to build the server, it can then be run with the -config input parameter



$ cd path/to/project
$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./Angelsen/Angelsen -config ../Angelsen/config.json

The server is now serving at http://localhost:8080

Summary

This was an example of how to design a website using C++ for logic.

If you want to experiement, please try the GitHub example. It is very easy to get started:git clone, compile, run!

Source code here: https://github.com/stianang/angelsenCppWeb

For a later blog post, I will show how to deploy the website, and I may also expand the blog feature by using databases.

Thank you for reading! - Stian