# Tìm hiểu SEO feed.xml, sitemap.ts, robots.ts nextjs-wordpress

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FGtIfNOO78y6hZaf00HOC%2Fimage.png?alt=media&#x26;token=5e5e9060-dc93-4a23-93f1-3b1548a70d41" alt=""><figcaption></figcaption></figure>

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FKPU5VaGL7t0Jg0DDUX7K%2Flocalhost_3000_.png?alt=media&#x26;token=7cc6add3-e240-4f74-af1b-8635714ddad9" alt=""><figcaption></figcaption></figure>

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FVTVd9ZjfWRXCwg6gFmgw%2Fimage.png?alt=media&#x26;token=c80f7e15-85cb-4ac5-92b0-fe110a6b6bfb" alt=""><figcaption></figcaption></figure>

Database cấu hình thực hành SEO

<https://drive.google.com/file/d/16upcMf9wTCihcL5IWtmxzL-HqGO_VnYt/view?usp=sharing>

{% file src="<https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FARSBwKDPJMFMiO3yacyd%2Fnextjs-wordpress.zip?alt=media&token=92fe86e8-f4bd-4ad6-9a1e-44f7d332c129>" %}

wp-config.php

```php
<?php
/**
 * The base configuration for WordPress
 *
 * The wp-config.php creation script uses this file during the installation.
 * You don't have to use the website, you can copy this file to "wp-config.php"
 * and fill in the values.
 *
 * This file contains the following configurations:
 *
 * * Database settings
 * * Secret keys
 * * Database table prefix
 * * ABSPATH
 *
 * @link https://developer.wordpress.org/advanced-administration/wordpress/wp-config/
 *
 * @package WordPress
 */
// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'wpclidemo' );
/** Database username */
define( 'DB_USER', 'root' );
/** Database password */
define( 'DB_PASSWORD', '' );
/** Database hostname */
define( 'DB_HOST', 'localhost' );
/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8mb4' );
/** The database collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', '' );
// Any random string. This must match the .env variable in the Next.js frontend.
define( 'NEXTJS_PREVIEW_SECRET', 'preview' );
// Any random string. This must match the .env variable in the Next.js frontend.
define( 'NEXTJS_REVALIDATION_SECRET', 'revalidate' );
define('AUTH_KEY',         '*N-t[-#n-3#-qv*c8K4iPf^^w*#WCu6$-(qD3cyatx^% A^QnG71Y0? O9ex}j]/');
define('SECURE_AUTH_KEY',  'F?&#3s`7T*.m0OwP7az$Iw}!iS>D# Z5SQr+QY)DLH)klS[|UX_EIn}vs%IT_+FG');
define('LOGGED_IN_KEY',    'brdC.{e#r9x&_U>|(D02U@)]KsUQ#c3 SqIR|Y^/5A!W|9%I?;}%rLFRrB2p,vG=');
define('NONCE_KEY',        '^e?>O4!YOCU|BXw`+m8NePd|QvTX~n,f8W;bUKpCU`PI|}+V;l~<j~>I>PZj7f,G');
define('AUTH_SALT',        '1x;V[+Qd}DyF2ZPB}g{QX!>NF)lG8$3MrFi],YVIJ@C`OD3$&ZG9l-kGE3o~hr?|');
define('SECURE_AUTH_SALT', 'qxrb)txU(M;Y$A6?=~bl_= rJ,7EAx`_<+|WnO_<Pz~B:actx!B5k!F#@ bKQ4q7');
define('LOGGED_IN_SALT',   '2vrPIByb8hZd7Z%4s~V8^=ZcyZ3TDi&I(c-X^0LxC[R|4A@=<yo|.#h=9;W!=]Cw');
define('NONCE_SALT',       'b?`&z:h)+3&w|o:<]gaS8zG^$P9f*l4FhE.YixsaE,U>EqQfn-v|Mo*465AE3+U|');
#define('GRAPHQL_JWT_AUTH_SECRET_KEY', 'wQ2gp%lGB(T0~!eV?FJg3M+tAy-R0YLF2rH_ Lou>k|7iFfGuH+0#oPTLXiG@8r-');
define('GRAPHQL_JWT_AUTH_SECRET_KEY', 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3dwY2xpZGVtby5kZXYiLCJpYXQiOjE3Mzk5OTU4MjgsIm5iZiI6MTczOTk5NTgyOCwiZXhwIjoxNzcxNTMxODI4LCJkYXRhIjp7InVzZXIiOnsiaWQiOiIxIiwidXNlcl9zZWNyZXQiOiJncmFwaHFsX2p3dF82N2I0ZTM1ZDhiODQyIn19fQ.FwrXtFD2flxFNEwDveyhByXJc7s5nL875QAz55P3TbM');
/**#@+
 * Authentication unique keys and salts.
 *
 * Change these to different unique phrases! You can generate these using
 * the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}.
 *
 * You can change these at any point in time to invalidate all existing cookies.
 * This will force all users to have to log in again.
 *
 * @since 2.6.0
 */
/**#@-*/
/**
 * WordPress database table prefix.
 *
 * You can have multiple installations in one database if you give each
 * a unique prefix. Only numbers, letters, and underscores please!
 *
 * At the installation time, database tables are created with the specified prefix.
 * Changing this value after WordPress is installed will make your site think
 * it has not been installed.
 *
 * @link https://developer.wordpress.org/advanced-administration/wordpress/wp-config/#table-prefix
 */
$table_prefix = 'wp_';
/**
 * For developers: WordPress debugging mode.
 *
 * Change this to true to enable the display of notices during development.
 * It is strongly recommended that plugin and theme developers use WP_DEBUG
 * in their development environments.
 *
 * For information on other constants that can be used for debugging,
 * visit the documentation.
 *
 * @link https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/
 */
define( 'WP_DEBUG', false );
define('NEXTJS_FRONTEND_URL', 'https://wpclidemo.dev/');
// define('ALLOW_UNFILTERED_UPLOADS', true);
define('WP_DEBUG_LOG', 'C:/xampp82/htdocs/wpclidemo/index.php/wp-errors.log');
/* Add any custom values between this line and the "stop editing" line. */
/* That's all, stop editing! Happy publishing. */
/** Absolute path to the WordPress directory. */
if ( ! defined( 'ABSPATH' ) ) {
	define( 'ABSPATH', __DIR__ . '/' );
}
/** Sets up WordPress vars and included files. */
require_once ABSPATH . 'wp-settings.php';

```

.env

```properties
# WordPress GraphQL URL. No trailing slash.
NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL="https://wpclidemo.dev/graphql"
# WordPress REST API URL. No trailing slash.
NEXT_PUBLIC_WORDPRESS_REST_API_URL="https://wpclidemo.dev/wp-json/wp/v2"
# Optional. JWT auth refresh token.
#NEXTJS_AUTH_REFRESH_TOKEN=""
# Preview Secret. Must match the constant in wp-config.php.
NEXTJS_PREVIEW_SECRET="preview"
# Revalidation Secret. Must match the constant in wp-config.php.
NEXTJS_REVALIDATION_SECRET="revalidate"
# Allow self-signed SSL certificates when working with local development environments.
NODE_TLS_REJECT_UNAUTHORIZED=0
NEXTJS_AUTH_REFRESH_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3dwY2xpZGVtby5kZXYiLCJpYXQiOjE3Mzk5OTU4MjgsIm5iZiI6MTczOTk5NTgyOCwiZXhwIjoxNzcxNTMxODI4LCJkYXRhIjp7InVzZXIiOnsiaWQiOiIxIiwidXNlcl9zZWNyZXQiOiJncmFwaHFsX2p3dF82N2I0ZTM1ZDhiODQyIn19fQ.FwrXtFD2flxFNEwDveyhByXJc7s5nL875QAz55P3TbM"

```

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FcRH7cqpqa3h86v6qdNTx%2Flocalhost_3000_.png?alt=media&#x26;token=9357d686-abff-4a65-a227-04eb12725a9c" alt=""><figcaption></figcaption></figure>

### \[Home Page] Function to execute a GraphQL query&#x20;

next.config.ts

```typescript
/** @type {import('next').NextConfig} */
const allowedImageWordPressDomain = new URL(process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL).hostname;
const nextConfig = {
  reactStrictMode: true,
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: allowedImageWordPressDomain,
      },
      {
        protocol: "https",
        hostname: 'secure.gravatar.com',
      },
    ],
  }
};
export default nextConfig;

