diff --git a/lektor_utils/src/dkmap.rs b/lektor_utils/src/dkmap.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3ddbf420bf30ce94a94b183966e15bdb02d08b63
--- /dev/null
+++ b/lektor_utils/src/dkmap.rs
@@ -0,0 +1,164 @@
+use anyhow::{bail, Result};
+use serde::{Deserialize, Deserializer, Serialize};
+use std::cmp::Eq;
+use std::collections::HashMap;
+use std::hash::Hash;
+
+#[derive(Debug, Serialize, Clone)]
+pub struct DKMap<K1: Hash + Eq, K2: Hash + Eq, Values> {
+    #[serde(skip)]
+    key_mapping_1: HashMap<K1, usize>,
+    #[serde(skip)]
+    key_mapping_2: HashMap<K2, usize>,
+
+    values: Vec<Values>,
+}
+
+#[derive(Debug, Deserialize, Clone)]
+struct DKMapDeserializeProxy<Values> {
+    values: Vec<Values>,
+}
+
+impl<K1, K2, RemainingValues> DKMap<K1, K2, (K1, K2, RemainingValues)>
+where
+    K1: Hash + Eq + Copy,
+    K2: Hash + Eq + Clone,
+    RemainingValues: Copy,
+{
+    pub fn get_k1(&self, key: &K1) -> Option<(K1, K2, RemainingValues)> {
+        if let Some(index) = self.key_mapping_1.get(key) {
+            Some(self.values[*index].clone())
+        } else {
+            None
+        }
+    }
+
+    pub fn get_k2(&self, key: &K2) -> Option<(K1, K2, RemainingValues)> {
+        if let Some(index) = self.key_mapping_2.get(key) {
+            Some(self.values[*index].clone())
+        } else {
+            None
+        }
+    }
+
+    pub fn update_from_k1(
+        &mut self,
+        key: &K1,
+        remaining_values: RemainingValues,
+    ) -> Result<(K1, K2, RemainingValues)> {
+        if let Some(index) = self.key_mapping_1.get(key) {
+            let ret = self.values[*index].clone();
+            self.values[*index] = (ret.0, ret.1.clone(), remaining_values);
+            Ok(ret)
+        } else {
+            bail!("Called DKMap::update_from_k1() on a non existing key")
+        }
+    }
+
+    pub fn update_from_k2(
+        &mut self,
+        key: &K2,
+        remaining_values: RemainingValues,
+    ) -> Result<(K1, K2, RemainingValues)> {
+        if let Some(index) = self.key_mapping_2.get(key) {
+            let ret = self.values[*index].clone();
+            self.values[*index] = (ret.0, ret.1.clone(), remaining_values);
+            Ok(ret)
+        } else {
+            bail!("Called DKMap::update_from_k2() on a non existing key")
+        }
+    }
+
+    pub fn insert(
+        &mut self,
+        value: (K1, K2, RemainingValues),
+    ) -> Option<(K1, K2, RemainingValues)> {
+        let k1 = value.0;
+        let k2 = value.1;
+        let remaining_values = value.2;
+
+        // let's arbitrarily do it through K1
+        if let Some(index) = self.key_mapping_1.get(&k1) {
+            let old_values = self.values[*index].clone();
+            self.values[*index] = (k1, k2, remaining_values);
+            Some(old_values)
+        } else {
+            self.values.push((k1, k2.clone(), remaining_values));
+            let index = self.values.len() - 1;
+            self.key_mapping_1.insert(k1, index);
+            self.key_mapping_2.insert(k2, index);
+            None
+        }
+    }
+}
+
+impl<K1, K2, RemainingValues> FromIterator<(K1, K2, RemainingValues)>
+    for DKMap<K1, K2, (K1, K2, RemainingValues)>
+where
+    K1: Hash + Eq + Copy,
+    K2: Hash + Eq + Clone,
+    RemainingValues: Copy,
+{
+    fn from_iter<T: IntoIterator<Item = (K1, K2, RemainingValues)>>(iter: T) -> Self {
+        let mut len = 0;
+        let iter = iter.into_iter().map(move |e| {
+            len = len + 1;
+            e
+        });
+
+        let mut ret = Self {
+            key_mapping_1: HashMap::new(),
+            key_mapping_2: HashMap::new(),
+            values: Vec::with_capacity(len),
+        };
+
+        for (key1, key2, remaining_values) in iter.into_iter() {
+            ret.insert((key1, key2, remaining_values));
+        }
+
+        ret
+    }
+}
+
+impl<K1, K2, RemainingValues> IntoIterator for DKMap<K1, K2, (K1, K2, RemainingValues)>
+where
+    K1: Hash + Eq + Copy,
+    K2: Hash + Eq + Clone,
+    RemainingValues: Copy,
+{
+    type Item = (K1, K2, RemainingValues);
+    type IntoIter = std::vec::IntoIter<Self::Item>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.values.into_iter()
+    }
+}
+
+// This is the trait that informs Serde how to deserialize MyMap.
+impl<'de, K1, K2, RemainingValues> Deserialize<'de> for DKMap<K1, K2, (K1, K2, RemainingValues)>
+where
+    K1: Hash + Eq + Copy + Deserialize<'de>,
+    K2: Hash + Eq + Clone + Deserialize<'de>,
+    RemainingValues: Copy + Deserialize<'de>,
+{
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let proxy = DKMapDeserializeProxy::<(K1, K2, RemainingValues)>::deserialize(deserializer)?;
+
+        let mut key_mapping_1 = HashMap::<K1, usize>::new();
+        let mut key_mapping_2 = HashMap::<K2, usize>::new();
+
+        for (index, (k1, k2, _)) in proxy.values.clone().into_iter().enumerate() {
+            key_mapping_1.insert(k1, index);
+            key_mapping_2.insert(k2, index);
+        }
+
+        Ok(DKMap {
+            key_mapping_1,
+            key_mapping_2,
+            values: proxy.values,
+        })
+    }
+}
diff --git a/lektor_utils/src/lib.rs b/lektor_utils/src/lib.rs
index 2b11df962f9ea14fb96d7e8046ebbc314e51c898..6fd5c39782aa6f469f696a82f71de35c3f7961ef 100644
--- a/lektor_utils/src/lib.rs
+++ b/lektor_utils/src/lib.rs
@@ -28,6 +28,7 @@ pub mod appimage;
 pub mod pathdiff;
 
 pub mod config;
+pub mod dkmap;
 pub mod logger;
 pub mod open;
 pub mod pushvec;