How to Integrate Astro with ApostropheCMS pt. 2
Tags: javascript, opensource, tutorial, devops
Published: 2024-03-21 16:45:38

This is the second and conclusive part of the ApostropheCMS / Astro integration tutorial. In Part 1, you learned how to set up an Astro frontend that communicates with an ApostropheCMS backend through the ApostropheCMS Astro Integration Starter Kit.

In this tutorial, you will complete the Astro blog application by integrating React into it and using React components to improve its user interface and interactivity.

Let’s look at the remaining aspects of the ApostropheCMS integration into Astro!

## What We Achieved So Far

In Part 1, you used the ApostropheCMS Astro Integration Starter Kit to integrate an ApostropheCMS backend with an Astro frontend application. In this setup, ApostropheCMS handles content management, URL routing, and content retrieval, while Astro renders the HTML documents and delivers them to the user's browser. 

The Astro-ready ApostropheCMS application we started from uses the `@apostrophecms/blog` module. This includes a blog index page with a list of all posts on the site and a page for each individual blog post.

This is what the blog index page currently looks like in the Astro frontend:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708605480150_image.png)

And this is the dedicated blog post page:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708592382696_image.png)

As you can see, there is still a lot to do when it comes to UI and UX to transform this web application into a more usable and interesting site. To accomplish that, you will see how to take advantage of the features offered by Astro, such as the support of multiple UI frameworks.

By adding custom styling and using interactive React components, you will learn how to take the ApostropheCMS-powered Astro blog application to the next level!

## Add React to Astro

Astro comes with a CLI command to automate the setup of React. In the Astro project folder, launch the command below to install `@astrojs/react` and configure it to use React components in Astro:

```
npx astro add react
```

This will launch a CLI wizard where you will be asked a few questions. Answer “yes” to each of them to:

1. Install the libraries required for integrating React.
2. Add the `react()` integration to the `astro.config.mjs` configuration file.
3. Update the `tsconfig.json` file for JSX support.

