<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ headless cms - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ headless cms - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 26 May 2026 16:23:03 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/headless-cms/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Headless WordPress Frontend with Astro SSR on Cloudflare Pages ]]>
                </title>
                <description>
                    <![CDATA[ This tutorial shows you how to run WordPress as a headless CMS with an Astro frontend deployed to Cloudflare Pages. For a project I was recently working on, the requirement was to use WordPress as the ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-headless-wordpress-frontend-with-astro-ssr-on-cloudflare-pages/</link>
                <guid isPermaLink="false">69e65d6ec9501dd0100e2105</guid>
                
                    <category>
                        <![CDATA[ WordPress ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Astro ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloudflare ]]>
                    </category>
                
                    <category>
                        <![CDATA[ headless cms ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tech With RJ ]]>
                </dc:creator>
                <pubDate>Mon, 20 Apr 2026 17:07:58 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/605584805f8d5121697263ca/87087639-b2d1-4641-a2c9-f8f369b49406.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>This tutorial shows you how to run WordPress as a headless CMS with an Astro frontend deployed to Cloudflare Pages.</p>
<p>For a project I was recently working on, the requirement was to use WordPress as the site's backend. Content management, blog posts, and media were all handled through the WordPress admin. The frontend was open: it could be a theme, a template, or something customized through Elementor.</p>
<p>I could've built the same result in Elementor, but the process would've been slower and harder to maintain. Drag-and-drop works until the design gets specific, and then every small tweak costs more time than it should.</p>
<p>As a full stack developer, writing code turned out to be faster for me and produced cleaner output. Tools like Claude Code make the iteration cycle even tighter. So I kept the requirement –&nbsp;WordPress as the backend – and decided to build the frontend separately in code.</p>
<p>I wanted to share how I did this so that, if you're facing similar requirements, you'll know the way forward.</p>
<p>By the end of this tutorial, you'll have:</p>
<ul>
<li><p>A WordPress install serving content through its REST API on a subdomain</p>
</li>
<li><p>An Astro SSR frontend rendering the content on the root domain</p>
</li>
<li><p>A Cloudflare Pages deployment triggered on every git push</p>
</li>
<li><p>Security hardening for a headless WordPress setup</p>
</li>
<li><p>Draft post preview working across both systems</p>
</li>
</ul>
<p><strong>Prerequisites:</strong> You should be comfortable with the command line, have basic familiarity with WordPress admin, and know enough JavaScript to read and write simple functions.</p>
<p>To follow along, you'll need a WordPress installation, a GitHub account, and a Cloudflare account.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-headless-wordpress">Why Headless WordPress?</a></p>
</li>
<li><p><a href="#heading-the-architecture">The Architecture</a></p>
</li>
<li><p><a href="#heading-why-astro">Why Astro?</a></p>
</li>
<li><p><a href="#heading-infrastructure-setup">Infrastructure Setup</a></p>
<ul>
<li><p><a href="#heading-step-1-move-dns-to-cloudflare">Step 1: Move DNS to Cloudflare</a></p>
</li>
<li><p><a href="#heading-step-2-create-the-cms-subdomain">Step 2: Create the CMS Subdomain</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-wordpress-configuration">WordPress Configuration</a></p>
<ul>
<li><p><a href="#heading-tell-wordpress-it-lives-on-the-subdomain">Tell WordPress it Lives on the Subdomain</a></p>
</li>
<li><p><a href="#heading-must-use-plugin-redirect-and-preview">Must-Use Plugin: Redirect and Preview</a></p>
</li>
<li><p><a href="#heading-clean-up-plugins">Clean Up Plugins</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-the-astro-frontend">The Astro Frontend</a></p>
<ul>
<li><p><a href="#heading-astroconfigmjs">astro.config.mjs</a></p>
</li>
<li><p><a href="#heading-env">.env</a></p>
</li>
<li><p><a href="#heading-srclibwordpressjs">src/lib/wordpress.js</a></p>
</li>
<li><p><a href="#heading-srcmiddlewarejs">src/middleware.js</a></p>
</li>
<li><p><a href="#heading-srclayoutslayoutastro">src/layouts/Layout.astro</a></p>
</li>
<li><p><a href="#heading-srcpagesblogindexastro">src/pages/blog/index.astro</a></p>
</li>
<li><p><a href="#heading-srcpagesblogslugastro">src/pages/blog/[slug].astro</a></p>
</li>
<li><p><a href="#heading-srcpagessitemapxmlts">src/pages/sitemap.xml.ts</a></p>
</li>
<li><p><a href="#heading-srcstylesglobalcss">src/styles/global.css</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-cicd-with-cloudflare-pages">CI/CD with Cloudflare Pages</a></p>
</li>
<li><p><a href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
<li><p><a href="#heading-good-to-know">Good to Know</a></p>
</li>
</ul>
<h2 id="heading-why-headless-wordpress">Why Headless WordPress?</h2>
<p>Headless WordPress separates content management from content delivery. WordPress keeps doing what it handles well: storing content and giving editors a familiar admin interface. A separate frontend handles rendering, routing, and performance.</p>
<p>A few situations where this split pays off:</p>
<ul>
<li><p>Your content team is trained on WordPress and moving them elsewhere would slow everyone down. Headless preserves their workflow and gives you a modern frontend.</p>
</li>
<li><p>Your site needs a design or interaction pattern that a WordPress theme or page builder struggles to deliver. Custom dashboards, interactive tools, data-driven layouts, or integrations with non-WordPress APIs all fit here.</p>
</li>
<li><p>You want edge delivery and modern tooling without rebuilding content management from scratch. WordPress handles content and media well. A JavaScript frontend on a CDN handles delivery well. Headless lets each side do its job.</p>
</li>
<li><p>You need the same content across multiple surfaces. One WordPress install feeds a marketing site, a mobile app, and an internal dashboard through the same REST API.</p>
</li>
</ul>
<p>Headless is not a fit for every site. Skip it if your site is a simple brochure, if one person does everything in the admin, or if you have no developer time to maintain a second codebase. A regular WordPress theme is the better answer there.</p>
<h2 id="heading-the-architecture">The Architecture</h2>
<p>The term "headless" means you strip WordPress of its frontend responsibility. Instead of WordPress generating and serving HTML pages to visitors, it only stores and serves content through its REST API. A separate frontend framework, in this case Astro, handles what the visitor actually sees.</p>
<img src="https://cdn.hashnode.com/uploads/covers/605584805f8d5121697263ca/0508174b-0740-47cf-a780-943a0c254ae1.png" alt="Diagram of a headless WordPress setup with Cloudflare Pages and Astro SSR fetching content via the WordPress REST API, with GitHub auto-deploy and a CMS subdomain for editors." style="display:block;margin:0 auto" width="7093" height="1017" loading="lazy">

<p>When a visitor loads a page, the request hits Cloudflare Pages, which runs the Astro server. Astro fetches the relevant content from WordPress via the REST API, builds the HTML, and returns it to the visitor. WordPress never touches the visitor's browser.</p>
<p>Content editors log into the WordPress admin at the CMS subdomain. They write, publish, and manage content as they normally would. The moment they publish, the content is live. There's no rebuild step because Astro fetches fresh data on every request.</p>
<p>The REST API has been built into WordPress since version 4.7. You don't need a GraphQL plugin, a paid headless CMS service, or any extra infrastructure.</p>
<h2 id="heading-why-astro">Why Astro?</h2>
<p>You could use Next.js, Nuxt, or SvelteKit here as well. But I chose Astro because its defaults fit this use case.</p>
<p>Astro compiles components to plain HTML and ships zero JavaScript to the browser by default. You only add client-side JavaScript where you explicitly need it.</p>
<p>For a CMS-driven site, most pages need none. SSR mode means every request fetches fresh data from WordPress at runtime, so content changes go live immediately without a rebuild. Cloudflare has an official adapter that handles the build output. Tailwind v4 integrates through a Vite plugin with no config file needed.</p>
<p>If WordPress wasn't a requirement, I would have used Next.js with Payload CMS. Payload gives you a fully typed CMS built in TypeScript that sits inside the same Next.js project, with more control over your content schema from day one. But the requirement was WordPress, and for a WordPress REST API frontend, Astro is the faster and cleaner choice.</p>
<h2 id="heading-infrastructure-setup">Infrastructure Setup</h2>
<p>Here's my setup: domain at Namecheap, WordPress on Hostinger shared hosting, and a Google Workspace email. The steps below apply to any host, whether shared hosting with cPanel or hPanel, a VPS with Apache or Nginx, or a self-managed server.</p>
<h3 id="heading-step-1-move-dns-to-cloudflare">Step 1: Move DNS to Cloudflare</h3>
<p>First, you'll need to move your domain's nameservers to Cloudflare. This gives you free DDoS protection, SSL, and the ability to attach a custom domain to Cloudflare Pages.</p>
<p>Before switching, verify that all DNS records transferred correctly, including your website A or CNAME records. For email, get your MX, SPF, DKIM, and DMARC values from your email provider's admin panel and add them to Cloudflare DNS first, otherwise email breaks during propagation.</p>
<h3 id="heading-step-2-create-the-cms-subdomain">Step 2: Create the CMS Subdomain</h3>
<p>Move WordPress to <code>cms.yourdomain.com</code> so the root domain is free for Astro. In Cloudflare DNS, add an A record pointing <code>cms</code> at your server IP, or a CNAME if your host uses a CDN hostname. Then create the subdomain in your hosting panel pointing to the same WordPress directory.</p>
<p>One thing people miss: your server needs its own SSL certificate for the connection between Cloudflare and your origin to work. Cloudflare handles SSL at its edge, but if the origin has no certificate, you get a 525 error.</p>
<p>On Hostinger, this isn't automatic for new subdomains. Install it manually through hPanel. On cPanel, use Let's Encrypt. On a VPS, use Certbot.</p>
<p>Moving WordPress off the root domain also means <code>/wp-admin</code> no longer exists at your main domain, which reduces exposure. But the default login path is still <code>/wp-admin</code> on the subdomain. That is the first thing you should change — more on this in the Good to Know section at the end.</p>
<h2 id="heading-wordpress-configuration">WordPress Configuration</h2>
<h3 id="heading-tell-wordpress-it-lives-on-the-subdomain">Tell WordPress it Lives on the Subdomain</h3>
<p>In <code>wp-config.php</code>, before the "That's all, stop editing!" comment:</p>
<pre><code class="language-php">define('WP_HOME',    'https://cms.yourdomain.com');
define('WP_SITEURL', 'https://cms.yourdomain.com');
</code></pre>
<p>WordPress admin is now at <code>cms.yourdomain.com/wp-admin</code>. The old path at the root domain stops working. That's intentional.</p>
<h3 id="heading-must-use-plugin-redirect-and-preview">Must-Use Plugin: Redirect and Preview</h3>
<p>WordPress has a folder called <code>mu-plugins</code> inside <code>wp-content</code>. Files placed there are treated as must-use plugins. They load automatically on every request, before regular plugins, and there is no way to activate or deactivate them through the admin UI. This makes them the right place for behaviour you never want accidentally turned off.</p>
<p>Create <code>wp-content/mu-plugins/headless-redirect.php</code>:</p>
<pre><code class="language-php">&lt;?php
/*
Plugin Name: Headless Redirect
Description: Redirects frontend visitors to the Astro site and rewires the WordPress preview link.
*/

add_action('template_redirect', function() {
    if (is_user_logged_in()) return;
    if ($_SERVER['HTTP_HOST'] === 'cms.yourdomain.com') {
        wp_redirect('https://yourdomain.com', 302);
        exit;
    }
});

add_filter('preview_post_link', function(\(link, \)post) {
    $token = HEADLESS_PREVIEW_SECRET;
    \(type  = \)post-&gt;post_type;
    return 'https://yourdomain.com/preview?type=' . \(type . '&amp;id=' . \)post-&gt;ID . '&amp;token=' . $token;
}, 10, 2);
</code></pre>
<p>The <code>template_redirect</code> action fires when WordPress is about to render a page. If the visitor isn't logged in and the request is on the CMS subdomain, it redirects them to the main frontend. Logged-in editors pass through to the admin normally. REST API requests to <code>/wp-json/...</code> don't go through <code>template_redirect</code> at all, so they are unaffected.</p>
<p>The <code>preview_post_link</code> filter changes what happens when an editor clicks Preview on a draft post. By default, WordPress previews using its own theme, which in a headless setup renders blank.</p>
<p>This filter replaces that URL with a request to your Astro <code>/preview</code> page, passing the post ID, post type, and a secret token. Your Astro preview page uses those values to fetch the draft via the REST API and renders it exactly as it would appear live.</p>
<h3 id="heading-clean-up-plugins">Clean Up Plugins</h3>
<p>Now it's time to remove everything that renders the frontend: page builders, caching plugins, and hosting onboarding plugins.</p>
<p>But you'll want to keep Akismet, Wordfence, and Yoast SEO. Yoast adds SEO meta and Open Graph data directly to the REST API response, which your Astro pages read through <code>post.yoast_head_json</code>.</p>
<p>Then switch the active theme to a lightweight default. WordPress requires one active, but nobody sees it.</p>
<h2 id="heading-the-astro-frontend">The Astro Frontend</h2>
<p>Start with <code>pnpm create astro@latest</code>, then install the Cloudflare adapter and Tailwind:</p>
<pre><code class="language-bash">pnpm add @astrojs/cloudflare
pnpm add -D @tailwindcss/vite tailwindcss
</code></pre>
<h3 id="heading-astroconfigmjs">astro.config.mjs</h3>
<pre><code class="language-js">import { defineConfig } from 'astro/config'
import cloudflare from '@astrojs/cloudflare'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  output: 'server',
  adapter: cloudflare({ imageService: 'passthrough' }),
  vite: { plugins: [tailwindcss()] },
})
</code></pre>
<p><code>output: 'server'</code> puts Astro into full SSR mode. Without it, Astro pre-renders pages at build time, which breaks dynamic routes like <code>/blog/[slug]</code> that depend on WordPress content that didn't exist at build time.</p>
<p><code>imageService: 'passthrough'</code> is required specifically for Cloudflare Workers. Astro's default image service uses Sharp, which depends on <code>child_process</code> and <code>fs</code>. Those Node.js built-ins don't exist in the Cloudflare Workers runtime. The deployment fails with a module resolution error. Setting passthrough skips image processing entirely and renders standard <code>&lt;img&gt;</code> tags instead.</p>
<h3 id="heading-env">.env</h3>
<pre><code class="language-bash">WORDPRESS_API_URL=https://cms.yourdomain.com
</code></pre>
<p>Add this same variable in Cloudflare Pages project settings under Environment Variables before deploying.</p>
<h3 id="heading-srclibwordpressjs">src/lib/wordpress.js</h3>
<p>This file is the single place all WordPress API calls go through. Centralising them means if the API URL or authentication changes, you update one file.</p>
<p>The <code>_embed</code> parameter is important. By default, a post response only includes the post data. Featured images, author details, and categories are separate entities with their own IDs. Without <code>_embed</code>, you would need additional API requests to fetch each one. Adding it inlines all that related data into the same response.</p>
<p><code>cache: 'no-store'</code> on every fetch call is not optional. Cloudflare Workers runs a fetch cache internally that's separate from HTTP <code>Cache-Control</code> headers. Without disabling it, Cloudflare caches your WordPress API responses at the edge. An editor publishes a post and sees the old version on the frontend because the cached response is being served.</p>
<pre><code class="language-js">const WP_URL = import.meta.env.WORDPRESS_API_URL

const fetchWP = (path) =&gt;
  fetch(`\({WP_URL}\){path}`, { cache: 'no-store' }).then((r) =&gt; r.json())

export const getPosts = (page = 1, perPage = 10) =&gt;
  fetchWP(`/wp-json/wp/v2/posts?_embed&amp;per_page=\({perPage}&amp;page=\){page}`)

export const getPostBySlug = async (slug) =&gt; {
  const posts = await fetchWP(`/wp-json/wp/v2/posts?_embed&amp;slug=${slug}`)
  return posts[0]
}

export const getCategories = () =&gt;
  fetchWP(`/wp-json/wp/v2/categories`)

export const getPostsByCategory = (categoryId, page = 1) =&gt;
  fetchWP(`/wp-json/wp/v2/posts?_embed&amp;categories=\({categoryId}&amp;page=\){page}`)

