Astro Landing Page Form: Netlify Serverless Contact Form
Tags: astro, webdev, svelte, serverless
Published: 2022-04-05 09:27:41

---
title: "Astro Landing Page Form: Netlify Serverless Contact Form"
published: "true"
description: "Astro landing page contact form: how to use Netlify serverless functions with Telegram bots to add a contact form to your Astro landing page."
tags: "astro, webdev, svelte, serverless"
canonical_url: "https://rodneylab.com/astro-landing-page-form/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/afuodoecta3nvfa7zvgy.png"
---

## ☁️ Serverless Astro

Here we will see how you can set up an Astro landing page form.  Astro builds static sites, which are typically faster and more secure.  Using serverless functions you can add functionality traditionally handled by a backend yet keep the Astro speed.  When it comes to serverless there are a number of options, writing code in JavaScript, Rust or other languages and also in terms of the platform the code runs on.  Netlify offers hassle-free setup and we will stick with JavaScript in case this is your first time trying serverless.

As just hinted, serverless functions let you run traditional back-end operations without having to configure and maintain a back end server.  Typically they are cheap to run and can easily scale up to handle high demand.  The main trade-off is spin-up time, the time between the request being received and the cloud server being ready to start working on the request.  This is falling all the time and in fact is not critical for our use case.

## 🧱 What we&rsquo;re Building

![Astro Landing Page Form: Netlify Serverless Contact Form: Screenshot of app showing contact form on landing page.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uwt52u23epw8o171izhx.png)

We will use Astro and Svelte to create a single page site; a landing page for a book launch.  The focus is on how to integrate serverless functions with Astro so we will keep the site fairly basic.  This makes this an ideal tutorial to follow if you are just **getting started with Astro**.  We will add a **contact form** and see how you can link that up to **Netlify serverless functions**.  The serverless function will **use a Telegram bot** to send a message to a Telegram private chat. This can make the app more convenient to use, as site admins can have access to messages via the Telegram mobile app as well as while they are at their desk.

Netlify makes the serverless function available via an endpoint on the same domain the site is hosted on.  We will invoke the Netlify function by sending a REST `POST` request (containing form data in the body). If this all sounds interesting, then let&rsquo;s get going!

## ⚙️ Astro Setup

We are going to use **Svelte** to create the contact form and interface with the serverless function, so need to add the Svelte integration as we set up Astro.  Let&rsquo;s do that now from the Terminal:

```shell
mkdir astro-contact-form && cd $_
pnpm init astro
pnpm install
pnpm astro add svelte
pnpm run dev
```
Choose the **Minimal** app template and accept suggested config when prompted.  Once that&rsquo;s all done, open up your browser just to check we&rsquo;re good to go.  The CLI will tell you where the local dev server is running, this will be <a aria-label="Open Astro dev site" href="http://localhost:3000">http://localhost:3000</a> if there is not already something running on port `3000`.  Don&rsquo;t expect too much (yet)! If all is working well, you will just see the word &ldquo;Astro&rdquo; in the browser window.

Take a look at `astro.config.mjs` in the project root folder.  Astro has added the Svelte integration for you!


## 🏡 Astro Landing Page Home Page

We will start with the home page.  The markup will all be done in Astro and we will only use Svelte for the contact form.  Replace the content in `src/pages/index.astro`:

```html
---
---

<html lang="en-GB">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Astro Contact Form: using Netlify Serverless Functions</title>
  </head>
  <body>
    <header class="header-container">
      <h1>Astro Landing Page: using Serverless Functions</h1>
    </header>
    <main class="main-container">
      <section class="card">
        <h2>New book is launching soon:</h2>
        <ul>
          <li>Why you should be using NewTech</li>
          <li>How to leverage latest NewTech features,</li>
          <li>10 Step plan for your business</li>
        </ul>
      </section>
      <section class="card card-alt">
        <h2>Find out more</h2>
        <p>Contact form will go here </p>
      </section>
    </main>
  </body>
</html>
```

Add some optional styling to make it look a little nicer:

```html
<style>
  /* hind-regular - latin */
  @font-face {
    font-family: Hind;
    font-style: normal;
    font-weight: var(--font-weight-normal);
    src: local(''), url('/fonts/hind-v15-latin-regular.woff2') format('woff2'),
      url('/fonts/hind-v15-latin-regular.woff') format('woff');
  }

  /* hind-700 - latin */
  @font-face {
    font-family: Hind;
    font-style: normal;
    font-weight: var(--font-weight-bold);
    src: local(''), url('/fonts/hind-v15-latin-700.woff2') format('woff2'),
      url('/fonts/hind-v15-latin-700.woff') format('woff');
  }

  :global(html) {
    background-color: var(--colour-light);
    font-family: var(--font-body);
    accent-color: var(--colour-brand);
  }

  :global(body) {
    margin: 0;
    font-weight: var(--font-weight-normal);
  }

  :global(h1, h2) {
    font-family: var(--font-heading);
  }

  :global(h1) {
    font-size: var(--font-size-6);
    font-weight: var(--font-weight-bold);
  }

  :global(h2) {
    font-size: var(--font-size-5);
    font-weight: var(--font-weight-bold);
  }

  :global(form) {
    display: flex;
    flex-direction: column;
    width: min(32rem, 100%);
    margin-left: auto;
    margin-right: auto;
    color: var(--colour-dark);
  }

  :global(input, textarea) {
    width: 100%;
    text-indent: var(--spacing-2);
    padding: var(--spacing-1) var(--spacing-0);
    margin: var(--spacing-0) var(--spacing-0) var(--spacing-5);
    border: var(--spacing-px) solid var(--colour-theme);
    border-radius: var(--spacing-1);
    font-size: var(--font-size-3);
    background-color: var(--colour-light);
  }

  :global(textarea) {
    padding: var(--spacing-2) var(--spacing-0);
    resize: none;
  }

  :global(button) {
    background-color: var(--color-brand);
    background-image: var(--colour-brand-gradient);
    border: var(--spacing-px-2) solid var(--colour-light);
    border-radius: var(--spacing-2);
    padding: var(--spacing-2) var(--spacing-6);
    font-size: var(--font-size-3);
    font-weight: var(--font-weight-bold);
    color: var(--colour-light);
    cursor: pointer;
  }

  :global(.screen-reader-text) {
    border: 0;
    clip: rect(1px, 1px, 1px, 1px);
    clip-path: inset(50%);
    height: 1px;
    margin: -1px;
    width: 1px;
    overflow: hidden;
    position: absolute !important;
    word-wrap: normal !important;
  }

  :global(.error-text) {
    color: var(--colour-brand);
  }

  :global(:root) {
    --colour-theme: hsl(334 43% 17%); /* dark purple */
    --colour-brand: hsl(332 97% 43%); /* dogwood rose */
    --colour-alt: hsl(201 11% 41%); /* cadet */
    --colour-light: hsl(0 0% 99%); /* cultured */
    --colour-dark: hsl(245 100% 15%); /* midnight blue */

    --colour-brand-gradient: linear-gradient(
      45deg,
      hsl(332deg 97% 36%) 0%,
      hsl(332deg 97% 37%) 21%,
      hsl(332deg 97% 38%) 30%,
      hsl(332deg 97% 38%) 39%,
      hsl(332deg 97% 39%) 46%,
      hsl(332deg 97% 40%) 54%,
      hsl(332deg 97% 41%) 61%,
      hsl(332deg 97% 42%) 69%,
      hsl(332deg 97% 42%) 79%,
      hsl(332deg 97% 43%) 100%
    );
    --colour-alt-gradient: linear-gradient(
      45deg,
      hsl(204deg 11% 44%) 0%,
      hsl(204deg 11% 46%) 21%,
      hsl(204deg 10% 48%) 30%,
      hsl(204deg 10% 49%) 39%,
      hsl(204deg 10% 51%) 46%,
      hsl(204deg 10% 53%) 54%,
      hsl(204deg 10% 55%) 61%,
      hsl(204deg 10% 56%) 69%,
      hsl(204deg 10% 58%) 79%,
      hsl(205deg 11% 60%) 100%
    );

    --spacing-0: 0;
    --spacing-px: 1px;
    --spacing-px-2: 2px;
    --spacing-1: 0.25rem;
    --spacing-2: 0.5rem;
    --spacing-4: 1rem;
    --spacing-5: 1.25rem;
    --spacing-6: 1.5rem;
    --spacing-12: 3rem;
    --spacing-18: 4.5rem;
    --max-width-wrapper: 48rem;

    --font-size-root: 16px;
    --font-size-3: 1.563rem;
    --font-size-5: 2.441rem;
    --font-size-6: 3.052rem;

    --font-weight-normal: 400;
    --font-weight-bold: 700;

    --font-heading: 'Hind';
    --font-body: 'Hind';

    /* CREDIT: https://www.joshwcomeau.com/shadow-palette/ */
    --shadow-color: 0deg 0% 58%;
    --shadow-elevation-medium: -1px 1px 1.4px hsl(var(--shadow-color) / 0.51),
      -2.7px 2.7px 3.7px -1.2px hsl(var(--shadow-color) / 0.43),
      -7.6px 7.6px 10.5px -2.3px hsl(var(--shadow-color) / 0.36),
      -20px 20px 27.6px -3.5px hsl(var(--shadow-color) / 0.29);
  }

  :global(input:focus, textarea:focus) {
    border-color: var(--colour-brand);
  }

  .header-container {
    display: grid;
    background-color: var(--colour-alt);
    background-image: var(--colour-alt-gradient);
    box-shadow: var(--shadow-elevation-medium);
    color: var(--colour-light);
    height: var(--spacing-24);
    place-content: center;
  }

  .main-container {
    display: flex;
    flex-direction: column;
    color: var(--colour-light);
    width: min(100% - var(--spacing-12), var(--max-width-wrapper));
    margin: var(--spacing-18) auto;
    padding: var(--spacing-12);
    font-size: var(--font-size-3);
  }

  .main-content {
    display: flex;
    flex-direction: column;
    margin: var(--spacing-6);
    padding: var(--spacing-2) var(--spacing-12) var(--spacing-6);
    background-color: var(--colour-light);
    border-radius: var(--spacing-1);
    color: var(--colour-dark);
  }
  .card {
    border: var(--spacing-px-2) solid var(--colour-theme);
    box-shadow: var(--shadow-elevation-medium);
    background-color: var(--colour-theme);
    background-image: var(--colour-brand-gradient);
    padding: var(--spacing-0) var(--spacing-6) var(--spacing-6);
    margin: var(--spacing-0) var(--spacing-0) var(--spacing-12);
    border-radius: var(--spacing-2);
  }

  .card-alt {
    border-color: var(--colour-dark);
    background-color: var(--colour-alt);
    background-image: var(--colour-alt-gradient);
    color: var(--colour-light);
  }
</style>
```