This process will take a while, so be patient. If something goes wrong, [refer to the official documentation](https://docs.astro.build/en/guides/integrations-guide/react/) for more information.

Excellent! You can now use React components in Astro. 

Prepare your `astro-frontend` project to host React components by adding a `components` folder to `./src`.

## Customize the UI of Your ApostropheCMS-Powered Astro Blog

Follow the steps below and learn how to improve the UI and interactivity of the ApostropheCMS Astro blog application!

If you are eager to take a look at the code of the final Astro codebase or want to use it as a reference while you follow the tutorial, clone the [GitHub repository supporting the article](https://github.com/Tonel/astro-frontend):

```
git clone https://github.com/Tonel/astro-frontend.git
```

Note that we will use the `apostrophecms/astro-frontend` project as a starting point.  

**General UI Improvements**
If you inspect the Astro template components under the `./src/templates` folder, you will see that they all share this structure:

```
---
// JS imports
---

<section class='bp-content'>
  <!-- HTML structure -->
</section>
```

This means you can target the `bp-content` class to add a responsive layout to your site. To define a global CSS rule that applies to those Astro components, you need to define a [local stylesheet file](https://docs.astro.build/en/guides/styling/#import-a-local-stylesheet).

In this tutorial, we are going to use [SCSS](https://en.wikipedia.org/wiki/Sass_(style_sheet_language)). Thus, add the `sass` npm package to your project’s dependency:

```
npm install sass 
```

Next, create a `styles` folder inside `./src` and add the following `blog.scss` file to it:

```
// .src/styles/blog.scss

.blog {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";

  .bp-content {
    padding-right: 15px;
    padding-left: 15px;
    margin-right: auto;
    margin-left: auto;

    @media (min-width: 576px) {
      width: 540px;
    }

    @media (min-width: 768px) {
      width: 720px;
    }

    @media (min-width: 992px) {
      width: 960px;
    }

    @media (min-width: 1200px) {
      width: 1140px;
    }
  }

  .h1 {
    text-align: center;
    margin: 0 0 20px;
    font-size: 4em;
    font-weight: 200;
  }

  a {
    color: #6236ff;
    text-decoration: none;
    background-color: transparent;

    &:hover {
      text-decoration: underline;
    }
  }
}
```

This defines global styles for the page structure, links, and blog fonts. Also, it contains useful global classes that we will use later on. As you can see, the top wrapping class of this SCSS file is `blog`.

Import `.src/styles/blog.scss` and set the `<body>` class to `blog` in `[...slug].astro` as follows:

```
---
// ./src/[...slug].astro

import aposPageFetch from '@apostrophecms/apostrophe-astro/lib/aposPageFetch.js'
import AposLayout from '@apostrophecms/apostrophe-astro/components/layouts/AposLayout.astro'
import AposTemplate from '@apostrophecms/apostrophe-astro/components/AposTemplate.astro'
import '../styles/blog.scss' // <-- import the custom SCSS file  

const aposData = await aposPageFetch(Astro.request)
const bodyClass = `blog` // <-- update the <body> class

if (aposData.redirect) {
  return Astro.redirect(aposData.url, aposData.status)
}
if (aposData.notFound) {
  Astro.response.status = 404
}
---
<AposLayout title={aposData.page?.title} {aposData} {bodyClass}>
    <Fragment slot='standardHead'>
      <meta name='description' content={aposData.page?.seoDescription} />
      <meta name='viewport' content='width=device-width, initial-scale=1' />
      <meta charset='UTF-8' />
    </Fragment>
    <AposTemplate {aposData} slot='main'/>
</AposLayout>
```

Open `http://localhost:4321/blog` in the browser and you will now see:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708592443438_image.png)

Similarly, this is what a blog post page will look like: 

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708592405795_image.png)

Awesome! The appearance of the blog has already improved a bit, but there is still much to be done.

**Create a Blog Card Component**
The current index page of the blog is nothing more than a list of links. As such, the user experience is very limited. Improve it with a custom React UI component representing a blog post card to use in that list!

In the `./src/components` folder, add a `BlogCard.jsx` file that contains these lines:

```
// ./src/components/BlogCard.jsx

import './BlogCard.scss'
import dayjs from 'dayjs'

export default function BlogPost({ blog }) {
  return (
    <div className='blog-card'>
      <div className='date'>
        Released On {dayjs(blog.publishedAt).format('MMMM D, YYYY')}
      </div>
      <div className='title'>
        <a href={blog._url}>{blog.title}</a>
      </div>
    </div>
  )
}
```

This JSX React component wraps and extends the following blog post representation logic in `./src/templates/BlogIndexPage.astro`:

```
<h4>
  Released On { dayjs(piece.publishedAt).format('MMMM D, YYYY') }
</h4>
<h3>
  <a href={ piece._url }>{ piece.title }</a>
</h3>
```

As you are about to learn, the main advantage of using React components is that they make it easier to implement advanced user interactions without any impact on SEO.

Notice that the first line of the component is an import to a SCSS file. So, define the `BlogCard.scss` file this way:

```
// ./src/components/BlogCard.scss

.blog-card {
  border-radius: 5px;
  border: solid #111111 1px;
  padding: 20px;
  margin-bottom: 10px;

  .date {
    font-style: italic;
    color: #505050;
    font-size: 14px;
    margin-bottom: 15px;
  }

  .title {
    font-size: 20px;
  }
}
```

Keep in mind that Astro supports CSS `import`s via ESM inside any JavaScript file, including JSX components. This is useful for writing granular, per-component styles for your React components.

**Import the Blog Card Component in the Blog Index Page**
Open over the `BlogIndexPage.astro` template component and add the `BlogCard` import in the JavaScript section:

```
import BlogCard from '../components/BlogCard'
```

You do not need to import `BlogCard.scss` as well, since the React component already imports it.

In the template section of the Astro component, replace the blog post HTML representation logic with `<BlogCard />`:

```
{
  pieces.map((piece) => {
    return <BlogCard blog={piece} />
  })
}
```

Note that Astro will manage the keys for each DOM element under the hood. So, you do not need to manually provide a `key` prop as you would normally do in React.

This is where the Astro magic happens. Your `BlogIndexPage.astro` Astro template component now contains HTML mixed with a React component. In the same way, you could use other components written in Vue.js, Preact, or other frameworks.

Your `BlogIndexPage.astro` file will now contain:

```
---
// ./src/templates/BlogIndexPage.astro

import setParameter from '@apostrophecms/apostrophe-astro/lib/aposSetQueryParameter.js'
import BlogCard from '../components/BlogCard'

const {
  page,
  user,
  query,
  piecesFilters,
  pieces,
  currentPage,
  totalPages
} = Astro.props.aposData

const pages = []
for (let i = 1; i <= totalPages; i++) {
  console.log(page, currentPage)
  pages.push({
    number: i,
    current: i === currentPage,
    url: setParameter(Astro.url, 'page', i),
  })
}
---
<section class='bp-content'>
  <h1>{page.title}</h1>

  <h2>Blog Posts</h2>

  {
    pieces.map((piece) => {
      return <BlogCard blog={piece} />
    })
  }

  {pages.map(page => (
    <a
      class={(page === currentPage) ? 'current' : ''} 
      href={page.url}>{page.number}
    </a>
  ))}
</section>
```

For more information on how `aposSetQueryParameter()` works, [check out the official documentation](https://github.com/apostrophecms/apostrophe-astro?tab=readme-ov-file#apossetqueryparameter-working-with-query-parameters).

This is what the `http://localhost:4321/blog` index page will look like:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708592573567_image.png)

The biggest concern you might have is that those React components are rendered client-side. This would not be good for SEO, especially in a blog. To check whether the React components are rendered on the client or the server, you need to inspect the HTML pages returned by Astro to the client.

To do so, stop the local development server and build your application with:

```
npm run build
```

The `/dist` folder in your project will now contain the Astro bundle. Serve it with:

```
npm run serve 
```

Visit `http://localhost:4321/blog` again, right-click, and select the “View page source” option. Take a look at the source code of the page, and you will see HTML code like:

```
<div class="blog-card">
  <div class="date">
    Released On February 1, 2023
   </div>
   <div class="title">
     <a href="/blog/lorem-ipsum-1">Lorem Ipsum 1</a>
   </div>
</div>
```

Fantastic! This is proof that Astro automatically renders React components on the server, as their HTML is embedded in the document sent to the client. Bear in mind that this is just the default behavior, and you can customize that with the [Astro template directives](https://docs.astro.build/en/reference/directives-reference/). Learn more in the next section.

**Add Client-Side Interaction to the Blog Card Component**
Currently, the only way to interact with the blog card component is to click on the title link inside it. Suppose you want to make the entire card interactive. Specifically, you want users to be redirected to the blog post page even when they click on the card element.

You could easily achieve that by passing an `onClick` handler to the `<div>` card in the React component, as shown below:

```
// ./src/components/BlogCard.scss

// ...

export default function BlogPost({ blog }) {
  return (
    <div
      className='blog-card'
      onClick={(e) => {
        e.preventDefault()
        // redirect to the blog post page
        window.location.href = blog._url
      }}
    >
      {/* ... */}
    </div>
  )
}
```

To help users understand this interaction, you should also add a hover effect to the card:

```
// ./src/components/BlogCard.scss

.blog-card {
  // ...

  &:hover {
    border: solid #6236ff 1px;
    background-color: #f2f0f9;
    cursor: pointer;
  }

  // ...
}
```

Note that this is just a simple example but you could implement more complex interactions using React state management and hooks.

Visit the `http://localhost:4321/blog` page and move the mouse on a card to see that the background of the card will change as expected. At the same time, if you click on it, nothing will happen. Why? Because Astro does not hydrate UI framework components in the client by default.

If you are not familiar with this process, [hydration](https://en.wikipedia.org/wiki/Hydration_(web_development)) refers to the process of attaching event listeners and state to UI components in the browser, making them interactive. In the scenario described above, the visual changes triggered by mouse movement and handled via CSS are possible, but interactive functionality such as clicking is not enabled.

To instruct Astro to hydrate the blog card component in the client, pass the `client:load` directive to `<BlogCard />` in `BlogIndexPage.astro`:

```
{
  pieces.map((piece) => {
    return <BlogCard blog={piece} client:load />
  })
}
```

Interact with the blog index page again, and the click interaction will now work as desired:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708617289542_Untitled+Project.gif)


To make this possible, Astro will send the minimum amount of JavaScript possible to the client. This way, the web pages served by your site will remain quick to be retrieved and rendered by the browser.

**Add a Pagination Component** 
You must have noticed that little "1" at the bottom of the index blog page:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708593027024_2.gif)

This is a pagination element that allows you to explore the list of all articles in your blog. Use ApostropheCMS to populate your application with more than 10 posts, and more pagination numbers will appear:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708593226304_image.png)

Currently, the pagination logic is handled by the following lines in `./src/templates/BlogIndexPage.astro`:

```
{pages.map(page => (
  <a
    class={(page === currentPage) ? 'current' : ''} 
    href={page.url}>{page.number}
  </a>
))}
```

This is definitely not an optimal interaction, and you should replace it with a dedicated React interactive component. Inside `./src/components`, add a `BlogPagination.jsx` file with this code:

```
// ./src/components/BlogPagination.jsx

import './BlogPagination.scss'

export default function BlogPagination({ pages = [] }) {
  return (
    <div className='blog-pagination'>
      {pages.map((page) => {
        return (
          <span
            key={page.number}
            className={`pagination-element ${page.current ? 'current' : ''}`}
            onClick={(e) => {
              e.preventDefault()
              // redirect to the blog index pagination page
              window.location.href = page.url
            }}
          >
            <a href={page.url}>{page.number}</a>
          </span>
        )
      })}
    </div>
  )
}
```

As before, this component contains both a crawlable link for SEO and a more intuitive click interaction for users.

The style and interactivity of each pagination element should change whether the number matches the current page or not. You can define this logic in a dedicated `BlogPagination.scss` style file:

```
// ./src/components/BlogPagination.scss

.blog-pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 40px;
  margin-bottom: 40px;

  .pagination-element {
    border: solid #111111 1px;
    text-align: center;
    padding: 10px 20px;
    margin-right: 10px;
    border-radius: 5px;

    a {
      text-decoration: none;
      color: inherit;
      &:hover {
        text-decoration: none;
      }
    }

    &.current {
      pointer-events: none;
      background-color: #111111;
      color: white;
    }

    &:hover {
      color: white;
      background-color: #111111;
      cursor: pointer;
    }
  }
}
```

Import the component in `BlogIndexPage.astro` and use it to replace the aforementioned pagination lines:

```
---
// ./src/templates/BlogIndexPage.astro

import setParameter from '@apostrophecms/apostrophe-astro/lib/aposSetQueryParameter.js'
import BlogCard from '../components/BlogCard'
import BlogPagination from '../components/BlogPagination'

const {
  page,
  user,
  query,
  piecesFilters,
  pieces,
  currentPage,
  totalPages
} = Astro.props.aposData

const pages = []
for (let i = 1; i <= totalPages; i++) {
  pages.push({
    number: i,
    current: i === currentPage,
    url: setParameter(Astro.url, 'page', i),
  })
}
---
<section class='bp-content'>
  <h1>{page.title}</h1>

  <h2>Blog Posts</h2>

  {
    pieces.map((piece) => {
      return <BlogCard blog={piece} client:load />
    })
  }

  <BlogPagination pages={pages} client:load />
</section>
```

Again, notice the `client:load` directive required to load the click interactivity in the client. This is what the new pagination component will look like:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708594526272_image.png)

