Rules to Better TinaCMS

  • Do you optimize your TinaCMS project for clarity, performance, and reliable builds?

    
    <introEmbed
      body={<>
    Structuring and optimizing your TinaCMS project is essential to achieve clarity, enhance performance, and prevent build failures. Poorly optimized projects can lead to slow site performance, increased server load, and even failed builds due to excessive or inefficient data requests.
    
    Let’s explore how to structure your project effectively and apply best practices to boost performance both in runtime and during the build process.
      </>}
    />
    ## 1. Structuring your TinaCMS Architecture
    
    When working with large datasets or generating multiple subcomponents, following best practices is crucial to maintain performance and clarity.
    
    ### ❌ Bad practices
    
    * **Use deeply nested schemas with nested references**
    
      * This increases the complexity of the project, making it harder to manage and more prone to build failures
      * It also leads to inefficient data fetching, further slowing down both runtime and build processes
    
    ### ✅ Good practices
    
    * **Make a single request in a top-level server component and share data via React Context or a state library**
    
      * Fetch the data once at the top level and store it in a React Context or global state (e.g., [Redux](https://redux.js.org/)). This lets all components access the data without prop drilling, improves scalability, and eliminates redundant API calls.
    
    <figureEmbed figureEmbed={{
      preset: "goodExample",
      figure: '```js
    export default async function Home({ params }: HomePageProps) {
        const location = params.location;
    
        const websiteProps = await client.queries.website({
            relativePath: `${location}/website.md`,
        });
    
        const { conferencesData, footerData, speakers } = websiteProps.data;
    
        return (
            <ConferenceContext.Provider value={conferencesData}>
                <FooterContext.Provider value={footerData}>
                    <PageTransition>
                        <HomeComponent speakers={speakers} />
                    </PageTransition>
                </FooterContext.Provider>
            </ConferenceContext.Provider>
        );
    }
    
    export async function generateStaticParams() {
        const contentDir = path.join(process.cwd(), \'content/websites\');
        const locations = await fs.readdir(contentDir);
    
        return locations.map((location) => ({ location }));
    }
    ```
    
    **Code: `conferencesData` and `footerData` are provided through context, while `speakers` are passed directly to `HomeComponent` as props.**',
      shouldDisplay: true
    } } />
    
    
    * **Cache data at a top-level and access it when necessary**
    
      * If passing props is not feasible (e.g., components relying on Next.js router data), fetch the data at a higher level, cache it, and access it directly within the component
      * This approach ensures efficient data retrieval and reduces the server load at build time
    
    ## 2. Improving Runtime Performance
    
    Optimizing runtime performance is key to delivering a fast and responsive user experience.
    
    ### ❌ Bad practices
    
    * **Use client-side requests instead of relying on cached data from build process**
    
      * This approach can negate the benefit of static site generation, where data is fetched and cached during the process
      * Making too many client-side requests increases server load and slows down the application
    
    ### ✅ Good practices
    
    * **Use static site generation (SSG) with TinaCMS to fetch and cache data during builds**
    
      * Minimizes dynamic fetching and enhances performance
      * Faster load time
      * Less strain on the server
    
    ## 3. Improving Build Performance
    
    To ensure smooth and reliable builds, it’s important to follow best practices that prevent excessive server load and manage data efficiently.
    
    ### ✅ Best practices
    
    * **Write custom GraphQL queries**
    
      * Auto-generated GraphQL queries are often unoptimized and may contain nested objects with redundant data. For example, a recipe might include an ingredients object, which in turn references the same recipe again. Creating custom queries can reduce the size of objects and improve performance
    
  • Do you keep your images and content self-contained in your TinaCMS + Next.js project?

    When building a website using TinaCMS and Next.js, not all images need to live next to your content. Shared assets like logos, icons, or other global visuals are best stored in a central media folder (e.g. /public/uploads). This keeps things simple and avoids duplication.

    However, for documents that embed images—like blog posts or rules like this one—it’s important to keep the content (Markdown/MDX files) and related media together in the same folder. This self-contained structure improves maintainability, makes GitHub editing clearer, and supports portability.

    Video: Tina.io - The 3 options for storing markdown in GitHub for TinaCMS (5 min)

    By default, Tina stores content in a /content folder and images in /public, which breaks self-containment and can cause confusion.

    Let's explore three solutions to fix that and store content and images together.

    You have 3 options:

    1. Default structure + matching folders

    • Content in /content
    • Images in /public
    • Add a link to image folder in frontmatter of content file

    ✅ Pros

    • Works out of the box

    ❌ Cons

    • Not self-contained
    • Prone to errors when renaming/moving files
    • You must manually manage matching folder names and use frontmatter to point to images.

    2. Everything inside content folder

    • Each document gets a folder in /content
    • Images are stored alongside the MDX file
    Image

    Figure: Option 2 - Folder structure - rules example

    ✅ Pros

    • Fully self-contained
    • Tina Media Manager works

    ❌ Cons

    • Requires extra setup: update config, collections, and add a middleware

    Example implementation (Rules)

    import { NextResponse } from 'next/server';
    export function middleware(req) {
    if (process.env.DEVELOPMENT !== 'true') {
    return NextResponse.next();
    }
    const url = req.nextUrl;
    // Check if the request is for an image in the content/rules folder
    if (url.pathname.startsWith('/rules/') && url.pathname.match(/\.(png|jpg|jpeg|gif|webp)$/i)) {
    const escapedUrl = encodeURIComponent(url.pathname);
    const apiUrl = `http://localhost:3000/api/serve-image?filepath=${escapedUrl}`;
    console.log('Redirecting to API for image:', apiUrl);
    return NextResponse.redirect(apiUrl);
    }
    return NextResponse.next();
    }
    export const config = {
    matcher: ['/rules/:path*'],
    };

    Figure: Middleware to intercept media requests and call internal API

    import { NextApiRequest, NextApiResponse } from 'next';
    import fs from 'fs';
    import path from 'path';
    export default function handler(req: NextApiRequest, res: NextApiResponse) {
    if (process.env.DEVELOPMENT !== 'true') {
    res.status(403).send('API route is disabled in production');
    return;
    }
    const { filepath } = req.query;
    if (!filepath || typeof filepath !== 'string') {
    res.status(400).send('Filepath is required');
    return;
    }
    const unescapedFilepath = decodeURIComponent(filepath);
    const imagePath = path.join(process.cwd(), 'content/', unescapedFilepath);
    try {
    const imageBuffer = fs.readFileSync(imagePath);
    const contentType = `image/${path.extname(unescapedFilepath).slice(1)}`;
    res.setHeader('Content-Type', contentType);
    res.send(imageBuffer);
    } catch (error) {
    console.error('Error reading image:', error);
    res.status(404).send('Image not found');
    }
    }

    Figure: Internal API to serve images from content folder

    You can find more details on this repository

    3. Everything inside public folder (✅ Recommended)

    • Each document has a folder in /public/uploads
    • Images and MDX file live together
    Image

    Figure: Option 3 - Folder structure - rules example

    ✅ Pros

    • Fully self-contained
    • Tina Media Manager works
    • No custom middleware needed

    ❌ Cons

    • MDX files live in public, which is unconventional—but works

    This option is clean, simple, and works with Tina’s Media Manager out of the box — no special setup required.

    Example Collection config (Rules)

    import { Collection } from "tinacms";
    const Rule: Collection = {
    label: "Rules",
    name: "rule",
    path: "public/uploads/rules",
    fields: [
    ...
    ],
    ...
    };
    export default Rule;

    Figure: Path pointing to public/uploads folder

    See more on Tina.io - Storing Media With Content.