```

lib\config.ts

```typescript
const config = {
  siteName: 'Next.js WordPress 15',
  siteDescription: "It's headless WordPress!",
  siteUrl: 'https://wpclidemo.dev',
  revalidate: 3600 // 1 hour
}
export default config
```

lib\types.d.ts

```typescript
export interface DynamicPageProps {
  params: Promise<{
    slug: string
  }>
}
export interface SearchResults {
  id: number
  title: string
  url: string
  type: string
  subtype: string
}
export interface Children {
  children: React.ReactNode
}
export interface GraphQLResponse<T = any> {
  data?: T
  errors?: Array<{message: string}>
}
export interface Menu {
  menuItems: {
    edges: [
      {
        node: {
          uri: string
          label: string
          databaseId: string
        }
      }
    ]
  }
}
export interface FeaturedImage {
  node: {
    altText: string
    sourceUrl: string
    mediaDetails: {
      height: number
      width: number
    }
  }
}
export interface Menu {
  menuItems: {
    edges: [
      {
        node: {
          uri: string
          label: string
          databaseId: string
        }
      }
    ]
  }
}
export interface Page {
  author: {
    node: {
      avatar: {
        url: string
      }
      name: string
    }
  }
  databaseId: string
  date: string
  modified: string
  slug: string
  title: string
  excerpt: string
  content: string
  featuredImage: FeaturedImage
  seo: {
    metaDesc: string
    title: string
  }
}
export interface Post {
  author: {
    node: {
      name: string
      avatar: {
        url: string
      }
    }
  }
  databaseId: string
  date: string
  modified: string
  modified: string
  slug: string
  title: string
  excerpt: string
  content: string
  commentCount: number
  categories: {
    nodes: [
      {
        databaseId: string
        name: string
      }
    ]
  }
  tags: {
    nodes: [
      {
        databaseId: string
        name: string
      }
    ]
  }
  featuredImage: FeaturedImage
  seo: {
    metaDesc: string
    title: string
  }
  comments: {
    nodes: [
      {
        databaseId: string
        content: string
        date: string
        status: string
        author: {
          node: {
            avatar: {
              url: string
            }
            email: string
            name: string
            url: string
          }
        }
      }
    ]
  }
}
export interface Book {
  bookFields: {
    affiliateUrl: string
    isbn: string
  }
  databaseId: string
  date: string
  modified: string
  slug: string
  title: string
  excerpt: string
  content: string
  featuredImage: FeaturedImage
  seo: {
    metaDesc: string
    title: string
  }
}
export interface AllPages {
  pages: {
    nodes: Page[]
  }
}
export interface AllPosts {
  posts: {
    nodes: Post[]
  }
}

```

lib\functions.ts

```typescript
import config from '@/lib/config'
import { GraphQLResponse, SearchResults } from '@/lib/types'
/**
 * Function to execute a GraphQL query.
 */
export async function fetchGraphQL<T = any>( query: string, variables?: { [key: string]: any }, preview = false ): Promise<GraphQLResponse<T>> {
  try {
    // Validate the WordPress GraphQL URL.
    const graphqlUrl = process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL
    if (!graphqlUrl) {
      throw new Error('Missing WordPress GraphQL URL environment variable!')
    }
    // Get the refresh token.
    const refreshToken = process.env.NEXTJS_AUTH_REFRESH_TOKEN
    // Prepare headers.
    const headers: { [key: string]: string } = {
      'Content-Type': 'application/json'
    }
    // If preview mode is enabled and we have a token.
    if (preview && refreshToken) {
      // Add refresh token to fetch headers.
      headers['Authorization'] = `Bearer ${refreshToken}`
    }
    // Get the slug.
    const slug = variables?.slug || variables?.id || 'graphql'
    // Fetch data from external API.
    const response = await fetch(graphqlUrl, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        query,
        variables
      }),
      next: {
        tags: [slug],
        revalidate: config.revalidate
      }
    })
    // If the response status is not 200, throw an error.
    if (!response.ok) {
      console.error('Response Status:', response.status)
      throw new Error(response.statusText)
    }
    // Read the response as JSON.
    const data = await response.json()
    // Throw an error if there was a GraphQL error.
    if (data.errors) {
      console.error('GraphQL Errors:', data.errors)
      throw new Error('Error executing GraphQL query')
    }
    // Finally, return the data.
    return data
  } catch (error) {
    console.error(error)
    throw error
  }
}
/**
 * Search the WordPress REST API for posts matching the query.
 *
 * @see https://developer.wordpress.org/rest-api/reference/search-results/
 */
export async function searchQuery(query: string): Promise<SearchResults[]> {
  // Sanitize the search query.
  query = encodeURIComponent(query.trim())
  try {
    // If there is no URL, throw an error.
    if (!process.env.NEXT_PUBLIC_WORDPRESS_REST_API_URL) {
      throw new Error('Missing WordPress REST API URL environment variable!')
    }
    // Always fetch fresh search results.
    const response = await fetch(
      `${process.env.NEXT_PUBLIC_WORDPRESS_REST_API_URL}/search?search=${query}&subtype=any&per_page=100`,
      {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json'
        },
        next: {
          tags: [`search-${query}`],
          revalidate: config.revalidate
        }
      }
    )
    // If the response status is not 200, throw an error.
    if (!response.ok) {
      console.error('Response Status:', response.status)
      throw new Error(response.statusText)
    }
    // Read the response as JSON.
    const data = await response.json()
    // Verify data has posts.
    if (!data || data.length === 0) {
      throw new Error('No posts found.')
    }
    // Return the data.
    return data as SearchResults[]
  } catch (error) {
    console.error('Error fetching data:', error)
    throw error
  }
}

```

lib\queries\getPageBySlug.ts

```typescript
import {fetchGraphQL} from '@/lib/functions'
import {Page} from '@/lib/types'
/**
 * Fetch a page by slug.
 */
export default async function getPageBySlug(slug: string) {
  const query = `
    query GetPageBySlug($slug: ID = "URI") {
      page(idType: URI, id: $slug) {
        databaseId
        date
        modified
        content(format: RENDERED)
        title(format: RENDERED)
        featuredImage {
          node {
            altText
            sourceUrl
            mediaDetails {
                height
                width
            }
          }
        }
        author {
          node {
            name
            avatar {
              url
            }
          }
        }
        seo {
          metaDesc
          title
        }
      }
    }
  `
  const variables = {
    slug: slug
  }
  const response = await fetchGraphQL(query, variables)
  return response.data.page as Page
}