If you click on one of the active number elements, you will be redirected to the selected pagination page:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708594900841_3.gif)

**Complete the Blog Index Page**
The blog index page has improved a lot in terms of both UI and UX. It is just a matter of adding the finishing touches. For example, you could assign the `h1` class from the global `blog.scss` style file to the `<h1>` element in `BlogIndexPage.astro`:

Also, assume you want to center the `<h2>` element in this Astro component. Instead of defining a global CSS rule, you can use the Astro special `<style>` tag to write scoped CSS:

```
<style>
  h2 {
    text-align: center
  }
</style>
```

CSS rules defined here always come last in the order of appearance. Therefore, if you import a style sheet that conflicts with a scoped style, the scoped style’s value will apply.

The final `BlogIndexPage.astro` template component will contain:

```
---
// ./src/templates/BlogIndexPage.astro

import setParameter from '@apostrophecms/apostrophe-astro/lib/aposSetQueryParameter.js'
import BlogCard from '../components/BlogCard'
import BlogPagination from '../components/BlogPagination'

const {
  page,
  user,
  query,
  piecesFilters,
  pieces,
  currentPage,
  totalPages
} = Astro.props.aposData

const pages = []
for (let i = 1; i <= totalPages; i++) {
  pages.push({
    number: i,
    current: i === currentPage,
    url: setParameter(Astro.url, 'page', i),
  })
}
---
<style>
  h2 {
    text-align: center
  }
</style>

<section class='bp-content'>
  <h1 class='h1'>{page.title}</h1>

  <h2>Blog Posts</h2>

  {
    pieces.map((piece) => {
      return <BlogCard blog={piece} client:load />
    })
  }

  <BlogPagination pages={pages} client:load />
</section>
```

