<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Apostrophe</title>
    <description>The latest articles on DEV Community by Apostrophe (@apostrophecms).</description>
    <link>https://dev.to/apostrophecms</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F8743%2Fc361c071-509e-4724-94a6-7888f18bc36e.png</url>
      <title>DEV Community: Apostrophe</title>
      <link>https://dev.to/apostrophecms</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/apostrophecms"/>
    <language>en</language>
    <item>
      <title>Creation of the ApostropheCMS Documentation Chatbot</title>
      <dc:creator>The Apostrophe Team</dc:creator>
      <pubDate>Thu, 29 Aug 2024 15:17:08 +0000</pubDate>
      <link>https://dev.to/apostrophecms/creation-of-the-apostrophecms-documentation-chatbot-1j7f</link>
      <guid>https://dev.to/apostrophecms/creation-of-the-apostrophecms-documentation-chatbot-1j7f</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falqkxk2constwrvvam55.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falqkxk2constwrvvam55.png" alt="Detailed steps in the processing of a user query in our LLM-powered docbot." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Chatbots have evolved significantly since their inception in the 1960s with simple programs like ELIZA, which could mimic human conversation through predefined scripts. Initially limited to basic interactions, typically in customer service roles, advancements in artificial intelligence have transformed chatbots into essential tools for businesses, enhancing user engagement and providing 24/7 customer support. Recently, the shift from rule-based systems to the utilization of large language models (LLMs) has enabled chatbots to understand and respond with greater comprehension and context awareness.&lt;/p&gt;

&lt;p&gt;AI-powered assistants are now revolutionizing the developer tools landscape, offering more efficient ways to interact with documentation. When they are at their best, these intelligent search assistants provide accurate, contextual, and efficient navigation through complex information, significantly boosting productivity and saving valuable time.&lt;/p&gt;

&lt;p&gt;Our decision to build an AI-powered documentation assistant was driven by the desire to provide rapid and customized responses to engineers developing with ApostropheCMS. While we remain committed to providing guidance and fostering community in Discord, support via this channel is limited by personnel availability. Implementing an AI-driven chatbot enables developers to receive instant, customized answers anytime, even outside of regular support hours, and expands accessibility by providing support in multiple languages. Our assistant aims to transform the documentation experience by moving beyond simple scripts, offering a seamless and intelligent solution tailored to developers' needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Design Highlights
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qJD7s9xM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://apos-website-prod.s3.us-east-1.amazonaws.com/attachments/cm054wohy0bv10bt04ugtw88q-doc-question-smaller.full.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qJD7s9xM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://apos-website-prod.s3.us-east-1.amazonaws.com/attachments/cm054wohy0bv10bt04ugtw88q-doc-question-smaller.full.gif" alt="Asking a question of the new ApostropheCMS documentation chatbot." width="728" height="480"&gt;&lt;/a&gt;&lt;br&gt;Asking a question of the new ApostropheCMS Documentation chatbot is easy!
  &lt;/p&gt;

&lt;p&gt;Building an AI-powered chatbot is more than just connecting a user’s query to an LLM. While commercial options often bundle everything—natural language processing, conversational memory, knowledge retrieval—into a neat package, they tend to be a black box, leaving us with little control over the finer details. When we set out to create our documentation chatbot, we knew we wanted more than just an out-of-the-box solution. We leveraged the power of an LLM, but also took steps to refine the process, enhancing accuracy and overall user experience by making thoughtful design choices along the way. In this section, we will highlight some of those key design decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieval-Augmented Generation (RAG) with Vector Database
&lt;/h3&gt;

&lt;p&gt;When designing our documentation chatbot, we knew from using commercial offerings that there were significant issues with hallucination, where AI generates confident but incorrect or irrelevant information, and references to outdated information about older versions of Apostrophe in answers. Hallucination occurs because large language models (LLMs) are designed to generate the most likely response based on their training data, even if the data doesn't contain relevant or up-to-date information.&lt;/p&gt;

&lt;p&gt;We toyed with “prompt engineering”, essentially adding extra information to guide the AI’s response to enhance the accuracy of answers. For example, for each query we added language like, “&lt;em&gt;Using only information about the latest version of Apostrophe, answer the following query: `{{ user_query }}.&lt;/em&gt;`” But as many of you have probably found in your own experiments, this approach on its own has its limits. The AI would still go off track, lacking the real-time knowledge to separate fact from fiction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv7kk2guae98x669abcne.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv7kk2guae98x669abcne.png" alt="Simplified Retrievel-Augmented Generation (RAG) workflow." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;
Simplified Retrieval-Augmented Generation (RAG) workflow.



&lt;p&gt;So rather than relying solely on prompt engineering, we chose a Retrieval-Augmented Generation (RAG) approach for our chatbot. Without diving too deep into the technicalities (there is a great &lt;a href="https://medium.com/@bijit211987/rag-vs-vectordb-2c8cb3e0ee52" rel="noopener noreferrer"&gt;article&lt;/a&gt; here for a more comprehensive explanation), RAG is fundamentally about enhancing an LLM's responses by providing it with additional, real-time info. At its simplest, RAG involves three main steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retrieval: Using the user's query to find relevant information from a knowledge base.&lt;/li&gt;
&lt;li&gt;Augmentation: Adding this retrieved information to context provided along with the query to the LLM.&lt;/li&gt;
&lt;li&gt;Generation: Having the LLM generate a response based on both the original query and the augmented context.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach lets us feed the LLM current knowledge that wasn't part of its original training, leading to more accurate and up-to-date answers. The retrieval part can be done in various ways, from simple keyword matching to fancier semantic search techniques. For us, it was a game-changer in helping to reduce hallucination and outdated info problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7zg0karg59ym3afr29e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7zg0karg59ym3afr29e.png" alt="Simple workflow for the creating of vector embeddings for a RAG DB." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;
Steps in the creation of a RAG vector database.



&lt;h3&gt;
  
  
  Vector Embedding
&lt;/h3&gt;

&lt;p&gt;We settled on using a vector database mechanism for our RAG implementation. This approach allows us to efficiently store and retrieve relevant information based on the semantic meaning of queries. Here's how we built our vector database:&lt;/p&gt;

&lt;p&gt;First, we gathered all of our documentation and extension README files into a collection of about 150 documents. We then split these documents into smaller chunks of 1000 characters each, with an overlap of 200 characters between chunks. This chunking process helps maintain context while allowing for more precise retrieval of relevant information.&lt;/p&gt;

&lt;p&gt;Next, we created embeddings for each of these chunks. Embeddings are numerical representations that capture the semantic meaning of the text, allowing for similarity comparisons. We used the OpenAI text-embedding-3-small model to convert each text chunk into a high-dimensional vector. This is a decision that we may re-think moving forward, based on a number of factors such as whether more context is worth the cost. We'll touch on this briefly in the last section of this article.&lt;/p&gt;

&lt;p&gt;Finally, we stored these vectors in our chosen database: the &lt;a href="https://github.com/activeloopai/deeplake" rel="noopener noreferrer"&gt;activeloop DeepLake database&lt;/a&gt;. This database is open source, something near and dear to our own open-source hearts. We will cover some additional details in a further section, but it is specifically designed to handle vector data and perform efficient similarity searches, which is crucial for quick and accurate retrieval during the RAG process.&lt;/p&gt;

&lt;p&gt;One of the key advantages of this approach is its flexibility and cost-effectiveness. Updating our RAG database is a straightforward process that costs only about five cents per update. This allows us to continuously expand and refine our knowledge base as our documentation evolves, ensuring that our chatbot always has access to the most up-to-date information.&lt;/p&gt;

&lt;p&gt;Compared to alternatives like fine-tuning an entire LLM, which can be time-consuming and expensive, especially with frequently changing content, our vector database approach for RAG is more accurate and cost-effective for maintaining current and constantly changing knowledge in our chatbot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fprpnww8e0aons2bd1aq5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fprpnww8e0aons2bd1aq5.png" alt="Flowchart of a generalized RAG flow." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;
Steps in responding to a user-submitted query.



&lt;h3&gt;
  
  
  Document Retrieval with Nearest Neighbor
&lt;/h3&gt;

&lt;p&gt;When a user submits a query to our docbot, we employ a process known as nearest neighbor search to find the most relevant information:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Vector Conversion: The query is first converted into a vector, representing its semantic meaning in a multi-dimensional space. This is done with the same embedding model as was used to create the database.&lt;/li&gt;
&lt;li&gt;Nearest Neighbor Search: We use a nearest neighbor algorithm to find the most similar document chunks in our knowledge base. This technique efficiently identifies the 'closest' vectors to our query vector in the high-dimensional space.&lt;/li&gt;
&lt;li&gt;Initial Retrieval: Our system retrieves the top 75 nearest neighbors (fetch_k = 75). In essence, we're finding the 75 document chunks that are most semantically similar to the query.&lt;/li&gt;
&lt;li&gt;Refinement: From these 75 chunks, we further refine our selection to the 12 most relevant (k = 12). This two-step process helps balance between broad coverage and focused relevance.&lt;/li&gt;
&lt;li&gt;Prompt Creation: The selected chunks, along with the original query, are formatted into a prompt for the LLM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nearest neighbor search is a fundamental concept in information retrieval and machine learning. It's particularly useful in our context because it allows us to quickly find the most similar documents without having to compare the query to every single document in our database. We use cosine similarity as our distance metric to determine how 'close' or similar two vectors are.&lt;/p&gt;

&lt;p&gt;This approach ensures that the model's answers are grounded in the most relevant and up-to-date information available in our documentation. Our initial parameter choices to fetch 75 document chunks and narrow it to 12 can likely be further optimized to balance between response accuracy and processing speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  LangChain Framework
&lt;/h3&gt;

&lt;p&gt;Any commercial or open-source LLM model is going to have some type of API that allows you to interact with the model, but using these APIs directly can lead to model-specific implementations and require significant custom development for advanced features. To enhance flexibility and streamline development, we chose to use the​ &lt;a href="https://www.langchain.com/langchain" rel="noopener noreferrer"&gt;LangChain framework&lt;/a&gt;. LangChain offers a layer of abstraction that allows us to create model-agnostic chains of processes, easily switch between different LLMs, and leverage pre-built components for common tasks like memory management and prompt engineering.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falqkxk2constwrvvam55.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falqkxk2constwrvvam55.png" alt="Detailed steps in the processing of a user query in our LLM-powered docbot." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;
Detailed steps in the processing of a user query in our LLM-powered docbot.



&lt;p&gt;As the name suggests, the LangChain framework allows for queries and retrieved documents to be passed down a chain of processes. Here's how we've implemented it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Query Reformulation&lt;/strong&gt;: We first combine the user's query with the current user’s chat history from that same session to create a new, stand-alone query. This process, using LangChain's ConversationBufferMemory, improves clarity by providing context from previous interactions. For example, if a user asks, "&lt;em&gt;How do I modify it?&lt;/em&gt;" the reformulated query might be, "&lt;em&gt;How do I modify the page template in ApostropheCMS?&lt;/em&gt;" based on the context of the previous conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document Retrieval and Prompt Engineering&lt;/strong&gt;: The reformulated query is used to retrieve relevant documents from our RAG database. We then apply prompt engineering using LangChain's PromptTemplate before querying the LLM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;u&gt;Other Useful Features&lt;/u&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model Flexibility&lt;/strong&gt;: LangChain's model-agnostic design allows for easy A/B testing between different LLMs. This means that you can easily switch between different LLMs or even fine-tuned versions of the same model to compare performance and outcomes. We've been comparing OpenAI's GPT-4 and Anthropic's Claude Sonnet. This flexibility allows for continuous optimization of the chatbot's responses and ensures that you can choose the best model for your specific needs without significant re-engineering.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F51p9gr3076csfx4jenoj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F51p9gr3076csfx4jenoj.png" alt="A screenshot of an example query chain in LangSmith." width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;
LangSmith provides a (highly) detailed dashboard that lets you monitor all of the aspects of your query chain.



&lt;p&gt;&lt;strong&gt;Performance Monitoring&lt;/strong&gt;: LangChain integrates with &lt;a href="https://www.langchain.com/langsmith" rel="noopener noreferrer"&gt;LangSmith&lt;/a&gt;, offering advanced tools for debugging and monitoring. Using LangSmith, we identified that the amount of chat history we were passing with our queries is likely creating excessive and unnecessary token usage. This is an area we can actively investigate to see if we can reduce costs without impacting response quality.&lt;/p&gt;

&lt;p&gt;This integrated suite of tools makes LangChain a powerful choice for building and optimizing AI-powered chatbots. It allows us to continually refine our implementation, ensuring we deliver the best possible user experience while managing resources efficiently.&lt;/p&gt;

&lt;h3&gt;
  
  
  DeepLake Database
&lt;/h3&gt;

&lt;p&gt;While we've already discussed the basics of our vector database implementation, it's worth diving deeper into why we chose activeloop DeepLake and how it enhances our chatbot's performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory-Resident Capability&lt;/strong&gt;: DeepLake offers the ability to create a memory-resident database. This feature significantly reduces latency by keeping the data in RAM, close to where it's processed. For our current dataset of about 150 documents, this in-memory approach provides very rapid retrieval times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versioning System&lt;/strong&gt;: DeepLake's built-in versioning system is a powerful tool for managing our knowledge base. It allows us to track changes over time, making it easy to update our documentation or roll back to previous versions if needed. This is particularly valuable as our product evolves and documentation is frequently updated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Efficient Querying and Compression&lt;/strong&gt;: The database supports efficient data querying, allowing us to quickly retrieve relevant information. Additionally, its data compression features help optimize memory usage, which is crucial for maintaining high performance as our dataset grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: While we currently use DeepLake in-memory due to our relatively small dataset, we have a clear path for scaling. If we expand our dataset to include a large codebase, for example, we can easily transition to DeepLake's cloud offering. However, this would come at the cost of increased latency in document retrieval and hosting costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Future Optimizations&lt;/strong&gt;: As our dataset grows and we potentially move to cloud storage, we're already considering optimizations. For instance, we may need to implement increased caching of the first document retrieval in our chain of processes to mitigate latency issues. Our current setup, with document retrieval occurring twice in our process flow, may need adjustment to maintain optimal performance.&lt;/p&gt;

&lt;p&gt;By leveraging DeepLake's features, we've created a flexible, efficient foundation for our RAG system. This allows us to focus on improving the chatbot's responses and user experience, knowing that our data retrieval system can scale and adapt as our needs evolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflection from Early Adoption
&lt;/h2&gt;

&lt;p&gt;Our experience with the chatbot in production is still in the early stages, but we’ve already seen some promising results.  For instance, there have been strong examples where the chatbot has effectively guided users through building features like custom widgets from scratch. This includes the main index.js file, Nunjucks template, and player code. As user adoption increases, we hope these positive outcomes will continue. While we've only touched on the key design highlights, there are several other noteworthy takeaways from our early experiences.&lt;/p&gt;

&lt;p&gt;One observation is that users often ask a series of highly similar questions sequentially. This behavior can increase hallucinations, as the LLM may attempt to modify its responses each time. Fortunately, since we control the chain of processes, we were able to introduce a step that evaluates new queries against previous ones in the chat history to mitigate this issue. Unless the two queries have a low similarity (&amp;lt;0.85), they are rejected as being the same questions.  Although this change has occasionally led to user frustration when they perceive their queries as different, it has generally reduced hallucinations.&lt;/p&gt;

&lt;p&gt;Similarly, we noticed that users sometimes ask questions using terminology that is absent in our documentation but appears in our codebase. Because the LLM is primarily limited to our documentation, it tends to hallucinate answers for these unfamiliar terms. By adjusting the process chain to examine the vector scores of the retrieved documents and ensuring that the returned documents have significant (0.85) semantic similarity with the query, we've also been able to reduce this type of hallucination.&lt;/p&gt;

&lt;p&gt;Another unexpected benefit of monitoring user interactions is that it has highlighted areas for improving our documentation. When the LLM struggles to find information, it's often a sign that the content is either not in the right context or not prominently featured. In one case, this led us to discover an incorrectly documented feature, allowing us to rectify the error and enhance our overall documentation quality.&lt;/p&gt;

&lt;p&gt;These early reflections have not only helped us refine our chatbot but have also provided valuable insights into our documentation strategy and user behavior. As we continue to learn and adapt, we're excited about the potential for further improvements and the positive impact on our users' experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Ahead
&lt;/h2&gt;

&lt;p&gt;When originally designing the chatbot, we opted to build it in Python, despite being a heavily JavaScript-oriented shop. This decision was driven by the availability of more mature analytic tools for objectively testing chatbot hallucination and accuracy in Python. So far, we've been evaluating answers qualitatively, but we plan to incorporate a tool like &lt;a href="https://github.com/Giskard-AI/giskard" rel="noopener noreferrer"&gt;Giskard&lt;/a&gt; to bring a more quantitative approach to our evaluations. This step is crucial and one that, anecdotally, is often overlooked in many production chatbots.&lt;/p&gt;

&lt;p&gt;Another goal on the horizon is optimizing how we manage passed history to reduce token usage. However, with the decreasing cost of most LLM models and the increasing token limits, this concern has become less pressing. These advancements allow us to pass more context for less money without the worry of truncated answers due to token constraints.&lt;/p&gt;

&lt;p&gt;We’re also exploring the idea of supplementing our RAG database with several codebases, including the core Apostrophe repository and potentially some of our &lt;a href="https://apostrophecms.com/starter-kits" rel="noopener noreferrer"&gt;starter kits&lt;/a&gt;. However, this would complicate the embedding process and might challenge the LLM’s primary goal of guiding users to the most relevant documentation for further reading. However, along with this, we have also thought about changing to the text-embedding-3-large model. This would provide more context for our documentation and might also be better suited for large amounts of code. We’re carefully weighing the best way forward, aiming to balance the richness of our database with the clarity and utility of the chatbot’s responses.&lt;/p&gt;

&lt;p&gt;As we continue refining our chatbot, our primary focus remains on providing developers with precise, actionable information. Every enhancement is a step toward that goal, and we're excited to see how these improvements will further elevate the user experience. One key item under discussion is how to best collect user feedback and ensure that when answers have a degree of hallucination and are not effective solutions for the end developer, we can help remedy this.&lt;/p&gt;

&lt;p&gt;We invite you to experience our AI-powered documentation assistant firsthand and explore how it can enhance your development workflow. Try it out &lt;a href="https://www.notion.so/Creation-of-the-ApostropheCMS-Documentation-Chatbot-11343dae468b45a9b0244e0794884e9f?pvs=21" rel="noopener noreferrer"&gt;here&lt;/a&gt;, and let us know your thoughts! Join our &lt;a href="https://discord.com/invite/HwntQpADJr" rel="noopener noreferrer"&gt;Discord community&lt;/a&gt; to share feedback, ask questions, and collaborate with other developers. Together, we can continue to refine and improve this tool to better serve the ApostropheCMS community.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>softwaredevelopment</category>
      <category>openai</category>
    </item>
    <item>
      <title>Expand Content Reach Using AI for SEO and Translation in Your CMS</title>
      <dc:creator>Antonello Zanini</dc:creator>
      <pubDate>Thu, 22 Aug 2024 15:38:48 +0000</pubDate>
      <link>https://dev.to/apostrophecms/expand-content-reach-using-ai-for-seo-and-translation-in-your-cms-2jfb</link>
      <guid>https://dev.to/apostrophecms/expand-content-reach-using-ai-for-seo-and-translation-in-your-cms-2jfb</guid>
      <description>&lt;p&gt;In recent months, the way we approach our work has changed significantly. AI has become central to many workflows, driving productivity, enhancing efficiency, and providing valuable insights. The CMS industry is no exception to this trend. That is why why Apostrophe recently released two new extensions that leverage AI for SEO and translation.&lt;/p&gt;

&lt;p&gt;In this article, you will discover the latest developments from Apostrophe in the AI space. You will learn what the new SEO Assistant and Automatic Translation extensions are, and see how they can streamline your SEO metadata definition and content translation processes with just a few clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Applications in the CMS Industry: From Ideas to Reality
&lt;/h2&gt;

&lt;p&gt;In our article published in mid-2023, we examined &lt;a href="https://apostrophecms.com/blog/how-ai-is-transforming-the-cms-industry" rel="noopener noreferrer"&gt;how AI could transform the CMS industry&lt;/a&gt;. During that discussion, we introduced the &lt;a href="https://apostrophecms.com/extensions/ai-helper" rel="noopener noreferrer"&gt;AI Helper&lt;/a&gt; extension to generate images and rich text directly from a prompt within ApostropheCMS.&lt;/p&gt;

&lt;p&gt;In the months that followed, those insights were validated and evolved into concrete developments. These ideas were incorporated into &lt;a href="https://portal.productboard.com/apostrophecms/1-product-roadmap/tabs/2-planned" rel="noopener noreferrer"&gt;Apostrophe's roadmap&lt;/a&gt; and have now materialized into two new AI-powered extensions for Apostrophe Pro. &lt;/p&gt;

&lt;p&gt;Let’s dive into these exciting new features!&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing New Apostrophe AI-Powered Plugins: SEO Assistant and Automatic Translation
&lt;/h2&gt;

&lt;p&gt;These are two new extensions in the Apostrophe AI offerings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apostrophecms.com/extensions/seo-assistant" rel="noopener noreferrer"&gt;SEO Assistant&lt;/a&gt;: Automatically generates SEO page metadata through AI.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apostrophecms.com/extensions/automatic-translation" rel="noopener noreferrer"&gt;Automatic Translation&lt;/a&gt;: Provides AI-driven translation of documents (pages and pieces) for content localization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Both extensions are exclusive to &lt;a href="https://apostrophecms.com/pro" rel="noopener noreferrer"&gt;Apostrophe Pro&lt;/a&gt; and &lt;a href="http://apostrophecms.com/assembly" rel="noopener noreferrer"&gt;Apostrophe Assembly&lt;/a&gt; users. These premium versions of Apostrophe offers advanced features tailored for both developers and editors.&lt;/p&gt;

&lt;p&gt;As you are about to learn, both extensions work together to optimize your site’s reach. To better showcase SEO Assistant and Automatic Translation, they will be integrated into the site showcased in our tutorial on &lt;a href="https://apostrophecms.com/blog/how-to-build-an-ecommerce-website-with-apostrophecms" rel="noopener noreferrer"&gt;how to create an e-commerce platform with Apostrophe&lt;/a&gt;. If you are not familiar with that article, &lt;a href="https://vimeo.com/907612831" rel="noopener noreferrer"&gt;you can see an example of in the following video.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using AI for SEO Optimization in Your CMS
&lt;/h2&gt;

&lt;p&gt;One of the most tedious, yet essential, tasks when handling the SEO of a website is defining the meta title and SEO description of a webpage. This information is key because search engines like Google use it to generate the SERP (&lt;a href="https://www.semrush.com/blog/serp/" rel="noopener noreferrer"&gt;Search&lt;/a&gt;&lt;a href="https://www.semrush.com/blog/serp/" rel="noopener noreferrer"&gt; Engine Results Page&lt;/a&gt;) entry associated with your page, as in the following image:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60y8x6kgdln7nugap6zm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60y8x6kgdln7nugap6zm.png" alt="Example of how SEO metadata is used in search results." width="677" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is even more important when considering that over &lt;a href="https://www.brightedge.com/resources/research-reports/channel_share" rel="noopener noreferrer"&gt;50% of traffic to business websites&lt;/a&gt; comes from organic search (i.e., SEO).&lt;/p&gt;

&lt;p&gt;When specifying the SEO meta title and description of a page, there are a few best practices to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both the title and description should be concise, actionable, and to the point. They should also include relevant keywords to help search engines rank your page higher.&lt;/li&gt;
&lt;li&gt;The meta title should be between 50 to 60 characters long to ensure that search engines do not truncate it in the SERP snippet.&lt;/li&gt;
&lt;li&gt;The meta description should ideally be between 50 to 160 characters, with an optimal length of around 150/160 characters. For more information, check out Google's &lt;a href="https://developers.google.com/search/docs/appearance/snippet" rel="noopener noreferrer"&gt;best practices of meta descriptions&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem is that writing effective SEO titles and descriptions is often seen as a boring task. Ask your editors, and they will likely tell you that they prefer to craft great content rather than focusing on SEO details. &lt;/p&gt;

&lt;p&gt;Additionally, SEO optimization is typically the final step in the publication process. As a result, some editors might overlook it or not give it the attention it deserves to push the page online.&lt;/p&gt;

&lt;p&gt;The good news is that SEO title and description values are directly related to the page's content, which opens the door to leveraging AI for SEO. Specifically, the AI-powered SEO Assistant extension can analyze the page content to generate effective meta titles and descriptions for you. &lt;/p&gt;

&lt;p&gt;Let’s explore how this Apostrophe Pro extension works in the section below!&lt;/p&gt;

&lt;h3&gt;
  
  
  SEO Assistant in Action  
&lt;/h3&gt;

