2024-10-30 Adding Contact Forms to Static Websites

I deployed a quick static landing page for Pointegrity, and also set up a CRM site for it using Corteza. I'd like to see how easy or difficult it is to allow visitors to post contact forms from the static website and to be processed by Corteza.

Background

Adding a contact form to a page of a static website seems simple yet complicated. If a company has many websites each provide contact forms for visitors to fill and submit, the company may want to aggregate the data in one place instead of handling them individually by each site. This is more obvious for static websites that have no corresponding backends.

The initial reason to try Corteza is to see if I can add contact forms to both this blog site and the Pointegrity website easily. If things go well I can also base more future features I want to do on Corteza later. After all, Corteza already comes with CRM, OIDC provider, flexible low-code charting and workflow management, and more.

Planning

Initial investigation shows that the "Lead" concept in Corteza's CRM system is the one for my use case. User can submit a contact form, which eventually leads to a lead created. Looking up the web for a while, I cannot find a simple example but partial information for this "obvious" use case. Since I am also interested in exploring how no-code and CRM systems work "typically," I don't mind using this opportunity to learn more.

Integration Gateways or Sink Routes

Further investigation indicates that integration gateways and sink routes are two similar but overlapping mechanisms that allow external requests to trigger data processing in Corteza. Roughly speaking, integration gateways are like sink routes but with more UI support for workflow configuration.

I tried both sink routes and integration gateways, but encountered a lot of problems. The main issue is that currently Corteza's documentation does not keep up with its latest version, and the given examples are a bit outdated. It took a while for me to create a working example.

Adding an Integration Gateway

After some struggles, I managed to create a working flow following the example here but with modifications. In Admin Area:

  • Create a new workflow named Hello (handle: hello). In the builder
    • Add a Trigger: Resource System and Event onManual
    • Add an X+1 Expression: Target body, Type Any, and Expression: request.Body
    • Add a Function: Type Process arbitrary data in jsenv
      • scope: body, type: Reader, expression: on
      • source: return JSON.parse(readRequestBody(input)), expression: off
    • Add a Function: Type Log error message, and message: body.email
  • Create a new integration gateway (docs) with endpoint /hello
    • Processing: Workflow processor: Hello
    • Post-filter: Default JSON Response

The workflow Hello looks like:

But if you run by clicking the Trigger, some error occurs:

But if you trigger the workflow from externally:

$ curl "<website url>/api/gateway/hello" -H "Content-Type: application/json" -d '{"email":"jy@example.com"}'

# {} returned

and look at the logs (in the folder for the Corteza container):

$ docker-compose logs -f
...
{"level":"error","ts":1730305075.076788,"logger":"workflow","msg":"jy@example.com"}

which shows that the integration gateway /hello and the workflow Hello are in action.

Note that I also change the Trigger from System onManual to System.sink onRequest and have the same result. See here for more info.

Return JSON Response

The example above always returns {}. We chose Default JSON Response for the post-filter for the /hello endpoint and did not do anything for the response. Corteza's documentation is not clear about how to do this, so after spending more time on the web I managed to do this:

  • Change the post-filter to Response
  • In the Input field, leave type to Any and set value to "{"msg":"Greetings from " + body["email"] + ""}"
  • In the Headers field, add name Content-Type and value application/json
So far I don't know the proper way to convert data into JSON in Corteza's X+1 Expression or functions, or to use the function Process arbitrary data in jsenv again, which seems to require extra IO conversions. So I just manually build a simple JSON string from the body variable defined in the workflow.

Running the command again does produce the JSON output:

$ curl "<website url>/api/gateway/hello" -H "Content-Type: application/json" -d '{"email":"jy@example.com"}'
{"msg":"Greetings from jy@example.com"}

Create Leads from API

With the basic ingredients ready, we can create an integration gateway at /contact that accepts contact form requests, and a workflow that creates leads based on the requests. To do this, it is convenient to look at existing workflows for references. For example, by studying the workflow CRM - Lead - To Account, we can create a similar workflow by adding more tasks following in Hello workflow:

  • Function: Comppose record maker with module Lead and namespace crm and target lead
  • X+1 Expression: just populate lead record from body (e.g. lead.values.Email to body.email, etc)
  • Function: Compose record create with record lead and also result target lead

So running the command:

$ curl "<...website>/api/gateway/hello" -H "Content-Type: application/json" -d '{"email":"jy@example.com","name":"jy"}'

creates a new lead visible from the Corteza interface. Of course running the command multiple times will create duplicate leads. I only try to make sure the processing logic works. Additional validation and checking steps are needed to make it complete.

Adding Contact Forms

I exercise on adding a contact form on this site, by adding an HTML card and custom JavaScript. See the About page.

I added the HTML:

<form ... id="contact-form" action="<website url>/api/gateway/hello" method="post">
  ...
  <input name="email" type="text">
  <input name="name" type="text">
  <textarea name="message"></textarea>
  ...
  <button type="submit">Send</button>
</form>

and the code from the Code injection sidebar:

document.addEventListener('DOMContentLoaded', function() {
    document.querySelector('#contact-form').addEventListener('submit', function (event) {
      event.preventDefault();
        
      var formData = new FormData(this);
      var body = {};
      formData.forEach(function(value, key){
          body[key] = value;
      });
      fetch(this.getAttribute('action'), {
        method: 'POST',
        body: JSON.stringify(body),
        headers: {
          "Content-Type": "application/json",
        },
      }).then(res=>res.text())
      .then(function (data) {
        alert("Thank you");
      });      
    });
});

The form currently looks like:

Again I was just trying to make it work. The form may not be enabled always 😏