This is how the definitive blog page index will appear: 

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708595112043_4.gif)

Wonderful! You just learned how to customize the UI and UX of the index page of your blog. Time to focus on the blog post page!
 
**Style the Blog Show Page**
The current page for individual blog posts is rather ugly and does not provide a good reading experience. As a first step to improve it, modify `BlogShowPage.astro` as below:

```
---
// ./src/templates/BlogShowPage.astro

import AposArea from '@apostrophecms/apostrophe-astro/components/AposArea.astro'
import dayjs from 'dayjs'

const { page, piece, user, query } = Astro.props.aposData
const { main } = piece
---
<style>
  .publication-date {
    text-align: center;
    font-style: italic;
    color: #505050;
    font-size: 16px;
    margin-bottom: 25px;
  }
</style>

<section class='bp-content'>
  <h1 class='h1'>{ piece.title }</h1>
  <div class='publication-date'>
    { dayjs(piece.publishedAt).format('MMMM D, YYYY') }
  </h4>
  <AposArea area={main} />
</section>
```

This Astro component has a custom style for the date element and relies on the global `h1` class seen earlier.

The new blog show page definitely looks better: 

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708595475778_image.png)

If you inspect the blog post content rendered in the `AposArea` component, you will notice that it is nothing more than a list of `<p>`, each of which represents a paragraph:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708595537136_image.png)