&lt;p&gt;Follow the &lt;a href="https://apostrophecms.com/extensions/seo-assistant#installation" rel="noopener noreferrer"&gt;official integration guide&lt;/a&gt; to install and configure the SEO Assistant extension in your Apostrophe application. As of this writing, the prerequisites for using the SEO Assistant are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Apostrophe 4 application with the &lt;a href="https://apostrophecms.com/extensions/seo-tools-3" rel="noopener noreferrer"&gt;SEO Tools&lt;/a&gt; and other required extensions installed, as described in the &lt;a href="https://apostrophecms.com/extensions/seo-assistant" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://openai.com/index/openai-api/" rel="noopener noreferrer"&gt;OpenAI API key&lt;/a&gt; or a custom integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this example, we will use OpenAI as the AI provider, but keep in mind that SEO Assistant also supports &lt;a href="https://apostrophecms.com/extensions/seo-assistant#custom-provider" rel="noopener noreferrer"&gt;custom AI providers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, assume you just created a special web page called “Summer Collection 2024" to promote your clothing line for the summer season. The final step before publishing it is to define the meta title and description.&lt;/p&gt;

&lt;p&gt;To reach the page edit modal, click on “Pages” in the top left menu, locate the “Summer Collection 2024” draft page, and select the “Edit” option:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fapos-website-prod.s3.us-east-1.amazonaws.com%2Fattachments%2Fclzvommpg0afq0bnomc9mmddx-6rxeaxbt.full.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fapos-website-prod.s3.us-east-1.amazonaws.com%2Fattachments%2Fclzvommpg0afq0bnomc9mmddx-6rxeaxbt.full.gif" alt="Example of how you get to an edit screen from the page manager." width="8" height="3"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Navigate to the “SEO” tab, where you will see that both the “Title” and “Description” fields are empty:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1j1pa5hrn9emgc8jf25b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1j1pa5hrn9emgc8jf25b.png" alt="Example of the SEO metadata fields when the SEO assistant is installed." width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Normally, you would have to manually fill in those fields, but no worries—your AI-powered SEO Assistant is here to help!&lt;/p&gt;

&lt;p&gt;Click on the button to access the SEO Assistant options. Select “Make a suggestion based on page content” to instruct AI to generate the selected meta field using the content found on the page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fko2xc7zhgfs9u4iv0dhx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fko2xc7zhgfs9u4iv0dhx.gif" alt="Example of using the SEO Assistant to suggest a page title." width="702" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you have three options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Use this suggestion&lt;/strong&gt;: Populate the field with the AI-generated text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjfc1mxwgqm05wbj1ijo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjfc1mxwgqm05wbj1ijo.gif" alt="An example of choosing to use an SEO suggestion and applying that text to be part of the form field that will be saved." width="702" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Try again&lt;/strong&gt;: Request an alternative suggestion on the fly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgv5vkeaceb3y30s6ge6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgv5vkeaceb3y30s6ge6.gif" alt="An example of regenerating an SEO title property with the SEO Assistant." width="702" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Edit prompt&lt;/strong&gt;: Manually configure the prompt used to query the AI for SEO meta field generation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfa6m7x5unwqv2t44ktm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfa6m7x5unwqv2t44ktm.gif" alt="An example of editing the prompt to tailor the SEO title suggestion." width="702" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Similarly, you can generate an SEO meta description with a couple of clicks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhmu2j3m9lkxbwb71m5g8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhmu2j3m9lkxbwb71m5g8.gif" alt="Using the SEO Assistant to suggest content for the Description field." width="702" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The above example focused on a page, but SEO Assistant also works for individual &lt;a href="https://docs.apostrophecms.org/guide/pieces.html" rel="noopener noreferrer"&gt;content pieces&lt;/a&gt;. Plus, it can be configured to produce results of a &lt;a href="https://apostrophecms.com/extensions/seo-assistant#additional-configuration" rel="noopener noreferrer"&gt;given minimum length&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Amazing! SEO Assistant generated a captivating meta title and description for you in just a fraction of a second. It only remains to click “Publish.”&lt;/p&gt;

&lt;p&gt;As shown here, using AI for SEO simplifies metadata optimization and removes the tediousness associated with the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating AI for Translation in your CMS
&lt;/h2&gt;

&lt;p&gt;Localizing a website into several languages is critical to reaching a global audience. As of 2023, approximately 1.46 billion people are estimated to speak English worldwide. That represents about &lt;a href="https://wordsrated.com/how-many-people-speak-english/" rel="noopener noreferrer"&gt;18.07% of the global population&lt;/a&gt;—less than 1 in 5 people.&lt;/p&gt;

&lt;p&gt;As you can imagine, localization becomes essential when targeting local markets in regions such as Asia, Europe, or Latin America. Thankfully, Apostrophe comes with comprehensive &lt;a href="https://docs.apostrophecms.org/guide/localization/overview.html" rel="noopener noreferrer"&gt;support for localization&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When translating a website, a common practice is to start with layout elements like menus, footers, and headers. Although this initial step can be time-consuming, it only needs to be done once as those elements are shared across all pages. That might create the misconception that translating a website into a new language is a quick task. Well, that is not true!&lt;/p&gt;

&lt;p&gt;The real challenge arises when realizing that you have to translate all pages, content, and media files. This process can take up to months, especially on large sites. Luckily, you can now automate the task by using AI for translation.&lt;/p&gt;

&lt;p&gt;Let’s see how the Apostrophe Pro Automatic Translation extension can help you translate pages and pieces with just a few clicks!&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic Translation in Action  
&lt;/h3&gt;

&lt;p&gt;Follow the &lt;a href="https://apostrophecms.com/extensions/automatic-translation#installation" rel="noopener noreferrer"&gt;integration guide&lt;/a&gt; to install and set up the Automatic Translation extension in your Apostrophe application.&lt;/p&gt;

&lt;p&gt;Automatic Translation comes with built-in support for &lt;a href="https://cloud.google.com/translate" rel="noopener noreferrer"&gt;Google Cloud Translation&lt;/a&gt; and &lt;a href="https://www.deepl.com/" rel="noopener noreferrer"&gt;DeepL&lt;/a&gt; as AI translation providers (with support for &lt;a href="https://azure.microsoft.com/en-us/products/ai-services/ai-translator" rel="noopener noreferrer"&gt;Azure AI Translator&lt;/a&gt; coming soon). To integrate them, you just need a valid API key. Keep in mind that the extension also supports &lt;a href="https://apostrophecms.com/extensions/seo-assistant#custom-provider" rel="noopener noreferrer"&gt;custom providers&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;In this example, we will use DeepL, but any other AI translation provider will do.&lt;/p&gt;

&lt;p&gt;Suppose you already configured the &lt;em&gt;es&lt;/em&gt; locale in your Apostrophe e-commerce project and translated all layout elements. The next step is to translate all content from English to Spanish, which will make your site more accessible to the &lt;a href="https://www.atalayar.com/en/articulo/culture/spanish-continues-grow-and-has-almost-500-million-native-speakers-according-cervantes/20221026154937158810.html" rel="noopener noreferrer"&gt;500 million native Spanish speakers worldwide&lt;/a&gt;. &lt;em&gt;¡Empezamos!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is what the English version of the e-commerce site currently looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9twulmu2vrie8395gqxu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9twulmu2vrie8395gqxu.gif" alt="Scrolling through an example of the ecommerce starter kit." width="200" height="96"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The goal is to have the entire site translated into Spanish.&lt;/p&gt;

&lt;p&gt;The first document to translate is undoubtedly the home page. To do that, click on "Pages" in the top left menu, locate the "Home" page, and select the "Localize…" option:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnzdtz53r27l25phi1mw1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnzdtz53r27l25phi1mw1.png" alt="Example of localizing a page via the context menu item." width="800" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will open the Automatic Translation modal below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fup9xb3vkdxtos3bnbmdv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fup9xb3vkdxtos3bnbmdv.png" alt="Screenshot of the Localize Content modal." width="659" height="723"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A wizard will guide you through the process of configuring how Apostrophe will use AI for translation. Specifically, you will be asked to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select the content to localize. The available options are the current document, the current document and its related documents (such as images and any other referenced documents), or only the related documents.&lt;/li&gt;
&lt;li&gt;Choose the locales to translate the content into.&lt;/li&gt;
&lt;li&gt;Specify the related document types to localize, if you chose to translate them in step 1. You can also decide whether to translate only new related documents or to overwrite existing translated documents. Select the “Translate text content option“ for automatic AI translation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The home page depends on some related documents. Therefore, the recommended option is "This document and related documents":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ndn7txkzi2wyoarjv3h.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ndn7txkzi2wyoarjv3h.gif" alt="An example of how the Localize Content UI is used in the automatic translation workflow." width="670" height="738"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the last step of the wizard, you can select which types of related documents you want to localize. Apostrophe will automatically translate them together with the current document.&lt;/p&gt;

&lt;p&gt;The following notifications will inform you that the current document and the selected related documents were translated successfully:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fden4pfl18m3x5lsn3vty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fden4pfl18m3x5lsn3vty.png" alt="Example of notifications received after successful localization and translation of content." width="703" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Visit the home page of the Spanish version of your e-commerce site, and you will now see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqvx8jljdge856reqgmb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqvx8jljdge856reqgmb.gif" alt="An example of scrolling through the ecommerce page after translating it to spanish." width="600" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wow, a good start! However, notice that there are no products or categories on the page. This is because they need to be translated as well.&lt;/p&gt;

&lt;p&gt;Before translating them, you may find that you are not satisfied with the translation results provided by the AI. No problem, you can always edit the localized documents using Apostrophe's editing capabilities:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qkzmdxhvzkpp4zw4nsd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qkzmdxhvzkpp4zw4nsd.png" alt="Example of editing a page's settings in the modal after translating it into Spanish." width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The translated pages will be saved as drafts in the selected locale(s). This way, a content manager will have to manually review and approve them for publication. Also, Apostrophe will automatically mark the AI-translated fields, as they require additional attention from your editors. &lt;/p&gt;

&lt;p&gt;Also, keep in mind that Automatic Translation also translates the SEO meta title and description into the target language. To verify that, explore the “SEO” tab of the “Summer Collection 2024” page mentioned in the previous chapter:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5ad7m6fz64d1it2jmo2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5ad7m6fz64d1it2jmo2.png" alt="Example of UI for SEO fields that have automated translations applied." width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Observe how both the meta title and the SEO description were translated into Spanish.&lt;/p&gt;

&lt;p&gt;Awesome, you are ready to translate products and categories!&lt;/p&gt;

&lt;p&gt;Click on "Content" in the top left menu, select a content category, and follow the same procedure as above to translate each piece:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0wipn2v56q0fwrtjf807.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0wipn2v56q0fwrtjf807.png" alt="Example of the localize menu action on a Product piece type." width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Considering that pieces often involve or are connected to other documents, you can choose the “this document and related documents” option to speed up the translation process.&lt;/p&gt;

&lt;p&gt;Use AI to translate all pages and pieces in your project, and this is the end result you will get in just a few minutes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffu4h5u3zee5gmorlbig6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffu4h5u3zee5gmorlbig6.gif" alt="The finished product after translating the ecommerce page and all of the product categories that are included." width="480" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While it may not be perfect, this is definitely impressive!&lt;/p&gt;

&lt;p&gt;Similarly, this is what an AI-translated page product looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh4xul0mxgr1l841blf4l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh4xul0mxgr1l841blf4l.png" alt="Example product show page after it's been translated to spanish via the localization capability." width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Automatic Translation offers a wide range of options and customizations to tailor the AI content translation experience to your needs. For more details, see the &lt;a href="https://apostrophecms.com/extensions/automatic-translation#usage" rel="noopener noreferrer"&gt;developer documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As proven here, leveraging AI for translation significantly accelerates the content localization process. That creates a tremendous opportunity to make your site accessible to millions of users worldwide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits of SEO Assistant and Automatic Translation Extensions
&lt;/h2&gt;

&lt;p&gt;A few months ago, it was merely a guess, but now there is little doubt that integrating AI directly into a CMS introduces substantial benefits. Specifically, using AI for SEO and translation provides these advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expanding content reach&lt;/strong&gt;: AI-driven SEO and translation help to reach a broader audience. Enhanced SEO rankings increase your site's visibility, while localized content resonates with diverse audiences in different regions, significantly boosting the chances of attracting new users to your pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improving productivity&lt;/strong&gt;: Tasks that once required hours of meticulous work can now be completed in seconds. AI handles the heavy lifting, leaving you to refine and personalize the generated content. This shift reduces time spent on manual tasks and helps you concentrate on what truly matters, such as creating outstanding user experiences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saving money&lt;/strong&gt;: By delegating specialized tasks like SEO optimization and translation to AI, you may no longer need to hire a team of dedicated experts in those fields. Such a cost-effective strategy enables you to allocate resources more efficiently while still achieving high-quality results, ultimately saving your business money.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The above improvements are made possible by SEO Assistant and Automatic Translation, the new AI-powered Apostrophe Pro extensions. &lt;/p&gt;

&lt;p&gt;These are just a few of the several powerful features available in the &lt;a href="http://apostrophecms.com/pricing" rel="noopener noreferrer"&gt;Pro and Assembly tiers&lt;/a&gt; of Apostrophe, which also include &lt;a href="https://apostrophecms.com/blog/how-to-use-apostrophe-s-advanced-permission-to-manage-editing-rights" rel="noopener noreferrer"&gt;advanced permissions&lt;/a&gt;, document version history, and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we looked at what the new AI-powered SEO Assistant and Automatic Translation extensions have to offer. With AI integrated directly into Apostrophe, you can now generate SEO metadata and translate content into any language within seconds and a handful of clicks.&lt;/p&gt;

&lt;p&gt;While the current results are already spectacular, this is just the beginning. The Apostrophe team is continually working to discover new and effective AI integrations, and you should be excited about what the future holds.&lt;/p&gt;

&lt;p&gt;To get a preview of what's next for Apostrophe, keep an eye on our &lt;a href="https://portal.productboard.com/apostrophecms/1-product-roadmap/tabs/2-planned" rel="noopener noreferrer"&gt;product roadmap&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cms</category>
      <category>translation</category>
      <category>seo</category>
    </item>
    <item>
      <title>How To Set Up the Advanced Permission Pro Extension</title>
      <dc:creator>Antonello Zanini</dc:creator>
      <pubDate>Tue, 07 May 2024 20:01:12 +0000</pubDate>
      <link>https://dev.to/apostrophecms/how-to-set-up-the-advanced-permission-pro-extension-5b29</link>
      <guid>https://dev.to/apostrophecms/how-to-set-up-the-advanced-permission-pro-extension-5b29</guid>
      <description>&lt;p&gt;The Advanced Permission module is a Pro extension that adds more granular control over content permissions. It provides the ability to create custom groups and assign them to users directly in the admin UI. &lt;/p&gt;

&lt;p&gt;A group is a set of rules that specify how users can create, edit, delete, and publish content, including creating new users and groups. The module provides granular control, allowing admins to give a group Create, Edit, Delete, and Publish permissions for each piece type on the site. Those four core permissions can be extended with new custom permissions.&lt;/p&gt;

&lt;p&gt;The Advanced Permission extension also enables admins to give groups and individual users granular per-document permissions on specific pages and pieces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The name of the npm package of the Advanced Permission module is &lt;code&gt;@apostrophecms-pro/advanced-permission&lt;/code&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;The requirements for setting up the Advanced Permission module are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Apostrophe 3+ application: If you don’t already have one, make sure you meet the requirements and then follow the instructions in the development setup guide.&lt;/li&gt;
&lt;li&gt;An Apostrophe Pro or Apostrophe Assembly subscription: To gain access to the Advanced Permission module, you first need to join Apostrophe Pro or Apostrophe Assembly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installing the Advanced Permission Module
&lt;/h2&gt;

&lt;p&gt;After joining Apostrophe Pro or Apostrophe Assembly, you'll be added to the &lt;code&gt;@apostrophecms-pro&lt;/code&gt; npm organization. This gives you the ability to install the Advanced Permission module in your Apostrophe project. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WARNING:&lt;/strong&gt; If you try to add &lt;code&gt;@apostrophecms-pro/advanced-permission&lt;/code&gt; to your project's dependency before being added to &lt;code&gt;@apostrophecms-pro&lt;/code&gt;, you’ll get the following error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@apostrophecms-pro/advanced-permission' is not in this registry.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@apostrophecms-pro/advanced-permission&lt;/code&gt; is a private npm package. You must authenticate in npm before installing it. The recommended authentication method changes depending on whether you’re in a development or production environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Development Setup&lt;/strong&gt;&lt;br&gt;
In a development environment, run the following command to start the npm authentication procedure:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This will produce an output as below:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Login at:
https://www.npmjs.com/login?next=/login/cli/&amp;lt;NPM_HASH&amp;gt;
Press ENTER to open in the browser...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Press ENTER to open the npm login page in your default browser. Type in your credentials, sign in, and return to the CLI.&lt;/p&gt;

&lt;p&gt;After logging in successfully, you’ll receive the following message: &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Logged in on https://registry.npmjs.org/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; To keep the session alive, npm will create a global &lt;code&gt;.npmrc&lt;/code&gt; configuration file containing your access token in the following line:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//registry.npmjs.org/:_authToken=&amp;lt;YOUR_NPM_ACCESS_TOKEN&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;cd&lt;/code&gt; to your project folder and then install the Advanced Permission Pro extension with the command below:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @apostrophecms-pro/advanced-permission
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;npm will use the authenticated URL in &lt;code&gt;.npmrc&lt;/code&gt; to download the private &lt;code&gt;@apostrophecms-pro/advanced-permission&lt;/code&gt; package. It will then install it as per usual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production Setup&lt;/strong&gt;&lt;br&gt;
In a production environment, authenticate npm commands by setting up a granular token related to the &lt;code&gt;@apostrophecms-pro&lt;/code&gt; organization. Follow the official guide for guidance. &lt;/p&gt;

&lt;p&gt;After setting up an npm granular token, add a &lt;code&gt;.npmrc&lt;/code&gt; file to the root folder of your Apostrophe project. Initialize it with the following line:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//registry.npmjs.org/:_authToken=${NPM_TOKEN}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;When launching an npm command, &lt;code&gt;${NPM_TOKEN}&lt;/code&gt; will be replaced with the value read from the &lt;code&gt;NPM_TOKEN&lt;/code&gt; environment variable. That URL will be used to retrieve packages from the npm registry as an authenticated user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WARNING:&lt;/strong&gt; Adding a local &lt;code&gt;.npmrc&lt;/code&gt; file will override your global &lt;code&gt;.npmrc&lt;/code&gt; file npm created on &lt;code&gt;npm login&lt;/code&gt;. To avoid authorization issues while installing private packages from the &lt;code&gt;@apostrophecms-pro&lt;/code&gt; npm organization, set the &lt;code&gt;NPM_TOKEN&lt;/code&gt; environment variable on your local machine when working with a project that has a &lt;code&gt;.npmrc&lt;/code&gt; defined as above. You can do that in your &lt;code&gt;.bashrc&lt;/code&gt; file using &lt;code&gt;EXPORT NPM_TOKEN=&lt;/code&gt;&lt;code&gt;"&lt;/code&gt;&lt;code&gt;&amp;lt;YOUR_PERSONAL_NPM_TOKEN&amp;gt;&lt;/code&gt;&lt;code&gt;"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In the production server, set the &lt;code&gt;NPM_TOKEN&lt;/code&gt; env to the value of your npm granular token:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export NPM_TOKEN="&amp;lt;YOUR_NPM_GRANULAR_TOKEN&amp;gt;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;When launching &lt;code&gt;npm install&lt;/code&gt;, the production environment will now be able to install the &lt;code&gt;"@apostrophecms-pro/doc-template-library"&lt;/code&gt; dependency in &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable the Module in Apostrophe
&lt;/h2&gt;

&lt;p&gt;Enable the Advanced Permission extension by adding the following two modules to the &lt;code&gt;app.js&lt;/code&gt; file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// app.js