```

lib\queries\getAllPosts.ts

```typescript
import {fetchGraphQL} from '@/lib/functions'
import {Post} from '@/lib/types'
/**
 * Fetch all blog posts.
 */
export default async function getAllPosts() {
  const query = `
    query GetAllPosts {
      posts(where: {status: PUBLISH}) {
        nodes {
          commentCount
          databaseId
          date
          modified
          title
          slug
          excerpt(format: RENDERED)
          featuredImage {
            node {
              altText
              sourceUrl
              mediaDetails {
                  height
                  width
              }
            }
          }
          seo {
            metaDesc
            title
          }
        }
      }
    }
  `
  const response = await fetchGraphQL(query)
  return response.data.posts.nodes as Post[]
}

```

components\Header.tsx

```typescript
import config from '@/lib/config'
import getMenuBySlug from '@/lib/queries/getMenuBySlug'
import Link from 'next/link'
/**
 * Header component.
 */
export default async function Header() {
  // Get menu items from WordPress.
  const menu = await getMenuBySlug('header')
  return (
    <header>
      <div>
        <h1 className="mb-0">{config.siteName}</h1>
        <p>{config.siteDescription}</p>
      </div>
      <nav className="flex gap-4">
        {!!menu &&
          menu.menuItems.edges.map((item) => (
            <Link key={item.node.databaseId} href={item.node.uri}>
              {item.node.label}
            </Link>
          ))}
      </nav>
    </header>
  )
}

```

components\Footer.tsx

```typescript
import config from '@/lib/config'
/**
 * Footer component.
 */
export default function Footer() {
  return (
    <footer className="border-t-2 p-8 text-center text-sm">
      &copy; 2022-{new Date().getFullYear()} - {config.siteName} by{' '}
      <a href="https://gregrickaby.com">Greg Rickaby</a> |{' '}
      <a href="https://github.com/gregrickaby/nextjs-wordpress">Github</a>
    </footer>
  )
}

```

components\CommentForm.tsx

```typescript
'use client'
import {createComment} from '@/lib/mutations/createComment'
import {useState} from 'react'
/**
 * The comment form component.
 */
export default function CommentForm({postID}: Readonly<{postID: string}>) {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const [website, setWebsite] = useState('')
  const [comment, setComment] = useState('')
  const [status, setStatus] = useState('')
  /**
   * Handle the comment form submission.
   */
  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    // Create the comment and await the status.
    const status = await createComment({
      name,
      email,
      website,
      comment,
      postID
    })
    // If the comment was created successfully...
    if (status && status.success) {
      // Clear the form.
      setName('')
      setEmail('')
      setWebsite('')
      setComment('')
      // Set the status message.
      setStatus(
        `Thank you ${name}! Your comment has been submitted and is awaiting moderation.`
      )
    }
    // If there was an error...
    if (status && !status.success) {
      setStatus(`There was an error submitting your comment: ${status.message}`)
    }
  }
  return (
    <>
      <h3>Leave a Comment</h3>
      <form className="flex flex-col gap-4" onSubmit={handleSubmit}>
        <div className="flex flex-col gap-2">
          <label htmlFor="name">Name*</label>
          <input
            id="name"
            onChange={(e) => setName(e.target.value)}
            required
            type="text"
            value={name}
          />
        </div>
        <div className="flex flex-col gap-2">
          <label htmlFor="email">Email*</label>
          <input
            id="email"
            onChange={(e) => setEmail(e.target.value)}
            pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$"
            required
            type="email"
            value={email}
          />
        </div>
        <div className="flex flex-col gap-2">
          <label htmlFor="website">Website*</label>
          <input
            id="website"
            onChange={(e) => setWebsite(e.target.value)}
            required
            type="url"
            value={website}
          />
        </div>
        <div className="flex flex-col gap-2">
          <label htmlFor="comment">Comment*</label>
          <textarea
            id="comment"
            onChange={(e) => setComment(e.target.value)}
            required
            value={comment}
          ></textarea>
        </div>
        <button type="submit">Submit</button>
        {status && <p>{status}</p>}
      </form>
    </>
  )
}

```

components\SearchForm.tsx

app\layout.tsx

```typescriptreact
import type { Metadata } from "next";
import Footer from '@/components/Footer'
import Header from '@/components/Header'
import config from '@/lib/config'
import "./globals.css";
export const metadata: Metadata = {
  metadataBase: new URL(config.siteUrl),
  title: `${config.siteName} - ${config.siteDescription}`,
  description: config.siteDescription
}
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`p-4 w-[1200px] m-auto`}>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}

```

app\page.tsx

```typescript
import getAllPosts from '@/lib/queries/getAllPosts';
import getPageBySlug from '@/lib/queries/getPageBySlug';
import { Post } from '@/lib/types';
import Link from 'next/link';
import { notFound } from 'next/navigation';
export default async function Home() {
  const homepage = await getPageBySlug('homepage');
  const posts = await getAllPosts();
  return (
    <div className="h-full flex flex-col gap-2">
      <article>
        <h1 className="font-bold text-5xl" dangerouslySetInnerHTML={{ __html: homepage.title }} />
        <div dangerouslySetInnerHTML={{ __html: homepage.content }} />
      </article>
      <h2 className="font-bold text-3xl">Latest Posts</h2>
      <aside className='grid grid-cols-3 gap-4 p-2 rounded-md'>
        {posts.map((post: Post) => (
          <article className='p-4 shadow-md' key={post.databaseId}>
            <Link href={`/${post.slug}`}>
              <h2 className="font-bold" dangerouslySetInnerHTML={{ __html: post.title }} />
            </Link>
            <p className="text-sm text-gray-500">{post.commentCount} Comments</p>
            <div className="mb-2" dangerouslySetInnerHTML={{ __html: post.excerpt }} />
            <Link className="bg-sky-500 hover:bg-sky-700 px-5 py-2 text-sm leading-5 rounded-full font-semibold text-white" href={`/${post.slug}`}>View Post</Link>
          </article>
        ))}
      </aside>
    </div>
  );
}

```

lib\queries\getMenuBySlug.ts

```typescript
import {fetchGraphQL} from '@/lib/functions'
import {Menu} from '@/lib/types'
/**
 * Fetch a menu by slug.
 */
export default async function getMenuBySlug(slug: string) {
  const query = `
    query GetMenuBySlug($slug: ID = "URI") {
      menu(id: $slug, idType: SLUG) {
        menuItems {
          edges {
            node {
              uri
              label
              databaseId
            }
          }
        }
      }
    }
  `
  const variables = {
    slug: slug
  }
  const response = await fetchGraphQL(query, variables)
  return response.data.menu as Menu
}

