diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
new file mode 100644
index 0000000000000000000000000000000000000000..35767b0942d1937c513f5829b597f707d589ae3a
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
@@ -0,0 +1,137 @@
+package fr.inra.urgi.faidare.web.study;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.Lists;
+import fr.inra.urgi.faidare.api.NotFoundException;
+import fr.inra.urgi.faidare.config.FaidareProperties;
+import fr.inra.urgi.faidare.domain.criteria.GermplasmPOSTSearchCriteria;
+import fr.inra.urgi.faidare.domain.data.TrialVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
+import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
+import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.repository.es.GermplasmRepository;
+import fr.inra.urgi.faidare.repository.es.StudyRepository;
+import fr.inra.urgi.faidare.repository.es.TrialRepository;
+import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository;
+import fr.inra.urgi.faidare.repository.file.CropOntologyRepository;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * Controller used to display a study card based on its ID.
+ * @author JB Nizet
+ */
+@Controller("webStudyController")
+@RequestMapping("/studies")
+public class StudyController {
+
+    private final StudyRepository studyRepository;
+    private final FaidareProperties faidareProperties;
+    private final XRefDocumentRepository xRefDocumentRepository;
+    private final GermplasmRepository germplasmRepository;
+    private final CropOntologyRepository cropOntologyRepository;
+    private final TrialRepository trialRepository;
+
+    public StudyController(StudyRepository studyRepository,
+                           FaidareProperties faidareProperties,
+                           XRefDocumentRepository xRefDocumentRepository,
+                           GermplasmRepository germplasmRepository,
+                           CropOntologyRepository cropOntologyRepository,
+                           TrialRepository trialRepository) {
+        this.studyRepository = studyRepository;
+        this.faidareProperties = faidareProperties;
+        this.xRefDocumentRepository = xRefDocumentRepository;
+        this.germplasmRepository = germplasmRepository;
+        this.cropOntologyRepository = cropOntologyRepository;
+        this.trialRepository = trialRepository;
+    }
+
+    @GetMapping("/{studyId}")
+    public ModelAndView site(@PathVariable("studyId") String studyId) {
+        StudyDetailVO study = studyRepository.getById(studyId);
+
+        // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find(
+        //     XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId()));
+        List<XRefDocumentVO> crossReferences = Arrays.asList(
+            createXref("foobar"),
+            createXref("bazbing")
+        );
+
+        // LocationVO site = createSite();
+
+        if (study == null) {
+            throw new NotFoundException("Study with ID " + studyId + " not found");
+        }
+
+        List<GermplasmVO> germplasms = getGermplasms(study);
+        List<ObservationVariableVO>variables = getVariables(study);
+        List<TrialVO> trials = getTrials(study);
+
+        return new ModelAndView("study",
+                                "model",
+                                new StudyModel(
+                                    study,
+                                    faidareProperties.getByUri(study.getSourceUri()),
+                                    germplasms,
+                                    variables,
+                                    trials,
+                                    crossReferences
+                                )
+        );
+    }
+
+    private List<GermplasmVO> getGermplasms(StudyDetailVO study) {
+        if (study.getGermplasmDbIds() == null || study.getGermplasmDbIds().isEmpty()) {
+            return Collections.emptyList();
+        } else {
+            GermplasmPOSTSearchCriteria germplasmCriteria = new GermplasmPOSTSearchCriteria();
+            germplasmCriteria.setGermplasmDbIds(Lists.newArrayList(study.getGermplasmDbIds()));
+            return germplasmRepository.find(germplasmCriteria)
+                .stream()
+                .sorted(Comparator.comparing(GermplasmVO::getGermplasmName))
+                .collect(Collectors.toList());
+        }
+    }
+
+    private List<ObservationVariableVO> getVariables(StudyDetailVO study) {
+        Set<String> variableIds = studyRepository.getVariableIds(study.getStudyDbId());
+        return cropOntologyRepository.getVariableByIds(variableIds)
+            .stream()
+            .sorted(Comparator.comparing(ObservationVariableVO::getObservationVariableDbId))
+            .collect(Collectors.toList());
+    }
+
+    private List<TrialVO> getTrials(StudyDetailVO study) {
+        if (study.getTrialDbIds() == null || study.getTrialDbIds().isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        return study.getTrialDbIds()
+                    .stream()
+                    .sorted(Comparator.naturalOrder())
+                    .map(trialRepository::getById)
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toList());
+    }
+
+    private XRefDocumentVO createXref(String name) {
+        XRefDocumentVO xref = new XRefDocumentVO();
+        xref.setName(name);
+        xref.setDescription("A very large description for the xref " + name + " which has way more than 120 characters bla bla bla bla bla bla bla bla bla bla bla bla");
+        xref.setDatabaseName("db_" + name);
+        xref.setUrl("https://google.com");
+        xref.setEntryType("type " + name);
+        return xref;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..980c78d43d3dcae233c2371de2f80add8f11d421
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
@@ -0,0 +1,82 @@
+package fr.inra.urgi.faidare.web.study;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.data.TrialVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
+import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
+import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
+import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+
+/**
+ * The model used by the study page
+ * @author JB Nizet
+ */
+public final class StudyModel {
+    private final StudyDetailVO study;
+    private final DataSource source;
+    private final List<GermplasmVO> germplasms;
+    private final List<ObservationVariableVO> variables;
+    private final List<TrialVO> trials;
+    private final List<XRefDocumentVO> crossReferences;
+    private final List<Map.Entry<String, Object>> additionalInfoProperties;
+
+    public StudyModel(StudyDetailVO study,
+                      DataSource source,
+                      List<GermplasmVO> germplasms,
+                      List<ObservationVariableVO> variables,
+                      List<TrialVO> trials,
+                      List<XRefDocumentVO> crossReferences) {
+        this.study = study;
+        this.source = source;
+        this.germplasms = germplasms;
+        this.variables = variables;
+        this.trials = trials;
+        this.crossReferences = crossReferences;
+
+        Map<String, Object> additionalInfo =
+            study.getAdditionalInfo() == null ? Collections.emptyMap() : study.getAdditionalInfo().getProperties();
+        this.additionalInfoProperties =
+            additionalInfo.entrySet()
+                          .stream()
+                          .filter(entry -> entry.getValue() != null && !entry.getValue().toString().isEmpty())
+                          .sorted(Map.Entry.comparingByKey())
+                          .collect(Collectors.toList());
+    }
+
+    public StudyDetailVO getStudy() {
+        return study;
+    }
+
+    public DataSource getSource() {
+        return source;
+    }
+
+    public List<XRefDocumentVO> getCrossReferences() {
+        return crossReferences;
+    }
+
+    public List<GermplasmVO> getGermplasms() {
+        return germplasms;
+    }
+
+    public List<ObservationVariableVO> getVariables() {
+        return variables;
+    }
+
+    public List<TrialVO> getTrials() {
+        return trials;
+    }
+
+    public List<Map.Entry<String, Object>> getAdditionalInfoProperties() {
+        return additionalInfoProperties;
+    }
+}
diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html
new file mode 100644
index 0000000000000000000000000000000000000000..a1cafc653a5706954e1a30249a63f68ce538abae
--- /dev/null
+++ b/backend/src/main/resources/templates/study.html
@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+
+<html
+  xmlns:th="http://www.thymeleaf.org"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+>
+<head>
+  <title>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></title>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+
+<body>
+<main>
+  <h1>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></h1>
+
+  <h2>Identification</h2>
+
+  <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div>
+  <div th:replace="fragments/row::text-row(label='Identifier', text=${model.study.studyDbId})"></div>
+
+  <th:block th:if="${model.source != null}">
+    <div th:replace="fragments/row::row(label='Source', content=~{::#source}, text='')">
+      <a id="source" target="_blank" th:href="${model.source.url}">
+        <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" />
+      </a>
+    </div>
+  </th:block>
+
+  <th:block th:if="${model.study.url != null && model.source != null}">
+    <div th:replace="fragments/row::row(label='Data link', content=~{::#url}, text='')">
+      <a id="url" target="_blank" th:href="${model.study.url}">
+        Link to this study on <th:block th:text="${model.source.name}" />
+      </a>
+    </div>
+  </th:block>
+
+  <div th:replace="fragments/row::text-row(label='Project name', text=${model.study.programName})"></div>
+  <div th:replace="fragments/row::text-row(label='Description', text=${model.study.studyDescription})"></div>
+  <th:block th:if="${model.study.active != null}">
+    <div th:replace="fragments/row::text-row(label='Active', text=${model.study.active ? 'Yes' : 'No'})"></div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.study.seasons)}">
+    <div th:replace="fragments/row::text-row(label='Seasons', text=${#strings.listJoin(model.study.seasons, ',')})"></div>
+  </th:block>
+  <th:block th:if="${model.study.startDate != null && model.study.endDate != null}">
+    <div th:replace="fragments/row::text-row(label='Date', text=${'From ' + #dates.format(model.study.startDate, 'yyyy-MM-dd') + ' to ' + #dates.format(model.study.endDate, 'yyyy-MM-dd') })"></div>
+  </th:block>
+  <th:block th:if="${model.study.startDate != null && model.study.endDate == null}">
+    <div th:replace="fragments/row::text-row(label='Date', text=${'Started on ' + #dates.format(model.study.startDate, 'yyyy-MM-dd')})"></div>
+  </th:block>
+
+  <th:block th:if="${model.study.locationDbId}">
+    <div th:replace="fragments/row::row(label='Location name', content=~{::#location})">
+      <a id="location" th:href="@{/sites/{siteId}(siteId=${model.study.locationDbId})}" th:text="${model.study.locationName}"></a>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.study.dataLinks)}">
+    <div th:replace="fragments/row::row(label='Data files', content=~{::#data-files})">
+      <ul id="data-files" class="list-unstyled">
+        <li th:each="dataLink : ${model.study.dataLinks}">
+          <a target="_blank" th:href="${dataLink.url}" th:text="${dataLink.name}"></a>
+        </li>
+      </ul>
+    </div>
+  </th:block>
+
+  <th:block th:unles="${#lists.isEmpty(model.germplasms)}">
+    <h2>Genotype</h2>
+
+    <div class="table-responsive scroll-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <thead>
+            <tr>
+              <th scope="col">Accession number</th>
+              <th scope="col">Name</th>
+              <th scope="col">Taxon</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.germplasms}">
+              <td>
+                <a th:href="@{/germplasms/{germplasmId}(germplasmId=${row.germplasmDbId})}" th:text="${row.accessionNumber}"></a>
+              </td>
+              <td th:text="${row.germplasmName}"></td>
+              <td th:text="${(row.genus == null ? '' : row.genus) + ' ' + (row.species == null ? '' : row.species)+ ' ' + (row.subtaxa == null ? '' : row.subtaxa) }"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.variables)}">
+    <h2>Variables</h2>
+    <div class="table-responsive scroll-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <thead>
+            <tr>
+              <th scope="col">Variable ID</th>
+              <th scope="col">Variable short name</th>
+              <th scope="col">Variable long name</th>
+              <th scope="col">Ontology name</th>
+              <th scope="col">Trait description</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.variables}">
+              <td>
+                <a th:unless="${#strings.isEmpty(row.documentationURL)}" th:href="${row.documentationURL}" th:text="${row.observationVariableDbId}" target="_blank" ></a>
+                <span th:if="${#strings.isEmpty(row.documentationURL)}" th:text="${row.observationVariableDbId}"></span>
+              </td>
+              <td th:text="${row.name}"></td>
+              <td th:text="${#lists.isEmpty(row.synonyms) ? '' : row.synonyms[0]}"></td>
+              <td th:text="${row.ontologyName}"></td>
+              <td th:text="${row.trait.description}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.trials)}">
+    <h2>Data Set</h2>
+    <div class="table-responsive scroll-big-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <thead>
+            <tr>
+              <th scope="col">Name</th>
+              <th scope="col">Type</th>
+              <th scope="col">Linked studies identifier</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.trials}">
+              <td>
+                <a th:unless="${#strings.isEmpty(row.documentationURL)}" th:href="${row.documentationURL}" th:text="${row.trialName}" target="_blank" ></a>
+                <span th:if="${#strings.isEmpty(row.documentationURL)}" th:text="${row.trialName}"></span>
+              </td>
+              <td th:text="${row.trialType}"></td>
+              <td style="width: 60%">
+                <th:block th:each="trialStudy, iterStat : ${row.studies}"
+                          th:if="${trialStudy.studyDbId != model.study.studyDbId}">
+                  <a th:href="@{/studies/{studyId}(studyId=${trialStudy.studyDbId})}"
+                     th:text="${trialStudy.studyName.trim()}">
+                  </a><th:block th:if="${iterStat.last}">; </th:block>
+                </th:block>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.study.contacts)}">
+    <h2>Contact</h2>
+    <div class="table-responsive scroll-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <thead>
+            <tr>
+              <th scope="col">Role</th>
+              <th scope="col">Name</th>
+              <th scope="col">Email</th>
+              <th scope="col">Institution</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.study.contacts}">
+              <td th:text="${row.type}"></td>
+              <td th:text="${row.name}"></td>
+              <td th:text="${row.email}"></td>
+              <td th:text="${row.institutionName}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.additionalInfoProperties)}">
+    <h2>Additional information</h2>
+    <div class="table-responsive scroll-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <tbody>
+            <tr th:each="row : ${model.additionalInfoProperties}">
+              <td style="width: 50%;" th:text="${row.key}"></td>
+              <td th:text="${row.value}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
+</main>
+</body>
+</html>