require('apostrophe')({
  shortName: 'my-project',
  modules: {
    // other modules...

    // enable the Advanced Permission extension
    '@apostrophecms-pro/advanced-permission-group': {},
    '@apostrophecms-pro/advanced-permission': {}
  },
  // remaining configs...
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; To use Advanced Permission in a multisite project, you can add the two modules outlined above to both the &lt;code&gt;site/index.js&lt;/code&gt; and &lt;code&gt;dashboard/index.js&lt;/code&gt; files. Adding the modules to the two files will enable the Pro extension for both the dashboard and all individual sites. It’s also possible to enable the extension only for the dashboard on individual sites. Before starting to use the Advanced Permission module in the dashboard, make sure the &lt;code&gt;privateDashboards&lt;/code&gt; feature is set to &lt;code&gt;false&lt;/code&gt;. This setting won’t affect individual sites.&lt;/p&gt;

&lt;p&gt;On the first run of your project after enabling the Advanced Permission module, some database migrations will automatically occur. These create a group for each role found in existing users and link them to the group corresponding to their &lt;code&gt;role&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;After adding the Advanced Permission extension, a “Groups” item will appear in the top left menu in the admin bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Custom Permissions
&lt;/h2&gt;

&lt;p&gt;In addition to the Create, Edit, Delete, and Publish core permissions, new custom permissions can be defined through the &lt;code&gt;permissions&lt;/code&gt; object in a piece-type &lt;code&gt;index.js&lt;/code&gt; file for an individual piece or in &lt;code&gt;@apostrophecms/page-type/index.js&lt;/code&gt; for all pages. You can’t define custom permissions in individual page types.&lt;/p&gt;

&lt;p&gt;Much like the &lt;code&gt;fields&lt;/code&gt; object, the &lt;code&gt;permissions&lt;/code&gt; object takes an &lt;code&gt;add&lt;/code&gt; property. This accepts permission properties with objects having the following three properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;label&lt;/code&gt;: A string that describes the new permission to the user. It determines what is shown in the group and per-document permission grids.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requires&lt;/code&gt;: An optional string with the name of an existing permission or an object with multiple permissions (e.g., &lt;code&gt;requires: { $or: [ 'edit', 'create' ] }&lt;/code&gt;). It determines whether the new permission is dependent on any other permission in the grid. For example, &lt;code&gt;requires: 'publish'&lt;/code&gt; would require the admin to select the "Publish" permission for the document or document type before they could select the new permission.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;perDoc&lt;/code&gt;: An optional boolean to define whether the new permission should appear in the user and group per-document permission matrices. The default value is &lt;code&gt;false&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, you can define a custom &lt;code&gt;decriptionField&lt;/code&gt; permission with:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = {
  // ...
  fields: {
    // ...
  },
  // ...
  permissions: {
    add: {
      decriptionField: {
        label: 'Description',
        requires: 'publish',
        perDoc: false
      }
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;TIP:&lt;/strong&gt; By using &lt;code&gt;permissions&lt;/code&gt; in an Apostrophe core module at the project level, you can add a new custom permission to multiple document types. For example, extending the &lt;code&gt;@apostrophecms/piece-type&lt;/code&gt; module with the &lt;code&gt;permissions&lt;/code&gt; object would add the custom permissions to all pieces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;editPermission&lt;/code&gt;: Limiting Access to a Single Field&lt;/strong&gt;&lt;br&gt;
After defining a custom permission, you can assign it to a specific field of a piece type by using &lt;code&gt;editPermission&lt;/code&gt;. This schema field property takes an object with the following two properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;action&lt;/code&gt;: A string with the name of one of the built-in permissions (e.g., &lt;code&gt;'&lt;/code&gt;&lt;code&gt;create&lt;/code&gt;&lt;code&gt;'&lt;/code&gt;) or a custom permission.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type&lt;/code&gt;: A string with the name of the module that permission is associated with. For core modules, make sure to prefix the module name with &lt;code&gt;@apostrophecms/&lt;/code&gt; or &lt;code&gt;@apostrophecms-pro/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, you can assign the custom &lt;code&gt;decriptionField&lt;/code&gt; permission to the &lt;code&gt;description&lt;/code&gt; field of a &lt;code&gt;product&lt;/code&gt; as below:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = {
  // ...
  fields: {
    add: {
      description: {
        type: 'string',
        label: 'Description',
        textarea: true,
        editPermission: {
          action: 'descriptionField', // custom permission
          type: 'product'
        }
      },
      // other fields...
    },
  },
  // ...
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Only users who have been granted the Create and/or Modify permissions as well as the &lt;code&gt;descriptionField&lt;/code&gt; permission will now be able to edit the description of &lt;code&gt;product&lt;/code&gt; pieces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; &lt;code&gt;perDoc: true&lt;/code&gt; isn’t compatible with the &lt;code&gt;editPermission&lt;/code&gt; feature. To grant a user per-document custom permission on a given field of a piece, follow this procedure instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define a custom permission with &lt;code&gt;perDoc: false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;editPermission&lt;/code&gt; to assign the custom permission to the desired field of the given piece.&lt;/li&gt;
&lt;li&gt;Define a group with the selected custom permission for the given piece.&lt;/li&gt;
&lt;li&gt;Assign the group to the user.&lt;/li&gt;
&lt;li&gt;Grant the user per-document permission to Modify the documents of the given piece.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A complete example: defining the custom “Pricing” permission&lt;br&gt;
Suppose your project has a &lt;code&gt;service&lt;/code&gt; piece. You want to add a custom “Pricing” permission so that only users with this permission can edit the price of services on your site. That can be achieved with the &lt;code&gt;permissions&lt;/code&gt; and &lt;code&gt;editPermission&lt;/code&gt; objects in &lt;code&gt;modules/service/index.js&lt;/code&gt; as below:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = {
  extend: '@apostrophecms/piece-type',
  options: {
    label: 'Service'
  },
  fields: {
    add: {
      title: {
        type: 'string',
        label: 'Title'
      },
      description: {
        type: 'string',
        label: 'Description',
        textarea: true
      },
      price: {
        type: 'float',
        label: 'Price',
        editPermission: {
          action: 'pricingField',
          type: 'service'
        }
      }
    },
    group: {
      basics: {
        label: 'Basics',
        fields: [
          'title',
          'description',
          'price'
        ]
      }
    }
  },
  permissions: {
    add: {
      pricingField: {
        label: 'Pricing'
      }
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Customized permission checks&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;editPermission&lt;/code&gt; is not the only way to take advantage of custom permissions. You can also verify if a particular user has a custom permission (including per-document permissions) when coding your own routes and methods server-side with the following line:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;self.apos.permission.can(req, '&amp;lt;custom_permission_name&amp;gt;', doc)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The function returns &lt;code&gt;true&lt;/code&gt; if the user has the custom permission, &lt;code&gt;false&lt;/code&gt; otherwise. This approach opens the door to custom use cases involving permission verification.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Integrate Astro with ApostropheCMS pt. 2</title>
      <dc:creator>Antonello Zanini</dc:creator>
      <pubDate>Thu, 21 Mar 2024 16:45:38 +0000</pubDate>
      <link>https://dev.to/apostrophecms/how-to-integrate-astro-with-apostrophecms-pt-2-46mc</link>
      <guid>https://dev.to/apostrophecms/how-to-integrate-astro-with-apostrophecms-pt-2-46mc</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Let’s look at the remaining aspects of the ApostropheCMS integration into Astro!&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Achieved So Far
&lt;/h2&gt;

&lt;p&gt;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. &lt;/p&gt;

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

&lt;p&gt;This is what the blog index page currently looks like in the Astro frontend:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fofeakssu1tqgur43nxij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fofeakssu1tqgur43nxij.png" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this is the dedicated blog post page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpd39ddriuiln7if0e3ki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpd39ddriuiln7if0e3ki.png" width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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!&lt;/p&gt;

&lt;h2&gt;
  
  
  Add React to Astro
&lt;/h2&gt;

&lt;p&gt;Astro comes with a CLI command to automate the setup of React. In the Astro project folder, launch the command below to install &lt;code&gt;@astrojs/react&lt;/code&gt; and configure it to use React components in Astro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx astro add react
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will launch a CLI wizard where you will be asked a few questions. Answer “yes” to each of them to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the libraries required for integrating React.&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;react()&lt;/code&gt; integration to the &lt;code&gt;astro.config.mjs&lt;/code&gt; configuration file.&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;tsconfig.json&lt;/code&gt; file for JSX support.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This process will take a while, so be patient. If something goes wrong, &lt;a href="https://docs.astro.build/en/guides/integrations-guide/react/" rel="noopener noreferrer"&gt;refer to the official documentation&lt;/a&gt; for more information.&lt;/p&gt;

&lt;p&gt;Excellent! You can now use React components in Astro. &lt;/p&gt;

&lt;p&gt;Prepare your &lt;code&gt;astro-frontend&lt;/code&gt; project to host React components by adding a &lt;code&gt;components&lt;/code&gt; folder to &lt;code&gt;./src&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customize the UI of Your ApostropheCMS-Powered Astro Blog
&lt;/h2&gt;

&lt;p&gt;Follow the steps below and learn how to improve the UI and interactivity of the ApostropheCMS Astro blog application!&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://github.com/Tonel/astro-frontend" rel="noopener noreferrer"&gt;GitHub repository supporting the article&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/Tonel/astro-frontend.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that we will use the &lt;code&gt;apostrophecms/astro-frontend&lt;/code&gt; project as a starting point.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;General UI Improvements&lt;/strong&gt;&lt;br&gt;
If you inspect the Astro template components under the &lt;code&gt;./src/templates&lt;/code&gt; folder, you will see that they all share this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// JS imports
---

&amp;lt;section class='bp-content'&amp;gt;
  &amp;lt;!-- HTML structure --&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means you can target the &lt;code&gt;bp-content&lt;/code&gt; 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 &lt;a href="https://docs.astro.build/en/guides/styling/#import-a-local-stylesheet" rel="noopener noreferrer"&gt;local stylesheet file&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this tutorial, we are going to use &lt;a href="https://en.wikipedia.org/wiki/Sass_(style_sheet_language)" rel="noopener noreferrer"&gt;SCSS&lt;/a&gt;. Thus, add the &lt;code&gt;sass&lt;/code&gt; npm package to your project’s dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install sass 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create a &lt;code&gt;styles&lt;/code&gt; folder inside &lt;code&gt;./src&lt;/code&gt; and add the following &lt;code&gt;blog.scss&lt;/code&gt; file to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// .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;

    &amp;amp;:hover {
      text-decoration: underline;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;blog&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Import &lt;code&gt;.src/styles/blog.scss&lt;/code&gt; and set the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; class to &lt;code&gt;blog&lt;/code&gt; in &lt;code&gt;[...slug].astro&lt;/code&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// ./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' // &amp;lt;-- import the custom SCSS file  

const aposData = await aposPageFetch(Astro.request)
const bodyClass = `blog` // &amp;lt;-- update the &amp;lt;body&amp;gt; class

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

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:4321/blog&lt;/code&gt; in the browser and you will now see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqezb6s44qsvngan7l39v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqezb6s44qsvngan7l39v.png" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Similarly, this is what a blog post page will look like: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpharzlz10k9iw51prku4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpharzlz10k9iw51prku4.png" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Awesome! The appearance of the blog has already improved a bit, but there is still much to be done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create a Blog Card Component&lt;/strong&gt;&lt;br&gt;
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!&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;./src/components&lt;/code&gt; folder, add a &lt;code&gt;BlogCard.jsx&lt;/code&gt; file that contains these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ./src/components/BlogCard.jsx

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

export default function BlogPost({ blog }) {
  return (
    &amp;lt;div className='blog-card'&amp;gt;
      &amp;lt;div className='date'&amp;gt;
        Released On {dayjs(blog.publishedAt).format('MMMM D, YYYY')}
      &amp;lt;/div&amp;gt;
      &amp;lt;div className='title'&amp;gt;
        &amp;lt;a href={blog._url}&amp;gt;{blog.title}&amp;lt;/a&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This JSX React component wraps and extends the following blog post representation logic in &lt;code&gt;./src/templates/BlogIndexPage.astro&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h4&amp;gt;
  Released On { dayjs(piece.publishedAt).format('MMMM D, YYYY') }
&amp;lt;/h4&amp;gt;
&amp;lt;h3&amp;gt;
  &amp;lt;a href={ piece._url }&amp;gt;{ piece.title }&amp;lt;/a&amp;gt;
&amp;lt;/h3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Notice that the first line of the component is an import to a SCSS file. So, define the &lt;code&gt;BlogCard.scss&lt;/code&gt; file this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ./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;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;strong&gt;Import the Blog Card Component in the Blog Index Page&lt;/strong&gt;&lt;br&gt;
Open over the &lt;code&gt;BlogIndexPage.astro&lt;/code&gt; template component and add the &lt;code&gt;BlogCard&lt;/code&gt; import in the JavaScript section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import BlogCard from '../components/BlogCard'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do not need to import &lt;code&gt;BlogCard.scss&lt;/code&gt; as well, since the React component already imports it.&lt;/p&gt;

&lt;p&gt;In the template section of the Astro component, replace the blog post HTML representation logic with &lt;code&gt;&amp;lt;BlogCard /&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  pieces.map((piece) =&amp;gt; {
    return &amp;lt;BlogCard blog={piece} /&amp;gt;
  })
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that Astro will manage the keys for each DOM element under the hood. So, you do not need to manually provide a &lt;code&gt;key&lt;/code&gt; prop as you would normally do in React.&lt;/p&gt;

&lt;p&gt;This is where the Astro magic happens. Your &lt;code&gt;BlogIndexPage.astro&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;BlogIndexPage.astro&lt;/code&gt; file will now contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// ./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 &amp;lt;= totalPages; i++) {
  console.log(page, currentPage)
  pages.push({
    number: i,
    current: i === currentPage,
    url: setParameter(Astro.url, 'page', i),
  })
}
---
&amp;lt;section class='bp-content'&amp;gt;
  &amp;lt;h1&amp;gt;{page.title}&amp;lt;/h1&amp;gt;

  &amp;lt;h2&amp;gt;Blog Posts&amp;lt;/h2&amp;gt;

  {
    pieces.map((piece) =&amp;gt; {
      return &amp;lt;BlogCard blog={piece} /&amp;gt;
    })
  }

  {pages.map(page =&amp;gt; (
    &amp;lt;a
      class={(page === currentPage) ? 'current' : ''} 
      href={page.url}&amp;gt;{page.number}
    &amp;lt;/a&amp;gt;
  ))}
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more information on how &lt;code&gt;aposSetQueryParameter()&lt;/code&gt; works, &lt;a href="https://github.com/apostrophecms/apostrophe-astro?tab=readme-ov-file#apossetqueryparameter-working-with-query-parameters" rel="noopener noreferrer"&gt;check out the official documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is what the &lt;code&gt;http://localhost:4321/blog&lt;/code&gt; index page will look like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5fmdi6xa1tjeji7mljc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5fmdi6xa1tjeji7mljc.png" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;To do so, stop the local development server and build your application with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/dist&lt;/code&gt; folder in your project will now contain the Astro bundle. Serve it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run serve 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;http://localhost:4321/blog&lt;/code&gt; 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="blog-card"&amp;gt;
  &amp;lt;div class="date"&amp;gt;
    Released On February 1, 2023
   &amp;lt;/div&amp;gt;
   &amp;lt;div class="title"&amp;gt;
     &amp;lt;a href="/blog/lorem-ipsum-1"&amp;gt;Lorem Ipsum 1&amp;lt;/a&amp;gt;
   &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;a href="https://docs.astro.build/en/reference/directives-reference/" rel="noopener noreferrer"&gt;Astro template directives&lt;/a&gt;. Learn more in the next section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Client-Side Interaction to the Blog Card Component&lt;/strong&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;You could easily achieve that by passing an &lt;code&gt;onClick&lt;/code&gt; handler to the &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; card in the React component, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ./src/components/BlogCard.scss

// ...

export default function BlogPost({ blog }) {
  return (
    &amp;lt;div
      className='blog-card'
      onClick={(e) =&amp;gt; {
        e.preventDefault()
        // redirect to the blog post page
        window.location.href = blog._url
      }}
    &amp;gt;
      {/* ... */}
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To help users understand this interaction, you should also add a hover effect to the card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ./src/components/BlogCard.scss

.blog-card {
  // ...

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

  // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that this is just a simple example but you could implement more complex interactions using React state management and hooks.&lt;/p&gt;

&lt;p&gt;Visit the &lt;code&gt;http://localhost:4321/blog&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;If you are not familiar with this process, &lt;a href="https://en.wikipedia.org/wiki/Hydration_(web_development)" rel="noopener noreferrer"&gt;hydration&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;To instruct Astro to hydrate the blog card component in the client, pass the &lt;code&gt;client:load&lt;/code&gt; directive to &lt;code&gt;&amp;lt;BlogCard /&amp;gt;&lt;/code&gt; in &lt;code&gt;BlogIndexPage.astro&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  pieces.map((piece) =&amp;gt; {
    return &amp;lt;BlogCard blog={piece} client:load /&amp;gt;
  })
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interact with the blog index page again, and the click interaction will now work as desired:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqggod9fmjujlsseyppxc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqggod9fmjujlsseyppxc.gif" width="1492" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add a Pagination Component&lt;/strong&gt; &lt;br&gt;
You must have noticed that little "1" at the bottom of the index blog page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F49xevcj92m76qzak0qeu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F49xevcj92m76qzak0qeu.gif" width="1492" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frr5nvrfdmn5hgjr4sft3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frr5nvrfdmn5hgjr4sft3.png" width="505" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Currently, the pagination logic is handled by the following lines in &lt;code&gt;./src/templates/BlogIndexPage.astro&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{pages.map(page =&amp;gt; (
  &amp;lt;a
    class={(page === currentPage) ? 'current' : ''} 
    href={page.url}&amp;gt;{page.number}
  &amp;lt;/a&amp;gt;
))}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ./src/components/BlogPagination.jsx

import './BlogPagination.scss'

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

&lt;/div&gt;



&lt;p&gt;As before, this component contains both a crawlable link for SEO and a more intuitive click interaction for users.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;BlogPagination.scss&lt;/code&gt; style file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ./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;
      &amp;amp;:hover {
        text-decoration: none;
      }
    }

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

    &amp;amp;:hover {
      color: white;
      background-color: #111111;
      cursor: pointer;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import the component in &lt;code&gt;BlogIndexPage.astro&lt;/code&gt; and use it to replace the aforementioned pagination lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// ./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 &amp;lt;= totalPages; i++) {
  pages.push({
    number: i,
    current: i === currentPage,
    url: setParameter(Astro.url, 'page', i),
  })
}
---
&amp;lt;section class='bp-content'&amp;gt;
  &amp;lt;h1&amp;gt;{page.title}&amp;lt;/h1&amp;gt;

  &amp;lt;h2&amp;gt;Blog Posts&amp;lt;/h2&amp;gt;

  {
    pieces.map((piece) =&amp;gt; {
      return &amp;lt;BlogCard blog={piece} client:load /&amp;gt;
    })
  }

  &amp;lt;BlogPagination pages={pages} client:load /&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, notice the &lt;code&gt;client:load&lt;/code&gt; directive required to load the click interactivity in the client. This is what the new pagination component will look like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1nn5b6sixycc7evbizcr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1nn5b6sixycc7evbizcr.png" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you click on one of the active number elements, you will be redirected to the selected pagination page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhj0lextu0gxsamkqqsvt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhj0lextu0gxsamkqqsvt.gif" width="1492" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complete the Blog Index Page&lt;/strong&gt;&lt;br&gt;
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 &lt;code&gt;h1&lt;/code&gt; class from the global &lt;code&gt;blog.scss&lt;/code&gt; style file to the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; element in &lt;code&gt;BlogIndexPage.astro&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;Also, assume you want to center the &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; element in this Astro component. Instead of defining a global CSS rule, you can use the Astro special &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag to write scoped CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;style&amp;gt;
  h2 {
    text-align: center
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The final &lt;code&gt;BlogIndexPage.astro&lt;/code&gt; template component will contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// ./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 &amp;lt;= totalPages; i++) {
  pages.push({
    number: i,
    current: i === currentPage,
    url: setParameter(Astro.url, 'page', i),
  })
}
---
&amp;lt;style&amp;gt;
  h2 {
    text-align: center
  }
&amp;lt;/style&amp;gt;

&amp;lt;section class='bp-content'&amp;gt;
  &amp;lt;h1 class='h1'&amp;gt;{page.title}&amp;lt;/h1&amp;gt;

  &amp;lt;h2&amp;gt;Blog Posts&amp;lt;/h2&amp;gt;

  {
    pieces.map((piece) =&amp;gt; {
      return &amp;lt;BlogCard blog={piece} client:load /&amp;gt;
    })
  }

  &amp;lt;BlogPagination pages={pages} client:load /&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how the definitive blog page index will appear: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fagj8bdqgou3ikpalljiy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fagj8bdqgou3ikpalljiy.gif" width="1492" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Style the Blog Show Page&lt;/strong&gt;&lt;br&gt;
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 &lt;code&gt;BlogShowPage.astro&lt;/code&gt; as below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// ./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
---
&amp;lt;style&amp;gt;
  .publication-date {
    text-align: center;
    font-style: italic;
    color: #505050;
    font-size: 16px;
    margin-bottom: 25px;
  }
&amp;lt;/style&amp;gt;

&amp;lt;section class='bp-content'&amp;gt;
  &amp;lt;h1 class='h1'&amp;gt;{ piece.title }&amp;lt;/h1&amp;gt;
  &amp;lt;div class='publication-date'&amp;gt;
    { dayjs(piece.publishedAt).format('MMMM D, YYYY') }
  &amp;lt;/h4&amp;gt;
  &amp;lt;AposArea area={main} /&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Astro component has a custom style for the date element and relies on the global &lt;code&gt;h1&lt;/code&gt; class seen earlier.&lt;/p&gt;

&lt;p&gt;The new blog show page definitely looks better: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9toj2lsqr4hml93736t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9toj2lsqr4hml93736t.png" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you inspect the blog post content rendered in the &lt;code&gt;AposArea&lt;/code&gt; component, you will notice that it is nothing more than a list of &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, each of which represents a paragraph:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fceafxwfml6bjc3mgmlji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fceafxwfml6bjc3mgmlji.png" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a first approach to style that section of the page, you might consider adding a CSS rule for &lt;code&gt;p&lt;/code&gt; tags in &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;style&amp;gt;
  // ...

  p {
    margin-bottom: 35px;
    line-height: 35px;
    font-size: 18px;
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will not work because that CSS snippet is scoped and the &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; elements are not directly in the template component but within &lt;code&gt;AposArea&lt;/code&gt;. In detail, it is ApostropheCMS that takes care of handling and returning that content.&lt;/p&gt;

&lt;p&gt;To define CSS rules for HTML documents whose content lives outside Astro—as in this case—Astro provides a special &lt;code&gt;global()&lt;/code&gt; CSS function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;style&amp;gt;
  // ...

  :global(p) {
    margin-bottom: 35px;
    line-height: 35px;
    font-size: 18px;
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That function allows you to write global, unscoped CSS in the &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag. &lt;/p&gt;

&lt;p&gt;Equivalently, you can add another &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag marked with the &lt;code&gt;is:global&lt;/code&gt; attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;style is:global&amp;gt;
  p {
    margin-bottom: 35px;
    line-height: 35px;
    font-size: 18px;
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CSS rules marked as “global” will apply to all the HTML elements in the DOM subtree of the Astro component.&lt;/p&gt;

&lt;p&gt;Put it all together, and you will get the following &lt;code&gt;BlogShowPage.astro&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// ./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
---
&amp;lt;style&amp;gt;
  .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;
  }
&amp;lt;/style&amp;gt;

&amp;lt;section class='bp-content'&amp;gt;
  &amp;lt;h1 class='h1'&amp;gt;{ piece.title }&amp;lt;/h1&amp;gt;
  &amp;lt;div class='publication-date'&amp;gt;
    { dayjs(piece.publishedAt).format('MMMM D, YYYY') }
  &amp;lt;/h4&amp;gt;
  &amp;lt;AposArea area={main} /&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what the blog show page will now look like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjl9ma301f0rzu91hlidr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjl9ma301f0rzu91hlidr.png" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations! You now know how to build a site that relies on Astro on the frontend and uses ApostropheCMS for content management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;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!&lt;/p&gt;

&lt;p&gt;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. &lt;a href="https://apostrophecms.com/demo" rel="noopener noreferrer"&gt;Try Apostrophe today&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How does Astro communicate with ApostropheCMS?&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;@apostrophecms/apostrophe-astro&lt;/code&gt; npm library automatically proxies certain ApostropheCMS endpoints in Astro. In particular, these are the routes proxied by the package:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/apos-frontend/[...slug]&lt;/code&gt;: For serving ApostropheCMS assets.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/uploads/[...slug]&lt;/code&gt;: For serving ApostropheCMS uploaded assets.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/v1/[...slug]&lt;/code&gt; and &lt;code&gt;/[locale]/api/v1/[...slug]&lt;/code&gt;: For contacting the ApostropheCMS API endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/login&lt;/code&gt; and &lt;code&gt;/[locale]/login&lt;/code&gt;: For accessing the ApostropheCMS login page.&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;p&gt;&lt;strong&gt;Do ApostropheCMS widget players still work in Astro?&lt;/strong&gt;&lt;br&gt;
In ApostropheCMS, &lt;a href="https://v3.docs.apostrophecms.org/guide/custom-widgets.html#client-side-javascript-for-widgets" rel="noopener noreferrer"&gt;widget players&lt;/a&gt; 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 &lt;a href="https://docs.astro.build/en/guides/client-side-scripts/#web-components-with-custom-elements" rel="noopener noreferrer"&gt;Astro web components&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Defining and using an &lt;code&gt;HTMLElement&lt;/code&gt; 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 &lt;code&gt;VideoWidget.astro&lt;/code&gt; &lt;a href="https://github.com/apostrophecms/astro-frontend/blob/main/src/widgets/VideoWidget.astro" rel="noopener noreferrer"&gt;in the apostrophecms/astro-frontend project&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How are error messages presented in Astro?&lt;/strong&gt;&lt;br&gt;
Astro comes with a default error page that contains the error message, the snippet that caused the error, and the stack trace as below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzmvp7rgmcjsdrn3u5gy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzmvp7rgmcjsdrn3u5gy8.png" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you receive the following error after integrating the &lt;code&gt;@apostrophecms/apostrophe-astro&lt;/code&gt; library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Only URLs with a scheme in: file and data are supported by the default ESM
loader. Received protocol 'virtual:'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, you most likely left out this part from the &lt;code&gt;astro.config.mjs&lt;/code&gt; file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default defineConfig({
  // other settings here ...
  vite: {
    ssr: {
      noExternal: [ '@apostrophecms/apostrophe-astro' ],
    }
  }
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Without this logic, the &lt;code&gt;virtual:&lt;/code&gt; URLs used to access configuration information will cause the build to fail.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>opensource</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to Integrate Astro With ApostropheCMS pt. 1</title>
      <dc:creator>Antonello Zanini</dc:creator>
      <pubDate>Thu, 21 Mar 2024 16:12:02 +0000</pubDate>
      <link>https://dev.to/apostrophecms/how-to-integrate-astro-with-apostrophecms-pt-1-366m</link>
      <guid>https://dev.to/apostrophecms/how-to-integrate-astro-with-apostrophecms-pt-1-366m</guid>
      <description>&lt;p&gt;Apostrophe recently announced the &lt;a href="https://apostrophecms.com/blog/introducing-the-apostrophe-astro-integration" rel="noopener noreferrer"&gt;integration for the Astro web framework&lt;/a&gt;, one of the hottest technologies in the IT community. Astro has become so popular because it offers developers the ability to create efficient and SEO-oriented web applications using multiple UI frameworks like React and Vue.js, even on the same project.&lt;/p&gt;

&lt;p&gt;In the first part of this two-article series, you will learn what Astro is, what the ApostropheCMS Astro Integration Starter Kit is, and how to use it to manage the pages and content of an Astro frontend via ApostropheCMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Astro?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://astro.build/" rel="noopener noreferrer"&gt;Astro&lt;/a&gt; is an open-source JavaScript framework known for its versatility, performance, and new approach to web development. It enables developers to create fast, modern, content-rich web applications and sites using the "Bring Your Own Framework" (BYOF) model. &lt;/p&gt;

&lt;p&gt;What makes Astro special is its ability to work with multiple JavaScript frameworks at the same time, including React, Preact, Svelte, Vue.js, and others. This gives developers the flexibility to choose the tools they prefer for any specific goal. For example, one UI component might be in Vue and another in React. In Astro, that is entirely possible.&lt;/p&gt;

&lt;p&gt;Astro relies on an innovative "&lt;a href="https://docs.astro.build/en/concepts/islands/" rel="noopener noreferrer"&gt;Island Architecture&lt;/a&gt;" that separates static content from interactive parts of a web page. This architecture allows you to mix static content, server-rendered components, and client-rendered components on the same page without conflict. Astro will take care of sending the minimum amount of JavaScript required for interactivity on the page.&lt;/p&gt;

&lt;p&gt;The framework also offers several other features, such as top-level waiting, support for Markdown and MDX, and themes, making it a complete solution for modern web development needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrate ApostropheCMS With Astro With the Starter Kit
&lt;/h2&gt;

&lt;p&gt;Integrating ApostropheCMS into an Astro application means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Let ApostropheCMS manage the content, handle URL routing, and content retrieval.&lt;/li&gt;
&lt;li&gt;Let Astro take responsibility for page rendering and implementing the UI logic using a frontend framework of your choice, such as &lt;a href="https://docs.astro.build/en/guides/integrations-guide/" rel="noopener noreferrer"&gt;React, Vue.js, or Svelte&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that this integration is possible because ApostropheCMS is unopinionated on the frontend. For example, you can also integrate it with new technologies such as HTMX. Learn more in our &lt;a href="https://apostrophecms.com/blog/htmx-examples-and-how-to-use-it" rel="noopener noreferrer"&gt;HTMX integration guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The easiest way to achieve the Astro integration is through the &lt;a href="https://apostrophecms.com/starter-kits/astro-integration-starter-kit" rel="noopener noreferrer"&gt;ApostropheCMS Astro Integration Starter Kit&lt;/a&gt;. This module uses the &lt;code&gt;@apostrophecms/apostrophe-astro&lt;/code&gt; npm library to bring the ApostropheCMS Admin UI to your Astro application. That way, you can manage your Astro site exactly as if you were in a regular ApostropheCMS application.&lt;/p&gt;

&lt;p&gt;Given the dual nature of this setup, you need two projects to get the most out of the starter kit module. These are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;An ApostropheCMS project&lt;/strong&gt;: This is where you define your page types, widget types, and other content types with their schemas and other customizations. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An Astro project&lt;/strong&gt;: This is where you write your templates and frontend code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s now see how to set up these two projects and integrate ApostropheCMS into Astro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set Up an ApostropheCMS Project for Astro
&lt;/h2&gt;

&lt;p&gt;The recommended way to set up an ApostropheCMS project for Astro integration is to clone the &lt;code&gt;apostrophecms/starter-kit-astro&lt;/code&gt; project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone apostrophecms/starter-kit-astro.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, equivalently, create a new Apostrophe project using the Astro starter kit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apos create my-apos-project-name --starter=astro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main benefit of starting with the provided project is that it contains an ApostropheCMS blog application that has already been configured for integration with an Astro frontend. This will save you a lot of time.&lt;br&gt;
If you want to use your ApostropheCMS project, make sure to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Be using Apostrophe &amp;gt;= 3.x.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npm update&lt;/code&gt; to bring your project to the latest version of &lt;code&gt;apostrophe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Update any page templates in your Apostrophe project to provide a link to your Astro frontend site and remove all other output. Everyone who accesses the backend, editors included, should go straight to the Astro frontend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Well done! You now have an ApostropheCMS project for Astro. From now on, we will assume that your ApostropheCMS project is in the &lt;code&gt;starter-kit-astro&lt;/code&gt; folder.&lt;/p&gt;
&lt;h2&gt;
  
  
  Set Up an Astro Project for ApostropheCMS
&lt;/h2&gt;

&lt;p&gt;Follow the steps below and learn how to set up an Astro frontend project that integrates with an ApostropheCMS backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialize an Astro Project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The recommended way to create an Astro-based site that relies on ApostropheCMS for content editing is to clone the &lt;code&gt;apostrophecms/astro-frontend&lt;/code&gt; repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/apostrophecms/astro-frontend.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This project will already contain all the configurations, components, and templates to connect Astro to the ApostropheCMS Astro starter kit. Again, the main advantage of using the provided project is that almost everything needed to run Astro with AspotropheCMS is done there. So, you can get a huge head start.&lt;/p&gt;

&lt;p&gt;If you instead prefer to start from scratch, run the command below and follow the wizard to &lt;a href="https://docs.astro.build/en/tutorial/1-setup/2/" rel="noopener noreferrer"&gt;initialize a new Astro project&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm create astro@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, add &lt;code&gt;@apostrophecms/apostrophe-astro&lt;/code&gt; to your project dependencies with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @apostrophecms/apostrophe-astro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As mentioned before, this module allows you to integrate ApostropheCMS into your Astro application.&lt;/p&gt;

&lt;p&gt;Great! You now have an Astro project for ApostropheCMS. From now on, we will assume that your Astro project is in the &lt;code&gt;astro-frontend&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add an Astro Configuration File&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To make Astro communicate with the Apostrophe backend, you need to customize how Astro works. To do so, add an &lt;code&gt;astro.config.mjs&lt;/code&gt; file to the project's root folder. Note that most Astro template projects—including &lt;code&gt;apostrophecms/astro-frontend&lt;/code&gt;—come with a working configuration file. But if you are not starting from our &lt;code&gt;astro-frontend&lt;/code&gt; project, then you will need to add Apostrophe-related configuration yourself.&lt;/p&gt;

&lt;p&gt;This is what your &lt;code&gt;astro.config.mjs&lt;/code&gt; configuration file should look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ./astro.config.mjs

import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
import apostrophe from '@apostrophecms/apostrophe-astro';

export default defineConfig({
  output: 'server',
  adapter: node({
    mode: 'standalone'
  }),
  integrations: [
    apostrophe({
      aposHost: 'http://localhost:3000',
      widgetsMapping: './src/widgets',
      templatesMapping: './src/templates',
      forwardHeaders: [
        'content-security-policy', 
        'strict-transport-security', 
        'x-frame-options',
        'referrer-policy',
        'cache-control',
        'host'
      ]
    })
  ],
  vite: {
    ssr: {
      // Do not externalize the @apostrophecms/apostrophe-astro plugin, we need
      // to be able to use virtual: URLs there
      noExternal: [ '@apostrophecms/apostrophe-astro' ],
    }
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To give editors the ability to edit the content directly on the page, the &lt;code&gt;@apostrophecms/apostrophe-astro&lt;/code&gt; module requires Astro to be configured in SSR (Server-Side Rendering) mode. In detail, the &lt;code&gt;output:&lt;/code&gt; &lt;code&gt;'&lt;/code&gt;&lt;code&gt;server&lt;/code&gt;&lt;code&gt;'&lt;/code&gt; setting enables on-demand rendering on the ApostropheCMS server at request time. As of this writing, support for SSG (Static Site Generation) is under consideration for the future.&lt;/p&gt;

&lt;p&gt;What you should focus your attention on is the object passed to the &lt;code&gt;apostrophe()&lt;/code&gt; integration function. That specifies the following options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aposHost&lt;/code&gt;: The base URL of your ApostropheCMS instance. When developing locally or contacting an ApostropheCMS instance on the same server, the URL must contain the port number. During development, its value should be &lt;code&gt;http://localhost:3000&lt;/code&gt;. You can override this option through the &lt;code&gt;APOS_HOST&lt;/code&gt; environment variable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;widgetsMapping&lt;/code&gt;: The optional path to the JavaScript file in your Astro project that contains the mapping between Apostrophe widget types and your Astro components.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;templatesMapping&lt;/code&gt;: The optional path to the JavaScript file in your Astro project that contains the mapping between Apostrophe templates and your Astro templates.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;forwardHeaders&lt;/code&gt;: An optional array of HTTP headers that you want to forward from the ApostropheCMS backend to the final response sent to the browser. This is useful if your backend is using a module like &lt;code&gt;@apostrophecms/security-headers&lt;/code&gt; and wants to keep the headers you configured in ApostropheCMS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more information on all supported options, &lt;a href="https://github.com/apostrophecms/apostrophe-astro?tab=readme-ov-file#options" rel="noopener noreferrer"&gt;see the documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, you may be wondering what the widget mapping file and the template mapping file are. Find that out in the next sections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mapping Apostrophe Widgets to Astro Components&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The core element of the in-context editing experience offered by ApostropheCMS revolves around areas. These are special sections of the page where editors can add one or more content widgets. A &lt;a href="https://v3.docs.apostrophecms.org/guide/areas-and-widgets.html" rel="noopener noreferrer"&gt;widget&lt;/a&gt; is a section of structured content, such as a block of rich text, an image, or a video. You can configure which widgets are allowed in a specific area in the ApostropheCMS &lt;a href="https://v3.docs.apostrophecms.org/guide/content-schema.html" rel="noopener noreferrer"&gt;field schema&lt;/a&gt; of a page or piece type. &lt;/p&gt;

&lt;p&gt;For example, these are the widgets supported by the &lt;code&gt;area&lt;/code&gt; field of the &lt;code&gt;blog&lt;/code&gt; page type in the &lt;code&gt;apostrophecms/starter-kit-astro&lt;/code&gt; project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;widgets: {
  '@apostrophecms/rich-text': {
   // extra configurations...
  },
  '@apostrophecms/image': {},
  '@apostrophecms/video': {},
  'two-column': {}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an editor clicks on the “+ Add Content” button of that area, these will be the corresponding options that they will see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59dor9ph3lncur3s6cuw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59dor9ph3lncur3s6cuw.png" alt=" " width="379" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To enable Astro to render the areas received from ApostropheCMS, you must define an Astro component for each ApostropheCMS widget in use. This is true even for basic widget types like &lt;code&gt;@apostrophecms/image&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As a best practice, you should place all Astro widget components in the &lt;code&gt;./src/widgets&lt;/code&gt; folder of your project.&lt;/p&gt;

&lt;p&gt;An &lt;a href="https://docs.astro.build/en/basics/astro-components/" rel="noopener noreferrer"&gt;Astro component&lt;/a&gt; is nothing more than a &lt;code&gt;.astro&lt;/code&gt; file that contains some reusable UI elements. For example, this is how you could define the Astro component for the &lt;code&gt;@apostrophecms/image&lt;/code&gt; widget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// ./src/widgets/ImageWidget.astro

const { widget } = Astro.props;
const placeholder = widget?.aposPlaceholder;
const src = placeholder ?
  '/images/image-widget-placeholder.jpg' :
  widget?._image[0]?.attachment?._urls['full'];
---
&amp;lt;style&amp;gt;
  .img-widget {
    width: 100%;
  }
&amp;lt;/style&amp;gt;
&amp;lt;img class="img-widget" {src} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will take care of rendering in an HTML &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag the image contained in the widget or a placeholder. For more information on how this component works, &lt;a href="https://github.com/apostrophecms/apostrophe-astro?tab=readme-ov-file#creating-astro-widget-components" rel="noopener noreferrer"&gt;check out the documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;apostrophecms/astro-frontend&lt;/code&gt; project currently ships only with the following Astro widget components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RichTextWidget&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ImageWidget&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VideoWidget&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TwoColumnWidget&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you use other widgets, you will have to manually add a component.&lt;/p&gt;

&lt;p&gt;Once you have defined all the required Astro widget components, you need to list them in the mapping file specified in the aforementioned &lt;code&gt;widgetsMapping&lt;/code&gt; option. In this case, this is what the &lt;code&gt;./src/widgets/index.js&lt;/code&gt; mapping file should contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ./src/widgets/index.js

import RichTextWidget from './RichTextWidget.astro';
import ImageWidget from './ImageWidget.astro';
import VideoWidget from './VideoWidget.astro';
import TwoColumnWidget from './TwoColumnWidget.astro';

const widgetComponents = {
  '@apostrophecms/rich-text': RichTextWidget,
  '@apostrophecms/image': ImageWidget,
  '@apostrophecms/video': VideoWidget,
  'two-column': TwoColumnWidget
};

export default widgetComponents;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, this file imports all the Astro widget component files, associates them with the equivalent ApostropheCMS widget types in a mapping object, and exports it. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mapping Apostrophe Templates to Astro Components&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In ApostropheCMS, templates are where code and content become web pages. Specifically, templates are written in normal HTML markup with special tags and are based on the &lt;a href="https://mozilla.github.io/nunjucks/" rel="noopener noreferrer"&gt;Nunjucks template language&lt;/a&gt;. Thus, they are &lt;code&gt;.html&lt;/code&gt; files placed in the &lt;code&gt;/views&lt;/code&gt; subfolder of an ApostropheCMS module.&lt;/p&gt;

&lt;p&gt;Do not forget that the frontend of this application is handled entirely by Astro. So, just like what happened with widgets, you need to create an Astro component corresponding to each template that ApostropheCMS would normally render with Nunjucks.&lt;/p&gt;

&lt;p&gt;Keep in mind that a template represents only a part of an entire web page. In other words, you do not want Astro to treat those components as routes. To prevent them from becoming &lt;a href="https://docs.astro.build/en/basics/astro-pages/" rel="noopener noreferrer"&gt;Astro pages&lt;/a&gt;, avoid placing them in the &lt;code&gt;./src/pages&lt;/code&gt; folder. Instead, you should place your Astro template components in the &lt;code&gt;./src/templates&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;Now, let’s look at how to write Astro template components. This is what a simple component for the template of the “default” page might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// ./src/templates/DefaultPage.astro

import AposArea from '@apostrophecms/apostrophe-astro/components/AposArea.astro';
const { page, user, query } = Astro.props.aposData;
const { main } = page;
---

&amp;lt;section class="bp-content"&amp;gt;
  &amp;lt;h1&amp;gt;{ page.title }&amp;lt;/h1&amp;gt;
  &amp;lt;AposArea area={main} /&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the &lt;code&gt;Astro.props.aposData&lt;/code&gt; object received from ApostropheCMS gives you access to &lt;code&gt;page&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;, and other useful data. In particular, &lt;code&gt;page&lt;/code&gt; represents the Nunjucks template and contains the &lt;code&gt;area&lt;/code&gt; field of the page called &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AposArea&lt;/code&gt; is a special component from the &lt;code&gt;@apostrophecms/apostrophe-astro&lt;/code&gt; package that gives Astro the ability to render ApostropheCMS areas. That means it provides the standard ApostropheCMS in-context editing experience to the Astro frontend application. Behind the scenes, Astro will automatically render the widgets used in the area through the widget components mapped previously.&lt;/p&gt;

&lt;p&gt;To see more Astro template component implementations, explore the &lt;code&gt;./src/templates&lt;/code&gt; folder of the &lt;code&gt;apostrophecms/astro-frontend&lt;/code&gt; repository.&lt;/p&gt;

&lt;p&gt;After defining all the necessary Astro template components, you have to list them in the mapping file specified in the aforementioned &lt;code&gt;templatesMapping&lt;/code&gt; option. In this sample application, the &lt;code&gt;./src/templates/index.js&lt;/code&gt; mapping file will contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ./src/templates/index.js

import HomePage from './HomePage.astro';
import DefaultPage from './DefaultPage.astro';
import BlogIndexPage from './BlogIndexPage.astro';
import BlogShowPage from './BlogShowPage.astro';
import NotFoundPage from './NotFoundPage.astro';

const templateComponents = {
  '@apostrophecms/home-page': HomePage,
  'default-page': DefaultPage,
  '@apostrophecms/blog-page:index': BlogIndexPage,
  '@apostrophecms/blog-page:show': BlogShowPage,
  '@apostrophecms/page:notFound': NotFoundPage
};

export default templateComponents;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file imports all the Astro template component files, associates them with the equivalent ApostropheCMS template in a mapping object, and exports it. &lt;/p&gt;

&lt;p&gt;For ordinary page templates, such as the home page or the “default” page type, just specify the name of the Apostrophe module. For special templates like &lt;code&gt;notFound&lt;/code&gt; and for modules that serve more than one template, you have to specify the full name. &lt;/p&gt;

&lt;p&gt;For instance, the page type &lt;code&gt;@apostrophecms/blog-page&lt;/code&gt; has an index template to display the main blog page and a &lt;code&gt;show&lt;/code&gt; template to display a single blog post. If you omit the specific template name, &lt;code&gt;:page&lt;/code&gt; is assumed.&lt;/p&gt;

&lt;p&gt;To map the &lt;code&gt;404&lt;/code&gt; page to a component, use &lt;code&gt;@apostrophecms/page:notFound&lt;/code&gt;. Also, bear in mind that the &lt;code&gt;@apostrophecms/apostrophe-astro&lt;/code&gt; library involves two additional template names that can be mapped to custom components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apos-fetch-error&lt;/code&gt;: Served when Apostrophe generates an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses" rel="noopener noreferrer"&gt;HTTP 5XX error&lt;/a&gt;. In this case, Astro will respond with the &lt;code&gt;500&lt;/code&gt; status code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apos-no-template&lt;/code&gt;: Served when there is no mapping corresponding to the ApostropheCMS template.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Handle Routing in Astro via ApostropheCMS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this setup, ApostropheCMS is responsible for managing URLs to content, including creating new content and pages on the fly. So, you will only need one top-level Astro page component matching all routes.&lt;/p&gt;

&lt;p&gt;To define it, add a &lt;code&gt;[...slug].astro&lt;/code&gt; dynamic route file in the &lt;code&gt;./src/pages&lt;/code&gt; folder of your project as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// ./src/pages/[...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';

const aposData = await aposPageFetch(Astro.request);
const bodyClass = `myclass`;

if (aposData.redirect) {
  return Astro.redirect(aposData.url, aposData.status);
}
if (aposData.notFound) {
  Astro.response.status = 404;
}
---
&amp;lt;AposLayout title={aposData.page?.title} {aposData} {bodyClass}&amp;gt;
    &amp;lt;Fragment slot="standardHead"&amp;gt;
      &amp;lt;meta name="description" content={aposData.page?.seoDescription} /&amp;gt;
      &amp;lt;meta name="viewport" content="width=device-width, initial-scale=1" /&amp;gt;
      &amp;lt;meta charset="UTF-8" /&amp;gt;
    &amp;lt;/Fragment&amp;gt;
    &amp;lt;AposTemplate {aposData} slot="main" /&amp;gt;
&amp;lt;/AposLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;aposPageFetch()&lt;/code&gt; function from the &lt;code&gt;@apostrophecms/apostrophe-astro&lt;/code&gt; library fetches the data for the current URL from ApostropheCMS. In detail, the &lt;code&gt;aposData&lt;/code&gt; object will contain all of the information normally provided by &lt;code&gt;data&lt;/code&gt; in an ApostropheCMS Nunjucks template. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;page&lt;/code&gt;: The HTML template document associated with the current URL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;piece&lt;/code&gt;: The piece document, when visiting a "show page" of a particular piece page type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pieces&lt;/code&gt;: An array of pieces, when visiting an "index page" of a given piece page type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user&lt;/code&gt;: General information about the currently logged-in user.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;global&lt;/code&gt;: The ApostropheCMS global settings.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;query&lt;/code&gt;: The query parameters of the URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any other custom data set by ApostropheCMS is also available here.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;AposLayout&lt;/code&gt; component finds the right Astro template component to render based on the template mapping defined earlier. It also automatically handles the switch between the editing UI when a user is logged in and the normal layout for visitors. &lt;code&gt;AposLayout&lt;/code&gt; accepts four props:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aposData&lt;/code&gt;: The data fetched from ApostropheCMS.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;title&lt;/code&gt;: The page title to set in the the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; HTML tag.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lang&lt;/code&gt;: The language code to set in the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; &lt;code&gt;lang&lt;/code&gt; attribute.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bodyClass&lt;/code&gt;: The custom &lt;code&gt;class&lt;/code&gt; attribute to add to the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; element.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To override any aspect of the global layout, take advantage of the &lt;a href="https://docs.astro.build/en/basics/astro-components/#named-slots" rel="noopener noreferrer"&gt;named Astro slots&lt;/a&gt; matching what &lt;a href="https://v3.docs.apostrophecms.org/guide/layout-template.html#layout-templates" rel="noopener noreferrer"&gt;template blocks ApostropheCMS offers in Nunjucks&lt;/a&gt;. In the example below, the &lt;code&gt;standardHead&lt;/code&gt; &lt;code&gt;&amp;lt;Fragment /&amp;gt;&lt;/code&gt; component is used to add a slot at the very beginning of the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; element of the page.&lt;/p&gt;

&lt;p&gt;Wonderful! Your Astro frontend project for an ApostropheCMS backend is ready!&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started With the ApostropheCMS Astro Application
&lt;/h2&gt;

&lt;p&gt;Now that you know how to set up an Astro project and an ApostorpheCMS project that can communicate with each other, it is time to see the resulting application in action!&lt;/p&gt;

&lt;p&gt;Here, we will assume that you cloned the two projects presented at the beginning of the article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start the Projects&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To prevent other users from retrieving a lot of data from your ApostropheCMS backend without your permission, you must use the &lt;code&gt;APOS_EXTERNAL_FRONT_KEY&lt;/code&gt; environment variable. Set that env to a secret value when running the Astro project and set the same variable to the same value when running the Apostrophe application.&lt;/p&gt;

&lt;p&gt;Use the commands below to start your ApostropheCMS backend locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd starter-kit-astro
npm install
export APOS_EXTERNAL_FRONT_KEY=&amp;lt;YOUR_SECRET&amp;gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, ApostropheCMS will start on port &lt;code&gt;3000&lt;/code&gt;. Visit &lt;code&gt;http://localhost:3000&lt;/code&gt; and you should see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6649si8s8ve6hm1zf9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6649si8s8ve6hm1zf9f.png" alt=" " width="800" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Similarly, run your Astro frontend with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd astro-frontend
npm install
export APOS_EXTERNAL_FRONT_KEY=&amp;lt;YOUR_SECRET&amp;gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, Astro will start on port &lt;code&gt;4321&lt;/code&gt;. Open &lt;code&gt;http://localhost:4321&lt;/code&gt; in your browser and you will see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frjmlyfvfyc3wdzlok4g3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frjmlyfvfyc3wdzlok4g3.png" alt=" " width="800" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you forget to set the &lt;code&gt;APOS_EXTERNAL_FRONT_KEY&lt;/code&gt; environment variable, you will receive the following error message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ERROR] APOS_EXTERNAL_FRONT_KEY environment variable must be set, here and in the Apostrophe app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the error page below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgy3wux0sykclpefbao5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgy3wux0sykclpefbao5.png" alt=" " width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add an ApostropheCMS Admin User and Log In&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As mentioned on the Astro frontend home page, the next step is to log in as an administrator. If you have already set up an admin user in the ApsotropheCMS project, you can skip to the next step. &lt;/p&gt;

&lt;p&gt;Otherwise, run the following command inside the ApostropheCMS project folder to add an administrator user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node app @apostrophecms/user:add myAdmin admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;myAdmin&lt;/code&gt; with the name you want to give the user. During the process, you will be prompted for a password. Type it in and press ENTER.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access the ApostorpheCMS Editing UI in Astro&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, click on the login link or visit &lt;code&gt;http://localhost:4321/login&lt;/code&gt;. Fill out the login form with your ApostropheCMS admin credentials:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5f6o9y63cwnlp693983f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5f6o9y63cwnlp693983f.png" alt=" " width="470" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press the “Login” button, and you will get access to the editing UI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7o2gw5c4o0ptocb49ggz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7o2gw5c4o0ptocb49ggz.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the "Edit" button in the upper right corner to enter the ApostropheCMS in-context editing mode:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjwujqxjklx73hqmtl8b.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjwujqxjklx73hqmtl8b.gif" width="720" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep in mind that this is all happening in Astro, not in the ApostropheCMS application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create a Blog Page&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;starter-kit-astro&lt;/code&gt; project is based on the ApostropheCMS &lt;code&gt;@apostrophecms/blog&lt;/code&gt; module. This helps you create a blog in ApostropheCMS in just a few minutes. To understand how this module works and what it offers, &lt;a href="https://apostrophecms.com/blog/how-to-build-a-blog-apostrophe" rel="noopener noreferrer"&gt;read our dedicated guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To get started with the blog module, create a new page of type "Blog Page.” Click on the "Pages" option in the top left menu, then hit the "New Page” button:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7o3ofymp5prkm4sb535o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7o3ofymp5prkm4sb535o.png" alt=" " width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fill out the ApostropheCMS page creation modal by selecting the “Blog Page” type, and click “Publish:”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvvudmj30esbp03d4kbkq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvvudmj30esbp03d4kbkq.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Visit the &lt;code&gt;http://localhost:4321/blog&lt;/code&gt; and you will now see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s4mjkmmivswo51unq03.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s4mjkmmivswo51unq03.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This corresponds to the &lt;code&gt;./src/templates/BlogIndexPage.astro&lt;/code&gt; template component in your Astro project. The page is currently empty as you still have to populate your blog with some posts. Learn how to do that in the next step!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Populate Your Blog&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To create a new blog post, select “Blog post” in the top left menu and then click the "New Blog post” button. You will reach the following form:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4unoojbp70tk7p02bb4z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4unoojbp70tk7p02bb4z.png" alt=" " width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Give your blog a title, a publication date, a slug, and write some content. Then, press "Publish.”&lt;/p&gt;

&lt;p&gt;Add a few posts to your application and visit &lt;code&gt;http://localhost:4321/blog&lt;/code&gt; again. This time, you will see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzoesri1py9kydh4yfgeq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzoesri1py9kydh4yfgeq.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on a blog post link and you will be redirected to the blog piece's show page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpu72bjjasl6kifxmq8s7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpu72bjjasl6kifxmq8s7.png" alt=" " width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Et voilà! You just used the ApostropheCMS content editing features to build your Astro site!&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;Part 1 of this two-article series ends here. Although the results achieved are pretty amazing, there is still a lot to be done. At the end of the day, the blog application is pretty ugly right now. In the next part, you will learn how to make your application more interactive and visually appealing using React components in Astro. &lt;/p&gt;

&lt;p&gt;Continue with the article “How to Integrate Astro With ApostropheCMS pt. 2” to complete the ApostropheCMS / Astro integration.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>tutorial</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Build an Ecommerce Website with ApostropheCMS</title>
      <dc:creator>Antonello Zanini</dc:creator>
      <pubDate>Fri, 16 Feb 2024 21:15:29 +0000</pubDate>
      <link>https://dev.to/apostrophecms/how-to-build-an-ecommerce-website-with-apostrophecms-55e8</link>
      <guid>https://dev.to/apostrophecms/how-to-build-an-ecommerce-website-with-apostrophecms-55e8</guid>
      <description>&lt;p&gt;Ecommerce sites are among the most popular and visited platforms on the Internet. There are many technologies available to create ecommerce platforms, and knowing how to build one is a critical skill to have in the IT world. While starting a new ecommerce project from scratch is good for learning purposes, isn't there a ready-made, more reliable solution for developers and their businesses? This is where the ApostropheCMS Ecommerce Starter Kit comes into play!&lt;/p&gt;

&lt;p&gt;In this article, you will find out why building an ecommerce site from the ground up may not be the best approach. You will then explore an alternative and more effective method of kicking off an ecommerce site project thanks to the ApostropheCMS Ecommerce Starter Kit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Should Not Build an Ecommerce Platform From Scratch
&lt;/h2&gt;

&lt;p&gt;Suppose you want to create an ecommerce site with popular technologies like Next.js, Vue.js, Laravel, or Angular. To have more control over the project, you may prefer to start from the ground up. Well, there are three major challenges if you want to go that route:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development time and costs:&lt;/strong&gt; Creating a robust ecommerce solution requires product, tag, and category management, content filtering, offers handling, and more. To implement those features, be prepared to invest dozens, if not hundreds, of hours in coding. That implies a significant financial investment in the project, without a guarantee of success.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability concerns:&lt;/strong&gt; Completing the project and deploying it online is just the beginning. Maintaining a custom solution can be complex, requiring meticulous attention to updates, security patches, and industry standards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content management complexity:&lt;/strong&gt; Most JavaScript framework allows you to present data nicely and crafting interactive pages, but what tools will you use for content management? Developing a user-friendly content management system (CMS) is a daunting challenge in itself. You could opt for a &lt;a href="https://apostrophecms.com/blog/apostrophe-headless-cms" rel="noopener noreferrer"&gt;headless CMS&lt;/a&gt;, but integrating and configuring it properly takes time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These non-negligible issues lead raise some doubts. Does it make sense to develop an ecommerce site from scratch? Isn’t out there a better solution? Of course, there is!&lt;/p&gt;

&lt;h2&gt;
  
  
  ApostropheCMS Ecommerce Starter Kit: The Solution You Were Looking For!
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apostrophecms.github.io/starter-kit-ecommerce/" rel="noopener noreferrer"&gt;ApostropheCMS Ecommerce Starter Kit&lt;/a&gt; serves as an open-source template for initializing a new JavaScript ecommerce project. In detail, the starter kit generates an ApostropheCMS project that provides all the features developers and content managers need to build an ecommerce platform.&lt;/p&gt;

&lt;p&gt;If you are not familiar with that technology, &lt;a href="https://github.com/apostrophecms/apostrophe" rel="noopener noreferrer"&gt;ApostropheCMS&lt;/a&gt; is an open-source website builder and CMS developed with modern technologies such as Vue.js and Node.js. It enables editors to effortlessly create and manage content through an intuitive UI, while developers have the ability to customize the admin UI by overriding existing Vue.js components and extending it with new menus and field types. At the same time, you keep the ability to use your technologies of choice on the front end. &lt;a href="https://v3.docs.apostrophecms.org/guide/custom-ui.html" rel="noopener noreferrer"&gt;Learn more in the documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks to the ApostropheCMS core, the ecommerce starter kit speeds up the creation of a fully functional ecommerce JavaScript project with advanced content management capabilities, a focus on user experience, and a clear path for further development. Thus, it gives content managers the ability to define tags, products, and categories and use them to design engaging ecommerce pages through a useful set of built-in widgets. &lt;/p&gt;

&lt;p&gt;In other words, the Ecommerce Starter Kit for ApostropheCMS equips editors, marketers, and developers with everything they need to create an efficient, effective, eye-catching ecommerce platform that delivers a high-quality user experience to customers.&lt;/p&gt;

&lt;p&gt;This starter Kit has been created by &lt;a href="https://corllete.com/" rel="noopener noreferrer"&gt;Corllete&lt;/a&gt; in partnership with Apostrophe. From a technical point of view, the ecommerce project is based on several UI components. Each of them relies entirely on &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt; for styling, with no additional CSS files. These components are organized in macros and fragments coming from the default server-side template engine &lt;a href="https://mozilla.github.io/nunjucks/" rel="noopener noreferrer"&gt;Nunjucks&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The advantages of this approach to creating an ecommerce store are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quick development.&lt;/li&gt;
&lt;li&gt;Built-in content management functionality.&lt;/li&gt;
&lt;li&gt;Excellent UX and highly customizable UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, learn how to use it in the guided section below!&lt;/p&gt;

&lt;h2&gt;
  
  
  Create an Ecommerce Site With ApostropheCMS
&lt;/h2&gt;

&lt;p&gt;Follow this step-by-step tutorial to learn how to use the &lt;a href="https://github.com/apostrophecms/starter-kit-ecommerce" rel="noopener noreferrer"&gt;Apostrophe CMS Ecommerce Starter Kit&lt;/a&gt; to create a full-featured ecommerce site in minutes.&lt;/p&gt;

&lt;p&gt;You can find the complete documentation on how to get the most out of this starter kit below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://apostrophecms.github.io/starter-kit-ecommerce/user/" rel="noopener noreferrer"&gt;User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apostrophecms.github.io/starter-kit-ecommerce/developer/" rel="noopener noreferrer"&gt;Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s dive in!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set Up an Ecommerce Starter Project&lt;/strong&gt;&lt;br&gt;
Keep in mind that the Ecommerce Starter Kit for ApostropheCMS is not designed to be installed into an existing project. So, you will have to create a new project from scratch. Before getting started, make sure to meet the &lt;a href="https://v3.docs.apostrophecms.org/guide/setting-up.html#system-requirements" rel="noopener noreferrer"&gt;requirements for Apostrophe 3+&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, launch the command below to initialize an ApostropheCMS project using the ecommerce starter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apos create my-apostrophe-ecommerce-site --starter=ecommerce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;my-apostrophe-ecommerce-site&lt;/code&gt; is the name of the project, while &lt;code&gt;ecommerce&lt;/code&gt; is the starter used to create the new boilerplate project. Follow the procedure to add an admin user. For finishing touches or more information, take a look at the &lt;a href="https://github.com/apostrophecms/starter-kit-ecommerce?tab=readme-ov-file#getting-started" rel="noopener noreferrer"&gt;official setup guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Run the local development server with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit the login page at &lt;code&gt;http://localhost:3000/login&lt;/code&gt; and use the credentials you set up earlier to log in as an admin. This is what you should be seeing:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Funyz4ut0vhxu7l9f9k26.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Funyz4ut0vhxu7l9f9k26.png" alt=" " width="800" height="343"&gt;&lt;/a&gt;&lt;br&gt;
Awesome! You now have everything you need to start working on your ApostropheCMS ecommerce platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Some Products and Categories&lt;/strong&gt;&lt;br&gt;
The first thing to do is to populate your ecommerce with some data. The ApostropheCMS ecommerce starter comes with the following &lt;a href="https://v3.docs.apostrophecms.org/guide/pieces.html" rel="noopener noreferrer"&gt;types of pieces&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Content Tag&lt;/strong&gt;: A label to describe products and product categories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product Category&lt;/strong&gt;: A set of tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product&lt;/strong&gt;: An item for sale in the ecommerce platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the project's &lt;a href="https://apostrophecms.github.io/starter-kit-ecommerce/user/getting-started.html#content-taxonomy" rel="noopener noreferrer"&gt;content taxonomy&lt;/a&gt; is based on tags, start by adding some Content Tag pieces. To do so, click “Content” in the upper left corner, select the “Content Tags” options, and click the “New Content Tag” button on the right:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1axywogjllex0vl7ntg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1axywogjllex0vl7ntg.gif" alt=" " width="600" height="289"&gt;&lt;/a&gt;&lt;br&gt;
You will reach the following form:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdd6hrt1ajlqnao93loi4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdd6hrt1ajlqnao93loi4.png" alt=" " width="800" height="382"&gt;&lt;/a&gt;&lt;br&gt;
Populate the “Title” field and press “Save” to add a new Content Tag record. Repeat the procedure to create all the tags you need.&lt;/p&gt;

&lt;p&gt;Examples of tags are “Shoes,” “T-Shirts,” “Bracelets,” “Necklaces,” “Kids,” “Adults,“ “Women,” “Men,” “Unisex,” “Summer Collection,” “Winter Collection”, “September Sale,” “Black Friday Sale,” etc.&lt;/p&gt;

&lt;p&gt;Next, add some “Product Category” records. If you do not plan to use categories, you can skip this part. Otherwise, click “Content,” “Product Categories”, and then the “New Product Category” button. You will reach the following form:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz9vkc3hcqdurd5b3jld7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz9vkc3hcqdurd5b3jld7.png" alt=" " width="800" height="382"&gt;&lt;/a&gt;&lt;br&gt;
Note that a product category can either correspond to a single tag or cover multiple tags. Thus, you could define the category “Shoes” that corresponds to the tag “Shoes” as well as the category “Accessories” for the tags “Bracelets” and “Necklaces.” By default, the category page associated will automatically show only products that have at least one tag in common among the selected ones.&lt;/p&gt;

&lt;p&gt;In the “Page” tab, you can add UI elements that will appear at the top or the bottom of the specific product page (e.g., a call to action or a promo). Instead, the “SEO” and “Open Graph” tabs allow you to specify information for internal search and search engine indexing.&lt;/p&gt;

&lt;p&gt;Once you are done, click “Publish” to create a new category for your product. Repeat the procedure to add as many product categories as you need.&lt;/p&gt;

&lt;p&gt;You are finally ready to add some products! Open the “Content” menu, select “Products,” and then click on “New Product.” As you can see from the GIF below, the product form allows you to enter a lot of useful information:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzh29o6wo00yfra89ir4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzh29o6wo00yfra89ir4.gif" alt=" " width="760" height="366"&gt;&lt;/a&gt;&lt;br&gt;
Here, you can upload multiple images, define the price and the discounted price, describe the product's details, add up to 4 meta fields, and more. In particular, there is also a “Buy Now URL” for integration with any online service that provides buy Now URL functionality. Through this, users will have the ability not only to explore products but also to buy them.&lt;/p&gt;

&lt;p&gt;In the “Page” tab, you can also customize the elements that will appear at the end of the page (i.e., add a promo or a specific call to action). Do not forget to explore the “SEO” and “Open Graph” tabs to define how search engines will index the page and how the link to the product page will be rendered on social platforms. To find out more, read the &lt;a href="https://apostrophecms.github.io/starter-kit-ecommerce/user/search-and-seo.html#seo-and-open-graph" rel="noopener noreferrer"&gt;SEO and Open Graph section of the starter documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When you are satisfied with the data you have entered, press “Publish” to make the product public. To add new products, perform this procedure again.&lt;/p&gt;

&lt;p&gt;Great! You now know how to handle content on your ecommerce site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create the Essential Pages&lt;/strong&gt;&lt;br&gt;
The first step to making your ecommerce site work properly is to add a product page. That is nothing more than a landing page to show all the products in your online store. To create it, click “Pages” in the upper left corner, then press the “New Page” button on the right, and add a new page of the type “Product:”&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fglvqbft0uad3mgpilvkm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fglvqbft0uad3mgpilvkm.gif" alt=" " width="600" height="289"&gt;&lt;/a&gt;&lt;br&gt;
At this point, you will have the form below in front of you:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpjbzhu73ysfvor5o7p9x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpjbzhu73ysfvor5o7p9x.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;br&gt;
Give your product page a title and a unique slug and click the “Publish” button. By default, the product page will show a list of all your products. If you want to add extra elements at the bottom of the page, you can do it through the rich-text field “Content,” but that is optional. In detail, the ApostropheCMS ecommerce starter pack comes with a set of widgets specifically designed for an online store: &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjg8ko8ddflg3t7tvt7un.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjg8ko8ddflg3t7tvt7un.png" alt=" " width="800" height="258"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://apostrophecms.github.io/starter-kit-ecommerce/user/widgets.html" rel="noopener noreferrer"&gt;Check out the docs&lt;/a&gt; to learn how these widgets work and what they offer.&lt;/p&gt;

&lt;p&gt;After publishing the product page, you are ready to explore it and see how it presents the list of all your products. Suppose the product page you just created has products as its slug page. To open it, visit &lt;code&gt;http://localhost/3000/products&lt;/code&gt; in your browser. This is what you will see:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08cpptxtv016ebjtgj14.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08cpptxtv016ebjtgj14.gif" alt=" " width="720" height="347"&gt;&lt;/a&gt;&lt;br&gt;
As anticipated earlier, it contains a list with all the products available on your site. If you click on one of the product cards, you will be redirected to the detail page with all the information about the selected product. For example, here is what the product detail page for the “Air Pro 47” sports shoes looks like:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhh1opsh8sfz5xtimq8to.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhh1opsh8sfz5xtimq8to.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;br&gt;
Note that it automatically presents all the information you provided for the product in a visually appealing way.&lt;/p&gt;

&lt;p&gt;Amazing, the ecommerce store is starting to come alive! &lt;/p&gt;

&lt;p&gt;If your ecommerce site uses categories, you will also need to define a category page. To do so, create a new page of type “Product Category." This will automatically show a list of the available product categories:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F75ybc9v3s16g0mstp8nl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F75ybc9v3s16g0mstp8nl.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;br&gt;
To learn more about products and categories, see the &lt;a href="https://apostrophecms.github.io/starter-kit-ecommerce/user/products-and-categories.html" rel="noopener noreferrer"&gt;dedicated page on the ecommerce starter documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Populating the Home Page&lt;/strong&gt;&lt;br&gt;
Your ecommerce site is starting to take shape, but the home page is still completely blank. Time to fix that! &lt;/p&gt;

&lt;p&gt;Visit &lt;code&gt;http://localhost:3000&lt;/code&gt; and click the “Edit” button on the right to enter the ApostropheCMS in-context live editing mode:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7m196k61m4i5oxl8u53f.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7m196k61m4i5oxl8u53f.gif" alt=" " width="800" height="385"&gt;&lt;/a&gt;&lt;br&gt;
Here, you can customize the content of your home page:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F448bxlxgzecb901dspty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F448bxlxgzecb901dspty.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;br&gt;
For example, you could add a hero section, followed by a list of the latest products added to the site, followed by a promo and the list of categories as below:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frep6nyts457lqro64qfh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frep6nyts457lqro64qfh.gif" alt=" " width="600" height="289"&gt;&lt;/a&gt;&lt;br&gt;
Fantastic! You are almost there!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Some Custom Pages&lt;/strong&gt;&lt;br&gt;
To promote your products and raise brand awareness, your ecommerce site is likely to need some additional pages. This is why the ApostropheCMS Ecommerce Starter Kit enables content creators and marketers to define custom pages.&lt;/p&gt;

&lt;p&gt;To add a custom page, click on “Pages” in the top left menu, press “New Page,” and create a new page of type “Default:”&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60zos34kbrtskgowuhbh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60zos34kbrtskgowuhbh.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;br&gt;
You might use this feature to add an “About Us” page or some landing pages. For example, you could create the following landing page to promote your summer collection:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmf0xfjfaiins1ziv1c0t.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmf0xfjfaiins1ziv1c0t.gif" alt=" " width="760" height="366"&gt;&lt;/a&gt;&lt;br&gt;
Explore the documentation to learn more about &lt;a href="https://apostrophecms.github.io/starter-kit-ecommerce/user/custom-pages.html" rel="noopener noreferrer"&gt;custom pages&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customize Your Ecommerce Site&lt;/strong&gt;&lt;br&gt;
Your ecommerce platform only requires the finishing touches. Get ready to make your ApostropheCMS ecommerce site unique!&lt;/p&gt;

&lt;p&gt;Click on the gear icon at the top right to open the global site settings:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuzuvghr5q3p3z5hj8t1p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuzuvghr5q3p3z5hj8t1p.png" alt=" " width="800" height="105"&gt;&lt;/a&gt;&lt;br&gt;
Here you can customize your company logo, site favicon, colors, navigation links, and more:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8sclrqz1t23136xk02g9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8sclrqz1t23136xk02g9.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;br&gt;
You can also enable the search feature by choosing the “Page” type and selecting the “Search” page &lt;a href="https://apostrophecms.github.io/starter-kit-ecommerce/user/search-and-seo.html#search" rel="noopener noreferrer"&gt;as explained in the documentation&lt;/a&gt;:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F122qkbyv9juav2d28y3d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F122qkbyv9juav2d28y3d.png" alt=" " width="742" height="337"&gt;&lt;/a&gt;&lt;br&gt;
That allows users to find pages based on the content of the “SEO — Description” field or “Tagline” field:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxprf2mr5vafzmnwtlad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxprf2mr5vafzmnwtlad.png" alt=" " width="800" height="390"&gt;&lt;/a&gt;&lt;br&gt;
Notice that not all customizations supported by the starter kit are available through that configuration modal. For example, if you want to change the brand palette, you must overwrite the &lt;code&gt;APP_BRAND&lt;/code&gt; env before running the npm build command. Find all available color palettes in the &lt;code&gt;/colors&lt;/code&gt; folder of your project.&lt;/p&gt;

&lt;p&gt;Suppose you want to test the purple palette locally. Then, launch the local development server with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_BRAND="purple" npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you like that and want to use it in production, you will have to build your ecommerce project with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;APP_BRAND="purple" npm run build&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is what your new site will look like:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26szkbph1hg741rtdgow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26szkbph1hg741rtdgow.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;br&gt;
Note the new logo, navigation menu, and how the palette has changed from aquamarine to purple.&lt;/p&gt;

&lt;p&gt;Do not forget that all ecommerce components are styled via Tailwind. You can find the Tailwind configuration in &lt;code&gt;tailwind.config.js&lt;/code&gt; file in the root folder of the project. Refer to the official &lt;a href="https://tailwindcss.com/docs/configuration" rel="noopener noreferrer"&gt;Tailwind CSS documentation&lt;/a&gt; for more information about the options you can set. For example, you could modify some Tailwind classes used in the application to fit your project needs.&lt;/p&gt;

&lt;p&gt;You can also define a custom brand palette for your site, again using Taliwind. Find out more on how to customize the style of your ecommerce site in the &lt;a href="https://apostrophecms.github.io/starter-kit-ecommerce/developer/branding-and-ui.html#tailwind-css" rel="noopener noreferrer"&gt;Tailwind CSS section in the developer documentation&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;For more branding and UI customizations, follow the instructions in the &lt;a href="https://apostrophecms.github.io/starter-kit-ecommerce/developer/branding-and-ui.html" rel="noopener noreferrer"&gt;developer documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Et voilà! Your ecommerce platform is finally ready. It is time to see it in action!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put It All Together&lt;/strong&gt;&lt;br&gt;
This is what your ecommerce webshop may look like:&lt;br&gt;
&lt;iframe src="https://player.vimeo.com/video/907612831" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;br&gt;
Congratulations! Thanks to the ApostropheCMS ecommerce starter kit, you were able to set up a full-featured ecommerce site in minutes and with a handful of lines of code!&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;The tutorial is over, but there are still some features you can add to improve this project! In this guide, you learned how easy and quick it is to set up a JavaScript ecommerce project with the ApostropheCMS Ecommerce Starter Kit. The next steps include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a shopping cart&lt;/strong&gt;: Offer complete cart functionality through a shopping cart provider such as Snipcart. Learn more about &lt;a href="https://apostrophecms.com/blog/using-snipcart-and-apostrophe-for-simple-e-commerce" rel="noopener noreferrer"&gt;ApostropheCMS and Snipcart integration&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrate a payment system platform&lt;/strong&gt;: Add a secure payment system platform that supports several payment methods such as Stripe or Braintree.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ApostropheCMS has proven to be a modern and efficient solution to help businesses build ecommerce sites. This is just a taste of what that technology can do. &lt;a href="https://apostrophecms.com/demo" rel="noopener noreferrer"&gt;Explore Apostrophe today&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Introducing the Apostrophe / Astro Integration</title>
      <dc:creator>The Apostrophe Team</dc:creator>
      <pubDate>Fri, 16 Feb 2024 18:57:25 +0000</pubDate>
      <link>https://dev.to/apostrophecms/introducing-the-apostrophe-astro-integration-4ifc</link>
      <guid>https://dev.to/apostrophecms/introducing-the-apostrophe-astro-integration-4ifc</guid>
      <description>&lt;h2&gt;
  
  
  What’s the big news?
&lt;/h2&gt;

&lt;p&gt;In another step towards making Apostrophe the most dynamic, feature-rich website ecosystem for building and maintaining your website, our team is pleased to announce the integration for the Astro web framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Astro?
&lt;/h2&gt;

&lt;p&gt;Astro is a free, open-source software that enables users to build fast, content-driven websites with an emphasis on ease-of-use. It operates using a BYOF or “Bring Your Own Framework” model that makes it compatible with popular JS frameworks such as React, Svelt, or Vue. Some other key features of Astro’s technology include static site generation, component-based architecture, and built in automations. You can read more on &lt;a href="https://astro.build/"&gt;Astro’s website&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the integration mean for Apostrophe users?
&lt;/h2&gt;

&lt;p&gt;With the new integration comes the ability to let Apostrophe manage your content, handle routing of URLs and fetch content, and let Astro take the responsibility for the rendering of pages and any associated logic using your framework(s) of choice. Additionally, you can now bring the ApostropheCMS Admin UI into your Astro application, meaning can manage your site exactly as if you were in a "normal" Apostrophe instance.&lt;/p&gt;

&lt;p&gt;Why is this such a big deal? Apostrophe has always been great for providing a rich live editing experience for marketing teams. When we originally introduced our native REST APIs in A3, we made it easier than ever to use Apostrophe as a headless CMS. However, it’s always been harder than it should be to deliver our standard fully WYSIWYG editing experience when running Apostrophe in headless mode. This new integration is an incredible solution to that puzzle, and we’re thrilled that we can now support such an elegant option for delivering a rich, in-context editing experience within a headless architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to get started…
&lt;/h2&gt;

&lt;p&gt;There’s more information about these exciting new capabilities and instructions on how to use the integration at the &lt;a href="https://docs.astro.build/en/guides/cms/apostrophecms/"&gt;Astro docs&lt;/a&gt; and on our &lt;a href="https://apostrophecms.com/starter-kits"&gt;Extensions page&lt;/a&gt;. Head over there now to get setup and install the module.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next?
&lt;/h2&gt;

&lt;p&gt;If you have questions about Astro or how to leverage some of the new features, feel free to reach out to us directly or, better yet, bring your feedback to Apostrophe’s Discord Community so others can benefit as well.&lt;/p&gt;

&lt;p&gt;Lastly, we’d love to see you at our next live Show &amp;amp; Tell event on January 25th! We’ll be hosting an open discussion around some of the more commonly used JS frameworks, including some of the ones mentioned earlier in this announcement, and how to use them with Astro. It’s all going down in the &lt;a href="https://discord.gg/UrNVwb6h"&gt;Discord&lt;/a&gt;, see you there!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Digging Into HTMX: Examples and How to Use It</title>
      <dc:creator>Antonello Zanini</dc:creator>
      <pubDate>Fri, 08 Dec 2023 17:15:21 +0000</pubDate>
      <link>https://dev.to/apostrophecms/digging-into-htmx-examples-and-how-to-use-it-5cna</link>
      <guid>https://dev.to/apostrophecms/digging-into-htmx-examples-and-how-to-use-it-5cna</guid>
      <description>&lt;p&gt;HTMX aims to provide access to modern browser functionality directly in HTML code, without a single line of JavaScript. Even though version 1.0 of the library was launched just a few years ago, in late 2020, the project has already become incredibly popular. As of this writing, it has more than &lt;a href="//github.com/bigskysoftware/htmx"&gt;20k stars on GitHub&lt;/a&gt; and has been accepted into the GitHub Open Source Accelerator!&lt;/p&gt;

&lt;p&gt;Why is the web developer community so excited about it? Embark on this journey and find out! You will learn how to build modern user interfaces with simple hypertext.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is HTMX?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="//htmx.org/"&gt;HTMX&lt;/a&gt; is a small, dependency-free, extendable library that allows you to access modern browser features directly from HTML, instead of using JavaScript. Specifically, it gives you access to AJAX (i.e., fetching content without reloading the whole page), CSS Transitions, WebSockets, and Server Sent Events directly via HTML attributes. The motivation behind the project is to overcome the limitations imposed by HTML to make it a true &lt;a href="https://en.wikipedia.org/wiki/Hypertext" rel="noopener noreferrer"&gt;hypertext&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The easiest way to understand what we are talking about? An example! Take a look at this snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;button hx-get="/api/v1/hello-world" hx-swap="outerHTML"&amp;gt;Click Me&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The special &lt;code&gt;hx-get&lt;/code&gt; and &lt;code&gt;hx-swap&lt;/code&gt; attributes tell HTMX:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“When a user clicks on this button, instruct the browser to perform an AJAX request to the ‘/api/v1/hello-world’ endpoint, and replace the entire button with the HTML content returned by the server”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In JavaScript, achieving the same result would take dozens of lines of code. That is the power of HTMX!&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Got to HTMX: From HTML to HTMX
&lt;/h2&gt;

&lt;p&gt;Web development has evolved a lot since its early days. It started with static web pages, where manual updates to HTML files were the norm. The introduction of JavaScript added interactivity, opening the door to a new era. AJAX then completed the revolution, enabling seamless content updates and new interactions.&lt;/p&gt;

&lt;p&gt;Over time, frameworks like React, Vue, and Angular took the stage and became the standard. These technologies are great for structured applications. However, they also involve a lot of complexity, and sometimes you want to keep things simple. That is why HTMX!&lt;/p&gt;

&lt;p&gt;HTMX aims to achieve efficient interactivity without the complexity of traditional JavaScript setups. In particular, it extends HTML with custom attributes, allowing the execution of AJAX requests without JavaScript. It also integrates seamlessly with existing technology stacks to provide an improved user experience without a complete overhaul.&lt;/p&gt;

&lt;p&gt;Let’s now explore the features and syntax of HTMX!&lt;/p&gt;

&lt;h2&gt;
  
  
  HTMX Overview: Syntax, Features, and Capabilities
&lt;/h2&gt;

&lt;p&gt;The core idea behind HTMX is the ability to send AJAX requests directly from HTML, with no JavaScript involved. This is possible thanks to the following attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hx-get&lt;/code&gt;: To perform a &lt;code&gt;GET&lt;/code&gt; request to the given URL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hx-post&lt;/code&gt;: To perform a &lt;code&gt;POST&lt;/code&gt; request to the given URL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hx-put&lt;/code&gt;: To perform a &lt;code&gt;PUT&lt;/code&gt; request to the given URL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hx-patch&lt;/code&gt;: To perform a &lt;code&gt;PATCH&lt;/code&gt; request to the given URL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hx-delete&lt;/code&gt;: To perform a &lt;code&gt;DELETE&lt;/code&gt; request to the given URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a specific event is triggered, the HTML element involving one of these HTMX attributes will make an AJAX request of the specified type to the given URL. Consider the example below:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;button hx-post="/api/v1/products/buy"&amp;gt;Buy&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This tells the browser:&lt;/p&gt;

&lt;p&gt;“When a user clicks on the &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, make a &lt;code&gt;POST&lt;/code&gt; request to the URL ‘/api/v1/products/buy’ and load the response into the inner HTML of the &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;”&lt;/p&gt;

&lt;p&gt;How can a single line of HTML result in that behavior? Well, the aspects to consider when making an AJAX request are three:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When to perform the request&lt;/li&gt;
&lt;li&gt;The query parameters and/or body to send&lt;/li&gt;
&lt;li&gt;What to do with the response&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Time to dig into how HTMX handles them!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request Triggers&lt;/strong&gt;&lt;br&gt;
By default, AJAX requests made by HTMX are triggered by the “natural” event associated with the HTML element:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;change&lt;/code&gt;: For &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; elements.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;submit&lt;/code&gt;: For the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;click&lt;/code&gt;: For every other element.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Going back to the snippet seen earlier, it should now be clear why the action that triggers the request is a click, even if not specified.&lt;/p&gt;

&lt;p&gt;To modify the default trigger behavior, you can use the &lt;code&gt;hx-trigger&lt;/code&gt; attribute to set which HTML event will cause the request. Check out the &lt;a href="//developer.mozilla.org/en-US/docs/Web/Events#event_listing"&gt;list of events supported by HTML&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Take a look at the example below:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;span hx-get="/api/v1/products" hx-trigger="mouseenter"&amp;gt;Hover Me!&amp;lt;/span&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This tells the browser:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"When a user moves the mouse over the &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;, perform a &lt;code&gt;GET&lt;/code&gt; request to the URL ‘/api/v1/products’ and render the response in the inner HTML of the &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Keep in mind that &lt;code&gt;hx-trigger&lt;/code&gt; also supports &lt;a href="//htmx.org/docs/#trigger-modifiers"&gt;modifiers&lt;/a&gt; and &lt;a href="//htmx.org/docs/#trigger-filters"&gt;filters&lt;/a&gt; to tailor the triggering logic to your needs. Plus, HTMX provides the following &lt;a href="//htmx.org/docs/#special-events"&gt;special events&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;load&lt;/code&gt;: Fires when the element is loaded for the first time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;revealed&lt;/code&gt;: Fires once when the element is scrolled into the viewport.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;intersect&lt;/code&gt;: Fires once when the element intersects with the viewport. As opposed to &lt;code&gt;revealed&lt;/code&gt;, it accepts an optional CSS selector of the root element for intersection and a float number between 0.0 and 1.0 to indicate the amount of intersection to trigger the event on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Query Parameters and Body Data&lt;/strong&gt;&lt;br&gt;
The way HTMX handles parameters and body data changes depending on the type of the request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;GET&lt;/code&gt; requests&lt;/strong&gt;: The query parameters should be specified in the URL passed to &lt;code&gt;hx-get&lt;/code&gt;. By default, &lt;code&gt;hx-get&lt;/code&gt; does not automatically include any parameters to the request. Anyway, you can control that with the &lt;code&gt;hx-params&lt;/code&gt; attribute as explained in the documentation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Non-&lt;code&gt;GET&lt;/code&gt; requests&lt;/strong&gt;: If an element is a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;, the body will include the values of all inputs within it, using their &lt;code&gt;name&lt;/code&gt; attribute as the parameter name. If it is not a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;, the body will include the values of all the inputs of the nearest enclosing &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;. Otherwise, if it has a &lt;code&gt;value&lt;/code&gt; attribute, it will be used in the body. When the default behavior is not enough, the &lt;code&gt;hx-include&lt;/code&gt; and &lt;code&gt;hx-params&lt;/code&gt; attributes allows you to control which values and which parameters to set, respectively. Otherwise, you can programmatically modify the body fields by listening to the &lt;code&gt;htmx:configRequest&lt;/code&gt; event.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result Content Handling&lt;/strong&gt;&lt;br&gt;
By default, HTMX replaces the inner HTML of the element firing the request with the HTML returned by the AJAX call. This means that HTMX-compliant AJAX endpoints should return HTML code.&lt;/p&gt;

&lt;p&gt;To change the swap strategy, use the &lt;code&gt;hx-swap&lt;/code&gt; attribute. That supports the following values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;innerHTML&lt;/code&gt;: Replace the inner HTML of the target element.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outerHTML&lt;/code&gt;: Replace the entire target element with the response.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;beforebegin&lt;/code&gt;: Insert the response before the target element.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;afterbegin&lt;/code&gt;: Insert the response before the first child of the target element.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;beforeend&lt;/code&gt;: Insert the response after the last child of the target element.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;afterend&lt;/code&gt;: Insert the response after the target element.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delete&lt;/code&gt;: Delete the target element regardless of the response.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;none&lt;/code&gt;: Does not append the content from the response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can change the target element the swap logic refers to with the &lt;code&gt;hx-target&lt;/code&gt; attribute, which accepts a &lt;a href="//developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors"&gt;CSS selector&lt;/a&gt;. Note that the attribute supports multiple triggers, each one separated by comma.&lt;/p&gt;

&lt;p&gt;Focus now on the following snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;button 
  hx-post="/api/v1/comments"
  hx-trigger="click" 
  hx-swap="afterend" 
  hx-target=".comments"
  &amp;gt;
Comment
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the browser:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“When a user clicks the &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, perform a &lt;code&gt;POST&lt;/code&gt; request to the URL ‘/api/v1/comments’ and add the resulting HTML to the &lt;code&gt;.comments&lt;/code&gt; element”&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  HTMX in Action: Integration With ApostropheCMS
&lt;/h2&gt;

&lt;p&gt;You now know what HTMX is, why it was created, and what it brings to the table. All that remains is to see it in action in a real-world example. What better way to do that than by integrating it with &lt;a href="//apostrophecms.com/"&gt;Apostrophe&lt;/a&gt;? If you are not familiar with this technology, ApostropheCMS is an open-source CMS and website builder built on top of modern technologies such as MongoDB and Node.js. &lt;/p&gt;

&lt;p&gt;As ApostropheCMS is unopinionated on the frontend, it represents a natural fit for HTMX. Its data management and website building capabilities will make it easy and intuitive to dynamically retrieve and render HTML content via HTMX. &lt;/p&gt;

&lt;p&gt;In this step-by-step section, you will look at how to integrate HTMX into an existing application. The starting point will be the blog application built in the &lt;a href="//apostrophecms.com/blog/how-to-build-a-blog-apostrophe"&gt;“How to Build a Blog with the Apostrophe Blog Module”&lt;/a&gt; tutorial. You will learn how to add HTMX and use it to achieve the following dynamic interactions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Load More” functionality&lt;/li&gt;
&lt;li&gt;Infinite scroll loading&lt;/li&gt;
&lt;li&gt;Live content filtering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s dive in!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting Started&lt;/strong&gt;&lt;br&gt;
First, make sure to meet &lt;a href="//v3.docs.apostrophecms.org/guide/setting-up.html#system-requirements"&gt;ApostropheCMS's system requirements&lt;/a&gt;. Next, launch the command below to clone the &lt;a href="//github.com/Tonel/apostrophe-blog"&gt;GitHub repository of the blog application&lt;/a&gt; you will soon extend with HTMX:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/Tonel/apostrophe-blog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the project’s dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, fire the following command to build the ApostropheCMS UI and start up the blog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;https://localhost:3000&lt;/code&gt; in the browser and you should see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fein0x0130tlxxxbp3xn5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fein0x0130tlxxxbp3xn5.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Follow the instructions, log in, and get familiar with the application. Play with the UI and populate the blog with several posts.&lt;/p&gt;

&lt;p&gt;You can then find the blog's home page at &lt;code&gt;http://localhost:3000/blog&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fks86t0ipaxjg9c68nhzh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fks86t0ipaxjg9c68nhzh.png" alt=" " width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Great! If you want to learn more about how this application works and was built, take a look at our &lt;a href="//apostrophecms.com/blog/how-to-build-a-blog-apostrophe"&gt;tutorial&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrating HTMX&lt;/strong&gt;&lt;br&gt;
&lt;a href="//htmx.org/docs/#installing"&gt;As stated in the documentation&lt;/a&gt;, integrating HTMX into an application boils down to adding a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag to the document &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. No build tools or special configurations are required.&lt;/p&gt;

&lt;p&gt;The fastest way to get going is to load the library via a CDN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script src="https://unpkg.com/htmx.org@1.9.6" integrity="sha384-FhXw7b6AlE/jyjlZH5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal of this section is to use HTMX to add dynamic interactions to the blog home page. So, you need to add the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; instruction to the HTML document of that page.&lt;/p&gt;

&lt;p&gt;To add HTMX to the blog home page, follow the &lt;code&gt;/modules/@apostrophecms/blog-page/views/&lt;/code&gt; path and open the &lt;code&gt;index.html&lt;/code&gt; file. Paste the following line after the &lt;code&gt;title&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% block extraHead %}
  &amp;lt;script src="https://unpkg.com/htmx.org@1.9.6" integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni" crossorigin="anonymous"&amp;gt;&amp;lt;/script&amp;gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;extraHead&lt;/code&gt; is a block from the &lt;a href="//v3.docs.apostrophecms.org/guide/layout-template.html"&gt;ApostropheCMS core layout template&lt;/a&gt; that allows you to add HTML elements at the end of the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;If you instead want to have HTMX in all pages, you can add it to your project's dependencies with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install htmx.org
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, import it in the &lt;code&gt;modules/asset/ui/src/index.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import 'htmx.org';

export default () =&amp;gt; {
  // your own project-level JS...
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000/blog&lt;/code&gt; in the browser and inspect its source code. You should see the following HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang="en" &amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;link href="/apos-frontend/default/apos-bundle.css" rel="stylesheet" /&amp;gt;
    &amp;lt;title&amp;gt;My Fantastic Blog &amp;lt;/title&amp;gt;
    &amp;lt;meta name="viewport" content="width=device-width, initial-scale=1"&amp;gt;
    &amp;lt;script src="https://unpkg.com/htmx.org@1.9.6" integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni" crossorigin="anonymous"&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;
&amp;lt;!-- Omitted for brevity... --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well done! The HTMX dependency script was added as required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding a Load More Button With HTMX&lt;/strong&gt;&lt;br&gt;
Right now, when the blog has more than 10 posts, the home page shows a pagination element.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7lrshsi7euoq3u1q19o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7lrshsi7euoq3u1q19o.png" alt=" " width="800" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on one of these buttons, and you will be redirected to the selected page. For example, “2” brings you to &lt;code&gt;/blog?page=2&lt;/code&gt;. What if you wanted to replace that interaction with a “Load More” button? Thanks to HTMX, that will take only a few lines of code!&lt;/p&gt;

&lt;p&gt;When the “Load More” button is clicked, the page should perform an AJAX request to retrieve the HTML to render other blog post cards. You could think of using HTMX to make the button target the &lt;code&gt;/blog?page=2&lt;/code&gt; endpoint and swap the current content with the retrieved HTML. However, keep in mind that &lt;code&gt;/blog?page=2&lt;/code&gt; returns the entire HTML of a new page. Following this approach is not recommended, as you ideally want to replace only a small portion of the page not the all the page. Specifically, you want to swap the “Load More” button with the new blog post tabs.&lt;/p&gt;

&lt;p&gt;To get close to the goal, you can take advantage of the &lt;code&gt;aposRefresh=1&lt;/code&gt; parameter. This query parameter instructs ApostropheCMS to return the rendered HTML of the inner template, excluding the wrapping markup.&lt;/p&gt;

&lt;p&gt;For example, the &lt;code&gt;/blog?page=2&amp;amp;aposRefresh=1&lt;/code&gt; endpoint returns something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="bg-container"&amp;gt;
  &amp;lt;h1 class="bg-h1"&amp;gt;My Fantastic Blog&amp;lt;/h1&amp;gt;
  &amp;lt;h2&amp;gt;Filters&amp;lt;/h2&amp;gt;
  &amp;lt;ul class="bg-filter-list"&amp;gt;
    &amp;lt;li&amp;gt;
      &amp;lt;a href="/blog?year=2023"&amp;gt;2023&amp;lt;/a&amp;gt;
    &amp;lt;/li&amp;gt;
    &amp;lt;li&amp;gt;
      &amp;lt;a href="/blog?year=2022"&amp;gt;2022&amp;lt;/a&amp;gt;
    &amp;lt;/li&amp;gt;
    &amp;lt;li&amp;gt;
      &amp;lt;a href="/blog?year=2021"&amp;gt;2021&amp;lt;/a&amp;gt;
    &amp;lt;/li&amp;gt;
    &amp;lt;li&amp;gt;
      &amp;lt;a class="is-active" href="/blog"&amp;gt;All&amp;lt;/a&amp;gt;
    &amp;lt;/li&amp;gt;
  &amp;lt;/ul&amp;gt;
  &amp;lt;h2&amp;gt;Blog post&amp;lt;/h2&amp;gt;
  &amp;lt;div class="bg-preview-card"&amp;gt;
    &amp;lt;div class="bg-preview-date"&amp;gt;
      Released on September 4, 2022
    &amp;lt;/div&amp;gt;
    &amp;lt;div class="bg-preview-title"&amp;gt;
      &amp;lt;a href="/blog/lorem-ipsum-8"&amp;gt;Lorem Ipsum 8&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class="bg-preview-card"&amp;gt;
    &amp;lt;div class="bg-preview-date"&amp;gt;
      Released on August 2, 2022
    &amp;lt;/div&amp;gt;
    &amp;lt;div class="bg-preview-title"&amp;gt;
      &amp;lt;a href="/blog/lorem-ipsum-12"&amp;gt;Lorem Ipsum 12&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;!-- Omitted for brevity... --&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much better! As you can see, this HTML involves only a partial section of the page. At the same time, it still includes the title and filter elements. To ignore them, you can change the &lt;code&gt;index.html&lt;/code&gt; file so that it behaves differently based on the presence of a custom query parameter.&lt;/p&gt;

&lt;p&gt;Achieve that by updating the rendering logic inside the &lt;code&gt;main&lt;/code&gt; block of &lt;code&gt;/modules/@apostrophecms/blog-page/views/index.html&lt;/code&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% block main %}
  {% if data.query.showOnlyList != "1" %}
    &amp;lt;div class="bg-container"&amp;gt;
      &amp;lt;h1 class="bg-h1"&amp;gt;{{ data.page.title }}&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;{{ __t('aposBlog:filters') }}&amp;lt;/h2&amp;gt;

      {% render filters.render({
           filters: data.piecesFilters,
           query: data.query,
           url: data.page._url
      }) %}

      &amp;lt;h2&amp;gt;{{ __t('aposBlog:pluralLabel') }}&amp;lt;/h2&amp;gt;

      {{ renderBlogList() }}
    &amp;lt;/div&amp;gt;
  {% else %}
    {{ renderBlogList() }}
  {% endif %}
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when the &lt;code&gt;showOnlyList=1&lt;/code&gt; query parameter is present, the endpoint for the blog home page will return only the list of blog posts. Otherwise, it will return the entire page as before.&lt;/p&gt;

&lt;p&gt;You may be wondering what &lt;code&gt;renderBlogList()&lt;/code&gt; is. This is a custom &lt;a href="//mozilla.github.io/nunjucks/templating.html#macro"&gt;Nunjucks macro&lt;/a&gt; that renders the list of blog post cards and the “Load More” button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% set page = data.query.page | default(1) | int %}
  {% for piece in data.pieces %}
    &amp;lt;div class="bg-preview-card"&amp;gt;
      &amp;lt;div class="bg-preview-date"&amp;gt;
        {{ __t('aposBlog:releasedOn') }} {{ piece.publishedAt | date('MMMM D, YYYY') }}
      &amp;lt;/div&amp;gt;
      &amp;lt;div class="bg-preview-title"&amp;gt;
        &amp;lt;a href="{{ piece._url }}"&amp;gt;{{ piece.title }}&amp;lt;/a&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  {% endfor %}
  &amp;lt;div class="load-more-div"&amp;gt;
    {% if page != data.totalPages %}
      &amp;lt;button
        hx-get="/blog?page={{page + 1}}&amp;amp;year={{data.query.year}}&amp;amp;showOnlyList=1&amp;amp;aposRefresh=1"
        hx-target=".load-more-div"
        hx-swap="outerHTML"
      &amp;gt;
        Load More
      &amp;lt;/button&amp;gt;
    {% endif %}
  &amp;lt;/div&amp;gt;
{% endmacro %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Focus on the &lt;code&gt;.load-more-div&lt;/code&gt; HTML element. That is where the HTMX magic happens!&lt;/p&gt;

&lt;p&gt;&lt;code&gt;page&lt;/code&gt; is a variable that stores the current page of the blog posts to render. If there are still some blogs to load, the “Load More” button is added to the page. When the user clicks it, the webpage makes an AJAX request to the endpoint specified in &lt;code&gt;hx-get&lt;/code&gt;. This will return the rendered HTML with the list of the posts related to the next page, considering the optional year filter. HTMX will then replace the outer HTML of the &lt;code&gt;.load-more-div&lt;/code&gt; element with that content.&lt;/p&gt;

&lt;p&gt;Put it all together, and you will get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- /modules/@apostrophecms/blog-page/views/index.html --&amp;gt;

{% extends data.outerLayout %}

{% import "filters.html" as filters %}
{% import "@apostrophecms/pager:macros.html" as pager with context %}

{% block title %}{{ data.page.title }} {% endblock %}

{% block extraHead %}
  &amp;lt;script src="https://unpkg.com/htmx.org@1.9.6" integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni" crossorigin="anonymous"&amp;gt;&amp;lt;/script&amp;gt;
{% endblock %}

{% macro renderBlogList() %}
  {% set page = data.query.page | default(1) | int %}
  {% for piece in data.pieces %}
    &amp;lt;div class="bg-preview-card"&amp;gt;
      &amp;lt;div class="bg-preview-date"&amp;gt;
        {{ __t('aposBlog:releasedOn') }} {{ piece.publishedAt | date('MMMM D, YYYY') }}
      &amp;lt;/div&amp;gt;
      &amp;lt;div class="bg-preview-title"&amp;gt;
        &amp;lt;a href="{{ piece._url }}"&amp;gt;{{ piece.title }}&amp;lt;/a&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  {% endfor %}
  &amp;lt;div class="load-more-div"&amp;gt;
    {% if page != data.totalPages %}
      &amp;lt;button
        hx-get="/blog?page={{page + 1}}&amp;amp;year={{data.query.year}}&amp;amp;showOnlyList=1&amp;amp;aposRefresh=1"
        hx-target=".load-more-div"
        hx-swap="outerHTML"
      &amp;gt;
        Load More
      &amp;lt;/button&amp;gt;
    {% endif %}
  &amp;lt;/div&amp;gt;
{% endmacro %}

{% block main %}
  {% if data.query.showOnlyList != "1" %}
    &amp;lt;div class="bg-container"&amp;gt;
      &amp;lt;h1 class="bg-h1"&amp;gt;{{ data.page.title }}&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;{{ __t('aposBlog:filters') }}&amp;lt;/h2&amp;gt;

      {% render filters.render({
           filters: data.piecesFilters,
           query: data.query,
           url: data.page._url
      }) %}

      &amp;lt;h2&amp;gt;{{ __t('aposBlog:pluralLabel') }}&amp;lt;/h2&amp;gt;

      {{ renderBlogList() }}
    &amp;lt;/div&amp;gt;
  {% else %}
    {{ renderBlogList() }}
  {% endif %}
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the pagination element has been removed by the template. You no longer need it.&lt;/p&gt;

&lt;p&gt;Style the “Load More” button in &lt;code&gt;/modules/asset/ui/src/scss/_blog.scss&lt;/code&gt;, and you are ready to test it. This is how your new &lt;code&gt;http://localhost:3000/blog&lt;/code&gt; page behaves:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2ljgzu13ukqjmilz70d.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2ljgzu13ukqjmilz70d.gif" alt=" " width="760" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you inspect the “Network” section of the browser's DevTools, you will notice that the “Load More” button triggers the following AJAX call:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0z7bd4p80x9n9s45hae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0z7bd4p80x9n9s45hae.png" alt=" " width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will return the HTML containing the new blog post cards to add to the page.&lt;/p&gt;

&lt;p&gt;Congrats! You just used HTMX to add a click-to-load feature to your blog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: You can find the entire code of this example in the &lt;code&gt;htmx-load-more&lt;/code&gt; branch of &lt;a href="//github.com/Tonel/apostrophe-blog/tree/htmx-load-more"&gt;the GitHub repository supporting the article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using HTMX to Implement Infinite Scrolling&lt;/strong&gt;&lt;br&gt;
Now that you have seen how to implement a “Load More” button with HTMX, achieving infinite scrolling is easy. All you have to do is change the &lt;code&gt;renderBlogList()&lt;/code&gt; function as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% macro renderBlogList() %}
  {% set page = data.query.page | default(1) | int %}
  {% for piece in data.pieces %}
    &amp;lt;div class="bg-preview-card"
       {% if loop.last %}
           hx-get="/blog?page={{page + 1}}&amp;amp;year={{data.query.year}}&amp;amp;showOnlyList=1&amp;amp;aposRefresh=1"
           hx-trigger="revealed"
           hx-swap="afterend"
       {% endif %}
    &amp;gt;
      &amp;lt;div class="bg-preview-date"&amp;gt;
        {{ __t('aposBlog:releasedOn') }} {{ piece.publishedAt | date('MMMM D, YYYY') }}
      &amp;lt;/div&amp;gt;
      &amp;lt;div class="bg-preview-title"&amp;gt;
        &amp;lt;a href="{{ piece._url }}"&amp;gt;{{ piece.title }}&amp;lt;/a&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  {% endfor %}
{% endmacro %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;revealed&lt;/code&gt; HTMX event triggers when an element is scrolled into the viewport. By adding it to the last blog post card, you can implement infinite scroll loading behavior:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdigu3ndg034wktckj48e.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdigu3ndg034wktckj48e.gif" alt=" " width="760" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Awesome! Focus on the scrollbar to notice that the page adds dynamic content as the user scrolls down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: You can find the complete code of this example in the &lt;code&gt;htmx-infinite-scrolling&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Achieving Live Content Filtering Through HTMX&lt;/strong&gt;&lt;br&gt;
The goal here is to use HTMX to dynamically update the content of the page when clicking on a year filter button. In this case, you do not have to update the blog post list, but replace it entirely. Also, you need to override the HTML section that contains the filters to ensure that the correct button is enabled.&lt;/p&gt;

&lt;p&gt;First, update &lt;code&gt;filters.html&lt;/code&gt; to introduce the HTMX logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- /modules/@apostrophecms/blog-page/views/filters.html --&amp;gt;

{%- macro here(url, changes) -%}
{{ url | build({
year: data.query.year
}, {
excludeContainer: 1,
aposRefresh: 1
}, changes) }}
{%- endmacro -%}

{% fragment render(data) %}
  &amp;lt;ul class="bg-filter-list"&amp;gt;
    {% for year in data.filters.year %}
      &amp;lt;li&amp;gt;
          &amp;lt;button
            class="{{ 'is-active' if data.query.year == year.value }}"
            hx-get="{{ here(data.url, { year: year.value })}}"
            hx-target=".blog-page"
            hx-swap="outerHTML"
          &amp;gt;
            {{ __t(year.label) }}
          &amp;lt;/button&amp;gt;
      &amp;lt;/li&amp;gt;
    {% endfor %}
&amp;lt;/ul&amp;gt;
{% endfragment %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the year filter elements are no longer links, but buttons that target a specific endpoint via HTMX. In particular, &lt;code&gt;here()&lt;/code&gt; has been updated to produce the URL required to dynamically retrieve the desired HTML content. Keep in mind that &lt;code&gt;build()&lt;/code&gt; accepts as many query parameter objects as you need. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;excludeContainer&lt;/code&gt; query parameter will control the rendering logic in the &lt;code&gt;index.html&lt;/code&gt; file as below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- /modules/@apostrophecms/blog-page/views/filters.html --&amp;gt;

{% extends data.outerLayout %}

{% import "filters.html" as filters %}
{% import "@apostrophecms/pager:macros.html" as pager with context %}

{% block title %}{{ data.page.title }} {% endblock %}

{% block extraHead %}
  &amp;lt;script src="https://unpkg.com/htmx.org@1.9.6" integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni" crossorigin="anonymous"&amp;gt;&amp;lt;/script&amp;gt;
{% endblock %}

{% block main %}
  {% if data.query.excludeContainer != "1" %}
    &amp;lt;div class="bg-container"&amp;gt;
      &amp;lt;h1 class="bg-h1"&amp;gt;{{ data.page.title }}&amp;lt;/h1&amp;gt;
  {% endif %}
     &amp;lt;div class="blog-page"&amp;gt;
        &amp;lt;h2&amp;gt;{{ __t('aposBlog:filters') }}&amp;lt;/h2&amp;gt;
        {% render filters.render({
             filters: data.piecesFilters,
             query: data.query,
             url: data.page._url
        }) %}
        &amp;lt;h2&amp;gt;{{ __t('aposBlog:pluralLabel') }}&amp;lt;/h2&amp;gt;
        {% for piece in data.pieces %}
          &amp;lt;div class="bg-preview-card"&amp;gt;
            &amp;lt;div class="bg-preview-date"&amp;gt;
              {{ __t('aposBlog:releasedOn') }} {{ piece.publishedAt | date('MMMM D, YYYY') }}
            &amp;lt;/div&amp;gt;
            &amp;lt;div class="bg-preview-title"&amp;gt;
              &amp;lt;a href="{{ piece._url }}"&amp;gt;{{ piece.title }}&amp;lt;/a&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
        {% endfor %}
        &amp;lt;div class="pagination"&amp;gt;
          {{ pager.render({ page: data.currentPage, total: data.totalPages }, data.url | build({ excludeContainer: null })) }}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
  {% if data.query.excludeContainer != "1" %}
    &amp;lt;/div&amp;gt;
  {% endif %}
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The home page of the blog will now have real-time filtering capabilities:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh84x5xcwi2q9xs4wnekr.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh84x5xcwi2q9xs4wnekr.gif" alt=" " width="1140" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Awesome! You have just learned how HTMX simplifies the integration of dynamic interactions into an existing frontend application.&lt;/p&gt;

&lt;p&gt;The next step is to add a loader through the &lt;code&gt;hx-indicator&lt;/code&gt; attribute. &lt;a href="//htmx.org/docs/"&gt;Check out the docs&lt;/a&gt; to see all the other cool features HTMX has to offer!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: You can find the entire code of the example in the &lt;code&gt;htmx-content-filtering&lt;/code&gt; branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is HTMX the Future of Web Development?
&lt;/h2&gt;

&lt;p&gt;HTMX is not here to replace React and similar frameworks. That should be clear. Instead, it presents a compelling solution for specific web development scenarios backed by its feature set. &lt;/p&gt;

&lt;p&gt;To be specific, HTMX proves particularly well-suited for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Efficient interactivity&lt;/strong&gt;: It represents a lightweight solution compared to most JavaScript frameworks, making it an excellent choice to keep projects simple and lean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seamless integration&lt;/strong&gt;: It natively supports integration with various technologies, such as ApostropheCMS, Django, and Flask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal overhead&lt;/strong&gt;: It simplifies web development via custom HTML attributes, removing the complexity associated with modern JavaScript development.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, HTMX fills a niche where its capabilities shine, providing a valuable tool for web developers seeking efficient and seamless interactivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, you explored HTMX, understanding what the library is, why it was born, what capabilities it provides, and why it is so popular. You learned how to get started with HTMX and how to use it to extend an existing ApostropheCMS application with dynamic interactions such as infinite scrolling. Once again, ApostropheCMS has proven to be a modern, solid, forward-looking technology that can support fresh libraries thanks to its unopinionated approach on the frontend. &lt;a href="https://apostrophecms.com/demo" rel="noopener noreferrer"&gt;Try Apostrophe today&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>htmx</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>howto</category>
    </item>
    <item>
      <title>How To Set Up the Template Library Pro Module in Apostrophe</title>
      <dc:creator>Antonello Zanini</dc:creator>
      <pubDate>Thu, 02 Nov 2023 11:30:00 +0000</pubDate>
      <link>https://dev.to/apostrophecms/how-to-set-up-the-template-library-pro-module-in-apostrophe-56p</link>
      <guid>https://dev.to/apostrophecms/how-to-set-up-the-template-library-pro-module-in-apostrophe-56p</guid>
      <description>&lt;p&gt;Code reuse is one of the fundamental pillars of programming. After spending time defining a UI component, procedure, or function, developers want to reuse what they have built as much as possible. But what about content creation? Editors also invest a lot of time in crafting quality content or devising original layouts. They, too, should have the opportunity to reuse what they create. This is where Apostrophe Template Library comes in!&lt;/p&gt;

&lt;p&gt;This powerful Apostrophe Pro module transforms the way editors can approach content creation and design in the digital world. In detail, Template Library allows content creators to create and manage both layout and content templates directly in &lt;a href="https://apostrophecms.com/" rel="noopener noreferrer"&gt;Apostrophe&lt;/a&gt;. The idea behind the module is to create content once and reuse it multiple times with just a few changes.&lt;/p&gt;

&lt;p&gt;In this step-by-step tutorial, you will understand what Apostrophe Pro’s Template Library module is, how to set it up, and what it has to offer.&lt;/p&gt;

&lt;p&gt;Let's dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the Apostrophe Template Library Module?
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apostrophecms.com/extensions/template-library" rel="noopener noreferrer"&gt;Template Library&lt;/a&gt; module is an &lt;a href="https://apostrophecms.com/blog/introducing-apostrophe-pro" rel="noopener noreferrer"&gt;Apostrophe Pro extension&lt;/a&gt;. Its goal is to allow product managers to create and manage reusable templates. Specifically, each template must be related to an existing &lt;a href="https://v3.docs.apostrophecms.org/guide/pages.html" rel="noopener noreferrer"&gt;page type&lt;/a&gt; or &lt;a href="https://v3.docs.apostrophecms.org/guide/pieces.html" rel="noopener noreferrer"&gt;piece type&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;In other words, there are two types of templates supported by the Pro module:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Page templates&lt;/strong&gt;: An existing page can become a reusable template that editors can apply to new pages to simplify the design process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Piece templates&lt;/strong&gt;: An existing piece instance can become a reusable template that editors can apply to new pieces to facilitate the creation of new content instances.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/@apostrophecms-pro/doc-template-library" rel="noopener noreferrer"&gt;@apostrophecms-pro/doc-template-library&lt;/a&gt; is the name of the npm package of the Template Library module. &lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits of the Template Library Extension
&lt;/h2&gt;

&lt;p&gt;The Template Library is a powerful tool that provides a lot of benefits to editors. Take a look at the three most important reasons why you should enable it in your Apostrophe application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enhanced Reusability&lt;/strong&gt;: Promoting a culture of element reuse is what a templating library is all about. After spending a lot of time designing layouts and creating great content, editors can save them in templates with just a few clicks and reuse them whenever they wish. Just think, “design once and reuse many times!”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved Productivity&lt;/strong&gt;: The module eliminates the need to start from scratch when defining content or designing new pages. This significantly boosts the productivity of content managers, allowing them to focus more on what matters the most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Increased Consistency&lt;/strong&gt;: Templates ensure that each new page or piece adheres to established style guidelines and design principles. This helps teams maintain a consistent look and feel, which is critical to building excellent user experiences.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting Up the Template Library Pro Module in Apostrophe
&lt;/h2&gt;

&lt;p&gt;In this section, you will learn how to integrate Template Library into your Apostrophe project.&lt;/p&gt;

&lt;p&gt;Since the Template Library is a Pro module, you first need to join &lt;a href="https://apostrophecms.com/pro" rel="noopener noreferrer"&gt;Apostrophe Pro&lt;/a&gt;. Keep in mind that the module is also available in Apostrophe Assembly. &lt;a href="https://apostrophecms.com/pricing" rel="noopener noreferrer"&gt;Check our pricing page&lt;/a&gt; to compare our plans and find the solution that best suits you. &lt;/p&gt;

&lt;p&gt;You will need an Apostrophe 3+ application to follow the tutorial below. If you do not already have one, read the &lt;a href="https://v3.docs.apostrophecms.org/guide/setting-up.html" rel="noopener noreferrer"&gt;setup guide in our documentation&lt;/a&gt;. Make sure you meet the requirements and then follow the instructions to create an Apostrophe 3+ project.&lt;/p&gt;

&lt;p&gt;Time to set up the Template Library module!&lt;/p&gt;

&lt;h4&gt;
  
  
  Install the Template Library Module
&lt;/h4&gt;

&lt;p&gt;After joining Apostrophe Pro, you will be added to the &lt;code&gt;@apostrophecms-pro&lt;/code&gt; npm organization. However, if you npm install Pro modules in your projects directly, you will get the following 404 error: &lt;code&gt;'@apostrophecms-pro/&amp;lt;pro_module_package_name&amp;gt;@*' is not in this registry&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The reason is that you must first authenticate your npm commands. In a development environment, the easiest way to do so is by running the following command: &lt;code&gt;npm login&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Press ENTER and the npm login page will automatically open in your browser. Type in your credentials and click the “Sign In” button.&lt;/p&gt;

&lt;p&gt;Once logged in, go back to your project. You should see the success message below in your terminal: &lt;code&gt;Logged in on https://registry.npmjs.org/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;To keep the session alive, npm will create a global &lt;a href="https://docs.npmjs.com/cli/v10/configuring-npm/npmrc" rel="noopener noreferrer"&gt;.npmrc configuration file&lt;/a&gt; for you containing the line below: &lt;code&gt;//registry.npmjs.org/:_authToken=&amp;lt;YOUR_NPM_ACCESS_TOKEN&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now, you can add the Template Library module to your project's dependencies with: &lt;code&gt;npm install @apostrophecms-pro/doc-template-library&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Behind the scenes, npm will use the authenticated URL seen earlier to download the private package and then install it.&lt;/p&gt;

&lt;p&gt;In a production environment, you cannot rely on that approach. The problem is that the global configuration file exposes your &lt;a href="https://docs.npmjs.com/about-access-tokens" rel="noopener noreferrer"&gt;npm access token&lt;/a&gt;, which should be treated as a secret. Instead, set up a granular token related to &lt;code&gt;@apostrophecms-pro&lt;/code&gt; by following the &lt;a href="https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-granular-access-tokens-on-the-website" rel="noopener noreferrer"&gt;official guide&lt;/a&gt;. Then, create a local &lt;code&gt;.npmrc&lt;/code&gt; file in the root folder of your project and add the following line to it: &lt;code&gt;//registry.npmjs.org/:_authToken=${NPM_TOKEN}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When running an npm command, the CLI will replace &lt;code&gt;${NPM_TOKEN}&lt;/code&gt; with the value read from the &lt;code&gt;NPM_TOKEN&lt;/code&gt; environment variable. Thus, set the &lt;code&gt;NPM_TOKEN&lt;/code&gt; env with the command below in the deployment server: &lt;code&gt;export NPM_TOKEN="&amp;lt;YOUR_NPM_GRANULAR_TOKEN&amp;gt;"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Replace &lt;code&gt;&amp;lt;YOUR_NPM_GRANULAR_TOKEN&amp;gt;&lt;/code&gt; with the value of your granular access token. The production environment is now allowed to install &lt;code&gt;@apostrophecms-pro/doc-template-library&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Wonderful! It only remains to configure the &lt;code&gt;doc-template-library&lt;/code&gt; module in your Apostrophe project.&lt;/p&gt;

&lt;h4&gt;
  
  
  Enable the Template Library Module in Your Project
&lt;/h4&gt;

&lt;p&gt;As with any other Apostrophe module, you need to enable Template Library in the &lt;code&gt;app.js&lt;/code&gt; file. To do so, make sure &lt;code&gt;app.js&lt;/code&gt; contains the following lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// app.js

require('apostrophe')({
  // the name of your project
  shortName: 'apos-template-library-demo',
  modules: {
    // other modules...

    // enable the doc-template-library module
    '@apostrophecms-pro/doc-template-library': {},
  },
  // remaining configs...
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch the development server, and you should now see a new button “Doc Template Library” in the upper right corner as below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwracp13msrc71l4t5q7.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwracp13msrc71l4t5q7.gif" alt="Doc Template Library Launcher" width="512" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congrats! You just set up the Template Library Pro module in Apostrophe.&lt;/p&gt;

&lt;h4&gt;
  
  
  Main Features of Apostrophe's Template Library
&lt;/h4&gt;

&lt;p&gt;Let's explore the features and capabilities offered to editors by the Template Library module. For a more in-depth look, read our article &lt;a href="https://apostrophecms.com/blog/how-to-use-template-library" rel="noopener noreferrer"&gt;“How to Use Apostrophe's Template Library to Improve Productivity.”&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the example below, we will assume that your Apostrophe application has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A single piece type&lt;/strong&gt;: “Article” &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three page types&lt;/strong&gt;: “Default,” “Article index,” and “Home”&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Explore Existing Templates
&lt;/h4&gt;

&lt;p&gt;The “Doc Template Library” button opens the following modal:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F37jdn8hg5izdo430mwgu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F37jdn8hg5izdo430mwgu.gif" alt="Doc Template Library modal" width="512" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, editors can see and manage all the page and piece templates available. Each template is represented by a card containing the image specified at creation time or a live preview automatically generated by Apostrophe. &lt;/p&gt;

&lt;p&gt;Given a template, content managers can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edit&lt;/strong&gt;: Change its settings or layout. This will only change the layout itself and not the pages or pieces based on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archive&lt;/strong&gt;: Remove the template from the library. This will not affect pages or pieces created using the template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate&lt;/strong&gt;: Generate a new template as a copy of the selected one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Define a New Template
&lt;/h4&gt;

&lt;p&gt;Editors can create a new template from scratch directly in the UI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupvhc02sbtdldu92qfmd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupvhc02sbtdldu92qfmd.gif" alt="create a new template modal" width="512" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the creation modal below, content managers need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give the template a name &lt;/li&gt;
&lt;li&gt;Add an optional overview image&lt;/li&gt;
&lt;li&gt;Select the content type associated with the template&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fqrw08m6g7bm6tz9dmw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fqrw08m6g7bm6tz9dmw.gif" alt="content type modal" width="512" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The content types available correspond to the list of page or piece types in the project.&lt;/p&gt;

&lt;p&gt;After creating the template, Apostrophe will open a new page to define its content through the in-context editing feature.&lt;/p&gt;

&lt;p&gt;For example, they could craft a template including instructions and tips for writing great articles:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F48a6u3sg36uwfy585v4t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F48a6u3sg36uwfy585v4t.png" alt="general article template" width="512" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Et voilà! Adding a new template takes only a few clicks. Keep in mind that editors also have the option of turning existing pages and pieces into templates.&lt;/p&gt;

&lt;h4&gt;
  
  
  Apply a Template
&lt;/h4&gt;

&lt;p&gt;When adding a new piece instance or page, editors can now select one of the templates available from the “Template” tab. After applying one, the &lt;a href="https://v3.docs.apostrophecms.org/reference/field-types/area.html" rel="noopener noreferrer"&gt;&lt;code&gt;area&lt;/code&gt;&lt;/a&gt; field of the piece instance will contain the same content as the template:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6d5b9kl15405ahanyzc2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6d5b9kl15405ahanyzc2.gif" alt="apply a template in the library" width="512" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just see how this mechanism simplifies creating high-quality content by providing effective starting points!&lt;/p&gt;

&lt;h3&gt;
  
  
  Extra: Excluding a Piece Type From Templates
&lt;/h3&gt;

&lt;p&gt;The ability to create templates based on existing pieces is great, but you may want to disable it for specific piece types. To do so, set the &lt;code&gt;hasTemplates&lt;/code&gt; field to &lt;code&gt;false&lt;/code&gt; in the &lt;code&gt;modules&lt;/code&gt; option in &lt;code&gt;app.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require('apostrophe')({
  shortName: 'apos-template-library-demo',
  modules: {
    // other modules...
    article: {
      options: {
        hasTemplates: false // &amp;lt;-- disable template for this piece type
      }
    }
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the app to refresh, and you will no longer be able to select the &lt;code&gt;article&lt;/code&gt; piece type in the template creation modal. Also, the “Save as template…” option on the piece type will disappear.&lt;/p&gt;

&lt;p&gt;Perfect, you just disabled the template features on the selected piece type to editors as desired. With Apostrophe, you are in control!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we dug into the Apostrophe Pro’s Template Library module from a technical point of view. As seen here, it brings a lot of benefits to editors and adding it to an existing Apostrophe project only takes a couple of minutes.&lt;/p&gt;

&lt;p&gt;After setting it up, editors and content managers on your team will have the ability to transform current content into reusable templates. The consequences in terms of productivity and consistency are huge and are only a few lines of code away!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Navigating the Generative AI Downpour</title>
      <dc:creator>The Apostrophe Team</dc:creator>
      <pubDate>Wed, 04 Oct 2023 12:05:00 +0000</pubDate>
      <link>https://dev.to/apostrophecms/navigating-the-generative-ai-downpour-56p2</link>
      <guid>https://dev.to/apostrophecms/navigating-the-generative-ai-downpour-56p2</guid>
      <description>&lt;p&gt;This year we have observed the flow of news around generative AI technologies go from a light trickle to a full on downpour. Today we are saturated, our inboxes full each morning with a deluge of new products, new funding rounds, new excitement about the power of generative AI and its immediate impact and future potential. It can be overwhelming (in both a positive and a negative sense) to observe this surging, turbulent flood of both opportunity and risk – and to try to figure out where to jump in and start swimming. At Apostrophe, we are letting our core principles guide us.&lt;/p&gt;

&lt;p&gt;Like many other tech shops out there, &lt;a href="https://apostrophecms.com/blog/how-ai-is-transforming-the-cms-industry"&gt;our thinking about this started at the beginning of the year&lt;/a&gt; after seeing some of the earliest integrations coming from platforms like OpenAI. Thanks to the flexible architecture of Apostrophe, we were able to easily experiment with some simple integrations between generative AI and our content creation tools. We quickly &lt;a href="https://apostrophecms.com/extensions/ai-helper"&gt;open sourced our first module&lt;/a&gt; to demonstrate this potential and have since shared more &lt;a href="https://apostrophecms.com/blog/how-to-integrate-generative-ai-into-apostrophe"&gt;in depth tutorials on the topic&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/853838055" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;br&gt;
This first small release served as a foundation for our team to strategize about the future roadmap for Apostrophe and generative AI. With some internal strategy workshops and a hackathon, we explored everything from “create fully baked dynamic page layouts via a text prompt” to “enhance the experience of creating and searching developer documentation” to “provide suggestions to help editors create more SEO friendly sites” to “automatically translate text when localizing content”. We were lucky to share some of this process with some of our closest collaborators and the results have informed the future of our product roadmap.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpnosqudlb5id1o97zhfi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpnosqudlb5id1o97zhfi.jpg" alt="Apostrophe Team discussing AI during an in-person workshop" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As we consider these many possibilities, we look to our guiding principles and core values to help us prioritize the features that we think bring the strongest alignment and most significant value. Over all the years of evolution in our field, our focus has remained:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How can the tools we create help digital teams do their very best work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is why we created Apostrophe in the first place. It is what animates all of our decisions, particularly in a moment like this where there are new opportunities to consider and prioritize.&lt;/p&gt;

&lt;p&gt;So what does that look like in practice? How do we apply a focus on helping digital teams do their best work? We think this means helping marketing and editorial teams produce content faster and more efficiently, but in a way that doesn’t risk degrading the quality of that content. It means enabling engineering and DevOps teams to focus more on innovation and less on setup and troubleshooting.&lt;/p&gt;

&lt;p&gt;Ultimately, we think Apostrophe can provide the most value by applying generative AI to automate project scaffolding, rote content tasks, and other busywork, with the goal of facilitating the best possible application of our human creativity to create unique, delightful, and inspiring digital experiences.&lt;/p&gt;

&lt;h3&gt;
  
  
  So what’s next on our roadmap?
&lt;/h3&gt;

&lt;p&gt;The first applications of our principles towards these features is taking the form of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic SEO and accessibility metadata generation&lt;/strong&gt;: Generating a first draft of simple metadata like page descriptions and alt text for images is one way Apostrophe can streamline the work of content teams, especially teams that need to manage large volumes of documents and media.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native translation support as part of our localization tools&lt;/strong&gt;: Apostrophe has powerful localization workflows built into the core, but they currently require either manual translation or some integration with a third party translation platform like Smartling. Hooking our native localization workflow into a simple automated translation back end can help accelerate the work of managing a multilingual site when you don’t have the capacity to bring more translation resources to the project.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find more details about both of these in our &lt;a href="https://portal.productboard.com/apostrophecms/1-product-roadmap/tabs/2-planned"&gt;public roadmap&lt;/a&gt;. Please share any feedback you like there, or submit additional ideas that could be considered for future features.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about the far distant future?
&lt;/h3&gt;

&lt;p&gt;As software engineers and digital creators, are we all just mowing the lawn while a tornado approaches in the background?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5abzjrsk1a70jejurgsa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5abzjrsk1a70jejurgsa.png" alt="mowing the lawn in tornado" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you were to draw a straight line following the trajectory over just the first half of 2023, it was frighteningly easy to imagine a future where the entire creative economy has been replaced by some form of generative AI. But we know that the path ahead will not follow a straight line, and we know that there is no true replacement for the quality of human vision and creativity when it comes to doing great work. &lt;/p&gt;

&lt;p&gt;As this technology makes its way through various technical and regulatory hurdles, and all of the other stops and starts and twists and turns influenced by the complex factors at play, we think it’s possible to guide it in a humane direction and make good choices as a society. As a company we’ll aim to do our part in that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let us know what you think!
&lt;/h3&gt;

&lt;p&gt;Once again, please do share your feedback on our roadmap, or better yet join us in &lt;a href="http://chat.apostrophecms.org/"&gt;Discord&lt;/a&gt; and get involved by contributing directly to the project. If you’re a company working with Apostrophe and looking to collaborate more directly with us on any of these features, we love rolling up our sleeves in this way with our partners so definitely reach out.&lt;/p&gt;

&lt;p&gt;There are so many more opportunities for what Apostrophe can do when paired with generative AI than we will ever be able to unlock just with our team alone - we look forward to hearing from you as we dive deeper into this work.&lt;/p&gt;

&lt;p&gt;This post was written by Apostrophe's leading conversation starter, CEO, and staunch board game advocate. We also call him &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Falex-gilbert-a361a152%2F"&gt;Alex&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>openai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Launch Faster with Apostrophe Starter Kits</title>
      <dc:creator>The Apostrophe Team</dc:creator>
      <pubDate>Mon, 21 Aug 2023 12:25:00 +0000</pubDate>
      <link>https://dev.to/apostrophecms/launch-faster-with-apostrophe-starter-kits-1n85</link>
      <guid>https://dev.to/apostrophecms/launch-faster-with-apostrophe-starter-kits-1n85</guid>
      <description>&lt;p&gt;Starter Kits offer the building blocks tailored to specific use cases, enabling developers to launch their websites faster and with enhanced functionality.&lt;/p&gt;

&lt;p&gt;In today's digital landscape, teams are always looking for more efficient solutions to streamline their design and development processes. &lt;a href="https://apostrophecms.com/"&gt;Apostrophe&lt;/a&gt;, the ultimate open source content management system and website builder, has introduced a suite of resources to kick start the upfront development work of new Apostrophe-powered projects. These pre-bundled &lt;a href="https://apostrophecms.com/starter-kits"&gt;Starter Kits&lt;/a&gt; offer the building blocks tailored to specific use cases, enabling developers to launch their websites faster and with enhanced functionality. In this blog post, we'll highlight the benefits of Apostrophe Starter Kits and how they can empower developers to build exceptional websites with ease.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffp7detay1xjz706x8yx5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffp7detay1xjz706x8yx5.png" alt="Ecommerce Starter Kit Example" width="800" height="839"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Effortless Website Development
&lt;/h2&gt;

&lt;p&gt;Building a website from scratch can be a time-consuming process, requiring digital teams to invest effort in both design and functionality. However, Apostrophe’s new Starter Kits simplify this upfront demand by providing pre-bundled projects designed for ecommerce, hospitality, and marketing sites, to go along with our existing minimalist Apostrophe 3 Essentials project. These Starter Kits act as a foundation, allowing developers to jumpstart their projects and reduce the time and effort required for initial setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tailored to Specific Industries
&lt;/h3&gt;

&lt;p&gt;Apostrophe Starter Kits are specifically designed to cater to the needs of various industries. The &lt;a href="https://apostrophecms.com/starter-kits/apostrophe-essentials-kit"&gt;Apostrophe Essentials Kit&lt;/a&gt; provides a solid blank canvas for general-purpose websites, allowing developers to start their projects with a robust and flexible base. The &lt;a href="https://apostrophecms.com/starter-kits/ecommerce-starter-kit"&gt;Ecommerce Kit&lt;/a&gt; equips developers with the necessary components and features to set up an online store seamlessly. Whereas the &lt;a href="https://apostrophecms.com/starter-kits/hospitality-and-restaurant-starter-kit"&gt;Hospitality Kit&lt;/a&gt; offers specialized functionality for restaurants, hotels, resorts, and other leisure businesses. Lastly, the &lt;a href="https://apostrophecms.com/starter-kits/marketing-starter-kit"&gt;Marketing Kit&lt;/a&gt; empowers marketers with tools to create simple promotional websites that effectively showcase their products or services. &lt;/p&gt;

&lt;p&gt;In releasing these Starter Kits as open source projects, we also hope to create new opportunities for the community to &lt;a href="https://github.com/apostrophecms/apostrophe/blob/main/CONTRIBUTING.md"&gt;collaborate with us&lt;/a&gt;. And this is just the beginning!  In partnership with our &lt;a href="https://apostrophecms.com/partners"&gt;Partner Network&lt;/a&gt;, additional Starter Kits are planned for the Apostrophe Library. If you would like to contribute a Starter Kit, &lt;a href="https://apostrophecms.com/contact-us"&gt;contact us today&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsbjgozf1ngqakhcbo9a6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsbjgozf1ngqakhcbo9a6.png" alt="Hospitality Starter Kit Example" width="800" height="839"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Accelerated Time-to-Market
&lt;/h3&gt;

&lt;p&gt;By leveraging Apostrophe Starter Kits, developers can significantly reduce their time-to-market. The pre-built nature of the components in the kits eliminates the need to start from scratch, enabling developers to focus on customizing and fine-tuning the website to meet their specific requirements. With the Starter Kits' strong foundation and optimized workflows, developers can rapidly iterate and deploy websites, giving businesses a competitive edge in the ever-evolving digital landscape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unleash Your Creativity
&lt;/h3&gt;

&lt;p&gt;Apostrophe Starter Kits not only offer efficiency but also provide developers with a platform to unleash their creativity. By streamlining the initial setup process, developers have more time and freedom to concentrate on implementing unique and innovative features that set their websites apart from the competition. Whether it's creating interactive elements, integrating third-party APIs, or designing a visually stunning user interface, Apostrophe Starter Kits empower developers to bring their ideas to life without being hindered by repetitive tasks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxysi6zhacerlpltqg7bk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxysi6zhacerlpltqg7bk.png" alt="Marketing Starter Kit Example" width="800" height="839"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started with a Starter Kit Today
&lt;/h2&gt;

&lt;p&gt;Apostrophe Starter Kits enhance efficiency and streamline the development process, allowing developers to invest more time and effort in the customization of the website. With the accelerated time-to-market and the ability to unleash creativity, developers can build exceptional websites that captivate users and deliver a seamless digital experience. Whether you're a developer or a business owner, Apostrophe Starter Kits provide the tools you need to begin your development journey on the Apostrophe platform. &lt;/p&gt;

&lt;p&gt;To get started, simply navigate to the &lt;a href="https://apostrophecms.com/starter-kits"&gt;Starter Kit Library&lt;/a&gt; and choose the kit that aligns with your goals and objectives. &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Integrate Generative AI Into Apostrophe</title>
      <dc:creator>Antonello Zanini</dc:creator>
      <pubDate>Tue, 15 Aug 2023 12:05:00 +0000</pubDate>
      <link>https://dev.to/apostrophecms/how-to-integrate-generative-ai-into-apostrophe-3oai</link>
      <guid>https://dev.to/apostrophecms/how-to-integrate-generative-ai-into-apostrophe-3oai</guid>
      <description>&lt;p&gt;As recently highlighted by Apostrophe's CTO, Tom Boutell, in a talk at Philly Tech Week, &lt;a href="https://apostrophecms.com/blog/how-ai-is-transforming-the-cms-industry" rel="noopener noreferrer"&gt;AI is transforming the CMS industry&lt;/a&gt;. At the same time, finding the right way to integrate AI into a CMS may not be easy. That is why the team behind Apostrophe built an AI Helper module to add text and image generation capabilities to the CMS and website builder with just a few lines of code. &lt;/p&gt;

&lt;p&gt;Here you will learn what the Apostrophe AI helper is, how it works, and how it can be used to generate content with AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the Apostrophe AI Helper Module?
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/apostrophecms/ai-helper" rel="noopener noreferrer"&gt;AI Helper&lt;/a&gt; module is one of Apostrophe’s many available extensions. As the name suggests, it is designed to facilitate the introduction of AI-based features in Apostrophe. In other words, AI Helper provides a simplified way to integrate OpenAI functionality into an Apostrophe project with just a few lines of code. &lt;/p&gt;

&lt;p&gt;In detail, it relies on the &lt;a href="https://openai.com/blog/openai-api" rel="noopener noreferrer"&gt;OpenAI API&lt;/a&gt; to enhance Apostrophe with AI-driven helpers. Once integrated, the module currently offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An insert menu option to generate rich text from a prompt via GPT, the AI model behind ChatGPT.&lt;/li&gt;
&lt;li&gt;A button to generate an image from a text prompt via DALL-E.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these two features, editors can ask artificial intelligence to generate Apostrophe-ready content without having to leave the platform. This is a huge productivity boost.&lt;/p&gt;

&lt;p&gt;As of this writing, the module is still in beta. The reasons are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AI models are still not perfect, and they are prone to produce unreliable results.&lt;/li&gt;
&lt;li&gt;The OpenAI API it depends on is subject to changes.&lt;/li&gt;
&lt;li&gt;Generative AI is evolving so rapidly that the best way to integrate it into Apostrophe could change rapidly in terms of both UI and overall technical approach.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This does not mean that you cannot use it to experiment with generative AI in the content creation process. Quite the opposite!&lt;/p&gt;

&lt;h2&gt;
  
  
  Add AI to Apostrophe with the AI Helper Module
&lt;/h2&gt;

&lt;p&gt;In this step-by-step tutorial, you will learn how to integrate the Apostrophe AI Helper module into your project. Follow the instructions below and start taking advantage of the power of generative AI!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you already have an Apostrophe app in place, you can skip the first two steps.&lt;/p&gt;

&lt;h4&gt;
  
  
  Prerequisites
&lt;/h4&gt;

&lt;p&gt;Before jumping into this section, you need to meet the Apostrophe 3+ prerequisites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/en/" rel="noopener noreferrer"&gt;Node.js 18.x+&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mongodb.com/docs/manual/administration/install-community/" rel="noopener noreferrer"&gt;MongoDB 4.4+&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To make the process of initializing an Apostrophe project easier, install the Apostrophe CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g @apostrophecms/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will now have access to the &lt;code&gt;apos&lt;/code&gt; command, through which you can quickly set up and manage Apostrophe projects. &lt;/p&gt;

&lt;p&gt;If you are a Windows user, you will also need to configure WSL (&lt;a href="https://learn.microsoft.com/en-us/windows/wsl/install" rel="noopener noreferrer"&gt;Windows Subsystem for Linux&lt;/a&gt;) as explained &lt;a href="https://v3.docs.apostrophecms.org/cookbook/windows-development.html" rel="noopener noreferrer"&gt;in the official docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Great! You are now ready to set up an Apostrophe 3+ project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initialize an Apostrophe Project
&lt;/h3&gt;

&lt;p&gt;First, make sure that the MongoDB local server is running by following the &lt;a href="https://www.mongodb.com/docs/manual/administration/install-community/" rel="noopener noreferrer"&gt;official guide for your OS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the terminal, enter the folder where you want to create your Apostrophe project and launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apos create apos-ai-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will create an Apostrophe project called &lt;code&gt;apos-ai-demo&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;During the process, you will be asked to enter the password for the “admin” user. Type in a secure password and then store it in a safe place.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;apos-ai-demo&lt;/code&gt; folder should now contain an ApostropheCMS project. If this is your first Apostrophe project, you should spend some time familiarizing yourself with the &lt;a href="https://v3.docs.apostrophecms.org/guide/modules.html" rel="noopener noreferrer"&gt;Apostrophe file structure&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To start your application, enter the project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd apos-ai-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, run the development server with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will receive the following warning message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARNING: No session secret provided, please set the secret property of the session property of the @apostrophecms/express module in app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not worry, you can easily fix that &lt;a href="https://v3.docs.apostrophecms.org/guide/setting-up.html#finishing-touches" rel="noopener noreferrer"&gt;as explained in the docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;http://localhost:3000/login&lt;/code&gt; page in your browser to make sure your Apostrophe starter project is running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffklcxbm9dq51ndx8r8ji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffklcxbm9dq51ndx8r8ji.png" alt="CMS demo login" width="578" height="730"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fill out the form with "admin" as the username and the password set during the initialization process.&lt;/p&gt;

&lt;p&gt;After successfully logging in, you will be redirected to the Apostrophe dashboard below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3ga29q07uzh2dhmx8or.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3ga29q07uzh2dhmx8or.png" alt="Apostrophe CMS dashboard" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, you have access to all the &lt;a href="https://v3.docs.apostrophecms.org/guide/introduction.html#what-is-apostrophecms" rel="noopener noreferrer"&gt;great features offered by ApostropheCMS&lt;/a&gt;, including website-building capabilities, an intuitive UI for content management, a drafting system, and in-context editing.&lt;/p&gt;

&lt;p&gt;Perfect! You now have an Apostrophe project to extend through the OpenAI APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrate the AI Helper Module
&lt;/h3&gt;

&lt;p&gt;In the project folder, run the command below to install the AI Helper module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @apostrophecms/ai-helper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will add the &lt;code&gt;[@apostrophecms/ai-helper](https://www.npmjs.com/package/@apostrophecms/ai-helper)&lt;/code&gt; library to your project’s dependencies.&lt;/p&gt;

&lt;p&gt;As with any other Apostrophe module, you must first enable it in the &lt;code&gt;app.js&lt;/code&gt; file. To do so, make sure &lt;code&gt;app.js&lt;/code&gt; contains the following lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// app.js

require('apostrophe')({
  // the name of your project
  shortName: 'apos-ia-demo',
  modules: {
    // other modules...

    // enable the ai-helper module
    '@apostrophecms/ai-helper': {},
  },
  // remaining configs...
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run the application, it will now crash with the following error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: APOS_OPENAI_KEY must be set in your environment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is that the &lt;code&gt;ai-helper&lt;/code&gt; module needs an OpenAI API key to function. If you do not have one, follow the procedure below to retrieve your free API key for limited usage:&lt;/p&gt;

&lt;p&gt;Create an &lt;a href="https://platform.openai.com/signup?launch" rel="noopener noreferrer"&gt;OpenAI account&lt;/a&gt;.&lt;br&gt;
Follow the instructions and verify your account.&lt;br&gt;
&lt;a href="https://platform.openai.com/login?launch" rel="noopener noreferrer"&gt;Sign in&lt;/a&gt;.&lt;br&gt;
In the main dashboard, click the “API” card:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjs1bs4wvens3jtekyywa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjs1bs4wvens3jtekyywa.png" alt="OpenAI dashboard" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on your profile image in the upper right corner and select the “View API keys” option:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flh6ojtcvdmnmk5pyyt90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flh6ojtcvdmnmk5pyyt90.png" alt="OpenAI profile view" width="286" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the “Create new secret key” button.&lt;/p&gt;

&lt;p&gt;Call the new API key “Apostrophe” as below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falwch3jhep5got1kih2d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falwch3jhep5got1kih2d.png" alt="OpenAI create secret key" width="800" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the “Create secret key” button.&lt;/p&gt;

&lt;p&gt;Copy the OpenAI API key and store it in a secret place:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1hh3eyvq25xv3yju6nc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1hh3eyvq25xv3yju6nc.png" alt="OpenAI secret key login" width="617" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Please keep in mind that when the trial ends, you will be subject to the &lt;a href="https://openai.com/pricing" rel="noopener noreferrer"&gt;OpenAI pricing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You now have to set an &lt;code&gt;APOS_OPENAI_KEY&lt;/code&gt; environment variable with the value of your OpenAI API key. On Linux, macOS, or Windows Subsystem for Linux (&lt;a href="https://learn.microsoft.com/en-us/windows/wsl/install" rel="noopener noreferrer"&gt;WSL&lt;/a&gt;) Windows with Git Bash, run the command below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export APOS_OPENAI_KEY=&amp;lt;YOUR_OPENAI_API_KEY&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows with PowerShell, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$Env:APOS_OPENAI_KEY = "&amp;lt;YOUR_OPENAI_API_KEY&amp;gt;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;YOUR_OPENAI_API_KEY&amp;gt;&lt;/code&gt; with the value of the secret API key retrieved above.&lt;/p&gt;

&lt;p&gt;That command will configure the required env in the terminal session. In the same terminal window, re-launch the project again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, Apostrophe should load with no error.&lt;/p&gt;

&lt;p&gt;You are ready to add OpenAI-based AI features to any page that supports the &lt;code&gt;[rich-text widget] (https://v3.docs.apostrophecms.org/guide/core-widgets.html#rich-text-widget)&lt;/code&gt;. To achieve that, update the &lt;code&gt;'@apostrophecms/rich-text'&lt;/code&gt; module configuration by adding the &lt;code&gt;ai&lt;/code&gt; option to the &lt;code&gt;insert&lt;/code&gt; property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;insert: [
    // other values...
    'ai'
],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you then want to ask GPT to generate text with subheadings, you will also need to make sure that &lt;code&gt;styles&lt;/code&gt; contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;styles: [
  {
    tag: 'p',
    label: 'Paragraph (P)'
  },
  {
    tag: 'h2',
    label: 'Heading 2 (H2)'
  },
  {
    tag: 'h3',
    label: 'Heading 3 (H3)'
  },
  {
    tag: 'h4',
    label: 'Heading 4 (H4)'
  }
  // other styles...
],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are using the &lt;a href="https://github.com/apostrophecms/apostrophe-security-headers" rel="noopener noreferrer"&gt;Apostrophe Security Headers&lt;/a&gt; plugin, you will also need to add the following config to &lt;code&gt;security-headers/index.js&lt;/code&gt; inside &lt;code&gt;modules/@apostrophecms&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// modules/@apostrophecms/security-headers/index.js

module.exports = {
  options: {
    policies: {
      ai: {
        'img-src': '*.blob.core.windows.net'
      }
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This policy allows you to deal with OpenAI images for editing purposes.&lt;/p&gt;

&lt;p&gt;Now, suppose you want to add AI content generation capabilities to the Apostrophe default page module. This is what &lt;code&gt;default-page/index.js&lt;/code&gt; inside &lt;code&gt;modules&lt;/code&gt; would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// modules/default-page/index.js

module.exports = {
  extend: '@apostrophecms/page-type',
  options: {
    label: 'Default Page'
  },
  fields: {
    add: {
      main: {
        type: 'area',
        options: {
          widgets: {
            '@apostrophecms/rich-text': {
              toolbar: [
                'styles',
                '|',
                'bold',
                'italic',
                'strike',
                'link',
                '|',
                'bulletList',
                'orderedList'
              ],
              styles: [
                {
                  tag: 'p',
                  label: 'Paragraph (P)'
                },
                {
                  tag: 'h2',
                  label: 'Heading 2 (H2)'
                },
                {
                  tag: 'h3',
                  label: 'Heading 3 (H3)'
                },
                {
                  tag: 'h4',
                  label: 'Heading 4 (H4)'
                }
              ],
              insert: [
                'table',
                'image',
                'ai' // add the AI features
              ]
            },
            '@apostrophecms/image': {},
            '@apostrophecms/video': {}
          }
        }
      }
    },
    group: {
      basics: {
        label: 'Basics',
        fields: [
          'title',
          'main'
        ]
      }
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great! Add a new ”Default” page to your site to see the artificial intelligence features in action.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Get Ready to Unleash the Power of AI
&lt;/h3&gt;

&lt;p&gt;In the Apostrophe dashboard, click “Pages,” and then “New Page.” This will open the modal below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjqvx63hkhojmh1llzos.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjqvx63hkhojmh1llzos.png" alt="CMS new page modal" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Give your new page a title, select “Default” as page type, and click “Publish.”&lt;/p&gt;

&lt;p&gt;A new record will now appear in the “Manage Pages” section:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rcyvvnhmvrvw8095tx4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rcyvvnhmvrvw8095tx4.png" alt="CMS manage pages modal" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the dots on the right and select “Preview” to open the new page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1zsypgcb8zj5l4m9uzx0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1zsypgcb8zj5l4m9uzx0.png" alt="CMS new page preview" width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Well done! This blank page is about to get filled with some AI-generated content.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Rich Text Generation
&lt;/h2&gt;

&lt;p&gt;Now that your page is ready to host some AI-generated content, hit the “Edit” button:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpcjfsx3npwt2533t9j7m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpcjfsx3npwt2533t9j7m.png" alt="CMS AI text generation" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click “Add Content” and select the “Rich Text” option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk8otr5u9rnbgwsvdjqvu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk8otr5u9rnbgwsvdjqvu.png" alt="CMS add AI content" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Type “/” in the empty rich text widget to access the element menu and select “Generate Text.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7cnzq48fw4w8b29wvpi2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7cnzq48fw4w8b29wvpi2.png" alt="CMS AI generate text dropdown" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will open an input area where you can write your prompt for the AI model: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs60scjxjv79ofm1ujund.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs60scjxjv79ofm1ujund.png" alt="CMS AI text enter prompt" width="782" height="724"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“100 words about why AI is the future, in the style of Ernest Hemingway, with subheadings”
To write a good prompt, it is also a good idea to specify a word count. Also, do not forget that you can ask GPT to include headings and links.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click “Generate” and wait for the AI magic to happen.&lt;/p&gt;

&lt;p&gt;💥 Boom! Your page will now contain AI-generated formatted text:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6tb6cinw3llxg9cx5szm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6tb6cinw3llxg9cx5szm.png" alt="CMS AI results text page" width="800" height="756"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that AI could produce surprising or inappropriate results.&lt;/p&gt;

&lt;p&gt;See the entire process in action in the video below:&lt;br&gt;
&lt;iframe src="https://player.vimeo.com/video/853838055" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  How AI Helper Rich Text Generation Works
&lt;/h3&gt;

&lt;p&gt;If you are wondering how this works, take a look at the &lt;a href="//ai-helper-rich-text-widget/index.js"&gt;&lt;code&gt;ai-helper-rich-text-widget/index.js&lt;/code&gt; file on GitHub&lt;/a&gt;. In detail, focus on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const marked = require('marked');

// ...

prompt = `
  Generate text in markdown format, based on the following prompt:

  ${prompt}
`;
const body = {
  prompt,
  model: aiHelper.options.textModel,
  max_tokens: aiHelper.options.textMaxTokens,
  n: 1
};
const result = await self.apos.http.post('https://api.openai.com/v1/completions', {
    headers: {
      Authorization: `Bearer ${process.env.APOS_OPENAI_KEY}`
    },
    body
});

if (!result?.choices?.[0]?.text) {
  throw self.apos.error('error');
}
let markdown = result.choices[0].text;

// Remap headings to levels actually available in this widget
markdown = markdown.replace(/(^|\n)(#+) /g, (all, before, hashes) =&amp;gt; {
  if (!headingLevels.length) {
    return '';
  }
  const level = headingLevels[hashes.length - 1];
  return before + (level ? (before + '#'.repeat(level) + ' ') : '');
});

const html = marked.parse(markdown);
return {
  html
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this code snippet does is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tweak the prompt behind the scenes to ask GPT to return a response in Markdown format.&lt;/li&gt;
&lt;li&gt;Perform the API call to the OpenAI server using the &lt;code&gt;APOS_OPENAI_KEY&lt;/code&gt; env for authentication.&lt;/li&gt;
&lt;li&gt;Convert the Markdown string returned by the API to HTML.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is how the Apostrophe AI Helper extension manages to turn an AI prompt into a structured page with links and subheadings.&lt;/p&gt;

&lt;p&gt;If you want to override this default behavior, create a file &lt;code&gt;index.js&lt;/code&gt; under &lt;code&gt;modules/@apostrophe/ai-helper-rich-text-widget&lt;/code&gt; and initialize it with your custom AI rich text generation logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Image Generation
&lt;/h2&gt;

&lt;p&gt;Your current page has some interesting content but lacks an eye-catching image. Again, you can use artificial intelligence to generate one.&lt;/p&gt;

&lt;p&gt;Click the “Add Content” button, and select “Image:” &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuaf2847lj77jimwu2p1e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuaf2847lj77jimwu2p1e.png" alt="CMS AI add content image" width="798" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Apostrophe will add a sample image for you. Click on it and select the “Edit Widget” icon:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd2haeddx69xry3e5ep8l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd2haeddx69xry3e5ep8l.png" alt="CMS image widget holder" width="800" height="615"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click “Browse Image” and hit the “🤖" (robot) button in the upper right corner.&lt;/p&gt;

&lt;p&gt;This will open an input area where you can type your DALL-E prompt:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01isag91l2h8n1okiz5j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01isag91l2h8n1okiz5j.png" alt="CMS generate AI image" width="615" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, write:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“An image of a futuristic city”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Click “Generate Image,” wait for OpenAI to do its magic, and you should be seeing something as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6c0dyzpykw58fk42dvb2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6c0dyzpykw58fk42dvb2.png" alt="CMS generate AI image prompt" width="578" height="796"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep in mind that AI image helper generates 1024x1024 images, which is the maximum size supported by OpenAI. At the time of writing, smaller images are not recommended as they are not much cheaper and are unlikely to look good on a website.&lt;/p&gt;

&lt;p&gt;Click on an AI-generated image to open it in a modal:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpws5qvu45k7t3nvyq5jx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpws5qvu45k7t3nvyq5jx.png" alt="CMS image generation modal" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since DALL-E may produce low-quality results, having the possibility to inspect each image before adding it to the multimedia library is an essential tool. If you are not convinced by the result, click "Delete" to remove the image forever. Otherwise, it will be cached for around one hour before disappearing from the selection section.&lt;/p&gt;

&lt;p&gt;In some cases, you may want to ask DALL-E to generate images similar to the current one. This is what the "Variations" button is all about. As you can see in the video below, that will produce four more images:&lt;br&gt;
&lt;iframe src="https://player.vimeo.com/video/853838265" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Once you found the image you like the most, click “Select” to add it to the article. Here is what the final page looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63kmw9rtqycy93wuisrx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63kmw9rtqycy93wuisrx.gif" alt="CMS AI generated page results" width="600" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watch the video below to better understand how easy the entire AI image generation process is:&lt;br&gt;
&lt;iframe src="https://player.vimeo.com/video/853838313" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Just as seen before, you can overwrite the default logic by properly defining a &lt;code&gt;modules/@apostrophe/ai-helper-image/index.js&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Congrats! You just integrated generative AI into Apostrophe! &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, you learned what the Apostrophe AI Helper module is and how it can provide generative AI to your editors. In detail, this extension offers a quick and easy way to integrate OpenAI features into your Apostrophe site. Thanks to Apostrophe's extensibility, you can also easily customize the AI content generation logic.&lt;/p&gt;

&lt;p&gt;Keep in mind that Apostrophe has just begun dipping its first toes into the AI space. Now that the team has this foundation and a deeper understanding of what is possible, they are excited introduce other features that leverage these powerful generative tools. Keep an eye on the &lt;a href="https://portal.productboard.com/apostrophecms/1-product-roadmap/tabs/1-under-consideration" rel="noopener noreferrer"&gt;Apostrophe product roadmap&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;By following this step-by-step tutorial, you saw how to initialize an Apostrophe project, integrate the AI Helper module, and take advantage of it to reach your content goals. Thanks to AI, writing great content so quickly has never been easier!&lt;/p&gt;

</description>
      <category>howto</category>
      <category>ai</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