```

### Tạo một custom post type in [`graphql`](https://www.wpgraphql.com/docs/custom-post-types)

wp-content\themes\astra-child\functions.php

```php
<?php
add_filter('big_image_size_threshold', '__return_false');
add_filter('intermediate_image_sizes', 'remove_default_img_sizes', 10, 1);
function remove_default_img_sizes($sizes)
{
  $targets =
    [
      'thumbnail',
      'medium',
      'medium_large',
      'large',
      '1536x1536',
      '2048x2048',
      'woocommerce_thumbnail',
      'woocommerce_single',
      'woocommerce_gallery_thumbnail',
    ];
  foreach ($sizes as $size_index => $size) {
    if (in_array($size, $targets)) {
      unset($sizes[$size_index]);
    }
  }
  return $sizes;
}
if (! function_exists('astra_register_menu_locations')) {
  /**
   * Register menus
   *
   * @since 1.0.0
   */
  function astra_register_menu_locations()
  {
    /**
     * Primary Menus
     */
    register_nav_menus(
      array(
        'primary' => esc_html__('Primary Menu', 'astra'),
      )
    );
    /**
     * Primary Menus
     */
    register_nav_menus(
      array(
        'menu' => esc_html__('Menu', 'astra'),
      )
    );
    if (true === Astra_Builder_Helper::$is_header_footer_builder_active) {
      /**
       * Register the Secondary & Mobile menus.
       */
      register_nav_menus(
        array(
          'secondary_menu' => esc_html__('Secondary Menu', 'astra'),
          'mobile_menu'    => esc_html__('Off-Canvas Menu', 'astra'),
        )
      );
      $component_limit = defined('ASTRA_EXT_VER') ? Astra_Builder_Helper::$component_limit : Astra_Builder_Helper::$num_of_header_menu;
      for ($index = 3; $index <= $component_limit; $index++) {
        if (! is_customize_preview() && ! Astra_Builder_Helper::is_component_loaded('menu-' . $index)) {
          continue;
        }
        register_nav_menus(
          array(
            'menu_' . $index => esc_html__('Menu ', 'astra') . $index,
          )
        );
      }
      /**
       * Register the Account menus.
       */
      register_nav_menus(
        array(
          'loggedin_account_menu' => esc_html__('Logged In Account Menu', 'astra'),
        )
      );
    }
    /**
     * Footer Menus
     */
    register_nav_menus(
      array(
        'footer_menu' => esc_html__('Footer Menu', 'astra'),
      )
    );
  }
}
/**
 * Register a book post type.
 */
function wpdocs_codex_custom_init()
{
  $labels = array(
    'name'                  => _x('Books', 'Post type general name', 'textdomain'),
    'singular_name'         => _x('Book', 'Post type singular name', 'textdomain'),
    'menu_name'             => _x('Books', 'Admin Menu text', 'textdomain'),
    'name_admin_bar'        => _x('Book', 'Add New on Toolbar', 'textdomain'),
    'add_new'               => __('Add New', 'textdomain'),
    'add_new_item'          => __('Add New Book', 'textdomain'),
    'new_item'              => __('New Book', 'textdomain'),
    'edit_item'             => __('Edit Book', 'textdomain'),
    'view_item'             => __('View Book', 'textdomain'),
    'all_items'             => __('All Books', 'textdomain'),
    'search_items'          => __('Search Books', 'textdomain'),
    'parent_item_colon'     => __('Parent Books:', 'textdomain'),
    'not_found'             => __('No books found.', 'textdomain'),
    'not_found_in_trash'    => __('No books found in Trash.', 'textdomain'),
    'featured_image'        => _x('Book Cover Image', 'Overrides the “Featured Image” phrase for this post type. Added in 4.3', 'textdomain'),
    'set_featured_image'    => _x('Set cover image', 'Overrides the “Set featured image” phrase for this post type. Added in 4.3', 'textdomain'),
    'remove_featured_image' => _x('Remove cover image', 'Overrides the “Remove featured image” phrase for this post type. Added in 4.3', 'textdomain'),
    'use_featured_image'    => _x('Use as cover image', 'Overrides the “Use as featured image” phrase for this post type. Added in 4.3', 'textdomain'),
    'archives'              => _x('Book archives', 'The post type archive label used in nav menus. Default “Post Archives”. Added in 4.4', 'textdomain'),
    'insert_into_item'      => _x('Insert into book', 'Overrides the “Insert into post”/”Insert into page” phrase (used when inserting media into a post). Added in 4.4', 'textdomain'),
    'uploaded_to_this_item' => _x('Uploaded to this book', 'Overrides the “Uploaded to this post”/”Uploaded to this page” phrase (used when viewing media attached to a post). Added in 4.4', 'textdomain'),
    'filter_items_list'     => _x('Filter books list', 'Screen reader text for the filter links heading on the post type listing screen. Default “Filter posts list”/”Filter pages list”. Added in 4.4', 'textdomain'),
    'items_list_navigation' => _x('Books list navigation', 'Screen reader text for the pagination heading on the post type listing screen. Default “Posts list navigation”/”Pages list navigation”. Added in 4.4', 'textdomain'),
    'items_list'            => _x('Books list', 'Screen reader text for the items list heading on the post type listing screen. Default “Posts list”/”Pages list”. Added in 4.4', 'textdomain'),
  );
  $args = array(
    'labels'             => $labels,
    'public' => true,
    'publicly_queryable' => true,
    'show_ui' => true,
    'show_in_menu'       => true,
    'query_var'          => true,
    'capability_type'    => 'post',
    'rewrite'            => array('slug' => 'books'),
    'label'  => __('Books', 'textdomain'),
    'has_archive'        => true,
    'hierarchical'       => false,
    'menu_position'      => null,
    'show_in_graphql' => true,
    'graphql_single_name' => 'books',
    'graphql_plural_name' => 'books',
    'supports'           => array('title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments')
  );
  register_post_type('books', $args);
}
add_action('init', 'wpdocs_codex_custom_init');
add_filter('register_post_type_args', function ($args, $post_type) {
  // Change this to the post type you are adding support for
  if ('book' === $post_type) {
    $args['show_in_graphql'] = true;
    $args['graphql_single_name'] = 'book';
    $args['graphql_plural_name'] = 'books'; # Don't set, and it will default to `all${graphql_single_name}`, i.e. `allDocument`.
  }
  return $args;
}, 10, 2);
```

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FJvMw8f8Btg4nVvDsPwab%2Fimage.png?alt=media&#x26;token=2b4d4bd2-53a6-4557-84a2-dda701e5dc5f" alt=""><figcaption></figcaption></figure>

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FPyOb2MKGzUokadt2lIQi%2Fimage.png?alt=media&#x26;token=c9fdcaf0-54a7-419d-bc8a-f8f59532e922" alt=""><figcaption></figcaption></figure>

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2F5Gq4HuRKv1ha3GFWk2It%2Fimage.png?alt=media&#x26;token=b0efb83e-997f-413e-9f36-9784c89e8920" alt=""><figcaption></figcaption></figure>

### \[Single Post]&#x20;

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FzFxZCbpguG4bSw85SDrb%2Flocalhost_3000_blog_hello-world.png?alt=media&#x26;token=dd47210c-7220-4041-a6c1-5aa76c03fcb7" alt=""><figcaption></figcaption></figure>

app\blog\[slug]\page.tsx

```typescript
import getAllPosts from '@/lib/queries/getAllPosts'
import getPostBySlug from '@/lib/queries/getPostBySlug'
import type {DynamicPageProps} from '@/lib/types'
import {Metadata} from 'next'
import Image from 'next/image'
import Link from 'next/link'
import {notFound} from 'next/navigation'
/**
 * Generate the static routes at build time.
 *
 * @see https://nextjs.org/docs/app/api-reference/functions/generate-static-params
 */
