diff --git a/.gitignore b/.gitignore
index f500892940542a8edf87c12a7c2502885f175003..db6a2a96fc1e7fae3d5a2f274b3e1f7536da7b4f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,6 @@ node_modules
 target
 .env
 public/**/*.js
-public/**/view/*
\ No newline at end of file
+public/**/view/*
+public/standard/test_sitemap.xml
+public/standard/sitemap.xml
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index c51b74f465564fd6274c1a98ddf84326592ac6b1..cfa0e817e6be045736e1bbea98e9c36866e019e0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -583,6 +583,15 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "chrono_utils"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f69ed74e2117892a1a4e05d31f6612178e8e827bfbd83bbf8ca8c1bcfbda710"
+dependencies = [
+ "chrono",
+]
+
 [[package]]
 name = "cipher"
 version = "0.3.0"
@@ -1284,6 +1293,7 @@ dependencies = [
  "rustls 0.18.1",
  "serde",
  "serde_json",
+ "sitemap",
  "time 0.2.27",
  "tokio",
  "wither",
@@ -2178,6 +2188,18 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "sitemap"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c697e1d88854f66d7d805e8b532e341661a4b8f49fab419be8e82b0cafdeb4d"
+dependencies = [
+ "chrono",
+ "chrono_utils",
+ "url",
+ "xml-rs",
+]
+
 [[package]]
 name = "slab"
 version = "0.4.4"
@@ -2956,3 +2978,9 @@ dependencies = [
  "winapi 0.2.8",
  "winapi-build",
 ]
+
+[[package]]
+name = "xml-rs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
diff --git a/Cargo.toml b/Cargo.toml
index 5a85d62a2b545ea8fa1186d258d0fe4c057da7a3..739a0298ba08e292c595723f136a32d05746bfe3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,3 +24,4 @@ dotenv = "0.15"
 time = "0.2.7"
 regex = "1.5"
 tokio = { version = "0.2", features = ["full"] }
+sitemap = "0.4.1"
diff --git a/Makefile b/Makefile
index ff3edcf9c4fdd3b8859ea796ebdb98ee05a43e94..b9e0d073ee7c5a04af2f6a7aef7e7dc820b0285e 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@ reload-api:
 	docker-compose restart kuadrado_server
 
 test:
-	RESOURCES_DIR="./" cargo test -- --test-threads=1
+	RESOURCES_DIR="./" CONTEXT=testing cargo test -- --test-threads=1
 
 doc:
 	cargo doc --no-deps
diff --git a/README.md b/README.md
index eb2bf92c2181e67e25e48ed3a16bc710a7fc49c6..f5fccb633e723b1d6385ee093309fb5a9cc93e5b 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,9 @@ _Env vars may be defined in a .env file at the root of the project_
 -   `CRYPT_KEY`: Any random ascii string that will be used to encrypt data like emails and passwords.
 
 
+## Tests
+Create a public/standard/test_sitemap.xml file with a sitemap structure in order to pass the tests.
+
 ## Admin panel - create a Play Button
 **Syntax**
 
diff --git a/admin-frontend/src/article.js b/admin-frontend/src/article.js
index c2cf4fb71a61cfe57ae1a23c1f6bfa4735790b3a..181e8ecfd972500d00ccc0400a76813c82ecdb3d 100644
--- a/admin-frontend/src/article.js
+++ b/admin-frontend/src/article.js
@@ -13,6 +13,7 @@ class Article {
             this.body = "";
             this.locale = "";
             this.display_priority_index = 1;
+            this.with_static_view = true;
             this.metadata = {
                 description: "",
             };
diff --git a/admin-frontend/src/components/create-article-form.js b/admin-frontend/src/components/create-article-form.js
index b728b5ac8f2f97725cab0788307e6ea8bc068d07..efda81086b643a19155a715e9d8f27fc91324b5f 100644
--- a/admin-frontend/src/components/create-article-form.js
+++ b/admin-frontend/src/components/create-article-form.js
@@ -26,6 +26,10 @@ class CreateArticleForm {
         this.state.output.metadata = metadata;
     }
 
+    handle_change_bool_checkbox(field, e) {
+        this.state.output[field] = e.target.checked;
+    }
+
     handle_text_input(field, e) {
         this.state.output[field] = e.target.value;
     }
@@ -335,6 +339,15 @@ class CreateArticleForm {
                     value: this.state.output.title,
                     oninput: this.handle_text_input.bind(this, "title")
                 },
+                {
+                    tag: "div", contents: [
+                        {
+                            tag: "input", type: "checkbox", checked: this.state.output.with_static_view,
+                            onchange: this.handle_change_bool_checkbox.bind(this, "with_static_view")
+                        },
+                        { tag: "label", contents: "Create static view" },
+                    ]
+                },
                 {
                     tag: "input", type: "text",
                     style_rules: {
diff --git a/mongo/scripts/migration.js b/mongo/scripts/migration.js
index 5e8e966680332b5e3f91e285eb9e854ae6ee74d8..3db6e73551411471d9e72bae3e3865205c9becf9 100644
--- a/mongo/scripts/migration.js
+++ b/mongo/scripts/migration.js
@@ -8,6 +8,11 @@ db.auth(adminname, adminpwd);
 articles = db.getCollection("articles");
 
 articles.update({},
-    { $set: { "metadata": { "description": "" } } },
+    {
+        $set: {
+            "metadata": { "description": "" },
+            "with_static_view": false
+        }
+    },
     { upsert: false, multi: true }
 );
\ No newline at end of file
diff --git a/src/model/article.rs b/src/model/article.rs
index 0b45eb0a64d711437e57ada64ee93accae774d5d..874ccfb868ca717ccbbf7462f6a9deb0e82d8448 100644
--- a/src/model/article.rs
+++ b/src/model/article.rs
@@ -35,6 +35,7 @@ pub struct Article {
     pub category: String,
     pub locale: String,
     pub display_priority_index: i8,
+    pub with_static_view: bool,
     pub metadata: ArticleMetadata,
 }
 
@@ -61,6 +62,7 @@ impl Article {
             category: "testing".to_string(),
             locale: "fr".to_string(),
             display_priority_index: 1,
+            with_static_view: true,
             metadata: ArticleMetadata {
                 description: "A test article".to_string(),
                 view_uri: None,
diff --git a/src/service/articles.rs b/src/service/articles.rs
index 7fa896c12d463f908efb8706aa9b416c93eeba77..6133cf292bf228dbf7dd111a14be83a424812b3c 100644
--- a/src/service/articles.rs
+++ b/src/service/articles.rs
@@ -43,20 +43,28 @@ fn slugify(s: &String) -> String {
 }
 
 fn create_article_static_view_metadata(app_state: &AppState, art: &mut Article) {
-    let article_slug = slugify(&art.title);
-    art.metadata.slug = Some(article_slug.to_owned());
-    art.metadata.view_uri = Some(format!(
-        "{}://{}/{}/{}/",
-        &app_state.env.server_protocol, &app_state.env.server_host, &art.category, &article_slug,
-    ));
-    art.metadata.static_resource_path = Some(
-        app_state
-            .env
-            .public_dir
-            .join(&art.category)
-            .join("view")
-            .join(&article_slug),
-    );
+    if art.with_static_view {
+        let article_slug = slugify(&art.title);
+        art.metadata.slug = Some(article_slug.to_owned());
+        art.metadata.view_uri = Some(format!(
+            "{}://{}/{}/view/{}/{}/",
+            &app_state.env.server_protocol,
+            &app_state.env.server_host,
+            &art.category,
+            &art.locale,
+            &article_slug,
+        ));
+
+        art.metadata.static_resource_path = Some(
+            app_state
+                .env
+                .public_dir
+                .join(&art.category)
+                .join("view")
+                .join(&art.locale)
+                .join(&article_slug),
+        );
+    }
 }
 
 async fn generate_article_view(app_state: &AppState, id: &ObjectId) -> Result<(), String> {
@@ -67,44 +75,10 @@ async fn generate_article_view(app_state: &AppState, id: &ObjectId) -> Result<()
             }
 
             let art = art.unwrap();
-
-            if art.metadata.view_uri.is_none() {
-                return Err(format!("Article {} doesn't have view uri", art.title));
-            }
-
-            let art_img_def = String::new();
-            let mut art_image_uri = art.images.iter().next().unwrap_or(&art_img_def).to_owned();
-
-            if !art_image_uri.is_empty() {
-                art_image_uri = format!("/assets/images/{}", art_image_uri);
+            if art.with_static_view {
+                return create_static_view(&app_state, &art);
             }
-
-            let slug = art.metadata.slug.unwrap();
-
-            let html = format!(
-                "
-<html lang='{}'>
-<head>
-    <meta charset='UTF-8'>
-    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
-    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
-    <meta name='author' content='Kuadrado Software' />
-    <meta name='image' content='{}'/>
-    <meta name='description' content='{}'>
-    <link rel='icon' type='image/svg+xml' href='/favicon.svg' />
-    <title>{}</title>
-    <link href='/style/style.css' rel='stylesheet' />
-</head>
-<body>
-    <div>{}<div>
-</body>
-<script type='text/javascript' src='/games/view.js'></script>
-</html>
-            ",
-                art.locale, art_image_uri, art.metadata.description, art.title, art.body,
-            );
-
-            create_static_view(&app_state, art.category, slug, html)
+            Ok(())
         }
         Err(e) => {
             return Err(format!(
@@ -178,10 +152,16 @@ pub async fn update_article(
     let mut article_data = article_data.into_inner();
     article_data.date = Some(DateTime(Utc::now()));
 
-    if let Err(e) = delete_static_view(&article_data.metadata.static_resource_path) {
-        return HttpResponse::InternalServerError()
-            .body(format!("Error removing previous static view : {}", e));
-    };
+    if article_data.with_static_view {
+        if let Err(e) = delete_static_view(
+            &app_state,
+            &article_data.metadata.static_resource_path,
+            &article_data.metadata.view_uri,
+        ) {
+            return HttpResponse::InternalServerError()
+                .body(format!("Error removing previous static view : {}", e));
+        };
+    }
 
     create_article_static_view_metadata(&app_state, &mut article_data);
 
@@ -223,13 +203,20 @@ pub async fn delete_article(
     };
 
     let articles = get_collection(&app_state);
+
     match articles.find_one(doc! {"_id": &article_id}, None).await {
         Ok(article) => match article {
             Some(art) => {
-                if let Err(e) = delete_static_view(&art.metadata.static_resource_path) {
-                    return HttpResponse::InternalServerError()
-                        .body(format!("Error removing article static view {}", e));
-                };
+                if art.with_static_view {
+                    if let Err(e) = delete_static_view(
+                        &app_state,
+                        &art.metadata.static_resource_path,
+                        &art.metadata.view_uri,
+                    ) {
+                        return HttpResponse::InternalServerError()
+                            .body(format!("Error removing article static view {}", e));
+                    };
+                }
 
                 match articles.delete_one(doc! {"_id": &article_id}, None).await {
                     Ok(_) => HttpResponse::Accepted().body("Article was deleted"),
@@ -339,12 +326,14 @@ mod test_articles {
         web::Bytes,
         App,
     };
+    use std::fs::remove_dir_all;
 
     async fn insert_test_article(
         app_state: &AppState,
         test_article: Article,
     ) -> Result<(ObjectId, String), String> {
         let title = test_article.title.to_owned();
+
         match get_collection(&app_state)
             .insert_one(test_article, None)
             .await
@@ -370,6 +359,17 @@ mod test_articles {
         }
     }
 
+    fn clear_testing_static_view_dir(app_state: &AppState) -> Result<(), String> {
+        let path = app_state.env.public_dir.join("testing/view");
+        if path.exists() && path.is_dir() {
+            let res = remove_dir_all(path);
+            if let Err(e) = res {
+                return Err(format!("Error removing testing stativ views {}", e));
+            }
+        }
+        Ok(())
+    }
+
     async fn get_authenticated_admin(app_state: &AppState) -> Administrator {
         Administrator::authenticated(
             app_state,
@@ -425,7 +425,17 @@ mod test_articles {
             .unwrap();
 
         assert!(find_inserted.is_some());
-        assert_eq!(find_inserted.unwrap().title, article.title);
+        let find_inserted = find_inserted.unwrap();
+        assert_eq!(find_inserted.title, article.title);
+
+        // Static view tests
+        let static_view_path = find_inserted.metadata.static_resource_path;
+        assert!(static_view_path.is_some());
+        let static_view_path = static_view_path.unwrap();
+        assert!(static_view_path.exists());
+
+        let cleared = clear_testing_static_view_dir(&app_state);
+        assert!(cleared.is_ok());
 
         get_collection(&app_state)
             .delete_one(doc! {"title": article.title}, None)
@@ -518,7 +528,17 @@ mod test_articles {
             .unwrap();
 
         assert!(find_inserted.is_some());
-        assert_eq!(find_inserted.unwrap().title, "changed title");
+        let find_inserted = find_inserted.unwrap();
+        assert_eq!(find_inserted.title, "changed title");
+
+        // Static view tests
+        let static_view_path = find_inserted.metadata.static_resource_path;
+        assert!(static_view_path.is_some());
+        let static_view_path = static_view_path.unwrap();
+        assert!(static_view_path.exists());
+
+        let cleared = clear_testing_static_view_dir(&app_state);
+        assert!(cleared.is_ok());
 
         let del_count = delete_test_article(&app_state, &article_id).await.unwrap();
         assert_eq!(del_count, 1);
diff --git a/src/static_view.rs b/src/static_view.rs
index e2e32949e6b1aa312180a6433728987c9c575f64..3d3436c2ba9807e2c396f91a049cbab56c714265 100644
--- a/src/static_view.rs
+++ b/src/static_view.rs
@@ -1,15 +1,27 @@
-use crate::app_state::AppState;
-use std::fs::{create_dir, create_dir_all, remove_dir, remove_file, File};
-use std::io::Write;
-use std::path::PathBuf;
+use crate::{app_state::AppState, model::Article};
+use chrono::{Datelike, FixedOffset, TimeZone, Utc};
+use sitemap::{
+    reader::{SiteMapEntity, SiteMapReader},
+    structs::UrlEntry,
+    writer::SiteMapWriter,
+};
+use std::{
+    fs::{create_dir, create_dir_all, remove_dir_all, remove_file, rename, File},
+    io::Write,
+    path::PathBuf,
+};
 
-pub fn create_static_view(
-    app_state: &AppState,
-    category: String,
-    filename: String,
-    html: String,
-) -> Result<(), String> {
-    let view_path = app_state.env.public_dir.join(&category).join("view");
+enum UpdateSitemapMode {
+    CreateUrl,
+    DeleteUrl,
+}
+
+pub fn create_static_view(app_state: &AppState, article: &Article) -> Result<(), String> {
+    let view_path = app_state
+        .env
+        .public_dir
+        .join(&article.category)
+        .join("view");
 
     if !view_path.exists() {
         if let Err(e) = create_dir(&view_path) {
@@ -17,46 +29,184 @@ pub fn create_static_view(
         }
     }
 
-    let d_path = app_state
-        .env
-        .public_dir
-        .join(&category)
-        .join("view")
-        .join(&filename);
+    let d_path = article.metadata.static_resource_path.as_ref().unwrap();
 
     if let Err(e) = create_dir_all(&d_path) {
         return Err(format!("Error creating directory {:?} : {}", d_path, e));
     }
 
+    let art_img_def = String::new();
+
+    let mut art_image_uri = article
+        .images
+        .iter()
+        .next()
+        .unwrap_or(&art_img_def)
+        .to_owned();
+
+    if !art_image_uri.is_empty() {
+        art_image_uri = format!("/assets/images/{}", art_image_uri);
+    }
+
+    let html = format!(
+        "
+<html lang='{}'>
+<head>
+    <meta charset='UTF-8'>
+    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
+    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
+    <meta name='author' content='Kuadrado Software' />
+    <meta name='image' content='{}'/>
+    <meta name='description' content='{}'>
+    <link rel='icon' type='image/svg+xml' href='/favicon.svg' />
+    <title>{}</title>
+    <link href='/style/style.css' rel='stylesheet' />
+</head>
+<body>
+    <div>{}<div>
+</body>
+<script type='text/javascript' src='/games/view.js'></script>
+</html>
+            ",
+        article.locale, art_image_uri, article.metadata.description, article.title, article.body,
+    );
+
     let f_path = d_path.join("index.html");
+
     match File::create(&f_path) {
         Ok(mut f) => {
             if let Err(e) = f.write_all(html.as_bytes()) {
                 return Err(format!("Error writing to {:?} : {}", f_path, e));
             }
+
+            if let Err(e) = update_sitemap(
+                app_state,
+                &article.metadata.view_uri,
+                UpdateSitemapMode::CreateUrl,
+            ) {
+                return Err(e);
+            };
+
             Ok(())
         }
         Err(e) => Err(format!("Error creating {:?} : {}", f_path, e)),
     }
 }
 
-pub fn delete_static_view(path: &Option<PathBuf>) -> Result<(), String> {
+pub fn delete_static_view(
+    app_state: &AppState,
+    path: &Option<PathBuf>,
+    uri: &Option<String>,
+) -> Result<(), String> {
     if let Some(path) = path {
         if path.exists() {
-            if let Err(e) = remove_file(path) {
-                return Err(format!("Error deleting static view at {:?} : {}", path, e));
-            };
-
             let parent = path.parent().unwrap();
 
-            if let Err(e) = remove_dir(parent) {
+            if let Err(e) = remove_dir_all(parent) {
                 return Err(format!(
-                    "Error deleting static view directory at {:?} : {}",
+                    "Error deleting static view at {:?} : {}",
                     parent, e
                 ));
             }
         }
     }
 
+    if let Err(e) = update_sitemap(app_state, uri, UpdateSitemapMode::DeleteUrl) {
+        return Err(e);
+    };
+
+    Ok(())
+}
+
+fn update_sitemap(
+    app_state: &AppState,
+    uri: &Option<String>,
+    mode: UpdateSitemapMode,
+) -> Result<(), String> {
+    if uri.is_none() {
+        return Ok(());
+    }
+
+    let sitemap_name = match std::env::var("CONTEXT") {
+        Ok(value) => {
+            if value.eq("testing") {
+                String::from("test_sitemap.xml")
+            } else {
+                String::from("sitemap.xml")
+            }
+        }
+        Err(_) => String::from("sitemap.xml"),
+    };
+
+    let standard_dir_pth = app_state.env.public_dir.join("standard");
+    let uri = uri.as_ref().unwrap().to_owned();
+
+    let sitemap =
+        File::open(standard_dir_pth.join(&sitemap_name)).expect("Couldn't open file sitemap.xml");
+
+    let mut urls = Vec::new();
+
+    for entity in SiteMapReader::new(sitemap) {
+        if let SiteMapEntity::Url(url_entry) = entity {
+            urls.push(url_entry.loc.get_url().unwrap().to_string());
+        }
+    }
+
+    let updated_sitemap = File::create(standard_dir_pth.join("tmp_sitemap.xml"))
+        .expect("Couldn't create temporary sitemap");
+
+    let writer = SiteMapWriter::new(updated_sitemap);
+    let mut url_writer = writer
+        .start_urlset()
+        .expect("Unable to write sitemap urlset");
+
+    match mode {
+        UpdateSitemapMode::CreateUrl => {
+            urls.push(uri);
+        }
+        UpdateSitemapMode::DeleteUrl => {
+            let mut updated_urls = Vec::new();
+            for u in urls {
+                if !u.eq(&uri) {
+                    updated_urls.push(u);
+                }
+            }
+            urls = updated_urls;
+        }
+    }
+
+    let now = Utc::today().naive_utc();
+
+    for u in urls {
+        url_writer
+            .url(
+                UrlEntry::builder()
+                    .loc(u)
+                    .lastmod(
+                        FixedOffset::west(0)
+                            .ymd(now.year(), now.month(), now.day())
+                            .and_hms(0, 0, 0),
+                    )
+                    .build()
+                    .unwrap(),
+            )
+            .expect("Unable to write url");
+    }
+
+    url_writer
+        .end()
+        .expect("Unable to write sitemap closing tags");
+
+    if let Err(e) = remove_file(standard_dir_pth.join(&sitemap_name)) {
+        return Err(format!("Error updating sitemap.xml {}", e));
+    };
+
+    if let Err(e) = rename(
+        standard_dir_pth.join("tmp_sitemap.xml"),
+        standard_dir_pth.join(&sitemap_name),
+    ) {
+        return Err(format!("Error updating sitemap.xml {}", e));
+    };
+
     Ok(())
 }