export const getAllPostsForSitemap = () =&gt;
  fetchWP(`/wp-json/wp/v2/posts?_fields=slug,modified&amp;per_page=100`)
</code></pre>
<p>The sitemap function uses <code>_fields</code> instead of <code>_embed</code> to fetch only the fields it needs, keeping that request lightweight.</p>
<h3 id="heading-srcmiddlewarejs">src/middleware.js</h3>
<p>Middleware runs on every request before the page handler. This one adds <code>Cache-Control: no-store</code> to every SSR response so Cloudflare doesn't cache the rendered HTML pages.</p>
<pre><code class="language-js">export function onRequest(_context, next) {
  return next().then(response =&gt; {
    const newResponse = new Response(response.body, response)
    newResponse.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate')
    newResponse.headers.set('CDN-Cache-Control', 'no-store')
    return newResponse
  })
}
</code></pre>
<p>The original Response from Astro has immutable headers, so you can't call <code>.headers.set()</code> on it directly. The fix is to construct a new Response using the original body and response as the init argument. The new Response has mutable headers, so <code>.set()</code> works. <code>CDN-Cache-Control</code> is a Cloudflare-specific header that controls caching at the edge independently from the standard <code>Cache-Control</code> header.</p>
<h3 id="heading-srclayoutslayoutastro">src/layouts/Layout.astro</h3>
<p>Every page goes through this layout. HTML structure, meta tags, and global imports live here so you don't repeat them on every page.</p>
<pre><code class="language-astro">---
interface Props {
  title: string
  description?: string
}
const { title, description = '' } = Astro.props
---
&lt;!doctype html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;title&gt;{title}&lt;/title&gt;
    &lt;meta name="description" content={description} /&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;slot name="nav" /&gt;
    &lt;main id="main-content"&gt;&lt;slot /&gt;&lt;/main&gt;
    &lt;slot name="footer" /&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Named slots let the navbar and footer sit outside <code>&lt;main&gt;</code>, keeping the HTML landmark structure correct for accessibility.</p>
<h3 id="heading-srcpagesblogindexastro">src/pages/blog/index.astro</h3>
<pre><code class="language-astro">---
import Layout from '../../layouts/Layout.astro'
import { getPosts, getCategories, getPostsByCategory } from '../../lib/wordpress'

const page = Number(Astro.url.searchParams.get('page') ?? 1)
const categoryId = Astro.url.searchParams.get('category')

const [posts, categories] = await Promise.all([
  categoryId ? getPostsByCategory(categoryId, page) : getPosts(page, 10),
  getCategories(),
])
---
&lt;Layout title="Blog"&gt;
  &lt;nav&gt;
    &lt;a href="/blog"&gt;All&lt;/a&gt;
    {categories.map((cat) =&gt; (
      &lt;a href={`/blog?category=${cat.id}`}&gt;{cat.name}&lt;/a&gt;
    ))}
  &lt;/nav&gt;

  &lt;ul&gt;
    {posts.map((post) =&gt; {
      const image   = post._embedded?.['wp:featuredmedia']?.[0]?.source_url
      const imageAlt = post._embedded?.['wp:featuredmedia']?.[0]?.alt_text ?? ''
      return (
        &lt;li&gt;
          {image &amp;&amp; &lt;img src={image} alt={imageAlt} /&gt;}
          &lt;a href={`/blog/${post.slug}`} set:html={post.title.rendered} /&gt;
          &lt;div set:html={post.excerpt.rendered} /&gt;
        &lt;/li&gt;
      )
    })}
  &lt;/ul&gt;

  {page &gt; 1 &amp;&amp; &lt;a href={`/blog?page=${page - 1}`}&gt;Previous&lt;/a&gt;}
  &lt;a href={`/blog?page=${page + 1}`}&gt;Next&lt;/a&gt;
&lt;/Layout&gt;
</code></pre>
<p><code>Promise.all</code> fetches posts and categories in parallel. The category filter reads from the URL query string so the same page handles both <code>/blog</code> and <code>/blog?category=5</code> without separate routes.</p>
<p>Featured images live inside <code>post._embedded['wp:featuredmedia'][0]</code> because <code>_embed</code> inlines the media object into the post response.</p>
<h3 id="heading-srcpagesblogslugastro">src/pages/blog/[slug].astro</h3>
<pre><code class="language-astro">---
import Layout from '../../layouts/Layout.astro'
import { getPostBySlug } from '../../lib/wordpress'

const { slug } = Astro.params
const post = await getPostBySlug(slug)
if (!post) return Astro.redirect('/404')

const image    = post._embedded?.['wp:featuredmedia']?.[0]?.source_url
const imageAlt = post._embedded?.['wp:featuredmedia']?.[0]?.alt_text ?? ''
const author   = post._embedded?.author?.[0]?.name
const seoTitle = post.yoast_head_json?.title ?? post.title.rendered
const seoDesc  = post.yoast_head_json?.og_description ?? ''
---
&lt;Layout title={seoTitle} description={seoDesc}&gt;
  &lt;article&gt;
    &lt;h1 set:html={post.title.rendered} /&gt;
    &lt;p&gt;{author} · {new Date(post.date).toLocaleDateString()}&lt;/p&gt;
    {image &amp;&amp; &lt;img src={image} alt={imageAlt} /&gt;}
    &lt;div set:html={post.content.rendered} /&gt;
  &lt;/article&gt;
&lt;/Layout&gt;
</code></pre>
<p>Use <code>set:html</code> for WordPress content, not <code>{post.content.rendered}</code>. Astro treats curly brace expressions as text and escapes the HTML, so you see raw tags printed on the page instead of rendered content.</p>
<p>Always guard with <code>if (!post) return Astro.redirect('/404')</code>. If someone visits a slug that doesn't exist, the API returns an empty array. Without the guard, accessing properties on <code>undefined</code> throws an error that crashes the Cloudflare Worker and returns a 500.</p>
<p><code>post.yoast_head_json</code> is available when Yoast SEO is active. It contains the computed SEO title and description that Yoast generates. Using it means the SEO work done in WordPress carries over to the Astro frontend automatically.</p>
<h3 id="heading-srcpagessitemapxmlts">src/pages/sitemap.xml.ts</h3>
<pre><code class="language-ts">import type { APIRoute } from 'astro'
import { getAllPostsForSitemap } from '../lib/wordpress'

export const GET: APIRoute = async () =&gt; {
  const posts = await getAllPostsForSitemap()

  const urls = [
    { loc: 'https://yourdomain.com/', lastmod: new Date().toISOString() },
    { loc: 'https://yourdomain.com/blog/', lastmod: new Date().toISOString() },
    ...posts.map((p) =&gt; ({
      loc: `https://yourdomain.com/blog/${p.slug}/`,
      lastmod: p.modified,
    })),
  ]

  const xml = `&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"&gt;
\({urls.map((u) =&gt; `  &lt;url&gt;\n    &lt;loc&gt;\){u.loc}&lt;/loc&gt;\n    &lt;lastmod&gt;${u.lastmod}&lt;/lastmod&gt;\n  &lt;/url&gt;`).join('\n')}
&lt;/urlset&gt;`

  return new Response(xml, { headers: { 'Content-Type': 'application/xml' } })
}
</code></pre>
<p>This generates fresh XML on every request, so the sitemap always reflects currently published posts without a rebuild.</p>
<h3 id="heading-srcstylesglobalcss">src/styles/global.css</h3>
<pre><code class="language-css">@import "tailwindcss";

