DEV Community

Cover image for Stećak Oracle: Exploring Medieval Stone Monuments with Sanity and Gemini
David Kljajo
David Kljajo

Posted on

Stećak Oracle: Exploring Medieval Stone Monuments with Sanity and Gemini

Sanity Challenge Path Two Submission

I decided to build something around a subject that is very close to where I live: stećci, the medieval tombstone monuments found across Bosnia and Herzegovina and neighboring countries.

The result is Stećak Oracle, a small knowledge-driven web application that combines Sanity, Next.js, and Google Gemini.

The idea is simple:

Store structured historical information in Sanity, then let an AI model answer questions using that information as its knowledge base.

This project was built for the Sanity Challenge.


The idea

Stećci are medieval tombstones found across Bosnia and Herzegovina, Croatia, Montenegro and Serbia.

There is a huge amount of historical information around them, but I wanted to approach the subject from a developer's perspective.

Instead of building another static website with paragraphs of text, I wanted to experiment with a small structured knowledge system.

The application would contain:

  • individual stećak records
  • locations
  • historical periods
  • descriptions
  • inscriptions
  • motifs
  • historical context
  • documented sources

Then I wanted to put an AI interface on top of that data.

That became Stećak Oracle.


Why Sanity?

The first important decision was how to structure the information.

I didn't want everything to be stored as one large JSON document or hardcoded directly into the application.

Sanity was a good fit because I could create actual content types and relationships between them.

The main content model became:

Stećak
 ├── Motifs
 ├── Sources
 └── Historical information

Motif
 ├── Description
 ├── Interpretations
 └── Interpretation caution

Source
 ├── Title
 ├── URL
 ├── Publisher
 ├── Type
 └── Notes
Enter fullscreen mode Exit fullscreen mode

This also made the project more interesting from a development perspective.

The AI doesn't have to be the source of truth.

Sanity is the structured knowledge layer.

Gemini is the generation layer.


The content model

I created three main document types in Sanity.

Stećak

A stećak contains information such as:

  • name
  • slug
  • location
  • region
  • historical period
  • description
  • inscription
  • motifs
  • historical context
  • sources

For example, one of the records is:

Stećak Radimlja — Vojvoda Vlatko Vuković

with Radimlja near Stolac as the location.

Motif

Motifs are separate documents because the same motif can appear on multiple monuments.

For example, the project contains a Sword motif.

But there is an important detail here.

I didn't want to tell the AI that a particular interpretation is an absolute historical fact.

So the motif also contains an Interpretation Caution field.

That allows the application to distinguish between documented information and interpretation.

Source

Sources are separate documents as well.

For the initial dataset I used institutional information from the UNESCO World Heritage Centre, including the Stećci Medieval Tombstone Graveyards entry.

This gives the application a simple evidence layer rather than treating AI-generated text as the source itself.


Building the frontend

The frontend is built with Next.js and TypeScript.

The application has several main areas:

/
├── Home
├── /stecci
├── /stecci/[slug]
├── /motifs
├── /sources
└── /oracle
Enter fullscreen mode Exit fullscreen mode

The homepage retrieves the current number of records directly from Sanity.

That means the statistics are not hardcoded.

If I add another stećak or source in Sanity, the application can reflect that change.


Stećak detail pages

Each stećak has its own dynamic page.

For example:

/stecci/stecak-radimlja-vojvoda-vlatko-vukovic
Enter fullscreen mode Exit fullscreen mode

The page retrieves the document from Sanity using its slug.

It also dereferences related motifs and sources.

This was useful because the frontend doesn't need to duplicate the actual content model.

Sanity remains responsible for the data.


Adding the Oracle

The final piece was the AI interface.

The Oracle page allows a user to ask a question about:

  • stećci
  • motifs
  • history
  • documented evidence

For example:

What does the sword motif on the Radimlja stećak represent?

The frontend sends the question to a Next.js API route.

The basic flow is:

User
  ↓
Oracle UI
  ↓
Next.js API route
  ↓
Sanity
  ↓
Knowledge base
  ↓
Gemini
  ↓