We are using <a aria-label="Learn how to set up self hosted fonts on Astro" href="https://rodneylab.com/astro-self-hosted-fonts">self-hosted fonts</a> here.  You can <a aria-label="Download the self-hosted fonts" href="https://google-webfonts-helper.herokuapp.com/api/fonts/hind?download=zip&subsets=latin&variants=700,regular&formats=woff,woff2">download the fonts from google-webfonts-helper</a>. Unzip and move the four files to a new `public/fonts` folder.

### Astro Aliases

If that&rsquo;s all working, we will move on to the contact form component.  Astro lets you define aliases for folders within your project.  This can be more convenient than writing something like `import ContactFrom from '../../components/ContactForm.svelte'`.

First create a `src/components` folder then edit `tsconfig.json` in the project root folder, so it looks like this:

```json
{
  "compilerOptions": {
    "moduleResolution": "node",
    "baseUrl": ".",
    "paths": {
      "$components/*": ["src/components/*"]
    }
  },
  "include": ["src/**/*.ts"]
}
```

Now add he component to the Home Page (it won&rsquo;t work until we actually create and define the Svelte component).

```html
---
import ContactForm from '$components/ContactForm.svelte';
---
```

```html
      <section class="card card-alt">
        <h2>Find out more</h2>
        <ContactForm client:load />
      </section>---
```

The `client:load` directive in line 26 is an Astro hydration parameter.  We use `load` here to tell Astro always to hydrate the `ContactForm` component, making it interactive by letting its JavaScript load.  This makes sense here as the form will probably be in view when the page loads, not to mention that it is a main feature of the page.  If we had a contact form which was JavaScript heavy and far down the page (so initially out of view) we could instruct Astro to hydrate it only when visible, using `client:visible`.  This would improve user experience, optimising loading the visible content faster.


