diff --git a/.gitignore b/.gitignore
index 2eea525d885d5148108f6f3a9a8613863f783d36..8c09811d196e402bd273d828868eacd5ae8e855d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-.env
\ No newline at end of file
+.env
+backoffice.db
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 2dd264bb9de099efcccaa667f4818af2de39830f..4c494f0f08e45b6cd8c98314131f99f2e6a2ed11 100644
--- a/go.mod
+++ b/go.mod
@@ -7,5 +7,6 @@ require (
 	golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
 	golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
 	gorm.io/driver/mysql v1.1.1
+	gorm.io/driver/sqlite v1.1.4
 	gorm.io/gorm v1.21.11
 )
diff --git a/go.sum b/go.sum
index bd99c0a6a6660d48b7d14b3cfd06992f7f02d3e8..6dbdd2a401fefe57f72c4ba809bd26bdda3a7f0f 100644
--- a/go.sum
+++ b/go.sum
@@ -105,6 +105,7 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
 github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -113,6 +114,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
+github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
 github.com/nicolaspernoud/vestibule v0.0.0-20210626100803-e2554e116746 h1:mMpAetOOm54X87qjKq+RiSNutdULgFWp1knqhUeYf4s=
 github.com/nicolaspernoud/vestibule v0.0.0-20210626100803-e2554e116746/go.mod h1:zQIZ4A7ZYJBcS/DBZpMadr5N8WrATlj7267VlvKSX88=
 github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
@@ -386,6 +389,9 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gorm.io/driver/mysql v1.1.1 h1:yr1bpyqiwuSPJ4aGGUX9nu46RHXlF8RASQVb1QQNcvo=
 gorm.io/driver/mysql v1.1.1/go.mod h1:KdrTanmfLPPyAOeYGyG+UpDys7/7eeWT1zCq+oekYnU=
+gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
+gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
+gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
 gorm.io/gorm v1.21.11 h1:CxkXW6Cc+VIBlL8yJEHq+Co4RYXdSLiMKNvgoZPjLK4=
 gorm.io/gorm v1.21.11/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
