The story of how structuring game specifications in Contentful made it easy to reuse for things like quiz bots

The story of how structuring game specifications in Contentful made it easy to reuse for things like quiz bots

Structuring game specifications as a Content Model in Contentful allows you to reuse the same data for peripheral tools such as quiz bots. In this article, we will implement a Next.js demo application that combines field designs such as stageType and bossName with REST API queries (content_type, select, etc.) to convert only battle stages containing bosses into multiple-choice quizzes.
2025.12.09

This page has been translated by machine translation. View original

Introduction

This article is the 9th blog in the Game Development Accelerated by SaaS - Advent Calendar 2025.

In this article, I'll introduce a demo of a small web application that automatically generates quizzes from game "stage" specifications by combining Contentful REST API and Next.js. By structuring game specifications on Contentful, you can quickly launch applications like quiz bots without adding much additional implementation.

contentful-quiz-bot-screenshot

What is Contentful

Contentful is a headless CMS service that manages content in JSON format and allows retrieval via REST API or GraphQL API. In this article, we'll use the Content Delivery API to query game stage information and convert it into quiz data on the Next.js side.

Why Use Contentful

By maintaining game specifications as structured data on Contentful rather than just documents, it becomes easier to reuse for various peripheral tools, including quiz demo apps like this one. Also, by specifying query parameters such as content_type, fields.stageType, select, etc., the application side can efficiently retrieve only the necessary information without having to implement filtering logic.

Target Audience

  • Those considering managing game specifications with CMS
  • Those wanting to try Contentful for gaming industry use cases
  • Those wanting to learn Contentful integration while building a light demo app with Next.js and TypeScript

Reference Information

Overall Structure

First, let's check the structure of the demo app we created.

  • Next.js App

    • Calls /api/quiz to retrieve quizzes and displays the question text, four choices, and judgment results in the browser.
  • Next.js API Route

    • Fetches Stage entries from Contentful REST API, assembles random quiz data for one question, and returns it as JSON.
  • Contentful

    • Manages configuration information such as stage number, stage name, boss name, and stage type as a Stage Content Model.
    • Distinguishes between battle stages, town stages, etc. with stageType, allowing only stages with bosses to be targeted for quizzes.

Content Model Design

First, we'll structure the game's "stage" specifications as structured data on Contentful. We create a Content Model called Stage and register each game stage as an entry.

Stage Content Model

Stage content model

We defined the following fields for the Stage Content Model:

  • stageNumber (required)

    • Type: Integer
    • Description: Stage number (1, 2, 3 ...)
  • name (required)

    • Type: Short text
    • Description: Stage name
  • bossName

    • Type: Short text
    • Description: Boss name
    • Not required as some stages like tutorial stages or town areas don't have bosses.
  • description

    • Type: Long text
    • Description: Stage description or world setting text
  • stageType (required)

    • Type: Short text
    • Description: Code indicating stage type

For stageType, we're using Contentful's Predefined values to allow only the following three types:

  • battle
  • town
  • event

predefined value

This prevents input variations while allowing the API to strictly filter only battle stages with conditions like fields.stageType=battle.

Setting the Appearance to Dropdown makes field input easier when creating models.

Appearance config

This is how it looks on the field input screen.

dropdown

Quiz Target Definition

For this demo, we defined stages we want to use for quizzes as follows:

  • Must be a battle stage

    • stageType is battle
  • Must have a boss

    • bossName is set

By clearly expressing this rule in Contentful's data, the quiz generation logic on the app side can be kept simple as "retrieve only battle stages that have boss names."

Data Retrieval with REST API

We combine query parameters to efficiently retrieve only the information needed for quiz generation.

Query to Retrieve Only Battle Stages

For quizzes, we only need battle stage entries that have bosses. Therefore, we include the following conditions in our REST API calls:

  • content_type=stage
    • Targets only the Stage Content Model.
  • fields.stageType=battle
    • Retrieves only battle stages.
  • fields.bossName[exists]=true
    • Targets only entries where bossName is set.
  • select=...
    • Selects only fields needed for quizzes, such as stageNumber, name, bossName, stageType.