@theme {
  --color-brand: #your-color;
  --font-sans: 'Your Font', sans-serif;
}
</code></pre>
<p>Tailwind v4 uses CSS-first configuration through the <code>@theme</code> block. CSS variables defined here become Tailwind utilities automatically. <code>--color-brand</code> becomes <code>bg-brand</code>, <code>text-brand</code>, and so on. No <code>tailwind.config.js</code> needed.</p>
<h2 id="heading-cicd-with-cloudflare-pages">CI/CD with Cloudflare Pages</h2>
<p>With the Astro code in place, the last piece is getting it deployed. Cloudflare Pages connects directly to GitHub, so you don't have to maintain a separate pipeline.</p>
<p>Here are the steps:</p>
<ol>
<li><p>Push your repo to GitHub.</p>
</li>
<li><p>Go to Cloudflare Pages, create a project, connect it to your GitHub repository.</p>
</li>
<li><p>Set the build command to <code>pnpm build</code> and the output directory to <code>dist</code>.</p>
</li>
<li><p>Under Environment Variables, add <code>WORDPRESS_API_URL</code> pointing to <code>https://cms.yourdomain.com</code>.</p>
</li>
<li><p>Deploy.</p>
</li>
</ol>
<p>After the first deploy, every push to <code>main</code> triggers a new deployment automatically. Cloudflare runs the build, and within minutes the new version is live globally. Content updates in WordPress go live immediately, since Astro fetches from WordPress on every request. A developer pushing code and an editor publishing a post are completely independent operations.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>This setup exists because of the specific requirement that the content team was already on WordPress and changing that was not on the table.</p>
<p>If you're starting fresh with no CMS in place, this is probably not the stack you want. Go with something like Next.js and Payload CMS where the backend and frontend are designed to work together from the start.</p>
<p>But if your situation matches where content editors are already familiar with WordPress, and you need a custom frontend that a page builder can't deliver cleanly, then this separation makes sense.</p>
<p>Pros:</p>
<ul>
<li><p>Content editors keep using WordPress. No retraining, no migration.</p>
</li>
<li><p>The frontend has full control over design and behaviour. No theme or plugin constraints.</p>
</li>
<li><p>Deployments are automatic on every push. Content changes go live immediately without a rebuild.</p>
</li>
<li><p>No added cost for most sites. WordPress stays on its existing host. Cloudflare Pages is free within generous limits, and scales to $5 per month on the Workers Paid plan if you outgrow them.</p>
</li>
</ul>
<p>Cons:</p>
<ul>
<li><p>Two systems to maintain instead of one. You operate the WordPress install (updates, plugins, backups) and maintain the Astro codebase separately.</p>
</li>
<li><p>The WordPress REST API has limitations. Complex content structures or real-time features need more work to handle compared to a purpose-built headless CMS.</p>
</li>
<li><p>Adapter and deployment target are tied together. @astrojs/cloudflare v13 drops Pages support in favor of Workers, so staying on Pages means staying on v12. Details in the Good to Know section.</p>
</li>
<li><p>Frontend changes require a developer. With Elementor, anyone with admin access could adjust layouts directly in the browser. Here, any visual change outside of content goes through code, which means it goes through you.</p>
</li>
</ul>
<p>The stack is WordPress on existing hosting, Astro on Cloudflare Pages, with GitHub as the bridge between development and production. It solves a specific problem cleanly. Outside of that problem, there are better options.</p>
<h2 id="heading-good-to-know">Good to Know</h2>
<p><strong>Change the default login URL immediately.</strong> Every bot targets <code>/wp-login.php</code> and <code>/wp-admin</code>. Install WPS Hide Login and move it to something custom. Anyone hitting the default paths gets a 404.</p>
<p><strong>Remove the</strong> <code>/wp-json/wp/v2/users</code> <strong>endpoint.</strong> It returns a public list of usernames. In headless mode you get author data through <code>_embed</code> and have no use for this endpoint. Add to the mu-plugin:</p>
<pre><code class="language-php">add_filter('rest_endpoints', function($endpoints) {
    unset($endpoints['/wp/v2/users']);
    unset($endpoints['/wp/v2/users/(?P&lt;id&gt;[\d]+)']);
    return $endpoints;
});
</code></pre>
<p><strong>Disable XML-RPC and enable 2FA.</strong> Add <code>add_filter('xmlrpc_enabled', '__return_false')</code> to the mu-plugin — you aren't using it in headless mode and it's a common brute force target. Enable Wordfence's Brute Force Protection and add two-factor authentication through WP 2FA for all admin accounts.</p>
<p><strong>Don't upgrade</strong> <code>@astrojs/cloudflare</code> <strong>to v13 if you deploy via Cloudflare Pages git-push CI.</strong> v12 outputs <code>dist/_worker.js</code> which Pages CI expects. v13 outputs a different format for <code>wrangler deploy</code> — Pages CI falls back to serving the <code>dist</code> folder as a static site and every SSR route returns 404 with no helpful error message.</p>
<p><strong>The v12 adapter throws a deprecation warning on</strong> <code>entrypointResolution</code><strong>.</strong> Silence it by adding <code>entrypointResolution: 'auto'</code> to the adapter options. Test before committing — it changes how the build locates the Worker entry file.</p>
<p><strong>Custom Post Types follow the same pattern.</strong> Register the CPT with <code>show_in_rest: true</code> and a <code>rest_base</code>, and it shows up at <code>/wp-json/wp/v2/your-base</code>. The same fetch helpers, <code>_embed</code>, and slug routing work exactly the same way.</p>
<p><strong>The REST API returns pagination headers.</strong> The raw response includes <code>X-WP-Total</code> and <code>X-WP-TotalPages</code> headers before you call <code>.json()</code>. If you want proper previous/next pagination, read those instead of guessing whether a next page exists.</p>
<p><strong>Wrap API calls in try/catch.</strong> If WordPress is unreachable, an unhandled fetch throws and returns a 500. A try/catch returns an empty page instead, which is a much better failure mode.</p>
<p><strong>Preview auth uses Application Passwords.</strong> WordPress 5.6 added Application Passwords under Users → Profile. That's what <code>WP_APP_USER</code> and <code>WP_APP_PASSWORD</code> in your <code>.env</code> should point to — not your regular admin password. Generate one per environment. Define the preview token as a constant in <code>wp-config.php</code> (<code>define('HEADLESS_PREVIEW_SECRET', '...')</code>) and reference that constant in the mu-plugin — never hardcode secrets in version-controlled files.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Portfolio Site with Sanity and Next.js ]]>
                </title>
                <description>
                    <![CDATA[ By Victor Eke Knowing how to handle content is important when creating a personal website for yourself or a client.  This is because maintaining and updating a site can result in substantial expenses if you don't do it correctly. This is even more th... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-portfolio-site-with-sanity-and-nextjs/</link>
                <guid isPermaLink="false">66d4617955db48792eed3fc3</guid>
                
                    <category>
                        <![CDATA[ headless cms ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ portfolio ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Fri, 28 Jul 2023 16:30:03 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/07/Sanity-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Victor Eke</p>
<p>Knowing how to handle content is important when creating a personal website for yourself or a client. </p>
<p>This is because maintaining and updating a site can result in substantial expenses if you don't do it correctly. This is even more the case if you're building for someone with a non-technical background.</p>
<p>To address this, you can integrate your website with a <a target="_blank" href="https://www.sanity.io/headless-cms">headless CMS</a> service that offers an API for content management and updates. In this case, we will utilize <a target="_blank" href="https://sanity.io">Sanity</a> for this purpose.</p>
<h2 id="heading-table-of-contents">Table of Contents:</h2>
<ol>
<li><a class="post-section-overview" href="#heading-what-is-sanity">What is Sanity?</a></li>
<li><a class="post-section-overview" href="#heading-step-1-install-nextjs">Step 1: Install Next.js</a></li>
<li><a class="post-section-overview" href="#heading-step-2-setup-sanity-studio">Step 2: Setup Sanity Studio</a></li>
<li><a class="post-section-overview" href="#heading-step-3-mount-sanity-studio-into-nextjs">Step 3: Mount Sanity Studio into Next.js</a></li>
<li><a class="post-section-overview" href="#heading-step-4-create-content-schemas">Step 4: Create Content Schemas</a></li>
<li><a class="post-section-overview" href="#heading-step-5-query-data-using-groq">Step 5: Query Data using GROQ</a></li>
<li><a class="post-section-overview" href="#heading-step-6-display-content-in-your-nextjs-app">Step 6: Display Content in your Next.js App</a></li>
<li><a class="post-section-overview" href="#heading-fix-studio-layout">Fix Studio Layout</a></li>
<li><a class="post-section-overview" href="#heading-step-7-deployment">Step 7: Deployment</a></li>
<li><a class="post-section-overview" href="#heading-setup-sanity-webhooks-for-studio-update">Setup Sanity Webhooks for Studio Update</a></li>
<li><a class="post-section-overview" href="#heading-what-next">What Next?</a></li>
</ol>
<h2 id="heading-what-is-sanity">What is Sanity?</h2>
<p>Sanity is a headless CMS framework for managing content. It provides tools to leverage APIs to connect to your web app providing instantaneous, rich and automated infrastructure for managing content on the cloud.</p>
<p>With Sanity, you can hook up pages or content that require regular updating to the studio and manage them from the content lake without having to touch code frequently. This makes the content creation and management process accessible to more people regardless of their technical background.</p>
<p>In this post, you'll learn how to use Sanity as a data source to build a portfolio site with Next.js 13 and Tailwind CSS. You'll also learn how to host it on <a target="_blank" href="https://vercel.com">Vercel</a> and set-up webhooks to trigger deployments.</p>
<p>Here is a screenshot of what the portfolio site will look like. Some of the designs for this site were inspired by <a target="_blank" href="https://tailwindui.com/templates/spotlight">Tailwind's Spotlight Portfolio Template</a>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/final-result-3.png" alt="Image" width="600" height="400" loading="lazy">
<em>Finished personal project</em></p>
<p>Want to play around with it? Check out the <a target="_blank" href="https://sanity-nextjs-site.vercel.app">live demo</a>. Also, you can find the source code for the project on <a target="_blank" href="https://github.com/Evavic44/sanity-nextjs-site">GitHub</a>.</p>
<h2 id="heading-step-1-install-nextjs">Step 1: Install Next.js</h2>
<p>Open a terminal and run this command to install the latest version of Next.js:</p>
<pre><code class="lang-bash">npx create-next-app@latest
</code></pre>
<p>Select all your preferred install options. Except for the project name, I'll go with the default options.</p>
<pre><code class="lang-bash">√ What is your project named? ... sanity-nextjs-site
√ Would you like to use TypeScript with this project? ... Yes
√ Would you like to use ESLint with this project? ... Yes
√ Would you like to use Tailwind CSS with this project? ... Yes
√ Would you like to use `src/` directory with this project? ... No
√ Would you like to use App Router? (recommended) ... Yes
√ Would you like to customize the default import <span class="hljs-built_in">alias</span>? ... No
</code></pre>
<p>This should install all the required dependencies, including Tailwind CSS into the project folder. To see it live run the command below:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> sanity-nextjs-site

npm run dev
</code></pre>
<p>Visit <a target="_blank" href="https://localhost:3000">http://localhost:3000</a> to see the site.</p>
<h2 id="heading-step-2-setup-sanity-studio">Step 2: Setup Sanity Studio</h2>
<p>Sanity studio is Sanity's open source single-page app for managing your data and operations. This is the interface from which you can create, delete, and update your data within Sanity.</p>
<h3 id="heading-install-sanity-studio">Install Sanity Studio</h3>
<p>Open up a new terminal outside of your Next.js application and type the commands below:</p>
<pre><code class="lang-bash">mkdir sanity-studio

<span class="hljs-built_in">cd</span> sanity-studio

npm create sanity@latest
</code></pre>
<p>Once your run the command in your terminal, you'll be prompted to select a login provider from the list of options. If you already have an account, it will authenticate your account and automatically log you in or else you can create a new account on Sanity.</p>
<p>Once your account has been successfully authenticated, more prompts will be provided in the terminal to configure your project. Here are the options set for the studio:</p>
<pre><code class="lang-bash">$ Project name: Sanity Next.js Site
$ Use the default dataset configuration?: Yes
$ Project output path: C:\Users\USER\Desktop\sanity-studio
$ Select project template: Clean project with no predefined schemas
$ Do you want to use TypeScript? Yes
$ Package manager to use <span class="hljs-keyword">for</span> installing dependencies?: npm
</code></pre>
<p>Once completed, this should install Sanity studio locally. To see the studio, run <code>npm run dev</code> and visit <a target="_blank" href="http://localhost:3333">localhost:3333</a>, log into your account using the same method used in creating your account, and you should see the studio running locally.</p>
<h2 id="heading-step-3-mount-sanity-studio-into-nextjs">Step 3: Mount Sanity Studio into Next.js</h2>
<p>You can choose to host your studio separately, but in this tutorial you'll be mounting it together with your Next.js application using the <a target="_blank" href="https://github.com/sanity-io/next-sanity">next-sanity</a> toolkit. </p>
<p>End the server running your Next app and run this command:</p>
<pre><code class="lang-bash">npm install sanity next-sanity
</code></pre>
<p>And then on the <code>sanity-studio</code> directory running the studio locally, copy the <code>schema</code> folder and <code>sanity.config.ts</code> file and paste into the root of your Next.js app.</p>
<p>The folder structure should look like this:</p>
<pre><code class="lang-bash">├── .next
├── app/
├── node_modules/
├── public/
├── schemas/
│   └── index.ts
├── .eslintrc.json
├── .gitignore
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── README.md
├── sanity.config.ts
├── tailwind.config.js
└── tsconfig.json
</code></pre>
<p>Next, inside the <code>sanity.config.ts</code> file, add a <code>basePath</code> key and give it a value of <code>/studio</code> or any valid URL path where you would like your studio to live.</p>
<pre><code class="lang-js"><span class="hljs-comment">// sanity.config.ts</span>

<span class="hljs-keyword">import</span> { defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">"sanity"</span>;
<span class="hljs-keyword">import</span> { deskTool } <span class="hljs-keyword">from</span> <span class="hljs-string">"sanity/desk"</span>;
<span class="hljs-keyword">import</span> { schemaTypes } <span class="hljs-keyword">from</span> <span class="hljs-string">"./schemas"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig({
  <span class="hljs-attr">name</span>: <span class="hljs-string">"sanity-nextjs-site"</span>,
  <span class="hljs-attr">title</span>: <span class="hljs-string">"Sanity Next.js Site"</span>,
  <span class="hljs-attr">projectId</span>: <span class="hljs-string">"ga8lllhf"</span>,
  <span class="hljs-attr">dataset</span>: <span class="hljs-string">"production"</span>,
  <span class="hljs-attr">basePath</span>: <span class="hljs-string">"/studio"</span>,
  <span class="hljs-attr">plugins</span>: [deskTool()],
  <span class="hljs-attr">schema</span>: { <span class="hljs-attr">types</span>: schemaTypes },
});
</code></pre>
<p>Here's a breakdown of each property:</p>
<ul>
<li><code>name</code>: Used to differentiate workspaces. Not compulsory for single workspace setup.</li>
<li><code>title</code>: Title of your project. This will show up on the Studio.</li>
<li><code>projectId</code>: This is a unique ID that points to the Sanity project you're working with.</li>
<li><code>dataset</code>: The name of the dataset to use for your studio. Common names are <em>production</em> and <em>development</em>.</li>
<li><code>basePath</code>: This is the URL path where your studio will be mounted.</li>
<li><code>schema</code>: The object where your schema files will be defined.</li>
</ul>
<h3 id="heading-create-the-studio-component">Create the Studio Component</h3>
<p>This is where the studio page will be mounted within your Next app. You can name this file whatever you prefer, but it must match with the <code>basePath</code> key specified inside the <code>sanity.config.ts</code> file. In my case, the file name will be <code>studio</code>.</p>
<p>To create the studio route, we'll utilize Next.js dynamic segments. Inside the app directory, create a <code>studio/[[...index]]/page.tsx</code> file. </p>
<pre><code class="lang-bash">app/
└── studio/
    └── [[...index]]/
         └── page.tsx
</code></pre>
<p>With this, when you visit any route that matches with <code>/studio</code>, the studio component <code>page.tsx</code> will be rendered.</p>
<p>To complete this setup, paste this code inside the component:</p>
<pre><code class="lang-js"><span class="hljs-comment">// app/studio/[[...index]]/page.tsx</span>

<span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> { NextStudio } <span class="hljs-keyword">from</span> <span class="hljs-string">"next-sanity/studio"</span>;
<span class="hljs-keyword">import</span> config <span class="hljs-keyword">from</span> <span class="hljs-string">"@/sanity.config"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Studio</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">NextStudio</span> <span class="hljs-attr">config</span>=<span class="hljs-string">{config}</span> /&gt;</span></span>;
}
</code></pre>
<p>First, <code>NextStudio</code> is imported from the <code>next-sanity</code> library and the configuration file is imported from the <code>sanity.config.ts</code> file you created earlier.</p>
<p>Now run <code>npm run dev</code> and visit <code>localhost:3000/studio</code>. You will get a prompt to add <code>localhost:3000</code> as a CORS origin to your Sanity project. Just click continue to add the URL. </p>
<p>Once added, log into your Sanity account using the same method you used in creating your account and you should see the Studio mounted into your Next.js application as shown in the image below:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/sanity-studio-admin-page-3.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>With the studio now running in your Next.js app, you don't need the separate <code>sanity-studio</code> directory anymore. You can delete or close it.</p>
<p>By default, the studio will be blank because you haven't created any schemas files. Let's do that in the next section.</p>
<h2 id="heading-step-4-create-content-schemas">Step 4: Create Content Schemas</h2>
<p>Schemas are essentially a way of organizing datasets in a database depending on what type of content you need. </p>
<p>Since we're building a portfolio site, we'll create schemas to handle projects, profile, and so on. To be more specific, you'll create three schemas files for this portfolio project:</p>
<ul>
<li><code>profile:</code> Schema file for defining your personal information like name, about, skills, and so on.</li>
<li><code>project:</code> Schema file for your projects.</li>
<li><code>work:</code> Schema file for defining your work experience.</li>
</ul>
<p>Let's start with the profile schema.</p>
<h3 id="heading-profile-schema">Profile Schema</h3>
<p>Inside the schemas directory, create a <code>profile.ts</code> file. </p>
<pre><code class="lang-bash">touch schemas/profile.ts
</code></pre>
<p>Let's start by defining the basic properties of a schema file.</p>
<pre><code class="lang-js"><span class="hljs-comment">// schemas/profile.ts</span>

<span class="hljs-keyword">import</span> { defineField } <span class="hljs-keyword">from</span> <span class="hljs-string">"sanity"</span>;
<span class="hljs-keyword">import</span> { BiUser } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-icons/bi"</span>;

<span class="hljs-keyword">const</span> profile = {
  <span class="hljs-attr">name</span>: <span class="hljs-string">"profile"</span>,
  <span class="hljs-attr">title</span>: <span class="hljs-string">"Profile"</span>,
  <span class="hljs-attr">type</span>: <span class="hljs-string">"document"</span>,
  <span class="hljs-attr">icon</span>: BiUser,
  <span class="hljs-attr">fields</span>: [],
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> profile;
</code></pre>
<p>Each schema file must contain a <code>name</code>, <code>title</code>, and <code>type</code> property. Here's a brief breakdown of the function of each property:</p>
<ul>
<li>The <code>name</code> key is the property that is used to reference a schema in the query language. The value must be a <a target="_blank" href="https://www.sanity.io/help/schema-object-fields-invalid">unique value</a> to avoid conflating schemas.</li>
<li><code>title</code> defines what the schema type is called in the Studio UI.</li>
<li><code>type</code> defines what schema type you're working with. The <code>document</code> value will tell the studio that it should make it possible to make new documents.</li>
<li>The <code>icon</code> is an optional property you can add alongside the <code>title</code>. To use the icon, install the <a target="_blank" href="https://react-icons.github.io/react-icons">react-icons</a> library with the command <code>npm install -D react-icons</code></li>
<li>The <code>fields</code> array, is where the individual input fields will be defined. Here are the fields for the profile schema:</li>
</ul>
<pre><code class="lang-js">fields: [
    defineField({
      <span class="hljs-attr">name</span>: <span class="hljs-string">"fullName"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Full Name"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
      <span class="hljs-attr">validation</span>: <span class="hljs-function">(<span class="hljs-params">rule</span>) =&gt;</span> rule.required(),
    }),
    defineField({
      <span class="hljs-attr">name</span>: <span class="hljs-string">"headline"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Headline"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"In one short sentence, what do you do?"</span>,
      <span class="hljs-attr">validation</span>: <span class="hljs-function">(<span class="hljs-params">Rule</span>) =&gt;</span> Rule.required().min(<span class="hljs-number">40</span>).max(<span class="hljs-number">50</span>),
    }),
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"profileImage"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Profile Image"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"image"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Upload a profile picture"</span>,
      <span class="hljs-attr">options</span>: { <span class="hljs-attr">hotspot</span>: <span class="hljs-literal">true</span> },
      <span class="hljs-attr">fields</span>: [
        {
          <span class="hljs-attr">name</span>: <span class="hljs-string">"alt"</span>,
          <span class="hljs-attr">title</span>: <span class="hljs-string">"Alt"</span>,
          <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
        },
      ],
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"shortBio"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Short Bio"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"text"</span>,
      <span class="hljs-attr">rows</span>: <span class="hljs-number">4</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"email"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Email Address"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"location"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Location"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"fullBio"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Full Bio"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"array"</span>,
      <span class="hljs-attr">of</span>: [{ <span class="hljs-attr">type</span>: <span class="hljs-string">"block"</span> }],
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"resumeURL"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Upload Resume"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"file"</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"socialLinks"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Social Links"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"object"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Add your social media links:"</span>,
      <span class="hljs-attr">fields</span>: [
        {
          <span class="hljs-attr">name</span>: <span class="hljs-string">"github"</span>,
          <span class="hljs-attr">title</span>: <span class="hljs-string">"Github URL"</span>,
          <span class="hljs-attr">type</span>: <span class="hljs-string">"url"</span>,
          <span class="hljs-attr">initialValue</span>: <span class="hljs-string">"https://github.com/"</span>,
        },
        {
          <span class="hljs-attr">name</span>: <span class="hljs-string">"linkedin"</span>,
          <span class="hljs-attr">title</span>: <span class="hljs-string">"Linkedin URL"</span>,
          <span class="hljs-attr">type</span>: <span class="hljs-string">"url"</span>,
          <span class="hljs-attr">initialValue</span>: <span class="hljs-string">"https://linkedin.com/in/"</span>,
        },
        {
          <span class="hljs-attr">name</span>: <span class="hljs-string">"twitter"</span>,
          <span class="hljs-attr">title</span>: <span class="hljs-string">"Twitter URL"</span>,
          <span class="hljs-attr">type</span>: <span class="hljs-string">"url"</span>,
          <span class="hljs-attr">initialValue</span>: <span class="hljs-string">"https://twitter.com/"</span>,
        },
        {
          <span class="hljs-attr">name</span>: <span class="hljs-string">"twitch"</span>,
          <span class="hljs-attr">title</span>: <span class="hljs-string">"Twitch URL"</span>,
          <span class="hljs-attr">type</span>: <span class="hljs-string">"url"</span>,
          <span class="hljs-attr">initialValue</span>: <span class="hljs-string">"https://twitch.com/"</span>,
        },
      ],
      <span class="hljs-attr">options</span>: {
        <span class="hljs-attr">collapsed</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">collapsible</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">columns</span>: <span class="hljs-number">2</span>,
      },
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"skills"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Skills"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"array"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Add a list of skills"</span>,
      <span class="hljs-attr">of</span>: [{ <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span> }],
    },
 ],
</code></pre>
<p>To understand how fields work, visualize each field object as a HTML <code>&lt;input&gt;</code> that will be available in the studio. The value in each input will be exported to a JSON object you can use to inject your data. You can add as many fields, but each must contain a <code>name</code>, <code>title</code>, and <code>type</code> property.</p>
<p>The <code>defineField()</code> helper function helps enable auto-completion of field types in your schema file.</p>
<p>Sanity comes with its own built-in schema types: <code>number</code>, <code>datetime</code>, <code>image</code>, <code>array</code>, <code>object</code>, <code>string</code>, <code>url</code>, and more. You can check out the full list of <a target="_blank" href="https://www.sanity.io/docs/schema-types">schema types here</a>.</p>
<p>To expose this newly created schema file to the Studio, you need to import it into the schemas array inside the <code>schemas/index.ts</code> file:</p>
<pre><code class="lang-js"><span class="hljs-comment">// schemas/index.ts</span>

<span class="hljs-keyword">import</span> profile <span class="hljs-keyword">from</span> <span class="hljs-string">"./profile"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> schemaTypes = [profile];
</code></pre>
<p>Now you can start working with it from within the studio. Visit your studio at <code>localhost:3000/studio</code> or whatever path you used to mount it. Then click on the Profile tab and select the edit button on the top corner to start editing the fields.</p>
<p>This is what that looks like:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/profile-schema-filled.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Fill in all the fields and click publish once completed. This will append the data into a parsed JSON document. To view this JSON output, click the menu button on the top right corner and hit "Inspect" or simply hold down <code>Ctrl Alt I</code> on your keyboard.</p>
<p>Here's what the structure for the profile schema looks like:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/inspect-schema-types-3.png" alt="Image" width="600" height="400" loading="lazy">
<em>Inspect schema document</em></p>
<p>With this, you can easily query the data to fetch the exact content you need in your front-end. Let's do that in the next section.</p>
<h2 id="heading-step-5-query-data-using-groq">Step 5: Query Data using GROQ</h2>
<p><a target="_blank" href="https://www.sanity.io/docs/groq">GROQ (Graph-Relational Object Queries)</a> is Sanity's query language designed to query collections of largely schema-less JSON documents. The idea behind the query language is to be able to describe exactly what information you need from your schema, or filter certain data, and return only specific elements from your data</p>
<p>To start using GROQ, first create a <code>sanity/sanity.client.ts</code> file in your project root directory.</p>
<pre><code class="lang-bash">mkdir sanity &amp;&amp; touch sanity/sanity.client.ts
</code></pre>
<p>Paste the code into this file:</p>
<pre><code class="lang-js"><span class="hljs-comment">// sanity/sanity.client.ts</span>

<span class="hljs-keyword">import</span> { createClient, type ClientConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">"@sanity/client"</span>;

<span class="hljs-keyword">const</span> config: ClientConfig = {
  <span class="hljs-attr">projectId</span>: <span class="hljs-string">"ga8lllhf"</span>,
  <span class="hljs-attr">dataset</span>: <span class="hljs-string">"production"</span>,
  <span class="hljs-attr">apiVersion</span>: <span class="hljs-string">"2023-07-16"</span>,
  <span class="hljs-attr">useCdn</span>: <span class="hljs-literal">false</span>,
};

<span class="hljs-keyword">const</span> client = createClient(config);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> client;
</code></pre>
<ul>
<li><code>apiVersion</code>:  The version of the Sanity API you're using. For the latest API version, use your current date in this format <code>YYYY-MM-DD</code>.</li>
<li><code>useCdn</code> is used to disable edge cases</li>
</ul>
<p>What this file does is provide a few configurations that will be defined in each query so this is just to avoid repeating it every time. Now for the main query, create a <code>sanity/sanity.query.ts</code> file.</p>
<pre><code class="lang-bash">touch sanity/sanity.query.ts
</code></pre>
<p>Note: There is not clear-cut way to arrange or name these files so feel free to change it up as needed.</p>
<p>Here's the basic query for the profile schema:</p>
<pre><code class="lang-js"><span class="hljs-comment">// sanity/sanity.query.ts</span>

<span class="hljs-keyword">import</span> { groq } <span class="hljs-keyword">from</span> <span class="hljs-string">"next-sanity"</span>;
<span class="hljs-keyword">import</span> client <span class="hljs-keyword">from</span> <span class="hljs-string">"./sanity.client"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getProfile</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> client.fetch(
    groq<span class="hljs-string">`*[_type == "profile"]{
      _id,
      fullName,
      headline,
      profileImage {alt, "image": asset-&gt;url},
      shortBio,
      location,
      fullBio,
      email,
      "resumeURL": resumeURL.asset-&gt;url,
      socialLinks,
      skills
    }`</span>
  );
}
</code></pre>
<p>Here we created an exported async function called <code>getProfile()</code> that returns a groq fetch query wrapped with the client config created in the first step.</p>
<p>The groq query starts with an asterisk (<code>*</code>) which represents every document in your dataset followed by a filter in brackets. The filter above returns the schema that has a <code>_type</code> of "profile".</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/profile-type-1.png" alt="Image" width="600" height="400" loading="lazy">
_Schema JSON showing profile schema <em>type</em></p>
<p>The filter is followed by curly braces which contains specific content from the dataset needed like: <code>fullName</code>, <code>headline</code>, <code>profileImage</code> and so on. This is called <a target="_blank" href="https://www.sanity.io/docs/how-queries-work#727ecb6f5e15">projections</a> in the Sanity docs and it returns the entire data as an array.</p>
<p>If you want to learn more about querying using GROQ, I suggest you go through the <a target="_blank" href="https://www.sanity.io/docs/how-queries-work">how queries work</a> section in the documentation. For syntax highlighting of your GROQ query, install the <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=sanity-io.vscode-sanity">sanity.io extension</a> available on the Visual Studio Code marketplace.</p>
<p>We're done with the configuration you need to start using your content. Let's look at how to display this content in your Next application.</p>
<h2 id="heading-step-6-display-content-in-your-nextjs-app">Step 6: Display Content in your Next.js App</h2>
<p>This section is broken down into two separate parts: Displaying the hero section, and about page content.</p>
<h3 id="heading-add-types-to-data-content">Add Types to Data Content</h3>
<p>Since you're using TypeScript for this project, it is important to first provide the types for the data coming from the studio.</p>
<p>Create a <code>types/index.ts</code> file in the root directory and add the profile type below:</p>
<pre><code class="lang-js"><span class="hljs-comment">// types/index.ts</span>

<span class="hljs-keyword">import</span> { PortableTextBlock } <span class="hljs-keyword">from</span> <span class="hljs-string">"sanity"</span>;

<span class="hljs-keyword">export</span> type ProfileType = {
  <span class="hljs-attr">_id</span>: string,
  <span class="hljs-attr">fullName</span>: string,
  <span class="hljs-attr">headline</span>: string,
  <span class="hljs-attr">profileImage</span>: {
    <span class="hljs-attr">alt</span>: string,
    <span class="hljs-attr">image</span>: string
  },
  <span class="hljs-attr">shortBio</span>: string,
  <span class="hljs-attr">email</span>: string,
  <span class="hljs-attr">fullBio</span>: PortableTextBlock[],
  <span class="hljs-attr">location</span>: string,
  <span class="hljs-attr">resumeURL</span>: string,
  <span class="hljs-attr">socialLinks</span>: string[],
  <span class="hljs-attr">skills</span>: string[],
};
</code></pre>
<p><code>PortableTextBlock</code> is a unique type coming from Sanity that properly defines the data type for the rich text editor.</p>
<p>Now you've defined the types for your content, it's easier to visualize the data you're expecting in your studio.</p>
<h3 id="heading-display-hero-section">Display Hero Section</h3>
<p>First, remove all the styling inside the <code>global.css</code> file, except for the necessary Tailwind imports at the top. Then clear everything inside the root <code>page.tsx</code> file of your Next.js app and paste the following code inside:</p>
<pre><code class="lang-js"><span class="hljs-comment">// app/page.tsx</span>

<span class="hljs-keyword">import</span> { getProfile } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/sanity/sanity.query"</span>;
<span class="hljs-keyword">import</span> type { ProfileType } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/types"</span>;
<span class="hljs-keyword">import</span> HeroSvg <span class="hljs-keyword">from</span> <span class="hljs-string">"./icons/HeroSvg"</span>;;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> profile: ProfileType[] = <span class="hljs-keyword">await</span> getProfile();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"max-w-7xl mx-auto lg:px-16 px-6"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex xl:flex-row flex-col xl:items-center items-start xl:justify-center justify-between gap-x-12 lg:mt-32 mt-20 mb-16"</span>&gt;</span>
        {profile &amp;&amp;
          profile.map((data) =&gt; (
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{data._id}</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"lg:max-w-2xl max-w-2xl"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-3xl font-bold tracking-tight sm:text-5xl mb-6 lg:leading-[3.7rem] leading-tight lg:min-w-[700px] min-w-full"</span>&gt;</span>
                {data.headline}
              <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-base text-zinc-400 leading-relaxed"</span>&gt;</span>
                {data.shortBio}
              <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center gap-x-6 my-10"</span>&gt;</span>
                {Object.entries(data.socialLinks)
                  .sort()
                  .map(([key, value], id) =&gt; (
                    <span class="hljs-tag">&lt;<span class="hljs-name">li</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{id}</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">a</span>
                        <span class="hljs-attr">href</span>=<span class="hljs-string">{value}</span>
                        <span class="hljs-attr">rel</span>=<span class="hljs-string">"noreferer noopener"</span>
                        <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center gap-x-3 mb-5 hover:text-purple-400 duration-300"</span>
                      &gt;</span>
                        {key[0].toUpperCase() + key.toLowerCase().slice(1)}
                      <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
                  ))}
              <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          ))}
        <span class="hljs-tag">&lt;<span class="hljs-name">HeroSvg</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></span>
  );
}
</code></pre>
<ul>
<li>First the <code>getProfile</code> query is imported from the <code>sanity.query.ts</code> file which is a filtered-down version of our data coming from the schema.</li>
<li><code>ProfileType</code> is imported to add types to the data.</li>
<li>The <code>profile</code> array is mapped inside the component to return the <code>headline</code>, <code>shortBio</code>, and <code>socialLinks</code>.</li>
<li><code>&lt;HeroSvg /&gt;</code> is essentially an <code>svg</code> element imported as a react component added just for UI aesthetics. You can download the <a target="_blank" href="https://github.com/Evavic44/sanity-nextjs-site/blob/main/app/(site)/icons/HeroSvg.tsx">HeroSVG icon component</a>.</li>
</ul>
<p>Here's the resulting output:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/hero-section-content-result-2.png" alt="Image" width="600" height="400" loading="lazy">
<em>hero section output</em></p>
<p>To speed things up, I've created the navbar and footer navigation components. Simply <a target="_blank" href="https://github.com/Evavic44/sanity-nextjs-site/tree/main/app/(site)/components/global">download the directory</a> and import them into the <code>layout.tsx</code> file like so:</p>
<pre><code class="lang-js"><span class="hljs-comment">// app/layout.tsx</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"./globals.css"</span>;
<span class="hljs-keyword">import</span> type { Metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">"next"</span>;
<span class="hljs-keyword">import</span> { Inter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/font/google"</span>;
<span class="hljs-keyword">import</span> Navbar <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/global/Navbar"</span>;
<span class="hljs-keyword">import</span> Footer <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/global/Footer"</span>;

<span class="hljs-keyword">const</span> inter = Inter({ <span class="hljs-attr">subsets</span>: [<span class="hljs-string">"latin"</span>] });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> metadata: Metadata = {
  <span class="hljs-attr">title</span>: <span class="hljs-string">"Sanity Next.js Portfolio Site"</span>,
  <span class="hljs-attr">description</span>: <span class="hljs-string">"A personal portfolio site built with Sanity and Next.js"</span>,
  <span class="hljs-attr">openGraph</span>: {
    <span class="hljs-attr">images</span>: <span class="hljs-string">"add-your-open-graph-image-url-here"</span>,
  },
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayout</span>(<span class="hljs-params">{children}: {children: React.ReactNode}</span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">body</span> <span class="hljs-attr">className</span>=<span class="hljs-string">{</span>`${<span class="hljs-attr">inter.className</span>} <span class="hljs-attr">bg-zinc-900</span> <span class="hljs-attr">text-white</span>`}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Navbar</span> /&gt;</span>
        {children}
        <span class="hljs-tag">&lt;<span class="hljs-name">Footer</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></span>
  );
}
</code></pre>
<p>With these components, the home page should look like this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/hero-section-with-component-2.png" alt="Image" width="600" height="400" loading="lazy">
<em>home page with navbar and footer components</em></p>
<h3 id="heading-display-about-page">Display About Page</h3>
<p>Let's build the about page using content from the <code>getProfile</code> query as well. In this section you'll need to install a React library called <code>PortableTextBlock</code> by Sanity. This library will allow you easily de-structure the block content of the rich text editor.</p>
<p>To install this package run <code>npm install -D @portabletext/react</code> and I'll explain how to use it later on. </p>
<p>Create an <code>about</code> folder inside the <code>app</code> directory and add a <code>page.tsx</code> file inside this new folder. You can also do this quickly using the following command:</p>
<pre><code class="lang-js">mkdir app/about &amp;&amp; touch app/about/page.tsx
</code></pre>
<p>Here's the code snippet for the about page:</p>
<pre><code class="lang-js"><span class="hljs-comment">// app/about/page.tsx</span>

<span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">"next/image"</span>;
<span class="hljs-keyword">import</span> { getProfile } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/sanity/sanity.query"</span>;
<span class="hljs-keyword">import</span> type { ProfileType } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/types"</span>;
<span class="hljs-keyword">import</span> { PortableText } <span class="hljs-keyword">from</span> <span class="hljs-string">"@portabletext/react"</span>;
<span class="hljs-keyword">import</span> { BiEnvelope, BiFile } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-icons/bi"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">About</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> profile: ProfileType[] = <span class="hljs-keyword">await</span> getProfile();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"lg:max-w-7xl mx-auto max-w-3xl md:px-16 px-6"</span>&gt;</span>
      {profile &amp;&amp;
        profile.map((data) =&gt; (
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{data._id}</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"grid lg:grid-cols-2 grid-cols-1 gap-x-6 justify-items-center"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"order-2 lg:order-none"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"lg:text-5xl text-4xl lg:leading-tight basis-1/2 font-bold mb-8"</span>&gt;</span>
                  I<span class="hljs-symbol">&amp;apos;</span>m {data.fullName}. I live in {data.location}, where I
                  design the future.
                <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col gap-y-3 text-zinc-400 leading-relaxed"</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">PortableText</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{data.fullBio}</span> /&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col lg:justify-self-center justify-self-start gap-y-8 lg:order-1 order-none mb-12"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">Image</span>
                    <span class="hljs-attr">className</span>=<span class="hljs-string">"rounded-2xl mb-4 object-cover max-h-96 min-h-96 bg-top bg-[#1d1d20]"</span>
                    <span class="hljs-attr">src</span>=<span class="hljs-string">{data.profileImage.image}</span>
                    <span class="hljs-attr">width</span>=<span class="hljs-string">{400}</span>
                    <span class="hljs-attr">height</span>=<span class="hljs-string">{400}</span>
                    <span class="hljs-attr">quality</span>=<span class="hljs-string">{100}</span>
                    <span class="hljs-attr">alt</span>=<span class="hljs-string">{data.profileImage.alt}</span>
                  /&gt;</span>

                  <span class="hljs-tag">&lt;<span class="hljs-name">a</span>
                    <span class="hljs-attr">href</span>=<span class="hljs-string">{</span>`${<span class="hljs-attr">data.resumeURL</span>}?<span class="hljs-attr">dl</span>=<span class="hljs-string">${data.fullName}_resume</span>`}
                    <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center justify-center gap-x-2 bg-[#1d1d20] border border-transparent hover:border-zinc-700 rounded-md duration-200 py-2 text-center cursor-cell font-medium"</span>
                  &gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">BiFile</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-base"</span> /&gt;</span> Download Resumé
                  <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

                <span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">a</span>
                      <span class="hljs-attr">href</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">mailto:</span>${<span class="hljs-attr">data.email</span>}`}
                      <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center gap-x-2 hover:text-purple-400 duration-300"</span>
                    &gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">BiEnvelope</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-lg"</span> /&gt;</span>
                      {data.email}
                    <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
                  <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>

            <span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-24 max-w-2xl"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"font-semibold text-4xl mb-4"</span>&gt;</span>Expertise<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-zinc-400 max-w-lg"</span>&gt;</span>
                I<span class="hljs-symbol">&amp;apos;</span>ve spent few years working on my skills. In no particular
                order, here are a few of them.
              <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

              <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-wrap items-center gap-3 mt-8"</span>&gt;</span>
                {data.skills.map((skill, id) =&gt; (
                  <span class="hljs-tag">&lt;<span class="hljs-name">li</span>
                    <span class="hljs-attr">key</span>=<span class="hljs-string">{id}</span>
                    <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-[#1d1d20] border border-transparent hover:border-zinc-700 rounded-md px-2 py-1"</span>
                  &gt;</span>
                    {skill}
                  <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
                ))}
              <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        ))}
    <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></span>
  );
}
</code></pre>
<ul>
<li>Similar to the home page, we're also fetching the data from the <code>getProfile</code> query and assigning the <code>ProfileType</code> for type safety.</li>
<li>The profile data is also mapped to get the individual properties: <code>fullName</code>, <code>location</code>, <code>fullBio</code>, <code>profileImage</code>, <code>resumeURL</code>, <code>email</code>, and <code>skills</code> array.</li>
<li>The portable text editor was de-structured using the <code>&lt;PortableText /&gt;</code> component which takes in a value prop that receives the content of the rich text editor.</li>
</ul>
<p>Adding the image from Sanity's CDN should throw an error in Next.js since you haven't added Sanity's image source hostname in your <code>next.config.ts</code> file. Here's how to do it in Next.js 13:</p>
<pre><code class="lang-js"><span class="hljs-comment">// next.config.ts</span>

<span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('next').NextConfig}</span> </span>*/</span>
<span class="hljs-keyword">const</span> nextConfig = {};

<span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">images</span>: {
    <span class="hljs-attr">remotePatterns</span>: [
      {
        <span class="hljs-attr">protocol</span>: <span class="hljs-string">"https"</span>,
        <span class="hljs-attr">hostname</span>: <span class="hljs-string">"cdn.sanity.io"</span>,
        <span class="hljs-attr">port</span>: <span class="hljs-string">""</span>,
      },
    ],
  },
};
</code></pre>
<p>Here's the resulting output:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/about-3.png" alt="Image" width="600" height="400" loading="lazy">
<em>About page</em></p>
<h3 id="heading-work-experience">Work Experience</h3>
<p>In a typical portfolio site, you may need to create a list of past work experience. This is what the schema would look like:</p>
<p>Create a <code>schemas/job.ts</code> file and paste the following code:</p>
<pre><code class="lang-js"><span class="hljs-comment">// schemas/job.ts</span>

<span class="hljs-keyword">import</span> { BiBriefcase } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-icons/bi"</span>;

<span class="hljs-keyword">const</span> job = {
  <span class="hljs-attr">name</span>: <span class="hljs-string">"job"</span>,
  <span class="hljs-attr">title</span>: <span class="hljs-string">"Job"</span>,
  <span class="hljs-attr">type</span>: <span class="hljs-string">"document"</span>,
  <span class="hljs-attr">icon</span>: BiBriefcase,
  <span class="hljs-attr">fields</span>: [
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"name"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Company Name"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"What is the name of the company?"</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"jobTitle"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Job Title"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Enter the job title. E.g: Software Developer"</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"logo"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Company Logo"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"image"</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"url"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Company Website"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"url"</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"description"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Job Description"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"text"</span>,
      <span class="hljs-attr">rows</span>: <span class="hljs-number">3</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Write a brief description about this role"</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"startDate"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Start Date"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"date"</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"endDate"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"End Date"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"date"</span>,
    },
  ],
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> job;
</code></pre>
<p>To expose this new schema file to the studio, add it to the <code>schemaTypes</code> array inside the <code>schemas/index.ts</code> and you should see it in your studio. </p>
<p>Here's the resulting output:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/job-schema-7.png" alt="Image" width="600" height="400" loading="lazy">
<em>job schema fields in sanity studio</em></p>
<p>Click the create button and add as many records as you want. Now you can move on to querying the data. </p>
<p>Similar to how the <code>profile</code> schema was queried inside the <code>sanity.query.ts</code> file, you will do that for the job schema too: </p>
<pre><code class="lang-js"><span class="hljs-comment">// sanity/sanity.query.ts</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getJob</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> client.fetch(
    groq<span class="hljs-string">`*[_type == "job"]{
      _id,
      name,
      jobTitle,
      "logo": logo.asset-&gt;url,
      url,
      description,
      startDate,
      endDate,
    }`</span>
  );
}
</code></pre>
<p>Next add the types for the returned dataset:</p>
<pre><code class="lang-js"><span class="hljs-comment">// types/index.ts</span>