Answer
Enter fullscreen mode Exit fullscreen mode

Connecting Gemini

I initially experimented with the Anthropic API.

The API itself worked, but I didn't have API credits available, so I decided not to add another paid dependency to the project.

I switched to Google Gemini through the @google/genai package.

The application uses an environment variable for the API key:

GEMINI_API_KEY=...
Enter fullscreen mode Exit fullscreen mode

The key is stored in .env.local and is excluded from Git.

The actual API request is made server-side through the Next.js API route.

This is important because the API key should not be exposed in browser-side JavaScript.


The important part: grounding Gemini with Sanity

This is probably the most important technical part of the project.

I didn't want to simply send the user's question to Gemini and display whatever it returned.

Instead, the API route first retrieves the knowledge base from Sanity.

Conceptually:

const knowledgeBase = await getKnowledgeBase()
Enter fullscreen mode Exit fullscreen mode

The query retrieves:

Stećci
Motifs
Sources
Enter fullscreen mode Exit fullscreen mode

including their relationships.

The resulting data is then serialized and included in the Gemini prompt.

The model receives something conceptually like:

Here is the knowledge base from Sanity:

{
  "stecci": [...],
  "motifs": [...],
  "sources": [...]
}

You are the Stećak Oracle...

Answer this question clearly and carefully:

[user question]
Enter fullscreen mode Exit fullscreen mode

I also explicitly tell the model:

- Do not invent historical facts.
- Distinguish documented facts from interpretation.
- If the available information is insufficient, say so.
- Keep the answer concise.
Enter fullscreen mode Exit fullscreen mode

For a small dataset like this, sending the complete knowledge base is simple enough.

For a much larger project, I would definitely replace this with retrieval of only the most relevant documents.


A real example

I tested the Oracle with:

What does the sword motif on the Radimlja stećak represent?

The response used the information stored in Sanity.

It identified the sword as a recurring figural motif and, importantly, preserved the caution around interpretation.

The answer did not present the interpretation as an absolute fact.

That distinction was intentional.

Historical subjects are a good example of why an AI application should be careful about the difference between:

documented information
Enter fullscreen mode Exit fullscreen mode

and

interpretation
Enter fullscreen mode Exit fullscreen mode

Why this approach?

One thing I wanted to avoid was building a generic chatbot and simply giving it a historical theme.

The interesting part of the project is the relationship between the CMS and the AI model.

The architecture is closer to:

             ┌───────────────┐
             │     User      │
             └───────┬───────┘
                     │
                     ▼
             ┌───────────────┐
             │    Next.js    │
             │    Frontend   │
             └───────┬───────┘
                     │
                     ▼
             ┌───────────────┐
             │  API Route    │
             └───────┬───────┘
                     │
             ┌───────┴────────┐
             ▼                ▼
      ┌────────────┐   ┌────────────┐
      │   Sanity   │   │   Gemini   │
      │ Knowledge  │──▶│    AI      │
      │   Base     │   │            │
      └────────────┘   └──────┬─────┘
                              │
                              ▼
                       ┌────────────┐
                       │   Answer   │
                       └────────────┘
Enter fullscreen mode Exit fullscreen mode

Sanity provides the structured information.

Gemini turns that information into a natural-language answer.

Next.js connects everything together.


What I learned

The project ended up teaching me a few things that were more interesting than I expected.

1. Structured data matters

It is tempting to start an AI project with only a prompt.

But once you have structured content, relationships and sources, you can build something much more controlled.

The content model became just as important as the AI integration.

2. AI needs boundaries

For historical information, I don't want the model to confidently invent an interpretation.

That's why the knowledge base includes fields such as:

Interpretations
Interpretation Caution
Sources
Enter fullscreen mode Exit fullscreen mode

The application can then give the model some context about how information should be treated.

3. A small dataset is enough to prove the concept

I didn't need hundreds of monuments to build the first version.

Two stećak records, a motif and documented sources were enough to demonstrate the architecture.

The important part was making the entire pipeline work.

4. Keep the architecture simple first

For this challenge, I intentionally didn't build a complicated vector database or RAG pipeline.

