There’s a lot of pieces to this thing when you consider just the basic parts of how the family site operates. Truthfully, this post is more for me than anything else, since once I get something to where it “just works” I tend to forget about it and then forget how I set it up initially.
Eventually, in the event that this ecosystem of projects ever were to take off and get utilized more, the software and coding is in place for a fairly dynamic suite of engagement and archival tools. It took me a bit over the week to realize exactly how much is going on…but there is a lot.

Meet…Me.

Well…the bot version of me anyway. Most anything automated is going to be posted under the bot account on the site or on Schleicher Social. There’s no way I’m going to manually send out post reminders, email reminders for birthdays or anniversaries, or anything else that is automated. We’re up to over 100 family members, each with a birthday and most with an anniversary…yeah. No. I’m not keeping up with that. So…anything under the Schleicher Bot is an automated post or automated send.
Core Sites
The Schleicher Homepage

Every project has a starting point. In this case…the Schleicher Homepage. Fun fact: from the homepage you can get to the spreadsheet, Schleicher Social, and manage email sends. I’ve built in over the last couple of years several tools:
Plus most of the mundane things have been automated, like email updates. Speaking of which…
What can be done on the homepage?
So, there are a few things that can be done from the homepage without having to wait for emails to go back and forth. Those are:
- Mailing list updates
- Family spreadsheet additions and changes
Note that from your profile tab you also can follow other page members; when followed you’ll receive notifications about new posts from those members.
The Help Page
The help page is worth a mention. Most issues can be solved (with regards to site and social functionality) on the help page. The help page includes directions for signing up for Schleicher Social, blog help, and the Minecraft server info.
Schleicher Social
Schleicher Social is our privately hosted Mastodon instance; think of it like a X-esque style of social media with short (500 characters or less) updates. Like social media, you have to follow, and be followed, to have a populated timeline. Unlike most social media, though, you can also see the public timeline with all posts.

That being said, you can see all public posts on the homepage too. I’ve got an RSS feed that populates with all public posts, so even if you don’t have an account, you can see what is happening on Schleicher Social.
Everything Else
Outside of the 2 core components (Schleicher Social and the Schleicher Homepage), the address book, while officially hosted on the homepage, is a separate SQL database. It is displayed, and directly accessed from the site, but is hosted on a separate database. Just more info that anything else for those interested.
Now the fun stuff…how it ties together.
So, how about all the other stuff? I mean, just the homepage and Schleicher Social alone have a bit going on. But…there’s a ton going on behind the scenes too. Most of it runs through Zapier, a third-party site that connects and automates different software platforms. Other things run locally on the server.
Zapier
Zapier is fun. Zapier lets me tie in all sorts of different platforms and automate processes between them as long as I can access the API or can create an access token. Its main use for the homepage, however, is to maintain the email list for site notifications. Now, I could do this in Brevo (my SMTP relay), but I’m limited in my email format and I can’t post to Schleicher Social from Brevo either. So…enter Zapier.

I’ve hidden the fields here that are sensitive, but this table drives any and all notifications through email (and eventually, maybe SMS). How’s it work?
The update flow
Braincaps on. When you submit a change in your preferences, this flow triggers.

Different paths are triggered depending on which radio button you select in the update form, but in a nutshell we “catch” a hook from the webform on the homepage and this flow decides what to do with it. So, it takes the information put into a Forminator form and parses it in a way that then updates the table.
const selected = inputData.checkboxes.split(',').map(s => s.trim().toLowerCase());
return {
birthday_reminders: selected.includes("birthday-reminders"),
new_site_posts: selected.includes("new-site-posts"),
sms_birthday: selected.includes("birthday-and-anniversary-reminder-sms-texts"),
sms_posts: selected.includes("new-site-post-sms-texts")
};
So this is the code block. All this does is take any checkboxes in the form and translate them into an array so that the next step (Update Record) can then update all options at once. Otherwise, we need a new path for all possible options. Not fun.
So, for the most part, when it catches the hook, the flow looks to see what we’re doing. If it’s a simple delete, it takes that info, finds a matching record (if any), and removes it. Otherwise, it takes the data, attempts to match it to an existing record and complete the request. If no record exists, it creates one.
The Calendar
Okay, there’s actually two calendars. One that feeds the site calendar and one that feeds the notifications. Both are in Google and essentially the notification calendar is a clone of the site calendar, just with the birthdays and anniversaries at 8:00 in the morning. So, how’s it work? The site calendar is simple, it’s just linked directly to the Google calendar and color aligned with the rest of the site. Notifications, on the other hand…coding caps on. Also, full disclosure, I can’t claim all the credit. ChatGPT helped substantially with the heavy lifting on the code and I just tweaked as needed.