As a first approach to style that section of the page, you might consider adding a CSS rule for `p` tags in `<style>`:

```
<style>
  // ...

  p {
    margin-bottom: 35px;
    line-height: 35px;
    font-size: 18px;
  }
</style>
```

This will not work because that CSS snippet is scoped and the `<p>` elements are not directly in the template component but within `AposArea`. In detail, it is ApostropheCMS that takes care of handling and returning that content.

To define CSS rules for HTML documents whose content lives outside Astro—as in this case—Astro provides a special `global()` CSS function:

```
<style>
  // ...

  :global(p) {
    margin-bottom: 35px;
    line-height: 35px;
    font-size: 18px;
  }
</style>
```

That function allows you to write global, unscoped CSS in the `<style>` tag. 

Equivalently, you can add another `<style>` tag marked with the `is:global` attribute:

```
<style is:global>
  p {
    margin-bottom: 35px;
    line-height: 35px;
    font-size: 18px;
  }
</style>
```

The CSS rules marked as “global” will apply to all the HTML elements in the DOM subtree of the Astro component.

Put it all together, and you will get the following `BlogShowPage.astro` file:

```
---
// ./src/templates/BlogShowPage.astro

import AposArea from '@apostrophecms/apostrophe-astro/components/AposArea.astro'
import dayjs from 'dayjs'

const { page, piece, user, query } = Astro.props.aposData
const { main } = piece
---
<style>
  .publication-date {
    text-align: center;
    font-style: italic;
    color: #505050;
    font-size: 16px;
    margin-bottom: 25px;
  }

  :global(p) {
    margin-bottom: 35px;
    line-height: 35px;
    font-size: 18px;
  }
</style>

<section class='bp-content'>
  <h1 class='h1'>{ piece.title }</h1>
  <div class='publication-date'>
    { dayjs(piece.publishedAt).format('MMMM D, YYYY') }
  </h4>
  <AposArea area={main} />
</section>
```

