As of
Est. 

You can Send me a Note

📧 I look forward to constructive feedback. Do not hesitate to send me a message.

You can easily send me feedback by clicking the mail 📧 icon in the navigation bar at the top right of the page.

Feedback Form with Rich Content Editor

The feedback form is a Vue component. The WYSIWYG editor I use is ProseMirror wrapped by Tiptap. This greatly simplifies the creation of the editor. Tiptaps StarterKit sets the typical extensions, such as markdown-like formatting with _, **, #, >, ` , ``` and others.

js
const StarterKit = (await import("@tiptap/starter-kit")).default;
    const Editor = (await import("@tiptap/vue-3")).Editor;
    editor.value = new Editor({
      content: "",
      extensions: [
        StarterKit.configure({
          heading: {
            levels: [3],
    }, }), ], });
const StarterKit = (await import("@tiptap/starter-kit")).default;
    const Editor = (await import("@tiptap/vue-3")).Editor;
    editor.value = new Editor({
      content: "",
      extensions: [
        StarterKit.configure({
          heading: {
            levels: [3],
    }, }), ], });

The content can be set and extracted as HTML. When the editor is created for the first time or after explicitly canceling the form, the content is empty. And this is how the text is initialized using VitePress's router composable:

js
if (editor.value.isEmpty) {
    editor.value.commands.insertContent(
      `<p>Hi Ergberg,</p><p></p><p>reading 
      <code>"${route.data.title}" (${route.path})</code>, 
      I want to tell you:</p><p></p><p></p>`
    );
  }
if (editor.value.isEmpty) {
    editor.value.commands.insertContent(
      `<p>Hi Ergberg,</p><p></p><p>reading 
      <code>"${route.data.title}" (${route.path})</code>, 
      I want to tell you:</p><p></p><p></p>`
    );
  }

Backend Service via Cloudflare Worker

I am emailing me the content of the feedback form. Since I already use SendGrid as a provider for sending outbound e-mails from GCP, I already have an account with them and decided to use their RESTful web API for sending e-mail as well.

At its core, the Cloudflare worker implements a function that answers an HTTP request with an HTTP response. My service only accepts POST requests. The values for the SendGrid URL and for my SendGrid API key are provided by the Workers environment. SEND_GRID_SEND_URL is a simple environment variable. SECRET_API_KEY is an encrypted secret. While the first variable is simply defined in the wrangler.toml of the feedback project, the second variable is set via the wrangler CLI.

js
export default {
 async fetch(request: Request, env: Env) {
  if (request.method !== "POST") {
    return new Response("POST only", { status: 403 });
  }
  ...

  function createSendMailRequest(
    from: string, to: string, message: string) {
    return fetch(
      new Request(env.SEND_GRID_SEND_URL, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${env.SECRET_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          personalizations: [
            { to: [{ email: to }], },
          ],
          from: { email: from },
          subject: "Blog Feedback",
          content: [
            {
              type: "text/html",
              value: message,
  },],}),}));}
  ...
}}
export default {
 async fetch(request: Request, env: Env) {
  if (request.method !== "POST") {
    return new Response("POST only", { status: 403 });
  }
  ...

  function createSendMailRequest(
    from: string, to: string, message: string) {
    return fetch(
      new Request(env.SEND_GRID_SEND_URL, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${env.SECRET_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          personalizations: [
            { to: [{ email: to }], },
          ],
          from: { email: from },
          subject: "Blog Feedback",
          content: [
            {
              type: "text/html",
              value: message,
  },],}),}));}
  ...
}}

This example does not include the details of handling CORS and throttling requests.