This is the birthday notification flow in Zapier. For the most part it’s pretty straightforward. We’re taking an event in Google Calendar and sending an email reminder and posting to Schleicher Social.
Now, the trigger here is specific: we’re searching for any event with the word “Birthday” in the description and firing it off at 8:00 am to both Brevo (to send the email) and Schleicher Social.
The Email Send
Let’s start with the easy part: sending an email. Not going to get into the nitty gritty details of the email server setup, but I’ve got my SMTP relay (Brevo) setup with my registrar (Cloudflare) so that all recipients see in the from field is “[email protected]” (or, in this case and all bot cases, [email protected]). This path is simple, though, since I’ve hooked into Brevo.
Remember the contact list table? The first step in this path is to find records that have the “Birthday/Anniversary” checkbox enabled. Once found, the next step formats the email and fires it off to those records.
More coding: HTML. We need the email to look consistent across devices. Enter HTML markdown. Explicitly stating where and how information should look and appear. So, the following HTML would get translated to:
<p>Happy Birthday <strong>{{193403396__summary}}</strong>! Have a GREAT day!</p>
<p><a href="https://schleicher.social">Wish {{193403396__summary}} a Happy Birthday</a> on Scheicher Social! Have an awesome day!</p>
<br>
<hr>
<h3>Site Links</h3>
<p><a href="https://family.schleicher.social/calendar/schleicher-birthdays-and-events/" target="_blank">Schleicher Birthday and Event Calendar</a></p>
<p><a href="https://family.schleicher.social/happenings/" target="_blank">Happenings</a> (Boar Report archives and blog posts)</p>
<p><a href="https://family.schleicher.social/photo-gallery/" target="_blank">Photo Gallery</a></p>
<p><a href="https://family.schleicher.social/recipes/" target="_blank">The Schleicher Cookbook</a> (All the old recipes plus a few new ones)</p>
<p><a href="https://family.schleicher.social/#address_book" target="_blank">Schleicher Spreadsheet</a> (Need to add or update? <a href="https://family.schleicher.social/add-update-spreadsheet/" target="_blank">Update Spreadsheet</a>)</p>
<br>
<hr>
<h3>Help Links</h3>
<p><a href="https://family.schleicher.social/help/">Schleicher Social FAQ</a></p>
<p><a href="https://family.schleicher.social/help/#joining">Joining Schleicher Social</a> (Joining and using Mastodon)</p>
<p><a href="https://family.schleicher.social/preference-manager/">Manage my email notifications</a></p>
<p><a href="https://family.schleicher.social/category/help/">Site Help Articles</a></p>
<br>
<hr>
<br>
<h2>We are the Schleichers</h2>
<br>
<a href="https://family.schleicher.social"><img src="https://family.schleicher.social/wp-content/uploads/2023/04/schleicher50x50.png"></a>
<br>
<footer>
<p>This was an automated e-mail sent by the Schleicher Bot</p>
<p>Don't want birthday or anniversary reminders? <a href="https://family.schleicher.social/preference-manager/" target="_blank">Unsubscribe</a></p>
</footer>
Happy Birthday {{193403396__summary}}! Have a GREAT day!
Wish {{193403396__summary}} a Happy Birthday on Scheicher Social! Have an awesome day!
Site Links
Schleicher Birthday and Event Calendar
Happenings (Boar Report archives and blog posts)
The Schleicher Cookbook (All the old recipes plus a few new ones)
Schleicher Spreadsheet (Need to add or update? Update Spreadsheet)
Help Links
Joining Schleicher Social (Joining and using Mastodon)
We are the Schleichers

Posting to Schleicher Social
And then we have the automated posting. Also thanks to Zapier. This automation runs parallel to the email sends, but instead shoots in a post to Schleicher Social. Its a little more complex because we actually have to pull and upload an image for post media each time, so we have a 2-step Java script path.
Step 1: Media Upload
const imageResponse = await fetch(inputData.imageUrl);
if (!imageResponse.ok) {
throw new Error(`Failed to fetch image: ${imageResponse.statusText}`);
}
// This is the correct Node-style way to get binary data
const buffer = await imageResponse.buffer();
const filename = "birthday.jpg";
const mimeType = imageResponse.headers.get("content-type") || "image/jpeg";
// Build multipart form-data
const boundary = "ZapierUpload" + Date.now();
const preamble = [
`--${boundary}`,
`Content-Disposition: form-data; name="file"; filename="${filename}"`,
`Content-Type: ${mimeType}`,
``,
].join("\r\n");
const closing = `\r\n--${boundary}--\r\n`;
const bodyBuffer = Buffer.concat([
Buffer.from(preamble + "\r\n"),
buffer,
Buffer.from(closing)
]);
// Upload to Mastodon
const uploadResponse = await fetch("https://schleicher.social/api/v2/media", {
method: "POST",
headers: {
"Authorization": `Bearer ${inputData.token}`,
"Content-Type": `multipart/form-data; boundary=${boundary}`
},
body: bodyBuffer
});
const uploadData = await uploadResponse.json();
if (!uploadResponse.ok) {
throw new Error(`Upload failed: ${JSON.stringify(uploadData)}`);
}
return { media_id: uploadData.id };
Step 2: Generate and post
const statusText = `🎉 Good morning! We've got a birthday today! Have a GREAT day ${inputData.summary}!`;
const response = await fetch("https://schleicher.social/api/v1/statuses", {
method: "POST",
headers: {
"Authorization": `Bearer ${inputData.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
status: statusText,
media_ids: [inputData.media_id],
visibility: "public"
})
});
const data = await response.json();
return { result: data };
For these steps we upload a media item and then reference that uploaded media item in the post body in step 2. Presto! A nice, clean post linked from the calendar!


Tying It All Together
So, boil that down. At the end of the day, for the cost of the domain name and the Zapier subscription we’ve got a full suite of components that most well-run small businesses can’t claim to have. All in the name of keeping family tied together.