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 EventonManual
- Add an X+1 Expression: Target
body
, TypeAny
, 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
- scope:
- Add a Function: Type
Log error message
, and message:body.email
- Add a Trigger: Resource
- Create a new integration gateway (docs) with endpoint
/hello
- Processing: Workflow processor:
Hello
- Post-filter:
Default JSON Response
- Processing: Workflow processor:
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 fromSystem onManual
toSystem.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 valueapplication/json
So far I don't know the proper way to convert data into JSON in Corteza'sX+1 Expression
or functions, or to use the functionProcess arbitrary data in jsenv
again, which seems to require extra IO conversions. So I just manually build a simple JSON string from thebody
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 moduleLead
and namespacecrm
and targetlead
- X+1 Expression: just populate
lead
record frombody
(e.g.lead.values.Email
tobody.email
, etc) - Function:
Compose record create
with recordlead
and also result targetlead
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 😏