## Contact Form

Typically you will add client-side and serverless-side form input validation.  To keep the code lean and stop the post getting too long, we will not add this here though.  Let me know if you would like to see some possible ways to do this; I could write a separate post.

Create `src/components/ContactForm.svelte` and add the content below.  You see we can add additional scoped styling within Astro Svelte components.  Remove this `style` block if you are skipping styling.

```html
<script>
  let botField = '';
  let name = '';
  let email = '';
  let message = '';

  let serverState;
  $: submitting = false;

  function handleServerResponse(ok, msg) {
    serverState = { ok, msg };
  }

  const handleSubmit = async () => {
    console.log({ email, name, message });
    try {
      submitting = true;
      await fetch(`/.netlify/functions/contact-form-message`, {
        method: 'POST',
        credentials: 'omit',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          botField,
          email,
          name,
          message,
        }),
      });
      submitting = false;
      handleServerResponse(true, '');
    } catch (error) {
      console.error(`Eror: ${error}`);
      submitting = false;
      handleServerResponse(
        false,
        'Unable to send your message at the moment.  Please try again later.',
      );
    }
  };
</script>

<form on:submit|preventDefault={handleSubmit} class="form-container">
  <input aria-hidden="true" type="hidden" name="bot-field" bind:value={botField} />
  <div>
    <span class="screen-reader-text"><label for="name">Name</label></span>
    <input bind:value={name} required id="name" placeholder="Name" title="Name" type="text" />
  </div>
  <div>
    <span class="screen-reader-text"><label for="email">Email</label></span>
    <input
      bind:value={email}
      required
      id="email"
      placeholder="blake@example.com"
      title="Email"
      type="email"
    />
  </div>
  <div>
    <span class="screen-reader-text"><label for="message">Message</label></span>
    <textarea
      bind:value={message}
      required
      id="message"
      rows={6}
      placeholder="Write your message here..."
      title="Message"
      type="text"
    />
  </div>
  <div class="button-container">
    <button type="submit" disabled={submitting}> Send</button>
  </div>
  {#if serverState}<p class={!serverState.ok ? 'errorMsg' : ''}>
      {serverState.msg}
    </p>{/if}
</form>

<style>
  .button-container {
    display: flex;
    width: 100%;
    margin: var(--spacing-2);
    justify-content: flex-end;
  }
</style>
```

We add very basic bot detection in the form of a honeypot field (line `45`).  This is not displayed in, or announced by, the browser but a bot would find it and might be tempted to fill it out.  So any time we see a response with this field filled out, we can assume a bot filled it out.  For a production app you might consider using a spam detection service like <a aria-label="Read how I used Akismet in another project" href="https://rodneylab.com/cloudflare-developer-challenge/">Akismet</a> or Cloudflare bot detection HTTP headers.  <a aria-label="Learn how to add an h Captcha to a Svelte Kit contact form" href="https://rodneylab.com/sveltekit-hcaptcha-contact-form">Captchas</a> might also be suitable in some cases.

The form fields use Svelte input bindings, this is a little different to (and simpler than) what you might be used to if you come from a React background.  Let me know if a separate post or video on Svelte form and input bindings would be useful.

Although we later add the Axios package for use in the serverless function, the fetch API helps us out here with actually reaching the serverless function.  We send a JSON `POST` request to `/.netlify/functions/contact-form-message`.  For this to work, we need to create the serverless function with a file name `contact-form-message.js`.  We will do that&nbsp;next!

## 🌥 Serverless Function

First we will add some Netlify configuration to the project.  Create `netlify.toml` in the project&rsquo;s root folder and add this content:

```toml
[build]
  command = "npm run build"
  functions = "netlify/functions"
  publish = "dist"
```

Notice the publish directory is `dist` which is where Astro outputs your build site to.  Next we can create the functions folder: `netlify/functions` then add `contact-form-message.js`.  If you prefer TypeScript, change the extension and also add the `@netlify/functions` package.  You can import types (only if you are working in TypeScript) adding `import type { Handler } from '@netlify/functions';` to the top of this file.