export async function generateStaticParams() {
  // Get all blog posts.
  const posts = await getAllPosts()
  // No posts? Bail...
  if (!posts) {
    return []
  }
  // Return the slugs for each post.
  return posts.map((post: {slug: string}) => ({
    slug: post.slug
  }))
}
/**
 * Generate the metadata for each static route at build time.
 *
 * @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata#generatemetadata-function
 */
export async function generateMetadata({
  params
}: DynamicPageProps): Promise<Metadata | null> {
  // Get the slug from the params.
  const {slug} = await params
  // Get the blog post.
  const post = await getPostBySlug(slug)
  // No post? Bail...
  if (!post) {
    return {}
  }
  return {
    title: post.seo.title,
    description: post.seo.metaDesc
  }
}
/**
 * The blog post route.
 *
 * @see https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#pages
 */
export default async function Post({params}: Readonly<DynamicPageProps>) {
  // Get the slug from the params.
  const {slug} = await params
  // Fetch a single post from WordPress.
  const post = await getPostBySlug(slug)
  // No post? Bail...
  if (!post) {
    notFound()
  }
  return (
    <article>
      <header>
        <h2 dangerouslySetInnerHTML={{__html: post.title}} />
        <p className="italic">
          By {post.author.node.name} on <time>{post.date}</time>
        </p>
      </header>
      <div dangerouslySetInnerHTML={{__html: post.content}} />
      <footer className="flex items-center justify-between gap-4 pb-4">
        <div>
          <h3>Categories</h3>
          <ul className="m-0 flex list-none gap-2 p-0">
            {post.categories.nodes.map((category) => (
              <li className="m-0 p-0" key={category.databaseId}>
                <Link href={`/blog/category/${category.name}`}>
                  {category.name}
                </Link>
              </li>
            ))}
          </ul>
        </div>
        <div>
          <h3>Tags</h3>
          <ul className="m-0 flex list-none gap-2 p-0">
            {post.tags.nodes.map((tag) => (
              <li className="m-0 p-0" key={tag.databaseId}>
                <Link href={`/blog/tag/${tag.name}`}>{tag.name}</Link>
              </li>
            ))}
          </ul>
        </div>
      </footer>
      <section className="border-t-2">
        <h3>Comments</h3>
        {post.comments.nodes.map((comment) => (
          <article key={comment.databaseId}>
            <header className="flex items-center gap-2">
              <Image
                alt={comment.author.node.name}
                className="m-0 rounded-full"
                height={64}
                loading="lazy"
                src={comment.author.node.avatar.url}
                width={64}
              />
              <div className="flex flex-col gap-2">
                <h4
                  className="m-0 p-0 leading-none"
                  dangerouslySetInnerHTML={{__html: comment.author.node.name}}
                />
                <time className="italic">{comment.date}</time>
              </div>
            </header>
            <div dangerouslySetInnerHTML={{__html: comment.content}} />
          </article>
        ))}
      </section>
    </article>
  )
}

```

lib\queries\getPostBySlug.ts

```typescript
import {fetchGraphQL} from '@/lib/functions'
import {Post} from '@/lib/types'

/**
 * Fetch a single blog post by slug.
 */
export default async function getPostBySlug(slug: string) {
  const query = `
    query GetPost($slug: ID!) {
      post(id: $slug, idType: SLUG) {
        databaseId
        date
        modified
        content(format: RENDERED)
        title(format: RENDERED)
        featuredImage {
          node {
            altText
            sourceUrl
            mediaDetails {
                height
                width
            }
          }
        }
        author {
          node {
            name
            avatar {
              url
            }
          }
        }
        tags {
          nodes {
            databaseId
            name
          }
        }
        categories {
          nodes {
            databaseId
            name
          }
        }
        seo {
          metaDesc
          title
        }
        comments(first: 30, where: {order: ASC}) {
          nodes {
            content(format: RENDERED)
            databaseId
            date
            status
            author {
              node {
                avatar {
                  url
                }
                email
                name
                url
              }
            }
          }
        }
      }
    }
  `

  const variables = {
    slug: slug
  }

  const response = await fetchGraphQL(query, variables)

  return response.data.post as Post
}

```

### \[Custom Post Type]

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FklP5g9NPfnQEy1vuwY6X%2Flocalhost_3000_books_book-1.png?alt=media&#x26;token=125ae856-3fa1-407c-91d5-8cad684feb67" alt=""><figcaption></figcaption></figure>

app\books\[slug]\page.tsx

```typescript
import getAllBooks from '@/lib/queries/getAllBooks'
import getBookBySlug from '@/lib/queries/getBookBySlug'
import type {DynamicPageProps} from '@/lib/types'
import {Metadata} from 'next'
import Link from 'next/link'
import {notFound} from 'next/navigation'
/**
 * Generate the static routes at build time.
 *
 * @see https://nextjs.org/docs/app/api-reference/functions/generate-static-params
 */
export async function generateStaticParams() {
  // Get a list of all books.
  const books = await getAllBooks()
  // No books? Bail...
  if (!books) {
    return []
  }
  // Return the slugs for each book.
  return books.map((book: {slug: string}) => ({
    slug: book.slug
  }))
}
/**
 * Generate the metadata for each static route at build time.
 *
 * @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata#generatemetadata-function
 */
export async function generateMetadata({ params }: Readonly<DynamicPageProps>): Promise<Metadata | null> {
  // Get the slug from the params.
  const {slug} = await params
  // Get the page.
  const book = await getBookBySlug(slug)
  // No post? Bail...
  if (!book) {
    return {}
  }
  return {
    title: book.seo.title,
    description: book.seo.metaDesc
  }
}
/**
 * A single book route.
 *
 * @see https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#pages
 */
export default async function Book({params}: Readonly<DynamicPageProps>) {
  // Get the slug from the params.
  const {slug} = await params
  // Fetch a single book from WordPress.
  const book = await getBookBySlug(slug)
  // No book? Bail...
  if (!book) {
    notFound()
  }
  return (
    <main className="flex flex-col gap-8">
      <article className="w-full">
        <h1 className="font-bold" dangerouslySetInnerHTML={{__html: book.title}} />
        <div dangerouslySetInnerHTML={{__html: book.content}} />
        <Link className="button" href={book.bookfields.affiliateurl}>View on Amazon</Link>
      </article>
    </main>
  )
}

```

lib\queries\getAllBooks.ts

```typescript
import {fetchGraphQL} from '@/lib/functions'
import {Post} from '@/lib/types'
/**
 * Fetch all books.
 */
export default async function getAllBooks() {
  const query = `
    query GetAllBooks {
      books(where: {status: PUBLISH}) {
        nodes {
          databaseId
          date
          modified
          title
          slug
          excerpt(format: RENDERED)
          featuredImage {
            node {
              altText
              sourceUrl
              mediaDetails {
                  height
                  width
              }
            }
          }
          seo {
            metaDesc
            title
          }
        }
      }
    }
  `
  const response = await fetchGraphQL(query)
  return response.data.books.nodes as Post[]
}

```

lib\queries\getBookBySlug.ts

```typescript
import {fetchGraphQL} from '@/lib/functions'
import {Book} from '@/lib/types'
/**
 * Fetch a book by slug.
 */
