Search code examples
htmlhtmxastrojs

Better way to sending HTML from Endpoint in Astro


I read in the documentation about how to use Endpoints to send json, to send images, for redirects, for form validation, but I want to send html.

I want to use Astro together with htmx and am trying to use server to send small snippets of html. Here's what I got:

Tha backend part:

// clicked.ts file

import type { APIContext } from 'astro';

export async function post({ cookies, params, request }: APIContext) {
  return new Response(
    `<button hx-post="/clicked" hx-swap="outerHTML">
      Click me again
     </button>`,
    { status: 200, headers: { 'Content-Type': 'text/html' } }
  );
}

and the frontend part:

  <button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
  </button>

But I don't like the ugly look of sending html as a string, would like to reuse Astro templates.


Solution

  • Using Astro's server-side rendering capabilities, we can implement this desired functionality without the need to create an additional endpoint. We can use the front-matter of an .astro page to accomplish this.

    First, make sure your astro.config.mjs includes output: "hybrid", like so:

    // astro.config.mjs
    import { defineConfig } from 'astro/config';
    
    export default defineConfig({
      output: "hybrid",
    });
    

    Then, create the src/pages/clicked.astro file:

    ---
    // src/pages/clicked.astro
    
    export const partial = true
    export const prerender = false
    ---
    <button hx-post="/clicked" hx-swap="outerHTML">
        Click me again
    </button>
    

    Your initial "Click Me" button should now work!

    Moreover, here's a better example of a comprehensive implementation for a server-side rendered partial page, directly from within an .astro file - ready to use with HTMX.

    --
    // src/pages/chat/gpt_response.astro
    
    import ChatMessage from "../../components/chat/ChatMessage.astro"
    import OpenAI from "openai"
    
    export const partial = true
    export const prerender = false
    
    const openai = new OpenAI({ apiKey: import.meta.env.OPENAI_API_KEY })
    
    // Get user input from query string (hx-vars)
    let userInput = Astro.url.searchParams.get('user_input') || 'Hello, AI!';
    
    // Prepare the chat messages
    let messages = [
        { role: "system", content: "Your system message here." },
        { role: "assistant", content: "Hey there! I'm ready to chat." },
        { role: "user", content: userInput }
    ];
    
    // Create chat completion with OpenAI
    const gptChatResponse = await openai.chat.completions.create({
        messages: messages,
        model: "gpt-3.5-turbo",
        max_tokens: 125,
    })
    
    // Extract response
    let gptResponse = gptChatResponse.choices[0].message.content
    
    ---
    <ChatMessage sender='gpt' text={gptResponse}/>
    

    It creates a chat completion using OpenAI's API, and then includes the generated response directly inside an Astro component's props.

    Note: The code in the front-matter runs within the server each time you make a request, prior to rendering the component. The response itself only includes the HTML of the rendered component.

    This page can now be rendered using HTMX, like so:

    <button hx-get="/chat/gpt_response" 
            hx-vars="{'user_input':'HTMX with Astro is pretty neat.'}" 
            hx-target="#chat-container" 
            hx-swap="beforeend">
        Click Me
    </button>
    

    By the way, they recently added official support for "partial" pages. However, I don't know what difference it makes to set the export const partial = true inside your .astro file yet.