How to create nested folders in Sanity with Next.js

6 replies
Last updated: Oct 15, 2024
Is it possible to have nested folders in sanity nextjs? please help
AI Update

I think you might be asking about organizing your content in a nested folder-like structure in Sanity Studio when working with a Next.js project. The good news is yes, this is absolutely possible using the Structure Builder API!

The Structure Builder lets you create infinitely-nestable categories that work just like folders. Instead of seeing a flat list of all your document types, you can organize them hierarchically however makes sense for your content.

Here's a simple example of creating nested "folders" in your Studio:

// structure/index.ts
import type {StructureResolver} from 'sanity/structure'

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Content')
    .items([
      // Create a "folder" for Blog content
      S.listItem()
        .title('Blog')
        .child(
          S.list()
            .title('Blog Content')
            .items([
              S.documentTypeListItem('post').title('Posts'),
              S.documentTypeListItem('author').title('Authors'),
              S.documentTypeListItem('category').title('Categories'),
            ])
        ),
      
      // Create another "folder" for Products
      S.listItem()
        .title('Products')
        .child(
          S.list()
            .title('Product Content')
            .items([
              S.documentTypeListItem('product').title('Products'),
              S.documentTypeListItem('collection').title('Collections'),
            ])
        ),
      
      // Single items at root level
      S.documentTypeListItem('settings').title('Settings'),
    ])

Then wire it up in your sanity.config.ts:

import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {structure} from './structure'

export default defineConfig({
  // ...other config
  plugins: [
    structureTool({structure}),
    // ...other plugins
  ],
})

You can nest these as deeply as you want, and you can even create filtered lists within folders (like "Published Posts" vs "Draft Posts"). You can also organize media assets in folders based on properties like size or type.

This folder structure is purely organizational in the Studio - it doesn't affect how you query or structure your Next.js app. Your Next.js routing and file structure are completely independent from how you organize content in the Studio.

Yes you can achieve this with a custom structure
https://www.sanity.io/docs/structure-builder-cheat-sheet#f48e79f54178
You'll want to do something like this

// structure.ts