Sample Code

Excerpt from src/lib/contentful.ts
import type { ContentfulResponse, StageEntry } from "@/types/contentful";

function getContentfulConfig() {
  const spaceId = process.env.CONTENTFUL_SPACE_ID || "vw67dgbqxobj";
  const environmentId = process.env.CONTENTFUL_ENVIRONMENT_ID || "master";
  const accessToken = process.env.CONTENTFUL_CDA_TOKEN;

  if (!accessToken) {
    throw new Error("CONTENTFUL_CDA_TOKEN is not set");
  }

  return { spaceId, environmentId, accessToken };
}

export async function getBattleStages(): Promise<StageEntry[]> {
  const { spaceId, environmentId, accessToken } = getContentfulConfig();

  const params = new URLSearchParams({
    content_type: "stage",
    "fields.stageType": "battle",
    "fields.bossName[exists]": "true",
    select:
      "sys.id,fields.stageNumber,fields.name,fields.bossName,fields.stageType",
  });

  const url = `https://cdn.contentful.com/spaces/${spaceId}/environments/${environmentId}/entries?${params}`;

  const response = await fetch(url, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
  });

  if (!response.ok) {
    throw new Error(
      `Failed to fetch stages from Contentful: ${response.status} ${response.statusText}`,
    );
  }

  const data: ContentfulResponse<StageEntry["fields"]> = await response.json();
  return data.items;
}

Specifying REST API filtering like this keeps the data handled by the application small. A major advantage of Contentful in this demo is that Contentful's content model and query parameters allow you to straightforwardly express requirements like "I only want stages that meet this condition."

Quiz API and Frontend Implementation

Quiz Generation API Route

The API for quiz generation is implemented at /api/quiz. The process flow is as follows:

  1. Retrieve a list of Stage entries with the conditions mentioned earlier.
  2. Randomly select one from the array and make that stage the current question.
  3. Extract bossName from other stages and randomly select a few as incorrect answer candidates.
  4. Shuffle correct and incorrect answers together to calculate the choices array and the correct answer index.
  5. Generate a Japanese question like "Which is the boss name of Stage X" using stageNumber and return as JSON.

Sample Code

Excerpt from src/app/api/quiz/route.ts
import { NextResponse } from "next/server";
import { getBattleStages } from "@/lib/contentful";
import type { Quiz } from "@/types/quiz";

function getRandomElement<T>(array: T[]): T {
  return array[Math.floor(Math.random() * array.length)];
}

function shuffle<T>(array: T[]): T[] {
  const shuffled = [...array];
  for (let i = shuffled.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
  }
  return shuffled;
}

export async function GET() {
  const stages = await getBattleStages();

  if (stages.length < 3) {
    return NextResponse.json(
      { error: "クイズを生成するには最低3つのバトルステージが必要です" },
      { status: 400 },
    );
  }

  // Randomly select one stage for the correct answer
  const correctStage = getRandomElement(stages);
  const correctBossName = correctStage.fields.bossName!;
  const stageNumber = correctStage.fields.stageNumber;

  // Collect boss names for incorrect answers
  const otherStages = stages.filter(
    (stage) => stage.sys.id !== correctStage.sys.id,
  );
  const wrongChoices = shuffle(otherStages)
    .slice(0, 3)
    .map((stage) => stage.fields.bossName!);

  const choices = shuffle([correctBossName, ...wrongChoices]);
  const answerIndex = choices.indexOf(correctBossName);

  const quiz: Quiz = {
    stageNumber,
    questionText: `${stageNumber}面のボスの名前はどれ`,
    choices,
    answerIndex,
  };

  return NextResponse.json(quiz);
}

With this setup, the frontend only needs to call /api/quiz to retrieve quiz data for one question.

Quiz Screen Composition