diff --git a/internal/backoffice/backoffice.go b/internal/backoffice/backoffice.go
deleted file mode 100644
index 135175db29963e4ae75c9707bf1b96ebd7d572d5..0000000000000000000000000000000000000000
--- a/internal/backoffice/backoffice.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package backoffice
-
-import (
-	"io/ioutil"
-	"log"
-	"net/http"
-
-	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/post"
-)
-
-func HandleCreatePost(w http.ResponseWriter, r *http.Request) {
-	if r.Method != http.MethodPost {
-		w.WriteHeader(http.StatusMethodNotAllowed)
-		return
-	}
-
-	body, err := ioutil.ReadAll(r.Body)
-	if err != nil {
-		w.WriteHeader(http.StatusInternalServerError)
-		return
-	}
-
-	err = post.Create(string(body))
-	if err != nil {
-		w.WriteHeader(http.StatusInternalServerError)
-		return
-	}
-
-	log.Printf("| New post | %v", r.RemoteAddr)
-}
diff --git a/internal/database/database.go b/internal/database/database.go
new file mode 100644
index 0000000000000000000000000000000000000000..7113933d37f7e262cea072d17415a2c7aa8e2351
--- /dev/null
+++ b/internal/database/database.go
@@ -0,0 +1,58 @@
+package database
+
+import (
+	"fmt"
+
+	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common"
+	"gorm.io/driver/mysql"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+)
+
+type MonthlyReport struct {
+	gorm.Model
+	Month       int
+	Year        int
+	ContentType string
+	Content     string
+}
+
+var (
+	db         *gorm.DB
+	dbUser     = common.StringValueFromEnv("DATABASE_USER", "")
+	dbPassword = common.StringValueFromEnv("DATABASE_PASSWORD", "")
+	dbName     = common.StringValueFromEnv("DATABASE_NAME", "")
+)
+
+func init() {
+	var err error
+	if dbUser == "" || dbPassword == "" || dbName == "" {
+		db, err = gorm.Open(sqlite.Open("backoffice.db"), &gorm.Config{})
+		if err != nil {
+			panic("failed to connect database")
+		}
+	} else {
+		dsn := fmt.Sprintf("%v:%v@tcp(127.0.0.1:3306)/%v?charset=utf8mb4&parseTime=True&loc=Local", dbUser, dbPassword, dbName)
+		db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
+		if err != nil {
+			panic("failed to connect database")
+		}
+	}
+	// Migrate the schema
+	db.AutoMigrate(&MonthlyReport{})
+}
+
+func Exists(month int, year int, contentType string, content string) bool {
+	var monthlyReport MonthlyReport
+	if err := db.Where("month = ? AND year = ? AND content_type = ? AND content = ?", content).First(&monthlyReport).Error; err != nil {
+		return false
+	}
+	return true
+}
+
+func Create(month int, year int, contentType string, content string) error {
+	if err := db.Create(&MonthlyReport{Month: month, Year: year, ContentType: contentType, Content: content}).Error; err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/internal/monthlyNews/monthlyNews.go b/internal/monthlyNews/monthlyNews.go
new file mode 100644
index 0000000000000000000000000000000000000000..d934a4cb1566a90c7407a20b7297e0a450c04a4f
--- /dev/null
+++ b/internal/monthlyNews/monthlyNews.go
@@ -0,0 +1,71 @@
+package monthlyNews
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+
+	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/database"
+)
+
+func ProcessMonthlyNews(w http.ResponseWriter, r *http.Request) {
+	switch method := r.Method; method {
+	case "GET":
+		SendMonthlyNews(w, r)
+	case "POST":
+		AddMonthlyNews(w, r)
+	case "DELETE":
+		DeleteMonthlyNews(w, r)
+	case "PUT":
+		UpdateMonthlyNews(w, r)
+	default:
+		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+	}
+}
+
+func AddMonthlyNews(w http.ResponseWriter, r *http.Request) {
+
+	if r.Body == http.NoBody {
+		http.Error(w, "request body is empty", http.StatusBadRequest)
+		return
+	}
+
+	type MonthlyNews struct {
+		Month  int    `json:"month"`
+		Year   int    `json:"year"`
+		Header string `json:"header"`
+		Quote  string `json:"quote"`
+	}
+
+	decoder := json.NewDecoder(r.Body)
+	var monthlyNews MonthlyNews
+	err := decoder.Decode(&monthlyNews)
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	err = database.Create(monthlyNews.Month, monthlyNews.Year, "header", monthlyNews.Header)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	err = database.Create(monthlyNews.Month, monthlyNews.Year, "quote", monthlyNews.Quote)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	log.Printf("| new monthly news | %v", r.RemoteAddr)
+}
+
+func SendMonthlyNews(w http.ResponseWriter, r *http.Request) {}
+
+// TODO:
+func UpdateMonthlyNews(w http.ResponseWriter, r *http.Request) {}
+
+// TODO:
+func DeleteMonthlyNews(w http.ResponseWriter, r *http.Request) {}
+
+// TODO:
diff --git a/internal/monthlyReport/monthlyReport.go b/internal/monthlyReport/monthlyReport.go
new file mode 100644
index 0000000000000000000000000000000000000000..65696c6df6ce895b47dafe302bcaf63062583226
--- /dev/null
+++ b/internal/monthlyReport/monthlyReport.go
@@ -0,0 +1 @@
+package monthlyReport
diff --git a/internal/poll/poll.go b/internal/poll/poll.go
new file mode 100644
index 0000000000000000000000000000000000000000..92424266f61d7275605b234508342938127e3e58
--- /dev/null
+++ b/internal/poll/poll.go
@@ -0,0 +1 @@
+package poll
diff --git a/internal/post/post.go b/internal/post/post.go
deleted file mode 100644
index 79a0d7adbb628dea14da062767f152ba5bf30b37..0000000000000000000000000000000000000000
--- a/internal/post/post.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package post
-
-import (
-	"fmt"
-
-	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common"
-	"gorm.io/driver/mysql"
-	"gorm.io/gorm"
-)
-
-type Post struct {
-	gorm.Model
-	Content string
-}
-
-var (
-	db         *gorm.DB
-	dbUser     = common.StringValueFromEnv("DATABASE_USER", "")
-	dbPassword = common.StringValueFromEnv("DATABASE_PASSWORD", "")
-	dbName     = common.StringValueFromEnv("DATABASE_NAME", "")
-)
-
-func init() {
-	var err error
-	dsn := fmt.Sprintf("%v:%v@tcp(127.0.0.1:3306)/%v?charset=utf8mb4&parseTime=True&loc=Local", dbUser, dbPassword, dbName)
-	db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
-	if err != nil {
-		panic("failed to connect database")
-	}
-	// Migrate the schema
-	db.AutoMigrate(&Post{})
-}
-
-func Exists(content string) bool {
-	var post Post
-	if err := db.Where("content = ?", content).First(&post).Error; err != nil {
-		return false
-	}
-	return true
-}
-
-func Create(content string) error {
-	if err := db.Create(&Post{Content: content}).Error; err != nil {
-		return err
-	}
-	return nil
-}
diff --git a/internal/rootmux/rootmux.go b/internal/rootmux/rootmux.go
index e628d182e03ac38d576e485fde08429676196955..40618d32e64e0bd97f132123d254a0c12dba649d 100644
--- a/internal/rootmux/rootmux.go
+++ b/internal/rootmux/rootmux.go
@@ -5,8 +5,8 @@ import (
 	"os"
 
 	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/auth"
-	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/backoffice"
 	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common"
+	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/monthlyNews"
 	"github.com/nicolaspernoud/vestibule/pkg/middlewares"
 )
 