export default async function getBookBySlug(slug: string) {
  const query = `
    query GetBookBySlug($slug: ID = "URI") {
      book(idType: SLUG, id: $slug) {
        databaseId
        date
        modified
        content(format: RENDERED)
        title(format: RENDERED)
        bookfields {
          affiliateurl
          isbn
        }
        featuredImage {
          node {
            altText
            sourceUrl
            mediaDetails {
                height
                width
            }
          }
        }
        seo {
          metaDesc
          title
        }
      }
    }
  `
  const variables = {
    slug: slug
  }
  const response = await fetchGraphQL(query, variables)
  return response.data.book as Book
}

```

### \[Page and Blog]

### — blog

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FkgdaOHu2iz4XdLz2yQJk%2Flocalhost_3000_blog.png?alt=media&#x26;token=94dcf20b-f151-41ef-acda-6d6cd4eb951d" alt=""><figcaption></figcaption></figure>

### — books

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FjOnlrLitBHkXafS8kplM%2Flocalhost_3000_books.png?alt=media&#x26;token=fa54604f-6f82-4838-8626-614ed03fa18a" alt=""><figcaption></figcaption></figure>

app\\\[slug]\page.tsx

```typescript
import getAllBooks from '@/lib/queries/getAllBooks'
import getAllPosts from '@/lib/queries/getAllPosts'
import getPageBySlug from '@/lib/queries/getPageBySlug'
import type { DynamicPageProps } from '@/lib/types'
import { Page, Post } from '@/lib/types'
import Image from 'next/image'
import Link from 'next/link'
import { notFound } from 'next/navigation'
/**
 * Fetches data from WordPress.
 */
async function fetchData(slug: string) {
  // If the slug is 'blog', fetch all posts.
  if (slug === 'blog') {
    return { posts: await getAllPosts(), context: 'blog' }
  }
  // If the slug is 'books', fetch all books.
  if (slug === 'books') {
    return { posts: await getAllBooks(), context: 'books' }
  }
  // Otherwise, this could be a page.
  const page = await getPageBySlug(slug)
  // If page data exists, return it.
  if (page) {
    return { post: page }
  }
  // Otherwise, return an error.
  return { error: 'No data found' }
}
/**
 * Render a single page.
 */
function RenderPage({ page }: { page: Page }) {
  return (
    <main className="flex flex-col gap-8">
      <article>
        <h1 dangerouslySetInnerHTML={{ __html: page.title }} />
        <div dangerouslySetInnerHTML={{ __html: page.content }} />
      </article>
    </main>
  )
}
/**
 * Render posts list.
 */
function RenderPostsList({ posts, context }: { posts: Post[]; context: string }) {
  return (
    <main className="flex flex-col gap-8">
      <h1 className="capitalize">Latest {context}</h1>
      <div className="grid grid-cols-4 gap-4">
        {posts.map((post: Post) => (
          <article className="p-4 shadow-md relative" key={post.databaseId}>
            {post?.featuredImage ? (
              <Image
                src={post?.featuredImage?.node?.sourceUrl}
                alt={post?.featuredImage?.node?.altText}
                width={280}
                height={233}
                className="h-[233px]"
                priority={true}
              />
            ) : (
              <img
                src={process.env.NEXT_PUBLIC_PLACEHOLDER_LARGE_IMAGE_URL as string}
                alt={"Featured"}
                width={280}
                height={233}
                className="h-[233px]"
              />
            )}
            <Link href={`/${context}/${post.slug}`}>
              <h2 className="font-bold py-2" dangerouslySetInnerHTML={{ __html: post.title }} />
            </Link>
            <p className="text-sm text-gray-500 italic">
              {post.commentCount} Comments
            </p>
            <div className="mb-2" dangerouslySetInnerHTML={{ __html: post.excerpt }} />
            <Link className=" bg-sky-500 hover:bg-sky-700 px-5 py-2 text-sm leading-5 rounded-full font-semibold text-white" href={`/${context}/${post.slug}`}>
              View Post
            </Link>
          </article>
        ))}
      </div>
    </main>
  )
}
/**
 * Catch-all Archive Page route.
 */
export default async function Archive({ params }: Readonly<DynamicPageProps>) {
  // Get the slug from the params.
  const { slug } = await params
  // Fetch data from WordPress.
  const data = await fetchData(slug)
  // If there's an error, return a 404 page.
  if (data.error) {
    notFound()
  }
  // If this is a single page, render the page.
  if (data.post) {
    return <RenderPage page={data.post} />
  }
  // Otherwise, this must be an archive. Render the posts list.
  if (data.posts && data.posts.length > 0) {
    return <RenderPostsList posts={data.posts} context={data.context} />
  }
  // Otherwise, return a 404 page.
  notFound()
}

```

### \[Category]

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FG1b1bOu9OihKgYisOpCd%2Flocalhost_3000_blog_category_design.png?alt=media&#x26;token=7c2523e6-5faa-41d3-9f36-ca6234afcdbb" alt=""><figcaption></figcaption></figure>

app\blog\category\[slug]\page.tsx

```typescriptreact
import config from '@/lib/config'
import getCategoryBySlug from '@/lib/queries/getCategoryBySlug'
import {DynamicPageProps} from '@/lib/types'
import {Metadata} from 'next'
import Image from 'next/image'
import Link from 'next/link'
import {notFound} from 'next/navigation'

/**
 * Generate the metadata for each static route at build time.
 *
 * @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata#generatemetadata-function
 */
export async function generateMetadata({
  params
}: DynamicPageProps): Promise<Metadata | null> {
  // Get the slug from the params.
  const {slug} = await params

  return {
    title: `${slug} Archives - ${config.siteName}`,
    description: `The category archive for ${slug}`
  }
}

/**
 * The category archive route.
 *
 * @see https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#pages
 */
export default async function CategoryArchive({
  params
}: Readonly<DynamicPageProps>) {
  // Get the slug from the params.
  const {slug} = await params

  // Fetch posts from WordPress.
  const posts = await getCategoryBySlug(slug)

  // No posts? Bail...
  if (!posts) {
    notFound()
  }

  return (
    <main className="flex flex-col gap-8">
      <h1 className="capitalize">Post Category: {slug}</h1>
      <div className="grid grid-cols-4 gap-4">
        {posts.map((post) => (
          <article className="p-4 shadow-md" key={post.databaseId}>
            {post?.featuredImage ? (
              <Image
                src={post?.featuredImage?.node?.sourceUrl}
                alt={post?.featuredImage?.node?.altText}
                width={280}
                height={233}
                className="h-[233px]"
                priority={true}
              />
            ) : (
              <img
                src={process.env.NEXT_PUBLIC_PLACEHOLDER_LARGE_IMAGE_URL as string}
                alt={"Featured"}
                width={280}
                height={233}
                className="h-[233px]"
              />
            )}
            <Link href={`/blog/${post.slug}`}>
              <h2 className="font-bold py-2" dangerouslySetInnerHTML={{__html: post.title}} />
            </Link>
            <p className="text-sm text-gray-500">
              {post.commentCount} Comments
            </p>
            <div className="mb-2" dangerouslySetInnerHTML={{__html: post.excerpt}} />
            <Link className="bg-sky-500 hover:bg-sky-700 px-5 py-2 text-sm leading-5 rounded-full font-semibold text-white" href={`/blog/${post.slug}`}>
              View Post
            </Link>
          </article>
        ))}
      </div>
    </main>
  )
}