The frontend is implemented on the top page of the Next.js App Router. The screen contains the following elements:

  • State before quiz generation
    • "Generate quiz" button

Before quiz generation

  • State during quiz

    • Question text
    • Four choices
    • Answer button
  • State after answering

    • Message indicating whether the answer is correct
    • "One more question" button

After answering

For state management, we use React hooks to switch between three states: quiz not generated, quiz displayed, and answered.

Sample Code

Excerpt from src/app/page.tsx
"use client";

import { useState } from "react";
import type { Quiz } from "@/types/quiz";

type GameState = "idle" | "playing" | "answered";

export default function Home() {
  const [quiz, setQuiz] = useState<Quiz | null>(null);
  const [selectedChoice, setSelectedChoice] = useState<number | null>(null);
  const [gameState, setGameState] = useState<GameState>("idle");

  const fetchQuiz = async () => {
    const response = await fetch("/api/quiz");
    if (!response.ok) {
      // See implementation example for error handling
      return;
    }
    const data: Quiz = await response.json();
    setQuiz(data);
    setSelectedChoice(null);
    setGameState("playing");
  };

  const handleSubmit = () => {
    if (selectedChoice === null) return;
    setGameState("answered");
  };

  const isCorrect =
    gameState === "answered" &&
    quiz &&
    selectedChoice !== null &&
    selectedChoice === quiz.answerIndex;

  return (
    <main>
      <button onClick={fetchQuiz} disabled={gameState === "playing"}>
        クイズを出題
      </button>

      {quiz && (
        <>
          <p>{quiz.questionText}</p>
          <ul>
            {quiz.choices.map((choice, index) => (
              <li key={choice}>
                <label>
                  <input
                    type="radio"
                    name="choice"
                    value={index}
                    checked={selectedChoice === index}
                    onChange={() => setSelectedChoice(index)}
                  />
                  {choice}
                </label>
              </li>
            ))}
          </ul>
          <button onClick={handleSubmit}>回答する</button>

          {gameState === "answered" && (
            <p>{isCorrect ? "正解です" : "残念、はずれです"}</p>
          )}
        </>
      )}
    </main>
  );
}

The style uses CSS Modules for a simple panel-like appearance, but since the main focus of the demo app is the flow of converting Contentful data into quizzes, the style explanation is kept to a minimum.

Verification and Consideration

Verification Process

  1. Create multiple Stage entries from the Contentful management screen.
    • Enter stageNumber, name, and stageType.
    • Set bossName for battle stages you want to include in quizzes.
      sample entry
  2. Run npm dev in the root of the Next.js project and access the top page from a browser.
  3. Press the "Generate quiz" button and confirm that quizzes corresponding to the data on Contentful are displayed.

contentful-quiz-bot-screenshot

If you want to add stages, just create new Stage entries in Contentful to increase quiz variations. Changes to boss names or stage configurations work the same way, allowing you to update quiz content without rewriting code.

Benefits of Using Contentful

Let me summarize the points I felt were beneficial about using Contentful through this demo:

  • By consolidating game setting materials in Contentful, they could be immediately reused in a different form as a quiz app.
  • Designing fields like stageType and bossName allowed us to push the quiz target definition to the data side, keeping the app-side logic thin.
  • The REST API allows condition specification and field selection via query parameters, enabling efficient retrieval of only the information needed for quiz generation.

In game development environments, stage setting lists are often needed for specification reviews and balance adjustments. By centrally managing such information on Contentful, it can be easily extended not only to quiz demos like this one but also to internal confirmation tools and debug views.

Summary

In this article, I introduced a demo app that automatically generates multiple-choice quizzes from game "stage" specifications stored in Contentful using Next.js. By defining stage numbers, boss names, and stage types in Stage and extracting only battle stages with REST API query parameters, we were able to keep the quiz generation logic simple.

Putting game specifications on a headless CMS like Contentful makes it easier to add peripheral tools later, including quiz apps like this one.

Share this article

FacebookHatena blogX

Related articles