This is what the blog show page will now look like:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708604885719_image.png)

Congratulations! You now know how to build a site that relies on Astro on the frontend and uses ApostropheCMS for content management.

## Conclusion

Part 2 of this two-article series ends up here. But, suggested next steps for you are revamping the home page, improving the 404 page, designing a proper 500 page, and adding new features such as a top menu, a footer, sharing buttons, the ability to comment, and more. Thanks to what you have learned here, you have all the building blocks you need to achieve those goals and get the most out of the Astro/ApostropheCMS integration!

Once again, ApostropheCMS has proven to be a modern, robust, future-oriented technology that can support new approaches to web development due to its unopinionated approach on the frontend. [Try Apostrophe today](https://apostrophecms.com/demo)!

## FAQ
**How does Astro communicate with ApostropheCMS?**
The `@apostrophecms/apostrophe-astro` npm library automatically proxies certain ApostropheCMS endpoints in Astro. In particular, these are the routes proxied by the package:

- `/apos-frontend/[...slug]`: For serving ApostropheCMS assets.
- `/uploads/[...slug]`: For serving ApostropheCMS uploaded assets.
- `/api/v1/[...slug]` and `/[locale]/api/v1/[...slug]`: For contacting the ApostropheCMS API endpoints.
- `/login` and `/[locale]/login`: For accessing the ApostropheCMS login page.

So, for example, when you visit the `/login` page in Astro, the `aposPageFetch()` function from `@apostrophecms/apostrophe-astro` takes care of forwarding the request to the `/login` endpoint of your ApostropheCMS backend and retrieving the returned HTML.

Note that this proxy mechanism forwards all the headers of the original request, including cookies. This also explains how Apostrophe login works in Astro.

**Do ApostropheCMS widget players still work in Astro?**
In ApostropheCMS, [widget players](https://v3.docs.apostrophecms.org/guide/custom-widgets.html#client-side-javascript-for-widgets) are a frontend feature that allows developers to provide special behavior to widgets, calling each widget's player exactly once at page load and when new widgets are inserted or replaced with new values. This interactive widget feature should still work without a page refresh, even if the widget was just added to the page. To achieve the same result, you can use [Astro web components](https://docs.astro.build/en/guides/client-side-scripts/#web-components-with-custom-elements). 

Defining and using an `HTMLElement` inside an Astro widget component has much the same effect as defining a widget player in a standalone ApostropheCMS project. For a complete example, check out the source code of `VideoWidget.astro` [in the apostrophecms/astro-frontend project](https://github.com/apostrophecms/astro-frontend/blob/main/src/widgets/VideoWidget.astro).

**How are error messages presented in Astro?**
Astro comes with a default error page that contains the error message, the snippet that caused the error, and the stack trace as below:

![](https://paper-attachments.dropboxusercontent.com/s_B5B22884D1002CAFB0E911656EB77E8DD910E24177CCC44E4E737783265C4E7B_1708613216360_image.png)

If you receive the following error after integrating the `@apostrophecms/apostrophe-astro` library:

```
Only URLs with a scheme in: file and data are supported by the default ESM
loader. Received protocol 'virtual:'
```

Then, you most likely left out this part from the `astro.config.mjs` file:

    export default defineConfig({
      // other settings here ...
      vite: {
        ssr: {
          noExternal: [ '@apostrophecms/apostrophe-astro' ],
        }
      }
    })

Without this logic, the `virtual:` URLs used to access configuration information will cause the build to fail.