```

lib\queries\getCategoryBySlug.ts

```typescript
import {fetchGraphQL} from '@/lib/functions'
import {Post} from '@/lib/types'
/**
 * Fetch a category archive by slug.
 */
export default async function getCategoryBySlug( slug: string, limit: number = 10 ) {
  const query = `
    query GetCategoryBySlug($slug: String!) {
      posts(where: {categoryName: $slug, status: PUBLISH}, first: ${limit}) {
        nodes {
          databaseId
          date
          excerpt(format: RENDERED)
          title(format: RENDERED)
          featuredImage {
            node {
              altText
              sourceUrl
              mediaDetails {
                height
                width
              }
            }
          }
          seo {
            metaDesc
            title
          }
          slug
        }
      }
    }
  `
  const variables = {
    slug: slug
  }
  const response = await fetchGraphQL(query, variables)
  return response.data.posts.nodes as Post[]
}

```

\[Tag]

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2Fr0PIbWZLXQ5xs2lRuaUj%2Flocalhost_3000_blog_tag_block-spacing.png?alt=media&#x26;token=09382ad1-049e-41ca-930f-0f744b1c04e2" alt=""><figcaption></figcaption></figure>

app\blog\tag\[slug]\page.tsx

```typescript
import config from '@/lib/config'
import getTagBySlug from '@/lib/queries/getTagBySlug'
import type {DynamicPageProps} from '@/lib/types'
import {Metadata} from 'next'
import Image from 'next/image'
import Link from 'next/link'
import {notFound} from 'next/navigation'

/**
 * Generate the metadata for each static route at build time.
 *
 * @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata#generatemetadata-function
 */
export async function generateMetadata({
  params
}: Readonly<DynamicPageProps>): Promise<Metadata | null> {
  // Get the slug from the params.
  const {slug} = await params

  return {
    title: `${slug} Archives - ${config.siteName}`,
    description: `The tag archive for ${slug}`
  }
}

/**
 * The tag archive route.
 *
 * @see https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#pages
 */
export default async function TagArchive({params}: Readonly<DynamicPageProps>) {
  // Get the slug from the params.
  const {slug} = await params

  // Fetch posts from WordPress.
  const posts = await getTagBySlug(slug)

  // No posts? Bail...
  if (!posts) {
    notFound()
  }

  return (
    <main className="flex flex-col gap-8">
      <h1 className="capitalize">Post Tag: {slug}</h1>
      <div className="grid grid-cols-4 gap-4">
        {posts.map((post) => (
          <article className="p-4 drop-shadow-lg border" key={post.databaseId}>
            {post?.featuredImage ? (
              <Image
                src={post?.featuredImage?.node?.sourceUrl}
                alt={post?.featuredImage?.node?.altText}
                width={280}
                height={233}
                className="h-[233px]"
                priority={true}
              />
            ) : (
              <img
                src={process.env.NEXT_PUBLIC_PLACEHOLDER_LARGE_IMAGE_URL as string}
                alt={"Featured"}
                width={280}
                height={233}
                className="h-[233px]"
              />
            )}
            <Link href={`/blog/${post.slug}`}>
              <h2 className="font-bold py-2" dangerouslySetInnerHTML={{__html: post.title}} />
            </Link>
            <p className="text-sm text-gray-500">
              {post.commentCount} Comments
            </p>
            <div className="mb-2" dangerouslySetInnerHTML={{__html: post.excerpt.substring(0, 120)}} />
            <Link className="bg-sky-500 hover:bg-sky-700 px-5 py-2 text-sm leading-5 rounded-full font-semibold text-white" href={`/blog/${post.slug}`}>
              View Post
            </Link>
          </article>
        ))}
      </div>
    </main>
  )
}

```

lib\queries\getTagBySlug.ts

```typescript
import {fetchGraphQL} from '@/lib/functions'
import {Post} from '@/lib/types'

/**
 * Fetch a tag archive by slug.
 */
export default async function getTagBySlug(slug: string, limit: number = 10) {
  const query = `
    query GetTagBySlug($slug: String!) {
      posts(where: {tag: $slug, status: PUBLISH}, first: ${limit}) {
        nodes {
          databaseId
          date
          excerpt(format: RENDERED)
          title(format: RENDERED)
          featuredImage {
            node {
              altText
              sourceUrl
              mediaDetails {
                  height
                  width
              }
            }
          }
          seo {
            metaDesc
            title
          }
          slug
        }
      }
    }
  `

  const variables = {
    slug: slug
  }

  const response = await fetchGraphQL(query, variables)

  return response.data.posts.nodes as Post[]
}

```

### \[Comment]

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FTNBUDVhyuEsXszroTbpV%2Flocalhost_3000_blog_hello-world.png?alt=media&#x26;token=f1d629f2-aae3-46f9-818b-a966e6ad93a9" alt=""><figcaption></figcaption></figure>

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FU6eSvgQBRAPD652azo0M%2Flocalhost_3000_blog_hello-world.png?alt=media&#x26;token=ce5b4166-3337-4f74-978d-28d832c5a8de" alt=""><figcaption></figcaption></figure>

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FBMTknjB1FJYDG9nYGYTD%2Flocalhost_3000_blog_hello-world%20(1).png?alt=media&#x26;token=fbf3f4da-a428-483e-82f1-d544757f6475" alt=""><figcaption></figcaption></figure>

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2F34zsECUNU533AgHuB8s4%2Fimage.png?alt=media&#x26;token=90df56ed-88e6-45b6-807c-7be951fc0232" alt=""><figcaption></figcaption></figure>

lib\mutations\createComment.ts

```typescript
import { fetchGraphQL } from '@/lib/functions'
/**
 * Create a comment.
 */
export async function createComment(comment: { name: string, email: string, website: string, comment: string, postID: string }) {
  const query = `
    mutation CREATE_COMMENT(
      $authorEmail: String!
      $authorName: String!
      $authorUrl: String
      $comment: String!
      $postID: Int!
    ) {
      createComment(
        input: {
          author: $authorName
          authorEmail: $authorEmail
          authorUrl: $authorUrl
          commentOn: $postID
          content: $comment
        }
      ) {
        success
        comment {
          author {
            node {
              avatar {
                url
              }
              email
              name
              url
            }
          }
          content(format: RENDERED)
          date
        }
      }
    }
  `
  const variables = {
    authorEmail: comment.email,
    authorName: comment.name,
    authorUrl: comment.website,
    comment: comment.comment,
    postID: comment.postID
  }
  const response = await fetchGraphQL(query, variables)
  return response.data.createComment
}

```

### \[Search]

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2Fs7HzD7AommELTqyWlq64%2F192.168.0.102_3000_search.png?alt=media&#x26;token=fbdd7520-55bf-4d7a-99f3-06386ba28a09" alt=""><figcaption></figcaption></figure>

app\search\page.tsx

```typescript
import SearchForm from '@/components/SearchForm'
/**
 * Search page.
 */
export default function Page() {
  return (
    <main className="flex flex-col gap-8">
      <h1>Search</h1>
      <SearchForm />
    </main>
  )
}