```javascript
import axios from 'axios';

const { TELEGRAM_BOT_API_TOKEN, TELEGRAM_BOT_CHAT_ID } = process.env;

async function notifyViaTelegramBot({ honeyBotFlaggedSpam, name, email, message }) {
  try {
    const data = JSON.stringify(
      {
        honeyBotFlaggedSpam,
        name,
        email,
        message,
      },
      null,
      2,
    );
    const text = `Contact form message: ${data}`;
    await axios({
      url: `https://api.telegram.org/bot${TELEGRAM_BOT_API_TOKEN}/sendMessage`,
      method: 'POST',
      data: {
        chat_id: TELEGRAM_BOT_CHAT_ID,
        text,
      },
    });
    return { successful: true };
  } catch (error) {
    let message;
    if (error.response) {
      message = `Telegram server responded with non 2xx code: ${error.response.data}`;
    } else if (error.request) {
      message = `No Telegram response received: ${error.request}`;
    } else {
      message = `Error setting up telegram response: ${error.message}`;
    }
    return { successful: false, error: message };
  }
}

export async function handler({ body, httpMethod }) {
  try {
    if (httpMethod !== 'POST') {
      return {
        statusCode: 405,
        body: 'Method Not Allowed',
      };
    }

    const data = JSON.parse(body);
    const { botField, email, name, message } = data;

    const { error: telegramError } = await notifyViaTelegramBot({
      honeyBotFlaggedSpam: botField !== '',
      email,
      name,
      message,
    });

    if (telegramError) {
      return {
        statusCode: 400,
        body: telegramError,
      };
    }

    return { statusCode: 200, body: 'Over and out.' };
  } catch (error) {
    return {
      statusCode: 400,
      body: `Handler error: ${error}`,
    };
  }
}