The dataset is small enough that retrieving the complete knowledge base is reasonable.

If the project grows, the architecture can evolve.


What I would improve next

There are several things I would add to a future version.

Better retrieval

Instead of sending every Sanity document to Gemini, I would retrieve only the documents relevant to the question.

For example:

Question
   ↓
Search Sanity
   ↓
Relevant stećak
Relevant motifs
Relevant sources
   ↓
Gemini
Enter fullscreen mode Exit fullscreen mode

That would scale much better.

More stećci

The obvious next step is a larger dataset covering more locations across Bosnia and Herzegovina and the wider region.

Better source presentation

The next version could show sources directly underneath individual claims or sections of an answer.

That would make the evidence trail more visible.

Images

The current version is deliberately data-first.

I didn't have a suitable image collection for every monument, so I chose not to invent one or fill the application with unrelated images.

A future version could add properly sourced photographs and image metadata to the Sanity model.

Search and filtering

I would also add:

  • location filtering
  • region filtering
  • historical period filtering
  • motif filtering
  • full-text search

That would make the knowledge base useful even without the AI interface.


github repo:https://github.com/dkljajo/Ste-ak-Oracle-frontend-and-Gemini-integration

Technology stack

The current version uses:

  • Next.js
  • TypeScript
  • Tailwind CSS
  • Sanity
  • Google Gemini
  • @sanity/client
  • @google/genai
  • Git / GitHub

The application is intentionally small.

There is no separate backend service.

The Next.js API route handles the server-side Gemini request and retrieves the Sanity data.


Project structure

The important parts of the repository look roughly like this:

stecak-oracle/
├── studio/
│   ├── schemaTypes/
│   │   ├── stecak.ts
│   │   ├── motif.ts
│   │   └── source.ts
│   └── sanity.config.ts
│
└── web/
    ├── app/
    │   ├── api/
    │   │   └── oracle/
    │   │       └── route.ts
    │   ├── motifs/
    │   ├── oracle/
    │   ├── sources/
    │   └── stecci/
    └── lib/
        └── sanity.ts
Enter fullscreen mode Exit fullscreen mode

This separation also makes the architecture easy to understand:

studio/
    → content model and Sanity configuration

web/
    → application and AI interface
Enter fullscreen mode Exit fullscreen mode

Final result

Stećak Oracle started as a simple idea:

What if I could combine a structured historical knowledge base with an AI interface?

The final application is still small, but the complete pipeline works:

Sanity
  ↓
Structured historical knowledge
  ↓
Next.js
  ↓
Gemini
  ↓
Grounded answer
Enter fullscreen mode Exit fullscreen mode

And that's what I wanted to demonstrate with this project.

Not a generic chatbot.

Not an AI that simply talks about stećci.

A small application where the content has structure, sources and relationships, and where the AI uses that content to generate an answer.


What's next?

There is a lot of room to take this further.

I would like to expand the dataset, improve retrieval, add properly sourced images, expose citations more clearly, and eventually turn the Oracle into a more complete exploration tool for stećci.

For now, I'm happy that the basic idea works end-to-end.


Project

The source code is available on GitHub:

Stećak Oracle — Sanity + Next.js + Gemini

https://github.com/dkljajo/Ste-ak-Oracle-frontend-and-Gemini-integration


Built for the Sanity Challenge

This project was built as my submission for the Sanity Challenge.

The challenge gave me a good excuse to combine several things I've been learning and working with: cloud technologies, application development, structured content, APIs and AI.

More importantly, it gave me a reason to build something connected to the history and culture of the region I live in.

That was the part I enjoyed most.


Conclusion

Stećak Oracle is a small project, but it brought together several technologies that I wanted to understand better.

Sanity handles the content.

Next.js handles the application.

Gemini handles the natural-language generation.

And the interesting part is what happens between them.

The result is a simple example of how a structured CMS can become the foundation for a grounded AI application.

Thanks for reading.

If you have ideas for improving the project, I'd be interested in hearing them.


Tags

#sanity #nextjs #typescript #ai #gemini #webdev

Top comments (0)