```

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FxNQlwaK5h26mwfey304f%2Fwpclidemo.dev_wp-json_wp_v2_search_search%3Da%26subtype%5B%5D%3Dpage.png?alt=media&#x26;token=3f89c7db-4660-49b1-823f-28cb8437c3c6" alt=""><figcaption></figcaption></figure>

components\SearchForm.tsx

```typescript
'use client'
import {searchQuery} from '@/lib/functions'
import {SearchResults} from '@/lib/types'
import Link from 'next/link'
import {useCallback, useEffect, useRef, useState} from 'react'
/**
 * Search component.
 */
export default function Search() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<SearchResults[]>([])
  const [isSearching, setIsSearching] = useState(false)
  const [hasSearched, setHasSearched] = useState(false)
  const inputRef = useRef<HTMLInputElement>(null)
  // Perform the search.
  const performSearch = useCallback(async () => {
    // If the query is empty or too long, return early.
    if (query.length === 0 || query.length > 100) return
    setIsSearching(true)
    setHasSearched(true)
    try {
      const data = await searchQuery(query)
      setResults(data)
    } catch (error) {
      console.error(error)
      setResults([])
    } finally {
      setIsSearching(false)
    }
  }, [query])
  // Debounce the search query.
  useEffect(() => {
    if (query.length > 0) {
      const debounceTimeout = setTimeout(performSearch, 500)
      return () => clearTimeout(debounceTimeout)
    } else {
      setResults([])
      setHasSearched(false)
    }
  }, [query, performSearch])
  // Reset the search.
  function resetSearch() {
    setIsSearching(false)
    setQuery('')
    setResults([])
    setHasSearched(false)
  }
  return (
    <>
      <div className="relative flex items-center pb-8">
        <input
          aria-label="Search"
          className="w-full block rounded-full bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-sky-500 sm:text-sm/6"
          name="search"
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Begin typing to search..."
          ref={inputRef}
          type="search"
          value={query}
        />
        <button className="absolute right-0 bg-sky-500 hover:bg-sky-700 px-5 py-2 text-sm leading-5 rounded-full font-semibold text-white" onClick={resetSearch} type="reset">
          Reset
        </button>
      </div>
      {query.length > 0 && !hasSearched && <p className="m-0">Searching...</p>}
      {!isSearching && hasSearched && results.length === 0 && (
        <p className="m-0">Bummer. No results found.</p>
      )}
      {!isSearching && results.length > 0 && (
        <div>
          <p className="m-0">
            Nice! You found{' '}
            <span className="border-b border-b-orange-300 font-bold">
              {results.length}
            </span>{' '}
            results for{' '}
            <span className="bg-orange-300 p-1 text-zinc-800">{query}</span>
          </p>
          <ol className='grid grid-cols-3 gap-4 p-2 rounded-md'>
            {results?.map((result) => (
              <li key={result.id}>
                <Link
                  href={result.url.replace('https://blog.', 'https://')}
                  onClick={resetSearch}
                >
                  <span
                    className="m-0 p-0"
                    dangerouslySetInnerHTML={{__html: result.title}}
                  />
                </Link>
              </li>
            ))}
          </ol>
        </div>
      )}
    </>
  )
}

```

### feed.xml

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FAPNj0Vzl6XohuTpqPLZg%2Flocalhost_3000_feed.xml.png?alt=media&#x26;token=e61ffdd4-730f-4bd4-b42d-ab75bde3dd96" alt=""><figcaption></figcaption></figure>

app\feed.xml\route.ts

```typescript
import config from '@/lib/config'
import getAllPosts from '@/lib/queries/getAllPosts'
import escape from 'xml-escape'

/**
 * Route handler for generating RSS feed.
 *
 * @see https://nextjs.org/docs/app/api-reference/file-conventions/route
 */
export async function GET() {
  // Fetch all posts.
  const allPosts = await getAllPosts()

  // If no posts, return response.
  if (!allPosts) {
    return new Response('No posts found.', {
      headers: {
        'Content-Type': 'application/xml; charset=utf-8'
      }
    })
  }

  // Start of RSS feed.
  let rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>${config.siteName}</title>
    <description>${config.siteDescription}</description>
    <link>${config.siteUrl}</link>
    <generator>RSS for Node and Next.js</generator>
    <pubDate>${new Date().toUTCString()}</pubDate>
    <ttl>60</ttl>`

  // Add posts to RSS feed.
  allPosts.forEach((post) => {
    rss += `
    <item>
      <title>${escape(post.title)}</title>
      <description>${escape(post.excerpt)}</description>
      <link>${config.siteUrl}/blog/${post.slug}</link>
      <guid>${config.siteUrl}/blog/${post.slug}</guid>
      <pubDate>${new Date(post.date).toUTCString()}</pubDate>
    </item>`
  })

  // Close channel and rss tag.
  rss += `
  </channel>
</rss>`

  // Return response.
  return new Response(rss, {
    headers: {
      'Content-Type': 'application/xml; charset=utf-8'
    }
  })
}

```

### sitemap

wp-content\themes\astra-child\functions.php

```
add_post_type_support( 'page', 'excerpt' );
```

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2Fawxycoc2Ek4haZfixn3v%2Flocalhost_3000_sitemap.xml.png?alt=media&#x26;token=d7182aef-f1df-45a0-a4eb-c9facc42f3bd" alt=""><figcaption></figcaption></figure>

app\sitemap.ts

```typescript
import config from '@/lib/config'
import getAllPages from '@/lib/queries/getAllPages'
import getAllPosts from '@/lib/queries/getAllPosts'
import type { Page, Post } from '@/lib/types'
import type { MetadataRoute } from 'next'
/**
 * Generate sitemap entries from WordPress pages and posts.
 */
function generateWPEntries(items: Post[] | Page[],urlPrefix: string): MetadataRoute.Sitemap {
  return items.map((item) => ({
    url: `${config.siteUrl}${urlPrefix}${item.slug}`,
    lastModified: new Date(),
    changeFrequency: 'monthly',
    priority: 0.5
  }))
}
/**
 * Generate sitemap for the website.
 *
 * @see https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap#generating-a-sitemap-using-code-js-ts
 */
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  try {
    // Fetch all posts and pages
    const allPosts = await getAllPosts()
    const allPages = await getAllPages()
    // Generate sitemap entries.
    const posts = generateWPEntries(allPosts, '/blog/')
    const pages = generateWPEntries(allPages, '/')
    // Combine posts and pages.
    const sitemap = [...pages, ...posts];
    console.log(sitemap)
    // Create sitemap.xml
    return sitemap
  } catch (error) {
    console.error('Error generating sitemap:', error)
    return []
  }
}

```

{% file src="<https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FhIHheJdywuhzGdokpgzD%2Fgraphnext.zip?alt=media&token=50dc8e61-56d2-483f-bf04-d662f389eb80>" %}

robots.ts

```typescript
import { MetadataRoute } from 'next';
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` : 'http://localhost:3000';
export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*'
      }
    ],
    sitemap: `${baseUrl}/sitemap.xml`,
    host: baseUrl
  };
}

```

<figure><img src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FU314ElNxXcfxiURy6Lra%2Fimage.png?alt=media&#x26;token=f9ec3a05-4168-4c02-aec7-21d9b1b7dc86" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://learnphp.gitbook.io/learnphp/wordpress-advand/tim-hieu-seo-feed.xml-sitemap.ts-robots.ts-nextjs-wordpress.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