export const structure = (S: any) => 
    S.list()
        .title('Content')
        .items([
            S.listItem()
                .title('FOLDER_NAME')
                .child(
                    S.list()
                        .title('FOLDER_NAME')
                        .items([
                            S.listItem()
                                .title("NESTED_FOLDER")
                                .id('team1')
                                .child(
                                    S.list()
                                        .title('Team 1 Resources')
                                        .items([
                                         //your items here
                                         ])
        ])
Okay,
user G
, I got it, Thank you for the response. If there is anything else I will get back to you.
Hi
user G
I am getting one issue with schema and query
Schema -import { UserIcon, } from '@sanity/icons';
import { defineField, defineType } from 'sanity';

export const socialproofType = defineType({
name: 'socialproof',
title: 'Socialproof',
type: 'document',
icon: UserIcon,
fields: [
defineField({
name: 'title',
type: 'string',
title: 'Title',
}),
defineField({
name: 'slug',
type: 'slug',
title: 'Slug',
options: {
source: 'title',
},
}),
defineField({
name: 'companies',
type: 'array',
title: 'Companies',
of: [
defineField({
name: 'image',
type: 'image',
title: 'Company Logo',
options: {
hotspot: true,
},
fields: [
defineField({
name: 'altText',
type: 'string',
title: 'Alt Text',
}),
],
}),
],
}),
],
preview: {
select: {
title: 'name',
media: 'image',
},
},
});

Query -
export const CUSTOMER_LOGOS_QUERY = defineQuery(
*[_type == "socialproof"]{
  title,
  companies[] {
    "imageUrl": image.asset->url,
    altText
  }
}[0]
)
QueryType -
export type CUSTOMER_LOGOS_QUERYResult = {
title: string | null;
companies: Array<{
imageUrl: null;
altText: string | null;
}> | null;
} | null;

Why the imageUrl: null; is showing null should be string and null both right ? Can you please help me out.
Hello
user G
can you please help figure out how I can iterate in this content section. It is a service page sanity document and there a three different of them. But i am getting the same data for every page.
// service.schema.ts
import { TagIcon } from '
user F
/icons'import { defineField, defineType } from 'sanity'

export const services_1 = defineType({
name: 'services1',
title: 'Service Page 1',
type: 'document',
icon: TagIcon,
groups: [
{
name: "content",
title: "Content",
},
{
name: "seo",
title: "SEO",
},
],
fields: [
defineField({
name: 'title',
type: 'string',
title: 'Title',
}),
// SEO Block
defineField({
name: "page_title",
title: "Page Title",
type: "string",
group: "seo",
validation: (rule) => rule.required(),
}),
defineField({
name: "meta_keywords",
title: "Meta Keywords",
type: "string",
group: "seo",
}),
defineField({
name: "page_meta_description",
title: "Page Meta Description",
type: "text",
rows: 4,
group: "seo",
}),
defineField({
name: "slug",
title: "Slug",
type: "slug",
options: {
source: "page_title",
maxLength: 200,
slugify: (input) => input.toLowerCase().replace("page", "").replace(/\s+/g, "-").replace(/-$/, "").slice(0, 200),
},
description: "Define a custom slug or generate one from the 'Page Title'. Maximum character limit is 200.",
group: "seo",
validation: (rule) => rule.required(),
}),
defineField({
name: 'header',
type: 'reference',
title: 'Header',
to: [{ type: 'header1' }],
description: 'Select a header option.',
group: "content",
}),
defineField({
name: 'content',
title: 'Content Sections',
type: 'array',
of: [
{
type: 'reference',
name: 'heroSection',
to: [{ type: 'heroSection2' }],
title: 'Hero Section',
description: 'Select an existing Hero document.',
options: {
// Use the preview property to customize display
// Select the title and media to show in the preview
preview: {
select: {
title: 'title', // Field from the hero document
media: 'image', // Assuming there's an image field in the hero document
},
},
},
},
{
type: "reference",
name: 'feature2Section',
to: [{ type: 'feature2' }],
title: 'Feature Section',
description: 'Include Feature content.',
options: {
preview: {
select: {
title: 'title', // Field from the socialproof document
media: 'image', // Assuming there's an image field
},
},
},
},
{
type: "reference",
name: 'feature1Section',
to: [{ type: 'process' }],
title: 'Feature Section',
description: 'Include Services content.',
options: {
preview: {
select: {
title: 'title', // Field from the socialproof document
media: 'image', // Assuming there's an image field
},
},
},
},
{
type: "reference",
name: 'caseStudiesSection',
to: [{ type: 'caseStudies' }],
title: 'Case Studies Section',
description: 'Include Case Studies content.',
options: {
preview: {
select: {
title: 'title', // Field from the socialproof document
media: 'image', // Assuming there's an image field
},
},
},
},
{
type: "reference",
name: 'productSection',
to: [{ type: 'cxfulSection' }],
title: 'Product Section',
description: 'Include Process content.',
options: {
preview: {
select: {
title: 'title', // Field from the socialproof document
media: 'image', // Assuming there's an image field
},
},
},
},
{
type: "reference",
name: 'insightsSection',
to: [{ type: 'insights' }],
title: 'Insights Section',
description: 'Include Insight content.',
options: {
preview: {
select: {
title: 'Insights Section', // Field from the socialproof document
media: 'image', // Assuming there's an image field
},
},
},
},
{
type: "reference",
name: 'faqsection',
to: [{ type: 'faq' }],
title: 'FAQ Section',
description: 'Include FAQ content.',
options: {
preview: {
select: {
title: 'FAQ Section', // Field from the socialproof document
media: 'image', // Assuming there's an image field
},
},
},
},
{
type: "reference",
name: 'footerCTA',
to: [{ type: 'footerCTA' }],
title: 'FooterCTA Section',
description: 'Include FooterCTA content.',
options: {
preview: {
select: {
title: 'FooterCTA Section', // Field from the socialproof document
media: 'image', // Assuming there's an image field
},
},
},
},
],
description: 'Select one or more content sections to display on the home page.',
group: "content",
}),
defineField({
name: 'footer',
title: 'Footer',
type: 'reference',
to: [{ type: 'footer1' }],
description: 'Select a footer option.',
group: "content",
}),
],
})

// servicepage.tsx
import {
HERO_SECTION_2_QUERYResult,
SECONDARY_NAVBAR_QUERYResult,
OVERVIEW_QUERYResult,
CXFUL_QUERYResult,
CASE_STUDY_QUERYResult,
INSIGHTS_QUERYResult,
FAQ_QUERYResult,
FooterCTA_QUERYResult
} from '@/sanity.types'; import CaseStudiesWrapper from "@/app/sections/CaseStudy/CaseStudySliderWrapper";
import FAQWrapper from "@/app/sections/FAQSection/FAQSectionWrapper";
import InsightsSectionWrapper from "@/app/sections/Insights/InsightsSectionWrapper";
import FooterCTAWrapper from "@/app/sections/FooterCTA/FooterCTAWrapper";
import Product1 from "@/app/sections/ProductsSection/Product1";
import SecondaryNavbar from "../Navbar/SecondaryNavbar";
import HeroSection_2_Service from "@/app/sections/HeroSection/HeroSection_2_Service";
import Feature_2 from "@/app/sections/Features/Feature_2";

type Section =
| { _type: "heroSection"; data: HERO_SECTION_2_QUERYResult }
| { _type: "seconaryNavbar"; data: SECONDARY_NAVBAR_QUERYResult }
| { _type: "product"; data: CXFUL_QUERYResult }
| { _type: "overview"; data: OVERVIEW_QUERYResult }
| { _type: "casestudy"; data: CASE_STUDY_QUERYResult }
| { _type: "insights"; data: INSIGHTS_QUERYResult }
| { _type: "faq"; data: FAQ_QUERYResult }
| { _type: "footercta"; data: FooterCTA_QUERYResult };

interface ContentProps {
sections: Section[];
}

const ServiceContent: React.FC<ContentProps> = ({ sections }) => {
if (!sections || sections.length === 0) {
return <div>No content available</div>;
}

return (
<div>
{sections.map((section, index) => {
switch (section._type) {
case "heroSection":
return <HeroSection_2_Service key={index} data={section.data} />;
case "seconaryNavbar":
return <SecondaryNavbar key={index} data={section.data} />;
case "product":
return <Product1 key={index} data={section.data} />;
case "overview":
return <Feature_2 key={index} data={section.data} />;
case "casestudy":
return <CaseStudiesWrapper key={index} data={section.data} />;
case "insights":
return <InsightsSectionWrapper key={index} data={section.data} />;
case "faq":
return <FAQWrapper key={index} data={section.data} />;
case "footercta":
return <FooterCTAWrapper key={index} data={section.data} />;
default:
return null;
}
})}
</div>
);
};



if any file requried please let me know and please help me out of this.

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?