Broadsheet Contact API
This site sends contact-page enquiries to Broadsheet through the API base URL configured in _config.yml:
broadsheetApiUrl: https://api.broadsheet.intellify.co.za
Jekyll exposes that value to the contact form script in _includes/contact.html:
const API_URL = 'https://api.broadsheet.intellify.co.za';
At build time, that becomes:
const API_URL = 'https://api.broadsheet.intellify.co.za';
Current Contact Page Flow
The /contact/ page is defined in contact.md and renders the shared contact form with:
<section class="contact-section" id="contact">
<div class="contact-container">
<div class="contact-hero">
<div class="eyebrow">Get in touch</div>
<h1>Contact <span class="gradient-text">Us</span></h1>
<p class="lead">Have a question or ready to get started? Send us a message and we'll get back to you.</p>
</div>
<div class="contact-grid">
<aside class="contact-panel">
<div class="contact-panel-top">
<p class="section-kicker">Start Here</p>
<h2>Tell us where your business is headed.</h2>
<p>Share what you do, what you need your website to achieve, and how soon you want to go live. We will help you choose a monthly plan that fits.</p>
</div>
<div class="contact-detail-list">
<a class="contact-detail" href="mailto:pieter@intellify.co.za?subject=Intellify Website Enquiry">
<span aria-hidden="true">Email</span>
<strong>pieter@intellify.co.za</strong>
</a>
<a class="contact-detail" href="https://wa.me/27818868834" target="_blank" rel="noopener noreferrer">
<span aria-hidden="true">WhatsApp</span>
<strong>Message us directly</strong>
</a>
</div>
</aside>
<form
id="contactForm"
class="contact-form-card"
novalidate
data-sending-message="Sending your message..."
data-sent-message="Message sent. Thank you, we'll be in touch soon."
data-error-prefix="Something went wrong. Please email"
data-error-email="pieter@intellify.co.za"
>
<div class="honeypot" aria-hidden="true">
<label for="location">Location</label>
<input id="location" name="location" type="text" tabindex="-1" autocomplete="off" hidden>
</div>
<div class="contact-form-grid">
<div class="field">
<label for="firstName">First Name *</label>
<input id="firstName" name="firstName" type="text" autocomplete="given-name" required>
<span class="field-error" id="error-firstName"></span>
</div>
<div class="field">
<label for="lastName">Last Name *</label>
<input id="lastName" name="lastName" type="text" autocomplete="family-name" required>
<span class="field-error" id="error-lastName"></span>
</div>
</div>
<div class="contact-form-grid">
<div class="field">
<label for="emailAddress">Email Address *</label>
<input id="emailAddress" name="emailAddress" type="email" autocomplete="email" required>
<span class="field-error" id="error-emailAddress"></span>
</div>
<div class="field">
<label for="phoneNumber">Phone Number</label>
<input id="phoneNumber" name="phoneNumber" type="tel" autocomplete="tel">
</div>
</div>
<div class="field">
<label for="content">Message *</label>
<textarea id="content" name="content" rows="6" placeholder="Tell us about your business, the kind of site you need, and which plan you are considering." required></textarea>
<span class="field-error" id="error-content"></span>
</div>
<button type="submit" id="submitBtn">
<span>Send Message</span>
<span aria-hidden="true">→</span>
</button>
<div class="form-status form-status-success" id="status-sent" aria-live="polite" hidden>Message sent. Thank you, we'll be in touch soon.</div>
<div class="form-status form-status-error" id="status-error" aria-live="polite" hidden>Something went wrong. Please email <a href="mailto:pieter@intellify.co.za">pieter@intellify.co.za</a>.</div>
<p class="contact-note">No upfront build cost. Domain, hosting, code management, and ongoing support are handled through your monthly subscription.</p>
</form>
</div>
</div>
</section>
<script>
(() => {
const API_URL = 'https://api.broadsheet.intellify.co.za';
const form = document.getElementById('contactForm');
if (!form) return;
const fields = {
firstName: document.getElementById('firstName'),
lastName: document.getElementById('lastName'),
emailAddress: document.getElementById('emailAddress'),
phoneNumber: document.getElementById('phoneNumber'),
location: document.getElementById('location'),
content: document.getElementById('content')
};
const errors = {
firstName: document.getElementById('error-firstName'),
lastName: document.getElementById('error-lastName'),
emailAddress: document.getElementById('error-emailAddress'),
content: document.getElementById('error-content')
};
const submitBtn = document.getElementById('submitBtn');
const sentStatus = document.getElementById('status-sent');
const errorStatus = document.getElementById('status-error');
const requiredMessage = 'This field is required.';
const hideStatuses = () => {
sentStatus.hidden = true;
errorStatus.hidden = true;
};
const validate = () => {
let isValid = true;
Object.entries(errors).forEach(([key, error]) => {
const field = fields[key];
const missing = !field.value.trim();
error.textContent = missing ? requiredMessage : '';
field.setAttribute('aria-invalid', missing ? 'true' : 'false');
if (missing) isValid = false;
});
return isValid;
};
form.addEventListener('submit', async (event) => {
event.preventDefault();
hideStatuses();
if (fields.location.value) return;
if (!validate()) return;
submitBtn.disabled = true;
const body = {
name: fields.firstName.value.trim() + ' ' + fields.lastName.value.trim(),
emailAddress: fields.emailAddress.value.trim(),
phoneNumber: fields.phoneNumber.value.trim(),
location: fields.location.value,
content: fields.content.value.trim()
};
try {
const response = await fetch(API_URL + '/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) throw new Error('Request failed');
form.reset();
Object.values(errors).forEach((error) => { error.textContent = ''; });
Object.values(fields).forEach((field) => field.removeAttribute('aria-invalid'));
sentStatus.hidden = false;
} catch {
errorStatus.hidden = false;
} finally {
submitBtn.disabled = false;
}
});
})();
</script>
The form in _includes/contact.html uses the id="contactForm" element and intercepts submit events with JavaScript. The browser does not submit to an HTML action; the script sends the enquiry with fetch().
Before sending, the script validates these required fields:
firstNamelastNameemailAddresscontent
phoneNumber is optional.
There is also a hidden honeypot field named location. Real visitors should leave it blank. Bots sometimes fill hidden fields, so this can help the receiving API identify suspicious submissions.
API Request
The contact form posts to:
POST https://api.broadsheet.intellify.co.za/v1/messages
The request uses JSON:
Content-Type: application/json
The current payload is:
{
"name": "First Last",
"emailAddress": "person@example.com",
"phoneNumber": "+27 82 000 0000",
"location": "",
"content": "Message text from the form"
}
The payload is built from the form fields like this:
const body = {
name: fields.firstName.value.trim() + ' ' + fields.lastName.value.trim(),
emailAddress: fields.emailAddress.value.trim(),
phoneNumber: fields.phoneNumber.value.trim(),
location: fields.location.value,
content: fields.content.value.trim()
};
Then sent like this:
const response = await fetch(API_URL + '/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
If response.ok is false, the form shows the error message. If the response is successful, the form resets and shows the success message.
Required Markup For Another Page
To implement this on another page, the simplest option is to reuse the include:
<section class="contact-section" id="contact">
<div class="contact-container">
<div class="contact-hero">
<div class="eyebrow">Get in touch</div>
<h1>Contact <span class="gradient-text">Us</span></h1>
<p class="lead">Have a question or ready to get started? Send us a message and we'll get back to you.</p>
</div>
<div class="contact-grid">
<aside class="contact-panel">
<div class="contact-panel-top">
<p class="section-kicker">Start Here</p>
<h2>Tell us where your business is headed.</h2>
<p>Share what you do, what you need your website to achieve, and how soon you want to go live. We will help you choose a monthly plan that fits.</p>
</div>
<div class="contact-detail-list">
<a class="contact-detail" href="mailto:pieter@intellify.co.za?subject=Intellify Website Enquiry">
<span aria-hidden="true">Email</span>
<strong>pieter@intellify.co.za</strong>
</a>
<a class="contact-detail" href="https://wa.me/27818868834" target="_blank" rel="noopener noreferrer">
<span aria-hidden="true">WhatsApp</span>
<strong>Message us directly</strong>
</a>
</div>
</aside>
<form
id="contactForm"
class="contact-form-card"
novalidate
data-sending-message="Sending your message..."
data-sent-message="Message sent. Thank you, we'll be in touch soon."
data-error-prefix="Something went wrong. Please email"
data-error-email="pieter@intellify.co.za"
>
<div class="honeypot" aria-hidden="true">
<label for="location">Location</label>
<input id="location" name="location" type="text" tabindex="-1" autocomplete="off" hidden>
</div>
<div class="contact-form-grid">
<div class="field">
<label for="firstName">First Name *</label>
<input id="firstName" name="firstName" type="text" autocomplete="given-name" required>
<span class="field-error" id="error-firstName"></span>
</div>
<div class="field">
<label for="lastName">Last Name *</label>
<input id="lastName" name="lastName" type="text" autocomplete="family-name" required>
<span class="field-error" id="error-lastName"></span>
</div>
</div>
<div class="contact-form-grid">
<div class="field">
<label for="emailAddress">Email Address *</label>
<input id="emailAddress" name="emailAddress" type="email" autocomplete="email" required>
<span class="field-error" id="error-emailAddress"></span>
</div>
<div class="field">
<label for="phoneNumber">Phone Number</label>
<input id="phoneNumber" name="phoneNumber" type="tel" autocomplete="tel">
</div>
</div>
<div class="field">
<label for="content">Message *</label>
<textarea id="content" name="content" rows="6" placeholder="Tell us about your business, the kind of site you need, and which plan you are considering." required></textarea>
<span class="field-error" id="error-content"></span>
</div>
<button type="submit" id="submitBtn">
<span>Send Message</span>
<span aria-hidden="true">→</span>
</button>
<div class="form-status form-status-success" id="status-sent" aria-live="polite" hidden>Message sent. Thank you, we'll be in touch soon.</div>
<div class="form-status form-status-error" id="status-error" aria-live="polite" hidden>Something went wrong. Please email <a href="mailto:pieter@intellify.co.za">pieter@intellify.co.za</a>.</div>
<p class="contact-note">No upfront build cost. Domain, hosting, code management, and ongoing support are handled through your monthly subscription.</p>
</form>
</div>
</div>
</section>
<script>
(() => {
const API_URL = 'https://api.broadsheet.intellify.co.za';
const form = document.getElementById('contactForm');
if (!form) return;
const fields = {
firstName: document.getElementById('firstName'),
lastName: document.getElementById('lastName'),
emailAddress: document.getElementById('emailAddress'),
phoneNumber: document.getElementById('phoneNumber'),
location: document.getElementById('location'),
content: document.getElementById('content')
};
const errors = {
firstName: document.getElementById('error-firstName'),
lastName: document.getElementById('error-lastName'),
emailAddress: document.getElementById('error-emailAddress'),
content: document.getElementById('error-content')
};
const submitBtn = document.getElementById('submitBtn');
const sentStatus = document.getElementById('status-sent');
const errorStatus = document.getElementById('status-error');
const requiredMessage = 'This field is required.';
const hideStatuses = () => {
sentStatus.hidden = true;
errorStatus.hidden = true;
};
const validate = () => {
let isValid = true;
Object.entries(errors).forEach(([key, error]) => {
const field = fields[key];
const missing = !field.value.trim();
error.textContent = missing ? requiredMessage : '';
field.setAttribute('aria-invalid', missing ? 'true' : 'false');
if (missing) isValid = false;
});
return isValid;
};
form.addEventListener('submit', async (event) => {
event.preventDefault();
hideStatuses();
if (fields.location.value) return;
if (!validate()) return;
submitBtn.disabled = true;
const body = {
name: fields.firstName.value.trim() + ' ' + fields.lastName.value.trim(),
emailAddress: fields.emailAddress.value.trim(),
phoneNumber: fields.phoneNumber.value.trim(),
location: fields.location.value,
content: fields.content.value.trim()
};
try {
const response = await fetch(API_URL + '/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) throw new Error('Request failed');
form.reset();
Object.values(errors).forEach((error) => { error.textContent = ''; });
Object.values(fields).forEach((field) => field.removeAttribute('aria-invalid'));
sentStatus.hidden = false;
} catch {
errorStatus.hidden = false;
} finally {
submitBtn.disabled = false;
}
});
})();
</script>
If the new page needs a different layout or different fields, copy the same JavaScript pattern and keep these parts aligned:
- The form element must exist before the script runs.
- The form ID must match
document.getElementById('contactForm'), or the script must be updated to use the new ID. - Every field referenced in the
fieldsobject must exist in the HTML. - Every required field referenced in the
errorsobject needs a matching error element, such asid="error-firstName". - The submit button ID must match
submitBtn. - The success and error message containers must match
status-sentandstatus-error, or the script must be updated. - The API base URL should keep coming from
https://api.broadsheet.intellify.co.zarather than being hard-coded in multiple places.
Adding Page-Specific Context
If another page needs to tell Broadsheet where the enquiry came from, add that context to content or add another field only if the API supports it.
Safe option:
content: '(Affordability calculator enquiry)<br>' + fields.content.value.trim()
This keeps the API contract the same because the request still sends the known content field.
Use a new top-level field only after confirming Broadsheet accepts it:
sourcePage: 'affordability-calculator'
Minimal Reuse Example
<form id="contactForm">
<input id="firstName" name="firstName" type="text">
<span id="error-firstName"></span>
<input id="lastName" name="lastName" type="text">
<span id="error-lastName"></span>
<input id="emailAddress" name="emailAddress" type="email">
<span id="error-emailAddress"></span>
<input id="phoneNumber" name="phoneNumber" type="tel">
<input id="location" name="location" type="text" tabindex="-1" autocomplete="off" hidden>
<textarea id="content" name="content"></textarea>
<span id="error-content"></span>
<div id="status-sent" style="display:none;">Thank you, your enquiry has been received.</div>
<div id="status-error" style="display:none;">Sorry, something went wrong.</div>
<button type="submit" id="submitBtn">
<span>Submit Enquiry</span>
</button>
</form>
Use the submit handler from _includes/contact.html with this markup, or include the whole contact component directly.