export default handler;
```

As mentioned earlier, we use Axios here to contact Telegram servers, relaying the contact message to our private chat.  We can add it as a project dependency now:

```shell
pnpm add axios
```

Let&rsquo;s set up a Telegram bot so we can get the environment variables needed to wire this&nbsp;up.

## 🤖 Telegram Bot

The process for getting Telegram API credentials is quite simple, just follow step by step and you will have API keys in a couple of minutes.

1. Bots are created by Telegram's Bot Father &mdash; isn't that cute! Open up a new chat with <a aria-label="Start a new conversation with Telegrams' Bot Father" href="https://t.me/BotFather">@BotFather</a>.
2. You interact with bots in Telegram by typing commands which begin with a `/` in the chat window. Create a new bot using the command `/newbot`. Bot Father will ask you for a name and then a username. The name can be anything but the username needs to end `bot` and should only contain alphanumeric characters and underscores. I will use &ldquo;Astro Landing Page Form Site&rdquo; as the name and `astro_landing_page_form_bot` as the username. Bot Father will respond with the **new API key for your bot** make a note of this.
3. Next we need to create a new group chat and add the bot to it (you can also add anyone whom you want to receive bot messages). From the Telegram menu select **New Group**. Enter a name for the group when prompted then in the **Add Members** window type the username of your bot.

### Retreive Chat ID

4. We&rsquo;re almost done. Next we need to get the ID for this new group chat so we can send messages to it from the Netlify Serverless Function. From the group chat, send a message to the bot by typing the following command as a message &ldquo;`/my_id @my_bot`&rdquo; replace `my_bot` with the name of your bot.
5. In the terminal use curl to see the bot&rsquo;s updates. Remember to replace `123456789:AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQq` with the API key you got earlier:

```shell
curl -L https://api.telegram.org/bot123456789:AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQq/getUpdates
```

If you don&rsquo;t have curl on your machine, just paste the link into your browser instead. If you are working on a shared machine, be sure to clear the link from the browser history as it contains an API key.

You will get a response back something like this:

```json
{
  "ok": true,
  "result": [
    {
      "update_id": 741497477,
      "message": {
        "message_id": 2,
        "from": {
          "id": 1234567890,
          "is_bot": false,
          "first_name": "Rodney",
          "last_name": "Lab",
          "username": "askRodney"
        },
        "chat": {
          "id": -123456789,
          "title": "Astro Landing Page Form Site",
          "type": "group",
          "all_members_are_administrators": true
        },
        "date": 1623667295,
        "text": "/my_id @astro_landing_page_form_bot",
        "entities": [
          { "offset": 0, "length": 6, "type": "bot_command" },
          { "offset": 7, "length": 29, "type": "mention" }
        ]
      }
    }
  ]
}
```

Ok this is just some JSON. It contains two ids, although we just need one. The first is the message ID. We don&rsquo;t need this one. The second, within the chat object, starts with a &ldquo;`-`&rdquo;, this is the chat ID we need, **including** the &ldquo;`-`&rdquo;.

We have all the API data we need to proceed. Let's carry on by setting up or function.

### Netlify Environment Variables

Netlify has the easiest way for handling environment.  You can add them manually using the web console, though I prefer the CLI.  If you want to try my way, add the Telegram credentials to a new `.env` file:

```plaintext
TELEGRAM_BOT_API_TOKEN="123456789:AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQq"
TELEGRAM_BOT_CHAT_ID="-123456789"
```

Notice you should not prefix these with `PUBLIC_` as you would normally do for Astro environment variables.  This is because we do not need to expose the credentials on the front end, just for the serverless function.  In fact another advantage of using serverless functions is the added security of not needing to expose credentials to the client.  Next you will need to install the Netlify CLI globally on your machine, if you do not already have it installed:

```shell
pnpm add -g netlify-cli
```

Next, commit your project to GitHub, GitLab or whichever service you use and set it up as you normally do. Be sure to add `.env` to the `.gitignore` file so the credentials do not end up in your remote repo. If this is your first time using Netlify, follow the <a aria-label="Open Netlify deploy instructions" href="https://www.netlify.com/blog/2016/10/27/a-step-by-step-guide-deploying-a-static-site-or-single-page-app/">step-by-step deploy instructions</a>.  Once the project is set up, just run the following commands from the project folder:

```shell
netlify init
netlify env:import .env
```

Just follow prompts to the right project and this will take the variables from the `.env` file and add them to your project on Netlify's servers.  There is not a massive convenience pickup as we only have two variable here, but for larger projects it is definitely worth the effort. Learn more about <a aria-label="Learn about managing environment variables fromthe terminal for Netlify" href="https://www.netlify.com/blog/2021/07/12/managing-environment-variables-from-your-terminal-with-netlify-cli/">managing Netlify environment variables from Netlify</a>.

You might need to rebuild the site once you have added the environment variables.

## 💯 Astro Landing Page Contact Form: Testing

Everything should be working now, so let&rsquo;s test it.  The Netlify console will give you a link to the freshly built site.  Follow the link and complete the contact form.  Open up your Telegram app and you should have a message in the group chat you created.

![Astro Landing Page Form: Netlify Serverless Contact Form: Screenshot of Netlify console showing a link for the built site.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oiaxwhvnh7ni1hafzv1u.png)

## 🙌🏽 Astro Landing Page Contact Form: Wrapping Up

In this post we have had an introduction to Astro and seen:

- how to set up an Astro Svelte project,
- how to use Netlify serverless functions to provide &ldquo;back-end&rdquo; functionality to your Astro app,
- a convenient and efficient way to manage environment variables in your Netlify project.

The full code for the app is available in the <a aria-label="Open the Astro scroll to anchor code in the Rodney Lab Git Hub Repo" href="https://github.com/rodneylab/astro/tree/main/demos/astro-contact-form">Astro demo repo on Rodney Lab GitHub</a>.

I hope you found this article useful and am keen to hear how you plan to use the Astro code in your own projects.


## 🙏🏽 Astro Landing Page Contact Form: Feedback

Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.


Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a aria-label="Reach out on Twitter" href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on Twitter and also <a aria-label="Contact Rodney Lab via Telegram" href="https://t.me/askRodney">askRodney on Telegram</a>. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact">further ways to get in touch with Rodney Lab</a>. I post regularly on <a aria-label="See posts on Astro" href="https://rodneylab.com/tags/astro/">Astro</a> as well as <a aria-label="See posts on svelte kit" href="https://rodneylab.com/tags/sveltekit/">SvelteKit</a>. Also <a aria-label="Subscribe to the Rodney Lab newsletter" href="https://rodneylab.com/about/#newsletter">subscribe to the newsletter to keep up-to-date</a> with our latest projects.