@@ -26,7 +26,7 @@ func CreateRootMux(staticDir string) RootMux {
 	mainMux.Handle("/api/common/WhoAmI", auth.ValidateAuthMiddleware(auth.WhoAmI(), []string{"*"}, false))
 
 	adminMux := http.NewServeMux()
-	adminMux.HandleFunc("/newPost", backoffice.HandleCreatePost)
+	adminMux.HandleFunc("/monthlyNews/", monthlyNews.ProcessMonthlyNews)
 	mainMux.Handle("/api/admin/", http.StripPrefix("/api/admin", auth.ValidateAuthMiddleware(adminMux, []string{os.Getenv("ADMIN_ROLE")}, true)))
 
 	// Serve static files falling back to serving index.html
diff --git a/internal/rootmux/rootmux_test.go b/internal/rootmux/rootmux_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..0b57990f613bb17c37d2ad6832f87c762147ff3b
--- /dev/null
+++ b/internal/rootmux/rootmux_test.go
@@ -0,0 +1,115 @@
+package rootmux
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/http/cookiejar"
+	"net/http/httptest"
+	"net/url"
+	"os"
+	"testing"
+
+	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/auth"
+	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/mocks"
+	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/tester"
+	"forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/tokens"
+)
+
+var (
+	newMonthlyNews = `{"month": 1,"year": 2000,"header":"newsHeader","quote":"newsQuote"}`
+	noH            map[string]string
+)
+
+func init() {
+	tokens.Init("testdata/tokenskey.json", true)
+}
+
+func TestAll(t *testing.T) {
+	// Create the mock OAuth2 server
+	oAuth2Server := httptest.NewServer(mocks.CreateMockOAuth2())
+	defer oAuth2Server.Close()
+	// Create the mock API server
+	go http.ListenAndServe(":8091", mocks.CreateMockAPI())
+	// Set the constants with environment variables
+	os.Setenv("HOSTNAME", "localhost")
+	os.Setenv("ADMIN_ROLE", "ADMINS")
+	os.Setenv("CLIENT_ID", "foo")
+	os.Setenv("CLIENT_SECRET", "bar")
+	os.Setenv("TOKEN_URL", oAuth2Server.URL+"/token")
+	os.Setenv("USERINFO_URL", oAuth2Server.URL+"/userinfo")
+	os.Setenv("LOGOUT_URL", oAuth2Server.URL+"/logout")
+	// Set up testers
+	os.Setenv("AUTH_URL", oAuth2Server.URL+"/auth-wrong-state") // Set the server to access failing OAuth2 endpoints
+	oauth2Tests(t)
+	os.Setenv("AUTH_URL", oAuth2Server.URL+"/auth") // Set the server to access the correct OAuth2Endpoint
+	unloggedTests(t)
+
+	os.Setenv("USERINFO_URL", oAuth2Server.URL+"/admininfo")
+	adminTests(t)
+
+}
+
+/**
+SECURITY TESTS (this tests are to check that the security protections works)
+**/
+func oauth2Tests(t *testing.T) {
+	// Create the tester
+	ts, do, _ := createTester(t)
+	defer ts.Close() // Close the tester
+	// Try to login (must fail)
+	do("GET", "/OAuth2Login", noH, "", http.StatusInternalServerError, "invalid oauth state")
+}
+
+/**
+UNLOGGED USER TESTS (this tests are to check that the security protections works)
+**/
+func unloggedTests(t *testing.T) {
+	// Create the tester
+	ts, do, _ := createTester(t)
+	defer ts.Close() // Close the tester
+
+	// Try to create a monthlyNews (must fail)
+	do("POST", "/api/admin/monthlyNews/", noH, newMonthlyNews, http.StatusUnauthorized, "error extracting token")
+}
+
+/**
+ADMIN TESTS (this tests are to check that an administrator can alter the apps)
+**/
+func adminTests(t *testing.T) {
+	// Create the tester
+	ts, do, _ := createTester(t)
+	defer ts.Close() // Close the tester
+	tests := func() {
+		// Get the XSRF Token
+		response := do("GET", "/api/common/WhoAmI", noH, "", http.StatusOK, "")
+		token := auth.TokenData{}
+		json.Unmarshal([]byte(response), &token)
+		xsrfHeader := map[string]string{"XSRF-TOKEN": token.XSRFToken}
+		// Try to create a monthly news without the XSRF-TOKEN (must fail)
+		do("POST", "/api/admin/monthlyNews/", noH, newMonthlyNews, http.StatusUnauthorized, "XSRF")
+		// Try to create an app (must pass)
+		do("POST", "/api/admin/monthlyNews/", xsrfHeader, newMonthlyNews, http.StatusOK, "")
+	}
+	// Try to login (must pass)
+	do("GET", "/OAuth2Login", noH, "", http.StatusOK, "<!DOCTYPE html>")
+	// Run the tests
+	tests()
+	// Try to logout (must pass)
+	do("GET", "/Logout", noH, "", http.StatusOK, "Logout OK")
+	// Try to create a monthly news again (must fail)
+	do("GET", "/api/admin/monthlyNews/", noH, "", http.StatusUnauthorized, "error extracting token")
+}
+
+func createTester(t *testing.T) (*httptest.Server, tester.DoFn, tester.DoFn) {
+	// Create the server
+	mux := CreateRootMux("../../web")
+	ts := httptest.NewServer(mux.Mux)
+	url, _ := url.Parse(ts.URL)
+	port := url.Port()
+	mux.Manager.Config.RedirectURL = "http://" + os.Getenv("HOSTNAME") + ":" + port + "/OAuth2Callback"
+	mux.Manager.Hostname = "http://" + os.Getenv("HOSTNAME") + ":" + port
+	// Create the cookie jar
+	jar, _ := cookiejar.New(nil)
+	// wrap the testing function
+	return ts, tester.CreateServerTester(t, port, os.Getenv("HOSTNAME"), jar), tester.CreateServerTester(t, port, os.Getenv("HOSTNAME"), nil)
+}
diff --git a/internal/tester/tester.go b/internal/tester/tester.go
new file mode 100644
index 0000000000000000000000000000000000000000..d7c6a6739062d632843205b3a7de13ef13795618
--- /dev/null
+++ b/internal/tester/tester.go
@@ -0,0 +1,92 @@
+package tester
+
+import (
+	"context"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"net/http/cookiejar"
+	"net/http/httptest"
+	"net/url"
+	"strings"
+	"testing"
+	"time"
+)
+
+type DoFn func(method string, url string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string
+
+// DoRequestOnHandler does a request on a router (or handler) and check the response
+func DoRequestOnHandler(t *testing.T, router http.Handler, method string, route string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string {
+	req, err := http.NewRequest(method, route, strings.NewReader(payload))
+	if err != nil {
+		t.Fatal(err)
+	}
+	for i, v := range headers {
+		req.Header.Set(i, v)
+	}
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, req)
+	if status := rr.Code; status != expectedStatus {
+		t.Errorf("Tested %v %v %v ; handler returned wrong status code: got %v want %v", method, route, payload, status, expectedStatus)
+	}
+	if !strings.HasPrefix(rr.Body.String(), expectedBody) {
+		t.Errorf("Tested %v %v %v ; handler returned unexpected body: got %v want %v", method, route, payload, rr.Body.String(), expectedBody)
+	}
+	return string(rr.Body.String())
+}
+
+// DoRequestOnServer does a request on listening server
+func DoRequestOnServer(t *testing.T, hostname string, port string, jar *cookiejar.Jar, method string, testURL string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string {
+	dialer := &net.Dialer{
+		Timeout:   30 * time.Second,
+		KeepAlive: 30 * time.Second,
+		DualStack: true,
+	}
+	// or create your own transport, there's an example on godoc.
+	http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
+		addrAndPort := strings.Split(addr, ":")
+		if strings.HasSuffix(addrAndPort[0], "vestibule.io") {
+			addr = "127.0.0.1:" + addrAndPort[1]
+		}
+		return dialer.DialContext(ctx, network, addr)
+	}
+	if strings.HasPrefix(testURL, "/") {
+		testURL = "http://" + hostname + ":" + port + testURL
+	} else {
+		u, _ := url.Parse("http://" + testURL)
+		testURL = "http://" + u.Host + ":" + port + u.Path + "?" + u.RawQuery
+	}
+	req, err := http.NewRequest(method, testURL, strings.NewReader(payload))
+	if err != nil {
+		t.Fatal(err)
+	}
+	for i, v := range headers {
+		req.Header.Set(i, v)
+	}
+	var client *http.Client
+	if jar != nil {
+		client = &http.Client{Jar: jar}
+	} else {
+		client = &http.Client{}
+	}
+	res, err := client.Do(req)
+	if err != nil {
+		t.Fatal(err)
+	}
+	body, _ := ioutil.ReadAll(res.Body)
+	bodyString := string(body)
+	if status := res.StatusCode; status != expectedStatus {
+		t.Errorf("Tested %v %v %v ; handler returned wrong status code: got %v want %v", method, testURL, payload, status, expectedStatus)
+	}
+	if !strings.HasPrefix(bodyString, expectedBody) {
+		t.Errorf("Tested %v %v %v ; handler returned unexpected body: got %v want %v", method, testURL, payload, bodyString, expectedBody)
+	}
+	return bodyString
+}
+
+// CreateServerTester wraps DoRequestOnServer to factorize t, port and jar
+func CreateServerTester(t *testing.T, hostname string, port string, jar *cookiejar.Jar) DoFn {
+	return func(method string, url string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string {
+		return DoRequestOnServer(t, port, hostname, jar, method, url, headers, payload, expectedStatus, expectedBody)
+	}
+}
diff --git a/web/components/post/post.js b/web/components/post/post.js
index b99a1faa08938153530a04ec1de82abfb9dc108b..748a948353d047e3b674557eba19bcc726cf7317 100644
--- a/web/components/post/post.js
+++ b/web/components/post/post.js
@@ -13,25 +13,22 @@ class Post {
   }
 
   // DOM elements
-  post_textarea;
   new_post_button;
 
   async mount(mountpoint) {
     document.getElementById(mountpoint).innerHTML = /* HTML */ ` <div
       class="container is-fluid"
     >
-      <div id="textarea-control" class="control my-2">
-        <textarea
-          id="post-textarea"
-          class="textarea is-info is-medium has-fixed-size"
-          placeholder="Contenu du post"
-        ></textarea>
-      </div>
-      <button id="post-submit" class="button is-success">
-        Créer nouveau post
-      </button>
+      <label for="month" class="label">Month</label>
+      <input id="month" name="month" type="text" />
+      <label for="year" class="label">Year</label>
+      <input id="year" name="year" type="text" />
+      <label for="header" class="label">Header</label>
+      <input id="header" name="header" type="text" />
+      <label for="quote" class="label">Quote</label>
+      <input id="quote" name="quote" type="text" />
+      <input id="post-submit" type="button" value="Submit form" />
     </div>`;
-    this.post_textarea = document.getElementById("post-textarea");
     this.new_post_button = document.getElementById("post-submit");
     this.new_post_button.addEventListener("click", async () => {
       await this.submitPost();
@@ -39,15 +36,19 @@ class Post {
   }
 
   async submitPost() {
-    const control = document.getElementById("textarea-control");
-    control.classList.add("is-loading");
+
     try {
-      const response = await fetch("/api/admin/newPost", {
+      const response = await fetch("/api/admin/monthlyNews/", {
         method: "POST",
         headers: new Headers({
           "XSRF-Token": this.current_user.xsrftoken,
         }),
-        body: document.getElementById("post-textarea").value
+        body: JSON.stringify({
+          month: parseInt(document.getElementById("month").value),
+          year: parseInt(document.getElementById("year").value),
+          header: document.getElementById("header").value,
+          quote: document.getElementById("quote").value,
+        })
       });
       if (response.status !== 200) {
         throw new Error(
@@ -55,11 +56,8 @@ class Post {
         );
       }
       Messages.Show("is-success", "Le post a été créé avec succès");
-
     } catch (e) {
       HandleError(e);
-      this.post_textarea.value = "Une erreur s'est produite !";
     }
-    control.classList.remove("is-loading");
   }
 }