<span class="hljs-keyword">export</span> type JobType = {
  <span class="hljs-attr">_id</span>: string;
  name: string;
  jobTitle: string;
  logo: string;
  url: string;
  description: string;
  startDate: <span class="hljs-built_in">Date</span>;
  endDate: <span class="hljs-built_in">Date</span>;
};
</code></pre>
<p>And then to display it in your front-end, create a <code>Job.tsx</code> file inside the <code>components</code> directory and add the following code:</p>
<pre><code class="lang-js"><span class="hljs-comment">// app/components/Job.tsx</span>

<span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">"next/image"</span>;
<span class="hljs-keyword">import</span> { getJob } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/sanity/sanity.query"</span>;
<span class="hljs-keyword">import</span> type { JobType } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/types"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Job</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> job: JobType[] = <span class="hljs-keyword">await</span> getJob();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-32"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mb-16"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"font-semibold text-4xl mb-4"</span>&gt;</span>Work Experience<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col gap-y-12"</span>&gt;</span>
        {job.map((data) =&gt; (
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
            <span class="hljs-attr">key</span>=<span class="hljs-string">{data._id}</span>
            <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-start lg:gap-x-6 gap-x-4 max-w-2xl relative before:absolute before:bottom-0 before:top-[4.5rem] before:left-7 before:w-[1px] before:h-[calc(100%-50px)] before:bg-zinc-800"</span>
          &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">a</span>
              <span class="hljs-attr">href</span>=<span class="hljs-string">{data.url}</span>
              <span class="hljs-attr">rel</span>=<span class="hljs-string">"noreferrer noopener"</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"min-h-[60px] min-w-[60px] rounded-md overflow-clip relative"</span>
            &gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">Image</span>
                <span class="hljs-attr">src</span>=<span class="hljs-string">{data.logo}</span>
                <span class="hljs-attr">className</span>=<span class="hljs-string">"object-cover"</span>
                <span class="hljs-attr">alt</span>=<span class="hljs-string">{</span>`${<span class="hljs-attr">data.name</span>} <span class="hljs-attr">logo</span>`}
                <span class="hljs-attr">fill</span>
              /&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col items-start"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-xl font-bold"</span>&gt;</span>{data.name}<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{data.jobTitle}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">small</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-sm text-zinc-500 mt-2 tracking-widest uppercase"</span>&gt;</span>
                {data.startDate.toString()} - {data.endDate.toString()}
              <span class="hljs-tag">&lt;/<span class="hljs-name">small</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-base text-zinc-400 my-4"</span>&gt;</span>{data.description}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        ))}
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span></span>
  );
}
</code></pre>
<p>To view the component, you can import it into the home page:</p>
<pre><code class="lang-js"><span class="hljs-comment">// Note: This is a truncated version of the home page (app/page.tsx) file to illustrate how the Job component is declared.</span>

<span class="hljs-keyword">import</span> { getProfile } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/sanity/sanity.query"</span>;
<span class="hljs-keyword">import</span> type { ProfileType } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/types"</span>;
<span class="hljs-keyword">import</span> HeroSvg <span class="hljs-keyword">from</span> <span class="hljs-string">"./icons/HeroSvg"</span>;
<span class="hljs-keyword">import</span> Job <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/Job"</span>; <span class="hljs-comment">// import job component</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> profile: ProfileType[] = <span class="hljs-keyword">await</span> getProfile();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"max-w-7xl mx-auto lg:px-16 px-6"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">section</span>&gt;</span> // code truncated for brevity
        <span class="hljs-tag">&lt;<span class="hljs-name">HeroSvg</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Job</span> /&gt;</span> // declare job component
    <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></span>
  );
}
</code></pre>
<p>Here's the resulting output:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/job-description-result-output-3.png" alt="Image" width="600" height="400" loading="lazy">
<em>work experience section</em></p>
<p>By now, you should have a clear understanding of the necessary steps to showcase content with Sanity: <strong>Create schema file, &gt; Query the dataset &gt; Display the content in your application</strong>. </p>
<p>Let's now focus on configuring data for dynamic routes in your application and leveraging it to construct the projects page.</p>
<h3 id="heading-project-schema">Project Schema</h3>
<p>As always, you'll start by creating the schema file:</p>
<pre><code class="lang-bash">touch schemas/project.ts
</code></pre>
<p>Here's the code for the schema fields:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { BiPackage } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-icons/bi"</span>;
<span class="hljs-keyword">import</span> { defineField } <span class="hljs-keyword">from</span> <span class="hljs-string">"sanity"</span>;

