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

> Source: <https://dev.to/davidkljajo/-stecak-oracle-exploring-medieval-stone-monuments-with-sanity-and-gemini-4hk9>
> Published: 2026-09-19 16:22:13+00:00

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**.

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:

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

That became Stećak Oracle.

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
```

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.

I created three main document types in Sanity.

A stećak contains information such as:

For example, one of the records is:

**Stećak Radimlja — Vojvoda Vlatko Vuković**

with Radimlja near Stolac as the location.

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.

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.

The frontend is built with **Next.js** and TypeScript.

The application has several main areas:

```
/
├── Home
├── /stecci
├── /stecci/[slug]
├── /motifs
├── /sources
└── /oracle
```

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.

Each stećak has its own dynamic page.

For example:

```
/stecci/stecak-radimlja-vojvoda-vlatko-vukovic
```

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.

The final piece was the AI interface.

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

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
```

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=...
```

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.

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:

``` js
const knowledgeBase = await getKnowledgeBase()
```

The query retrieves:

```
Stećci
Motifs
Sources
```

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]
```

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.
```

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.

I tested the Oracle with:

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
```

and

```
interpretation
```

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   │
                       └────────────┘
```

Sanity provides the structured information.

Gemini turns that information into a natural-language answer.

Next.js connects everything together.

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

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.

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
```

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

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.

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.

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

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

```
Question
   ↓
Search Sanity
   ↓
Relevant stećak
Relevant motifs
Relevant sources
   ↓
Gemini
```

That would scale much better.

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

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

That would make the evidence trail more visible.

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.

I would also add:

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](https://github.com/dkljajo/Ste-ak-Oracle-frontend-and-Gemini-integration)

The current version uses:

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.

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
```

This separation also makes the architecture easy to understand:

```
studio/
    → content model and Sanity configuration

web/
    → application and AI interface
```

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
```

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.

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.

The source code is available on GitHub:

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

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.

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.

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