<span class="hljs-keyword">const</span> project = {
  <span class="hljs-attr">name</span>: <span class="hljs-string">"project"</span>,
  <span class="hljs-attr">title</span>: <span class="hljs-string">"Project"</span>,
  <span class="hljs-attr">description</span>: <span class="hljs-string">"Project Schema"</span>,
  <span class="hljs-attr">type</span>: <span class="hljs-string">"document"</span>,
  <span class="hljs-attr">icon</span>: BiPackage,
  <span class="hljs-attr">fields</span>: [
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"name"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Name"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Enter the name of the project"</span>,
    },
    defineField({
      <span class="hljs-attr">name</span>: <span class="hljs-string">"tagline"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Tagline"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
      <span class="hljs-attr">validation</span>: <span class="hljs-function">(<span class="hljs-params">rule</span>) =&gt;</span> rule.max(<span class="hljs-number">60</span>).required(),
    }),
    defineField({
      <span class="hljs-attr">name</span>: <span class="hljs-string">"slug"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Slug"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"slug"</span>,
      <span class="hljs-attr">description</span>:
        <span class="hljs-string">"Add a custom slug for the URL or generate one from the name"</span>,
      <span class="hljs-attr">options</span>: { <span class="hljs-attr">source</span>: <span class="hljs-string">"name"</span> },
      <span class="hljs-attr">validation</span>: <span class="hljs-function">(<span class="hljs-params">rule</span>) =&gt;</span> rule.required(),
    }),
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"logo"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Project Logo"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"image"</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"projectUrl"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Project URL"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"url"</span>,
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"coverImage"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Cover Image"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"image"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Upload a cover image for this project"</span>,
      <span class="hljs-attr">options</span>: { <span class="hljs-attr">hotspot</span>: <span class="hljs-literal">true</span> },
      <span class="hljs-attr">fields</span>: [
        {
          <span class="hljs-attr">name</span>: <span class="hljs-string">"alt"</span>,
          <span class="hljs-attr">title</span>: <span class="hljs-string">"Alt"</span>,
          <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
        },
      ],
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"description"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Description"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"array"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Write a full description about this project"</span>,
      <span class="hljs-attr">of</span>: [{ <span class="hljs-attr">type</span>: <span class="hljs-string">"block"</span> }],
    },
  ],
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> project;
</code></pre>
<p>Next, expose the schema to the <code>schemaTypes</code> array:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> job <span class="hljs-keyword">from</span> <span class="hljs-string">"./job"</span>;
<span class="hljs-keyword">import</span> profile <span class="hljs-keyword">from</span> <span class="hljs-string">"./profile"</span>;
<span class="hljs-keyword">import</span> project <span class="hljs-keyword">from</span> <span class="hljs-string">"./project"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> schemaTypes = [profile, job, project];
</code></pre>
<p>Visit your studio, click the project schema, and add as many projects as you want. You can download the <a target="_blank" href="https://github.com/Evavic44/sanity-nextjs-site/tree/main/public">asset files</a> used for each project from the repository.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/project-schema-3.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sanity studio showing project schema fields</em></p>
<p>Here's the query to get all the projects:</p>
<pre><code class="lang-js"><span class="hljs-comment">// sanity/sanity.query.ts</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getProjects</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> client.fetch(
    groq<span class="hljs-string">`*[_type == "project"]{
      _id, 
      name,
      "slug": slug.current,
      tagline,
      "logo": logo.asset-&gt;url,
    }`</span>
  );
}
</code></pre>
<p>Next, add the types.</p>
<pre><code class="lang-js"><span class="hljs-comment">// types/index.ts</span>

<span class="hljs-keyword">export</span> type ProjectType = {
  <span class="hljs-attr">_id</span>: string;
  name: string;
  slug: string;
  tagline: string;
  projectUrl: string;
  logo: string;
  coverImage: {
    <span class="hljs-attr">alt</span>: string | <span class="hljs-literal">null</span>;
    image: string;
  };
  description: PortableTextBlock[];
};
</code></pre>
<p>And then display the content in your front-end.</p>
<pre><code class="lang-bash">mkdir app/projects &amp;&amp; touch app/projects/page.tsx
</code></pre>
<p>This will create a <code>page.tsx</code> file inside a directory called project. Here's the code for the projects:</p>
<pre><code class="lang-js"><span class="hljs-comment">// app/projects/page.tsx</span>

<span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">"next/image"</span>;
<span class="hljs-keyword">import</span> Link <span class="hljs-keyword">from</span> <span class="hljs-string">"next/link"</span>;
<span class="hljs-keyword">import</span> { getProjects } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/sanity/sanity.query"</span>;
<span class="hljs-keyword">import</span> type { ProjectType } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/types"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Project</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> projects: ProjectType[] = <span class="hljs-keyword">await</span> getProjects();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"max-w-7xl mx-auto md:px-16 px-6"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"max-w-2xl mb-16"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-3xl font-bold tracking-tight sm:text-5xl mb-6 lg:leading-[3.7rem] leading-tight"</span>&gt;</span>
          Featured projects I<span class="hljs-symbol">&amp;apos;</span>ve built over the years
        <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-base text-zinc-400 leading-relaxed"</span>&gt;</span>
          I<span class="hljs-symbol">&amp;apos;</span>ve worked on tons of little projects over the years but these
          are the ones that I<span class="hljs-symbol">&amp;apos;</span>m most proud of. Many of them are
          open-source, so if you see something that piques your interest, check
          out the code and contribute if you have ideas for how it can be
          improved.
        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"grid xl:grid-cols-3 md:grid-cols-2 grid-cols-1 gap-5 mb-12"</span>&gt;</span>
        {projects.map((project) =&gt; (
          <span class="hljs-tag">&lt;<span class="hljs-name">Link</span>
            <span class="hljs-attr">href</span>=<span class="hljs-string">{</span>`/<span class="hljs-attr">projects</span>/${<span class="hljs-attr">project.slug</span>}`}
            <span class="hljs-attr">key</span>=<span class="hljs-string">{project._id}</span>
            <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center gap-x-4 bg-[#1d1d20] border border-transparent hover:border-zinc-700 p-4 rounded-lg ease-in-out"</span>
          &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Image</span>
              <span class="hljs-attr">src</span>=<span class="hljs-string">{project.logo}</span>
              <span class="hljs-attr">width</span>=<span class="hljs-string">{60}</span>
              <span class="hljs-attr">height</span>=<span class="hljs-string">{60}</span>
              <span class="hljs-attr">alt</span>=<span class="hljs-string">{project.name}</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-zinc-800 rounded-md p-2"</span>
            /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"font-semibold mb-1"</span>&gt;</span>{project.name}<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-sm text-zinc-400"</span>&gt;</span>{project.tagline}<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">Link</span>&gt;</span>
        ))}
      <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></span>
  );
}
</code></pre>
<p>Here's the resulting output:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/project-page-4.png" alt="Image" width="600" height="400" loading="lazy">
<em>project page</em></p>
<h3 id="heading-display-dynamic-routes">Display Dynamic Routes</h3>
<p>Each project card is wrapped in a link that points to their respective page based on the slug: <code>/projects/${project.slug}</code>. With this, the dynamic component can be easily created in next.js</p>
<p>Create a folder called <code>[project]</code> (wrapped in square brackets) inside the projects directory, and add a <code>page.tsx</code> file.</p>
<p>You can also do this via the terminal:</p>
<pre><code class="lang-bash">mkdir app/projects/[project] &amp;&amp; touch app/projects/[project]/page.tsx
</code></pre>
<p>This folder enclosed in square brackets is known as a <a target="_blank" href="https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes#convention">dynamic segment</a> in Next.js, and it allows the component to be mounted based on the params property. </p>
<p>Since you've already created the project schema type, all that's left is to query the dataset to fetch single projects.</p>
<p>Here's the query to get single projects:</p>
<pre><code class="lang-js"><span class="hljs-comment">// sanity/sanity.query.ts</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSingleProject</span>(<span class="hljs-params">slug: string</span>) </span>{
  <span class="hljs-keyword">return</span> client.fetch(
    groq<span class="hljs-string">`*[_type == "project" &amp;&amp; slug.current == $slug][0]{
      _id,
      name,
      projectUrl,
      coverImage { alt, "image": asset-&gt;url },
      tagline,
      description
    }`</span>,
    { slug }
  );
}
</code></pre>
<p>To fetch the slug from the route, we've added a parameter called <code>slug</code> into the function, which will allow the <code>getSingleProject</code> function to be called with the respective slug using the Next.js <a target="_blank" href="https://nextjs.org/docs/pages/api-reference/functions/get-static-props#context-parameter">params property</a>.</p>
<pre><code class="lang-js"><span class="hljs-comment">// app/projects/[project]/page.tsx</span>

<span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">"next/image"</span>;
<span class="hljs-keyword">import</span> { Metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">"next"</span>;
<span class="hljs-keyword">import</span> { getSingleProject } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/sanity/sanity.query"</span>;
<span class="hljs-keyword">import</span> type { ProjectType } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/types"</span>;
<span class="hljs-keyword">import</span> { PortableText } <span class="hljs-keyword">from</span> <span class="hljs-string">"@portabletext/react"</span>;
<span class="hljs-keyword">import</span> fallBackImage <span class="hljs-keyword">from</span> <span class="hljs-string">"@/public/project.png"</span>;

type Props = {
  <span class="hljs-attr">params</span>: {
    <span class="hljs-attr">project</span>: string;
  };
};

<span class="hljs-comment">// Dynamic metadata for SEO</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateMetadata</span>(<span class="hljs-params">{ params }: Props</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Metadata</span>&gt; </span>{
  <span class="hljs-keyword">const</span> slug = params.project;
  <span class="hljs-keyword">const</span> project: ProjectType = <span class="hljs-keyword">await</span> getSingleProject(slug);

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">title</span>: <span class="hljs-string">`<span class="hljs-subst">${project.name}</span> | Project`</span>,
    <span class="hljs-attr">description</span>: project.tagline,
    <span class="hljs-attr">openGraph</span>: {
      <span class="hljs-attr">images</span>: project.coverImage?.image || <span class="hljs-string">"add-a-fallback-project-image-here"</span>,
      <span class="hljs-attr">title</span>: project.name,
      <span class="hljs-attr">description</span>: project.tagline,
    },
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Project</span>(<span class="hljs-params">{ params }: Props</span>) </span>{
  <span class="hljs-keyword">const</span> slug = params.project;
  <span class="hljs-keyword">const</span> project: ProjectType = <span class="hljs-keyword">await</span> getSingleProject(slug);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"max-w-6xl mx-auto lg:px-16 px-8"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"max-w-3xl mx-auto"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-start justify-between mb-4"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"font-bold lg:text-5xl text-3xl lg:leading-tight mb-4"</span>&gt;</span>
            {project.name}
          <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">a</span>
            <span class="hljs-attr">href</span>=<span class="hljs-string">{project.projectUrl}</span>
            <span class="hljs-attr">rel</span>=<span class="hljs-string">"noreferrer noopener"</span>
            <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-[#1d1d20] text-white hover:border-zinc-700 border border-transparent rounded-md px-4 py-2"</span>
          &gt;</span>
            Explore
          <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">Image</span>
          <span class="hljs-attr">className</span>=<span class="hljs-string">"rounded-xl border border-zinc-800"</span>
          <span class="hljs-attr">width</span>=<span class="hljs-string">{900}</span>
          <span class="hljs-attr">height</span>=<span class="hljs-string">{460}</span>
          <span class="hljs-attr">src</span>=<span class="hljs-string">{project.coverImage?.image</span> || <span class="hljs-attr">fallBackImage</span>}
          <span class="hljs-attr">alt</span>=<span class="hljs-string">{project.coverImage?.alt</span> || <span class="hljs-attr">project.name</span>}
        /&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col gap-y-6 mt-8 leading-7 text-zinc-400"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">PortableText</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{project.description}</span> /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></span>
  );
}
</code></pre>
<p>Since the data coming from the dataset is a single project and not an array, no de-structuring is required.</p>
<p>Here's the resulting output:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/dynamic-project-page-2.gif" alt="Image" width="600" height="400" loading="lazy">
<em>Dynamic project page</em></p>
<h3 id="heading-add-loading-states">Add Loading States</h3>
<p>Next.js 13 introduced a special file <code>loading.js</code> that helps you create an instant loading state from the server while the content of a route segment loads. This helps users understand the app is responding and provides a better user experience.</p>
<p>With this special file, you can create a loading state that mimics the UI of the single project page easily.</p>
<p>Create a <code>loading.tsx</code> file inside the <code>[project]</code> directory and add the code snippet:</p>
<pre><code class="lang-js"><span class="hljs-comment">// projects/[project]/loading.tsx</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Loading</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"max-w-3xl mx-auto lg:px-0 px-8"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center justify-between mb-6"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"w-52 h-11 bg-[#1d1d20] rounded-sm animate-pulse"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"w-20 h-11 bg-[#1d1d20] rounded-sm animate-pulse"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"w-full h-96 mb-8 bg-[#1d1d20] rounded-sm animate-pulse"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col gap-y-2"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"w-full h-5 bg-[#1d1d20] rounded-sm animate-pulse"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"w-full h-5 bg-[#1d1d20] rounded-sm animate-pulse"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>Here's the resulting output:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/loading-state-2.gif" alt="Image" width="600" height="400" loading="lazy">
<em>dynamic project page showing next.js instant loading state</em></p>
<h2 id="heading-fix-studio-layout">Fix Studio Layout</h2>
<p>You may have noticed the <code>navbar</code> and <code>footer</code> components are showing up in the studio route. This is because these components we're defined in the root layout —which applies to all routes in the application.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/studio-component-ui-error-2.png" alt="Image" width="600" height="400" loading="lazy">
<em>navbar and footer components in studio page</em></p>
<p>To fix this, you'll have to create a separate <code>layout.tsx</code> file for the studio component:</p>
<p>Create two folders wrapped in parenthesis inside the <code>app</code> directory. Name one folder <code>(site)</code>, and the other <code>(studio)</code>. These folders are wrapped in parenthesis to prevent Next.js from mounting them as routes.</p>
<p>Move all the files in the app directory that relates to the next app except the <code>studio</code> folder, <code>global.css</code> and <code>favicon.ico</code> into the <code>(site)</code> directory, and then move the studio folder inside the <code>(studio)</code> directory.</p>
<p>The only files that will live in the app root is <code>global.css</code> and <code>favicon.ico</code>.</p>
<p>Here's what your new folder structure should look like:</p>
<pre><code class="lang-bash">app/
├── (site)/
│   ├── about/
│   ├── components/
│   ├── icons/
│   ├── projects/
│   ├── layout.tsx
│   └── page.tsx
├── (studio)/
│   └── studio/
├── favicon.ico
└── global.css
</code></pre>
<p>Once completed, create a <code>layout.tsx</code> file inside the <code>(studio)</code> directory and paste the following code snippet inside:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> <span class="hljs-string">"../globals.css"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">StudioLayout</span>(<span class="hljs-params">{children}: {children: React.ReactNode}</span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>{children}<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></span>
  );
}
</code></pre>
<p>Update all the imports that may have changed, run your server again and you should see your studio up and running, without the components.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/sanity-studio-without-navbar-components-1.png" alt="Image" width="600" height="400" loading="lazy">
<em>Studio page without navbar and footer components</em></p>
<h2 id="heading-step-7-deployment">Step 7: Deployment</h2>
<p>Deploying a Sanity powered Next.js application is a pretty straightforward process. Follow this guide to <a target="_blank" href="https://vercel.com/docs/getting-started-with-vercel/import">set-up your account and deploy with Vercel</a>.</p>
<p>After successfully deploying your site, visit the studio route; <code>your-site-name/studio</code>, and you should get a prompt to add the URL to the CORS setting in Sanity:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/add-hosted-site-URL-to-sanity-cors-settings.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sanity CORS settings prompt</em></p>
<p>Simply click "continue" and follow the on-screen instructions to do so. If successful, you should be able to see your studio.</p>
<h2 id="heading-setup-sanity-webhooks-for-studio-update">Setup Sanity Webhooks for Studio Update</h2>
<p>Updates made to your site would be triggered only on build time. What this means is that if you update a field in your studio using the hosted link, you would have to manually trigger a deployment on Vercel to see the changes.</p>
<p>Having to trigger the deployment server each time can be a cumbersome task, especially when building for a client. </p>
<p>In this section, I'll guide you through the steps to manually deploy your site whenever a change is made to your studio using Sanity <a target="_blank" href="https://www.sanity.io/docs/webhooks">GROQ-powered Web Hooks</a>.</p>
<h3 id="heading-create-a-deploy-hook-on-vercel">Create a Deploy Hook on Vercel</h3>
<p>First, you will need the URL endpoint from your hosting service to trigger the deployment.</p>
<p>Navigate to your project settings on Vercel and click the <strong>Git</strong> tab. Under the <strong>Deploy Hooks</strong> section, choose a name for your hook and the select the branch that will be deployed when the generated URL is requested.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/create-hook-endpoint-on-vercel-1.png" alt="Image" width="600" height="400" loading="lazy">
<em>creating the hook</em></p>
<p>Submit the form and copy the URL endpoint generated by Vercel.</p>
<h3 id="heading-trigger-hook-using-sanity-groq-powered-webhooks">Trigger Hook Using Sanity GROQ-powered Webhooks</h3>
<p>Visit <a target="_blank" href="https://www.sanity.io/manage">sanity.io/manage</a>, pick your project, navigate to the <strong>API</strong> section and click on the "Create webhook" button.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/sanity-api-tab-1.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sanity GROQ-powered Webhooks</em></p>
<p>Fill in the form with information about the hook you want to create. </p>
<ul>
<li><code>Name</code>:  Portfolio Deployment.</li>
<li><code>Description</code>: Trigger rebuild when portfolio content is created, updated, and deleted.</li>
<li><code>URL</code>: [Paste the URL endpoint generated by Vercel here].</li>
<li><code>Dataset</code>: The dataset to apply the hook to.</li>
<li><code>Trigger on</code>: Check the <strong>"create"</strong>, <strong>"update"</strong>, and <strong>"delete"</strong> boxes.</li>
</ul>
<p>Leave <code>filter</code> and <code>projection</code> inputs blank so the hook will be applied to all documents, and for the rest of the fields, leave it as is and hit save.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/sanity-groq-powered-hook-created-2.png" alt="Image" width="600" height="400" loading="lazy">
<em>sanity groq powered hook created</em></p>
<p>Now visit your hosted studio and update any document. Once you click publish, this should trigger the deploy hook and update your site when completed.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/sanity-hook-trigger-build-on-vercel-2.png" alt="Image" width="600" height="400" loading="lazy">
<em>deploy hook triggering deployment on Vercel</em></p>
<p>Another good alternative to setting up live updates in your Sanity/Next.js app is using <a target="_blank" href="https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration">Incremental Static Regeneration (ISR)</a>, which is a better option if you're building a large scale application.</p>
<p>And that's it! You can see the <a target="_blank" href="https://sanity-nextjs-site.vercel.app">Live Preview here</a> and find the <a target="_blank" href="https://github.com/Evavic44/sanity-nextjs-site">GitHub URL here</a>.</p>
<h2 id="heading-what-next">What Next?</h2>
<p>Although this tutorial covered a lot of useful information, there are still many more possibilities with Sanity that you can explore. </p>
<p>You can customize your studio, integrate third-party APIs, build a storefront with Shopify, and much more.</p>
<p>If you found this article enjoyable and want to dive deeper into the world of Sanity, I recommend checking out the following resources:</p>
<ul>
<li><a target="_blank" href="https://www.sanity.io/docs/customizing-the-portable-text-editor">Customizing the Portable Text Editor</a></li>
<li><a target="_blank" href="https://www.sanity.io/guides/singleton-document">How to Create a Singleton Document</a></li>
<li><a target="_blank" href="https://www.sanity.io/plugins/code-input">Syntax Highlight Code Block</a></li>
</ul>
<p>Thanks for reading. Share, and subscribe to my blog for future updates.</p>
<p><a target="_blank" href="https://github.com/Evavic44">GitHub</a> | <a target="_blank" href="https://twitter.com/victorekea">Twitter</a> | <a target="_blank" href="https://eke.hashnode.dev">Blog</a> | <a target="_blank" href="https://www.linkedin.com/in/victorekeawa/">LinkedIn</a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Headless CMS Explained – What it is and When You Should Use it ]]>
                </title>
                <description>
                    <![CDATA[ By Daniel Madalitso Phiri CMSs are pretty hard to ignore because they're everywhere on the internet. WordPress, for example, powers nearly 40% of the internet today. In this article, we'll cover what CMSs are and why you should care about them. I'll ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/what-is-headless-cms-explained/</link>
                <guid isPermaLink="false">66d45e0073634435aafcef6a</guid>
                
                    <category>
                        <![CDATA[ cms ]]>
                    </category>
                
                    <category>
                        <![CDATA[ headless cms ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Daniel Madalitso Phiri ]]>
                </dc:creator>
                <pubDate>Tue, 27 Jul 2021 17:53:47 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2021/06/siora-photography-hgFY1mZY-Y0-unsplash-1.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Daniel Madalitso Phiri</p>
<p>CMSs are pretty hard to ignore because they're everywhere on the internet. WordPress, for example, powers nearly <a target="_blank" href="https://kinsta.com/wordpress-market-share/">40% of the internet</a> today.</p>
<p>In this article, we'll cover what CMSs are and why you should care about them. I'll also introduce you to a new type of CMS that seems to be everywhere at the moment – the Headless CMS. And we'll do all this with a story!</p>
<p>Life has a funny way of making you try things. And after years of ignoring CMSs as a technology, in mid 2020 I got a job at <a target="_blank" href="https://strapi.io">Strapi</a>, a headless CMS tool. Since then, I have developed a pretty good understanding of what these things do – so let's get into it.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/ezgif.com-gif-maker-2-.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h2 id="heading-what-is-a-cms">What is a CMS?</h2>
<p>A Content Management System (CMS) is a tool that helps users create, manage, and modify digital content.</p>
<p>In this article, however, I won’t get into the nitty-gritty of it. Instead, if you want to learn more you can have a look at <a target="_blank" href="https://strapi.io/blog/frontend-developers-headless-cms">an article I wrote</a> that goes deeper into various types of CMSs.</p>
<h2 id="heading-what-is-a-headless-cms">What is a Headless CMS?</h2>
<p>A headless CMS has a back end where content is prepared – and that's it. The content and its data are only accessible via calls made to the API, whether it's REST or GraphQL.</p>
<p>I like to use this diagram to illustrate how Headless works, so hopefully it paints a clearer picture.</p>
<p><img src="https://lh3.googleusercontent.com/zPdxhP33Y_kqX1SY-KXDq0Ma1IiJv15urJ_PVuiogvtI86tCa_A2qMJ0L2UqFD_U_MmTy0VHED1Oz9w5uumNipKSBzwmHYkHRrgXrdrLU0PNg9nvhUQC28Hy09H9Wrn5iiBep95U" alt="Image" width="1600" height="802" loading="lazy"></p>
<p><em>Monolithic vs decoupled vs headless architectures</em></p>
<p>Besides serving content to multiple platforms, there are a couple other reasons why you would want to use a Headless CMS.</p>
<h3 id="heading-you-dont-want-to-give-up-developer-flexibility">You don’t want to give up developer flexibility</h3>
<p>Adopting a Headless architecture by default means that you have the flexibility of selecting a front end tool of your choosing. And for many developers, this is a critical advantage.</p>
<h3 id="heading-you-need-a-secure-content-solution">You need a secure content solution</h3>
<p>Decoupling the front end from the back end makes targeted attacks much harder. This is something some traditional CMSs still struggle with today.</p>
<h3 id="heading-you-want-to-future-proof-your-tech-stack">You want to future proof your tech stack</h3>
<p>Going headless also means that you’re less dependent on a single solution for a front end. Should you need to upgrade to a more modern front end or add a new front end altogether, headless makes this much easier.</p>
<h3 id="heading-you-need-to-create-custom-and-personalized-experiences">You need to create custom and personalized experiences</h3>
<p>This is becoming a really important benefit for headless CMSs for many organizations.</p>
<p>With headless you have the opportunity to tailor different experiences for different platforms all from a single content source.</p>
<h2 id="heading-how-i-got-into-headless-cmss">How I got into Headless CMSs</h2>
<p>So I really like GraphQL, and that’s how I got started with Strapi. Working for a CMS was like diving head first into this ecosystem. I thought I understood Headless CMSs because to me they were “data, API, frontend” and that’s how I thought about it.</p>
<p>Well, we use these things to build our front ends, but we often overlook the content management side of things when we think about building a front end like that. And it wasn't until I started working with Strapi that I recognized my assumption.</p>
<p>“Content Management” sounds kinda boring, right? And CMS? “Ewww why would I want to use such a tool?” I know! Me too, but hear me out. CMSs actually pretty useful. So let’s talk about how and why a CMS might help you out.</p>
<h2 id="heading-why-do-you-need-a-headless-cms">Why Do You Need a Headless CMS?</h2>
<p>For starters, there’s no downplaying the role of content in today's world. Content is everywhere and manifests itself in so many forms through text, audio, video, and more.</p>
<p>For a long time, computers and browsers were the main tool for content consumption. We read blogs, watched YouTube videos, and listened to podcasts on our personal computers.</p>
<p>Gradually our computers got smaller and less obvious. Content in its many shapes and forms started to appear all around us. It showed itself in mobile phones, on our smart televisions, in our cars, in our virtual assistants and wearable devices.</p>
<p>The way people consumed content changed, and so did the way we had to build content-consuming experiences.</p>
<h3 id="heading-so-how-does-headless-cms-help">So How Does Headless CMS Help?</h3>
<p>Traditionally, CMSs were monoliths with the front end and back end tightly coupled. The content you added in the CMS back end only showed up on the front end it was coupled to - think WordPress and Drupal.</p>
<p>This proved inefficient as developers needed a better way to build and adapt to this new consumer behavior.</p>
<p>The solution? Rip the head off a traditional CMS and make it possible for your back end to deliver content to multiple platforms. This was how Headless was born.</p>
<h2 id="heading-why-you-might-not-need-a-headless-cms">Why You Might Not Need a Headless CMS</h2>
<p>Headless isn't necessarily the right solution for all use cases, though. It might not be for you if...</p>
<h3 id="heading-you-have-a-small-team">You have a small team</h3>
<p>Adopting and building a headless architecture takes quite a bit of effort. To reap all its benefits you would have to have a dedicated developer team to build your front end as well as people on your team to work on adding content to your CMS.</p>
<h3 id="heading-you-rely-heavily-on-a-simple-live-preview-implementation">You rely heavily on a simple live preview implementation</h3>
<p>Live previews on Headless CMSs aren’t the most intuitive to set up (as of writing this) and take some effort from developers to implement.</p>
<h3 id="heading-you-only-require-simple-publishing-capabilities">You only require simple publishing capabilities</h3>
<p>As we just learned, headless takes a reasonable amount of effort to get it working efficiently and effectively.</p>
<p>If you need only simple publishing capabilities without features like internationalization or role-based access control, then it’s best to wait till you need those additional features to use a Headless CMS.</p>
<h2 id="heading-use-cases-for-a-headless-cms">Use Cases for a Headless CMS</h2>
<p>A lot of my early CMS projects centered around corporate sites and personal blogs, which are both solid use cases for headless. But I don’t build sites full time so I don’t ship code often.</p>
<p>Personally I’ve used a CMS to help build a <a target="_blank" href="https://foodadvisor.strapi.io/">Restaurant Catalog</a>, an <a target="_blank" href="https://conf.strapi.io/speakers">Event Website</a>, and an <a target="_blank" href="https://conf.strapi.io/quizz">Online Quiz</a>.</p>
<p>There’s folks using headless CMSs to build eCommerce sites, Covid tracking projects, hospital management systems, inventory management applications, mobile catalogs, VR games, and some people even run email campaigns with them. So many possibilities.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Seeing what people are building with CMSs is very inspiring. And I’ve gained a huge appreciation for CMSs as a technology. What I once thought was a boring tool actually powers so much of the world around me.</p>
<p>There are lots of use cases for Headless CMS these days. And while at the moment there is a huge focus on serving developers (which many CMSs are doing well), we still have a ways to go to make the content editor's experience better.</p>
<p>It’s exciting having a foot in the race, and all I can tell you is that it's going to be an amazing next few years for Headless technology in general.</p>
<p>So hopefully this article helps you jump on the bandwagon and get a better understanding of what the technology can and cannot do.</p>
<p>After all, at the end of the day, the choice is yours!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/javier-allegue-barros-C7B-ExXpOIE-unsplash.jpg" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-resources">Resources</h3>
<ul>
<li><p><a target="_blank" href="https://jamstack.org/headless-cms/">Headless CMS</a> | Jamstack.org</p>
</li>
<li><p><a target="_blank" href="https://strapi.io/what-is-headless-cms">What is a Headless CMS</a></p>
</li>
<li><p><a target="_blank" href="https://www.stackbit.com/blog/what-is-a-headless-cms/">What’s a Headless CMS and Why Should You Care</a></p>
</li>
<li><p><a target="_blank" href="https://cms-comparison.io/#/card">CMS Comparison</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create a Travel Bucket List Map with Gatsby, React Leaflet, & Hygraph ]]>
                </title>
                <description>
                    <![CDATA[ Traveling is fun and we all have a lot of places we want to visit, but rarely do we have time to do it all at once. That’s what bucket lists are for! How can we create a custom mapping app that we can show all of our the destinations ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-create-a-travel-bucket-list-map-with-gatsby-react-leaflet-graphcms/</link>
                <guid isPermaLink="false">66b8e3580cedc1f2a4f7069b</guid>
                
                    <category>
                        <![CDATA[ beginners guide ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cms ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Gatsby ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GatsbyJS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ graphcms ]]>
                    </category>
                
                    <category>
                        <![CDATA[ headless cms ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ leaflet ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mapping ]]>
                    </category>
                
                    <category>
                        <![CDATA[ maps ]]>
                    </category>
                
                    <category>
                        <![CDATA[ react-leaflet ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tech  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ technology ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Travel ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Colby Fayock ]]>
                </dc:creator>
                <pubDate>Tue, 23 Jun 2020 14:45:00 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2020/06/travel-bucket-list.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Traveling is fun and we all have a lot of places we want to visit, but rarely do we have time to do it all at once. That’s what bucket lists are for! How can we create a custom mapping app that we can show all of our the destinations on our bucket list?</p>
<p>Note: As of July 2022, GraphCMS is now <a target="_blank" href="https://hygraph.com/">Hygraph</a>.</p>
<ul>
<li><a class="post-section-overview" href="#heading-what-are-we-going-to-build">What are we going to build?</a></li>
<li><a class="post-section-overview" href="#heading-step-1-creating-a-new-app-with-gatsby-starter-leaflet">Step 1: Creating a new app with Gatsby Starter Leaflet</a></li>
<li><a class="post-section-overview" href="#heading-step-2-creating-and-managing-a-list-of-travel-locations-with-graphcms">Step 2: Creating and managing a list of travel locations with GraphCMS</a></li>
<li><a class="post-section-overview" href="#heading-step-3-querying-our-graphcms-location-data-with-gatsby-and-graphql">Step 3: Querying our GraphCMS location data with Gatsby and GraphQL</a></li>
<li><a class="post-section-overview" href="#heading-step-4-creating-a-bucket-list-of-destinations-and-adding-them-to-the-map">Step 4: Creating a bucket list of destinations and adding them to the map</a></li>
<li><a class="post-section-overview" href="#heading-what-else-other-features-can-we-add-to-our-app">What else other features can we add to our app?</a></li>
<li><a class="post-section-overview" href="#heading-want-to-learn-more-about-maps">Want to learn more about maps?</a></li>
</ul>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/isbr52VKjb0" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<h2 id="heading-what-are-we-going-to-build">What are we going to build?</h2>
<p>We’re going to build a mapping app with <a target="_blank" href="https://www.gatsbyjs.org/">Gatsby</a> managed by a CMS that will both display markers on a map and show our locations in a simple text-based list for our bucket list locations.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/travel-bucket-list-demo.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Demo of a Travel Bucket List mapping app</em></p>
<p>We’ll spin up the app with a <a target="_blank" href="https://github.com/colbyfayock/gatsby-starter-leaflet">Gatsby Starter for Leaflet</a> and then we’ll use <a target="_blank" href="https://graphcms.com/">GraphCMS</a> to create and manage the list of locations for our map!</p>
<h2 id="heading-woah-a-mapping-app">Woah, a mapping app?</h2>
<p>Yup. If you haven't played with maps before, don't be discouraged! It's not as bad as you probably think. If you'd rather start with mapping basics, you can  <a target="_blank" href="https://www.freecodecamp.org/news/easily-spin-up-a-mapping-app-in-react-with-leaflet/">read more about how mapping works</a>  first.</p>
<h2 id="heading-step-1-creating-a-new-app-with-gatsby-starter-leaflet">Step 1: Creating a new app with Gatsby Starter Leaflet</h2>
<p>We’ll start off with Gatsby Starter Leaflet. This is going to give us a basic React application with our mapping tools already built in.</p>
<h3 id="heading-creating-a-new-gatsby-app-with-gatsby-starter-leaflet">Creating a new Gatsby app with Gatsby Starter Leaflet</h3>
<p>To get started, navigate to where you want to create your new app and run:</p>
<pre><code class="lang-shell">gatsby new my-travel-bucket-list https://github.com/colbyfayock/gatsby-starter-leaflet
</code></pre>
<p><em>Note: you can replace <code>my-travel-bucket-list</code> with whatever you want. This will be used to create the new folder for the app.</em></p>
<p>Once you run that, Gatsby will pull down the Starter and install the dependencies. After it’s complete, navigate to that directory and run the development command:</p>
<pre><code class="lang-shell">cd my-travel-bucket-list
yarn develop
# or
npm run develop
</code></pre>
<p>Once it’s finished location, your app should be ready to go!</p>
<h3 id="heading-cleaning-our-some-demo-code">Cleaning our some demo code</h3>
<p>Because we’re using a Starter, it has a little bit of demo code. Let’s clean that out to avoid any confusion.</p>
<p>Open up the <code>src/pages/index.js</code> file.</p>
<p>First, remove everything inside of <code>mapEffect</code> except the first line and set up an alias for <code>leafletElement</code> to <code>map</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mapEffect</span>(<span class="hljs-params">{ leafletElement: map } = {}</span>) </span>{
  <span class="hljs-keyword">if</span> ( !map ) <span class="hljs-keyword">return</span>;
}
</code></pre>
<p>With that gone, we can remove the <code>markerRef</code> definition at the top of the <code>IndexPage</code> component, remove the <code>ref={markerRef}</code> prop from our <code>&lt;Marker&gt;</code> component, and the <code>useRef</code> import next to React.</p>
<p>Now, we can remove all of the variables that start with <code>popup</code> and <code>time</code>, including:</p>
<ul>
<li>timeToZoom</li>
<li>timeToOpenPopupAfterZoom</li>
<li>timeToUpdatePopupAfterZoom</li>
<li>popupContentHello</li>
<li>popupContentGatsby</li>
</ul>
<p>Lastly, you can remove all of the following lines:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> L <span class="hljs-keyword">from</span> <span class="hljs-string">'leaflet'</span>;
...
import { promiseToFlyTo, getCurrentLocation } <span class="hljs-keyword">from</span> <span class="hljs-string">'lib/map'</span>;
...
import gatsby_astronaut <span class="hljs-keyword">from</span> <span class="hljs-string">'assets/images/gatsby-astronaut.jpg'</span>;
...
const ZOOM = <span class="hljs-number">10</span>;
</code></pre>
<p>Once done, we should be ready to go with a basic app with a map!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/new-app-gatsby-starter-leaflet.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>New app with Gatsby Starter Leaflet</em></p>
<p><a target="_blank" href="https://github.com/colbyfayock/my-travel-bucket-list/commit/63eed5a7a208ede6f8eeec44e0c08b594b407360">Follow along with the commit!</a></p>
<h2 id="heading-step-2-creating-and-managing-a-list-of-travel-locations-with-graphcms">Step 2: Creating and managing a list of travel locations with GraphCMS</h2>
<h3 id="heading-creating-a-graphcms-account">Creating a GraphCMS account</h3>
<p>To get started with GraphCMS, you’ll need an account. I’m not going to walk you through this part, but the good news is they have a generous free tier that makes it easy to sign up for us to use for our demo!</p>
<p><a target="_blank" href="https://app.graphcms.com/signup">Sign up for GraphCMS</a></p>
<p>Alternatively, if you already have an account, you can make sure you’re logged in.</p>
<h3 id="heading-creating-a-new-graphcms-project">Creating a new GraphCMS project</h3>
<p>Once logged in, we’ll want to create a new project. We’re going to create one manually, so once at the <a target="_blank" href="https://app.graphcms.com/">GraphCMS Dashboard</a>, select <strong>Create new project</strong>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-create-new-project.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Creating a new project in GraphCMS</em></p>
<p>Here, you can enter whatever you’d like for the <strong>Name</strong> and <strong>Description</strong> such as:</p>
<ul>
<li>Name: My Travel Bucket List</li>
<li>Description: The locations that I want to travel to some day!</li>
</ul>
<p>Below that you’ll see a map where you’ll select a <strong>Region</strong>. This is where your database data will live, so while it probably doesn’t matter too much for our purposes, you can choose the one that’s closest to you.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-configure-new-project.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Configuring a new project in GraphCMS</em></p>
<p>After you select your options, go ahead and click <strong>Create Project</strong>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-select-plan.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Selecting the Personal plan in GraphCMS</em></p>
<p>Next, you’ll be presented with billing options. Since we’re just creating a demo, under <strong>Personal</strong> select <strong>Continue</strong> at which point we’ll be dropped into our new GraphCMS project dashboard.</p>
<h3 id="heading-creating-a-new-content-model-schema-with-graphcms">Creating a new Content Model Schema with GraphCMS</h3>
<p>In GraphCMS, a Content Model refers to a specific type of data that has specific properties associated with it. In our case, our Model will be a Destination, which will be defined by a Name and a Location.</p>
<p>First, navigate to the <strong>Schema</strong> section of GraphCMS in the left sidebar and select <strong>Create Model</strong>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-create-new-schema-model.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Creating a new Schema Model in GraphCMS</em></p>
<p>Once selected, you’ll see a popup that asks for a bit more information. Here, you can type in “Destination” as the <strong>Display Name</strong>, which will also fill in most of the other fields. We’ll leave those as is.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-configure-new-content-model.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Configuring a new Model in GraphCMS</em></p>
<p>Feel free to add a description if you’d like, but it’s not required. Then select <strong>Create model</strong>.</p>
<p>Now that we have our Model, we need our properties.</p>
<p>First, select <strong>Single line text</strong> in the right list of fields and add a <strong>Display Name</strong> of “Name”. This will also fill out <strong>App Id</strong> which you can leave as is. Then click <strong>Create</strong>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-configure-text-field.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Adding and configuring a new text field in GraphCMS</em></p>
<p>Next, scroll down in the field options on the right and under <strong>Location</strong> select <strong>Map</strong>. Add “Location” as the <strong>Display Name</strong>, which will set the <strong>App Id</strong> as “location” which you can leave as is. Then same as before, click <strong>Create</strong>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-configure-new-map-field.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Adding and configuring a new map field in GraphCMS</em></p>
<p>Now we have a Content Model which we’ll use to create our locations!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-destination-content-model.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Destination content Model in GraphCMS</em></p>
<h3 id="heading-creating-our-locations">Creating our locations</h3>
<p>Finally, let’s create our locations. Navigate over to <strong>Content</strong> in the GraphCMS dashboard, make sure you’ve selected <strong>Destination</strong> under <strong>System</strong> (should be the only one), and select <strong>Create New</strong>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-add-new-content.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Create new Destination Content in GraphCMS</em></p>
<p>Now we can start adding all of our locations! First, add the name of your location in the <strong>Name</strong> field, then you can use the <strong>Search</strong> box under <strong>Location</strong> to find that location on the map.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-create-new-destination-content-item.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Adding a new Destination Content item in GraphCMS</em></p>
<p>Once you’re good, hit <strong>Save and publish</strong>. This will create your first location!</p>
<p>Follow those same steps and create as many locations as you want.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-destination-content-items.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>List of Destination Content items in GraphCMS</em></p>
<p>We’ll use these for our map and bucket list.</p>
<h2 id="heading-step-3-querying-our-graphcms-location-data-with-gatsby-and-graphql">Step 3: Querying our GraphCMS location data with Gatsby and GraphQL</h2>
<p>Now that we have our locations, let’s use them!</p>
<h3 id="heading-adding-a-plugin-to-gatsby-to-query-our-graphql-data">Adding a plugin to Gatsby to query our GraphQL data</h3>
<p>First, we need to <a target="_blank" href="https://www.gatsbyjs.org/packages/gatsby-source-graphql/">add a new plugin</a> to our Gatsby project to query our GraphQL data. In your terminal make sure your development server isn’t running and run:</p>
<pre><code class="lang-shell">yarn add gatsby-source-graphql
# or
npm install gatsby-source-graphql
</code></pre>
<p>Next, open up your <code>gatsby-config.js</code> file in the root of your project and add the following to your plugins:</p>
<pre><code class="lang-json">{
  resolve: 'gatsby-source-graphql',
  options: {
    typeName: 'GCMS',
    fieldName: 'gcms',
    url: '[API ENDPOINT]',
  }
}
</code></pre>
<p>This will be what sources our data from GraphCMS, but we need an endpoint.</p>
<h3 id="heading-finding-our-api-endpoint-for-graphcms">Finding our API endpoint for GraphCMS</h3>
<p>Open back up your browser and head over to your GraphCMS project. After selecting <strong>Settings</strong> in the left navigation, select <strong>API Access</strong>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-api-access.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>API Access in GraphCMS</em></p>
<p>Before we copy our API Endpoint, first we need to update our permissions so we can query our API. Under <strong>Public API Permissions</strong>, check the box next to <strong>Content from stage Published</strong> and click <strong>Save</strong>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-configure-api-access.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Configuring API permissions in GraphCMS</em></p>
<p>Next, copy the URL under <strong>Endpoints</strong>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/graphcms-copy-api-access-endpoint.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Copying API Endpoint in GraphCMS</em></p>
<p>And paste that in to your <code>gatsby-config.js</code> file that we modified above:</p>
<pre><code class="lang-json">{
  resolve: 'gatsby-source-graphql',
  options: {
    typeName: 'GCMS',
    fieldName: 'gcms',
    url: 'https:<span class="hljs-comment">//[region-id].graphcms.com/v2/[project-id]/master',</span>
  },
},
</code></pre>
<p><em>Note: your URL will have actual values inside of <code>[region-id]</code> and <code>[project-id]</code>.</em></p>
<p>Save your <code>gatsby-config.js</code> file and start your development server backup (<code>yarn develop</code>) and we’re ready to go!</p>
<h3 id="heading-querying-our-locations-via-graphql">Querying our locations via GraphQL</h3>
<p>Finally, let’s actually query our data so that we’ll be able to use it in our app.</p>
<p>We’re going to create a new <a target="_blank" href="https://reactjs.org/docs/hooks-reference.html">React Hook</a> that we’ll be able to use to grab our locations anywhere within our app.</p>
<p>Under <code>src/hooks/index.js</code>, add the following line to the existing list:</p>
<pre><code class="lang-js"><span class="hljs-keyword">export</span> { <span class="hljs-keyword">default</span> <span class="hljs-keyword">as</span> useDestinations } <span class="hljs-keyword">from</span> <span class="hljs-string">'./useDestinations'</span>;
</code></pre>
<p>This will allow us to more conveniently import our hook which we’ll create next.</p>
<p>Under <code>src/hooks</code>, create a new file <code>useDestinations.js</code> and paste in this code:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { graphql, useStaticQuery } <span class="hljs-keyword">from</span> <span class="hljs-string">'gatsby'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useDestinations</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> { gcms = {} } = useStaticQuery( graphql<span class="hljs-string">`
    query {
      gcms {
        destinations {
          id
          name
          location {
            latitude
            longitude
          }
        }
      }
    }
  `</span> );

  <span class="hljs-keyword">let</span> { destinations } = gcms;

  <span class="hljs-keyword">return</span> {
    destinations,
  };
}
</code></pre>
<p>Here, we’re:</p>
<ul>
<li>Importing the <code>graphql</code> and <code>useStaticQuery</code> utilities from Gatsby</li>
<li>We’re creating a new function (or hook) that is exported by default</li>
<li>In that function, we’re using <code>useStaticQuery</code> to create a new GraphQL query which asks GraphCMS to return the data structure we defined.</li>
<li>That query returns a value which we destructure immediately to grab the <code>gmcs</code> object</li>
<li>We destructure <code>destinations</code> from <code>gmcs</code> and return it as part of a new object from our hook</li>
</ul>
<p>With this, we can now use our hook anywhere in our app!</p>
<p>Head over to your <code>src/pages/index.js</code> file, first import our new hook:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { useDestinations } <span class="hljs-keyword">from</span> <span class="hljs-string">'hooks'</span>;
</code></pre>
<p>And at the top of the <code>IndexPage</code> component, query our data:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> { destinations } = useDestinations();
</code></pre>
<p>This puts all of our locations into the <code>destinations</code> variable. We can test that this works by console logging it out:</p>
<pre><code class="lang-js"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">'destinations'</span>, destinations);
</code></pre>
<p>And once we open up our browser and look in our web developer tools console, we can see our location data!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/gatsby-starter-leaflet-logging-graphcms-destinations.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Logging destinations data to the web console</em></p>
<h2 id="heading-step-4-creating-a-bucket-list-of-destinations-and-adding-them-to-the-map">Step 4: Creating a bucket list of destinations and adding them to the map</h2>
<p>We’re going to start with creating a simple text list of our destinations. This will let us see all of our destinations in an easy to read format.</p>
<h3 id="heading-creating-a-text-list-of-our-destinations">Creating a text list of our destinations</h3>
<p>Inside of our <code>IndexPage</code> and above “Still Getting Started?”, let’s add the following code:</p>
<pre><code class="lang-jsx">&lt;h2&gt;My Destinations&lt;/h2&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span>
  { destinations.map(destination =&gt; {
    const { id, name } = destination;
    return <span class="hljs-tag">&lt;<span class="hljs-name">li</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{id}</span>&gt;</span>{ name }<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
  })}
<span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span></span>
</code></pre>
<p>This code:</p>
<ul>
<li>Adds a new header for our list</li>
<li>Creates a new unordered list</li>
<li>Loops through our <code>destinations</code> and creates a new list item for each destination that include’s the location’s name</li>
</ul>
<p>Once we hit save and reload, we should see our list under our map!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/app-adding-list-of-destinations.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>New basic list of destinations in the app</em></p>
<p>The list looks a little odd though right? We probably want to format it a little better to fit into the page.</p>
<p>Open up <code>src/assets/stylesheets/pages/_home.scss</code> and inside of the <code>.home-start</code> class, add:</p>
<pre><code class="lang-scss"><span class="hljs-selector-class">.home-start</span> {

  ...

  <span class="hljs-selector-tag">ul</span> {
    <span class="hljs-attribute">list-style</span>: none;
    <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">margin</span>: <span class="hljs-number">1.2em</span> <span class="hljs-number">0</span>;
  }
</code></pre>
<p>Let’s also modify the <code>h2</code> to space things out a little better:</p>
<pre><code class="lang-scss"><span class="hljs-selector-class">.home-start</span> {

  ...

  <span class="hljs-selector-tag">h2</span> {

    <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">2em</span>;

    &amp;<span class="hljs-selector-pseudo">:first-child</span> {
      <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">0</span>;
    }

  }
</code></pre>
<p>Once you hit save and reload, it should look a little better.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/app-fixing-styles-list-of-destinations.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Destinations in the app with cleaned up styles</em></p>
<p>Feel free to make additional changes, but we’ll leave it there for now.</p>
<h3 id="heading-adding-our-destinations-to-the-map">Adding our destinations to the map</h3>
<p>Now we can finally add our destinations to the map!</p>
<p>Inside of our <code>&lt;Map&gt;</code> component, we already have a <code>&lt;Marker&gt;</code>. This allows us to easily add a marker to the map given a position. We’ll take this concept and combine it with our text list to add our locations to the map.</p>
<p>Let’s update our <code>&lt;Map&gt;</code> code to match the following:</p>
<pre><code class="lang-jsx">&lt;<span class="hljs-built_in">Map</span> {...mapSettings}&gt;
  { destinations.map(<span class="hljs-function"><span class="hljs-params">destination</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> { id, name, location } = destination;
    <span class="hljs-keyword">const</span> position = [location.latitude, location.longitude];
    <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Marker</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{id}</span> <span class="hljs-attr">position</span>=<span class="hljs-string">{position}</span> /&gt;</span></span>
  })}
&lt;/<span class="hljs-built_in">Map</span>&gt;
</code></pre>
<p>Here we:</p>
<ul>
<li>Loop through our <code>destinations</code> to dynamically create a new list of components inside our <code>&lt;Map&gt;</code></li>
<li>Inside each loop instance, we destructure our date from <code>destination</code></li>
<li>We create a new <code>position</code> array with the latitude and longitude</li>
<li>Create a new <code>Marker</code> where we use our position to add it to the map</li>
</ul>
<p>This gives us our markers on the map!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/mapping-app-with-destination-markers.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Markers for each destination in the mapping app</em></p>
<p>But we want to know what each of those locations are, so let’s also add a popup to each marker that will show the name.</p>
<p>First, we need to import <code>Popup</code> from <code>react-leaflet</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Marker, Popup } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-leaflet'</span>;
</code></pre>
<p>Then, let’s update our <code>&lt;Marker&gt;</code> component to return:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">return</span> (
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Marker</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{id}</span> <span class="hljs-attr">position</span>=<span class="hljs-string">{position}</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Popup</span>&gt;</span>{ name }<span class="hljs-tag">&lt;/<span class="hljs-name">Popup</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">Marker</span>&gt;</span></span>
);
</code></pre>
<p>And once we save and open back up our map, you can now click on each marker and see our destinations name!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/mapping-app-with-destination-marker-popup.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Popup for each destination marker in the mapping app</em></p>
<h3 id="heading-before-were-done-center-the-map">Before we’re done, center the map</h3>
<p>Previously, our demo map centered on Washington, DC. Let’s update that to the center of the world since our map doesn’t focus on the United States.</p>
<p>Update the <code>LOCATION</code> variable to:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> LOCATION = {
  <span class="hljs-attr">lat</span>: <span class="hljs-number">0</span>,
  <span class="hljs-attr">lng</span>: <span class="hljs-number">0</span>,
};
</code></pre>
<p>And with that, we have our map!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/mapping-app-with-travel-bucket-list-markers.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Final mapping app with markers and popups for each destination</em></p>
<p><a target="_blank" href="https://github.com/colbyfayock/my-travel-bucket-list/commit/56dbadb74cea2770174eb8ea7c039be27ca18971">Follow along with the commit!</a></p>
<h2 id="heading-what-else-other-features-can-we-add-to-our-app">What else other features can we add to our app?</h2>
<h3 id="heading-add-a-way-to-check-off-each-location">Add a way to check off each location</h3>
<p>Inside GraphCMS, you can add a new field to your Destination content model that allows you to select whether you visited each location or not.</p>
<p>With this value, we can add it to our query and update our map with some kind of indicator like a checkmark to show that we’ve checked it off our bucket list!</p>
<h3 id="heading-customize-your-map-background-styles">Customize your map background styles</h3>
<p>We’re using a public version of <a target="_blank" href="https://www.openstreetmap.org/#map=5/38.007/-95.844">OpenStreetMap</a> which is open source, but <a target="_blank" href="https://www.mapbox.com/">Mapbox</a> offers some cool maps we can use to make it look a little more impressive.</p>
<p>If you want to get started changing your map styles, you can <a target="_blank" href="https://www.freecodecamp.org/news/how-to-set-up-a-custom-mapbox-basemap-with-gatsby-and-react-leaflet/">check out this other walkthrough</a> of mine to learn how to use Mapbox.</p>
<p><a target="_blank" href="https://www.colbyfayock.com/2020/04/how-to-set-up-a-custom-mapbox-basemap-style-with-react-leaflet-and-leaflet-gatsby-starter">Check out the blog post</a> or <a target="_blank" href="https://www.youtube.com/watch?v=KcPJr1b_rv0">watch the video</a>!</p>
<h3 id="heading-style-the-map-markers-with-a-custom-image">Style the map markers with a custom image</h3>
<p>You can check out my video walk through on how to change the markers to a custom image.</p>
<p>Take that a step further and use the feature above to dynamically show a different marker image when you’ve checked off a location.</p>
<p><a target="_blank" href="https://egghead.io/lessons/react-customize-geojson-data-markers-with-a-react-leaflet-icon-image?pl=mapping-with-react-leaflet-e0e0&amp;af=atzgap">Check out the video on Egghead.io!</a></p>
<h2 id="heading-want-to-learn-more-about-maps">Want to learn more about maps?</h2>
<p>Check out some of my other tutorials and videos:</p>
<ul>
<li><a target="_blank" href="https://egghead.io/playlists/mapping-with-react-leaflet-e0e0?af=atzgap">Mapping with React Leaflet</a> (<a target="_blank" href="https://egghead.io/?af=atzgap">egghead.io</a>)</li>
<li><a target="_blank" href="https://www.youtube.com/playlist?list=PLFsfg2xP7cbJTnTFH3OGXEAt9O1mpoqpR">Mapping Apps with React, Gatsby, &amp; Leaflet</a> (<a target="_blank" href="https://www.youtube.com/channel/UC7Wpv0Aft4NPNhHWW_JC4GQ">youtube.com</a>)</li>
<li><a target="_blank" href="https://www.colbyfayock.com/2020/03/how-to-create-a-coronavirus-covid-19-dashboard-map-app-with-gatsby-and-leaflet">How to create a Coronavirus (COVID-19) Dashboard &amp; Map App with Gatsby and Leaflet</a> (colbyfayock.com)</li>
<li><a target="_blank" href="https://www.colbyfayock.com/2020/03/how-to-create-a-summer-road-trip-mapping-app-with-gatsby-and-leaflet">How to Create a Summer Road Trip Mapping App with Gatsby and Leaflet</a> (colbyfayock.com)</li>
<li><a target="_blank" href="https://www.freecodecamp.org/news/easily-spin-up-a-mapping-app-in-react-with-leaflet/">How to build a mapping app in React the easy way with Leaflet</a> (colbyfayock.com)</li>
<li><a target="_blank" href="https://www.colbyfayock.com/2020/03/anyone-can-map-inspiration-and-an-introduction-to-the-world-of-mapping">Anyone Can Map! Inspiration and an introduction to the world of mapping</a> (colbyfayock.com)</li>
</ul>
<h2 id="heading-whats-on-your-travel-bucket-list">What’s on your travel bucket list?</h2>
<p><a target="_blank" href="https://twitter.com/colbyfayock">Let me know on Twitter!</a></p>
<div class="embed-wrapper">
        <blockquote class="twitter-tweet">
          <a href="https://twitter.com/colbyfayock/status/1275441134144110595"></a>
        </blockquote>
        <script defer="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></div>
<div id="colbyfayock-author-card">
  <p>
    <a href="https://twitter.com/colbyfayock">
      <img src="https://res.cloudinary.com/fay/image/upload/w_2000,h_400,c_fill,q_auto,f_auto/w_1020,c_fit,co_rgb:007079,g_north_west,x_635,y_70,l_text:Source%20Sans%20Pro_64_line_spacing_-10_bold:Colby%20Fayock/w_1020,c_fit,co_rgb:383f43,g_west,x_635,y_6,l_text:Source%20Sans%20Pro_44_line_spacing_0_normal:Follow%20me%20for%20more%20JavaScript%252c%20UX%252c%20and%20other%20interesting%20things!/w_1020,c_fit,co_rgb:007079,g_south_west,x_635,y_70,l_text:Source%20Sans%20Pro_40_line_spacing_-10_semibold:colbyfayock.com/w_300,c_fit,co_rgb:7c848a,g_north_west,x_1725,y_68,l_text:Source%20Sans%20Pro_40_line_spacing_-10_normal:colbyfayock/w_300,c_fit,co_rgb:7c848a,g_north_west,x_1725,y_145,l_text:Source%20Sans%20Pro_40_line_spacing_-10_normal:colbyfayock/w_300,c_fit,co_rgb:7c848a,g_north_west,x_1725,y_222,l_text:Source%20Sans%20Pro_40_line_spacing_-10_normal:colbyfayock/w_300,c_fit,co_rgb:7c848a,g_north_west,x_1725,y_295,l_text:Source%20Sans%20Pro_40_line_spacing_-10_normal:colbyfayock/v1/social-footer-card" alt="Follow me for more Javascript, UX, and other interesting things!" width="2000" height="400" loading="lazy">
    </a>
  </p>
  <ul>
    <li>
      <a href="https://twitter.com/colbyfayock">? Follow Me On Twitter</a>
    </li>
    <li>
      <a href="https://youtube.com/colbyfayock">?️ Subscribe To My Youtube</a>
    </li>
    <li>
      <a href="https://www.colbyfayock.com/newsletter/">✉️ Sign Up For My Newsletter</a>
    </li>
  </ul>
</div>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
