diff --git a/Dockerfile b/Dockerfile
index 18a2c18a8..029e6a58e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,10 @@
 FROM bitwalker/alpine-elixir:latest
 
 RUN apk add inotify-tools postgresql-client yarn
+RUN apk add --no-cache make gcc libc-dev
 
 RUN mix local.hex --force && mix local.rebar --force
 
-COPY docker/entrypoint.sh /bin/entrypoint
-
 WORKDIR /app
 
 EXPOSE 4000 4001 4002
diff --git a/Makefile b/Makefile
index 7e329f251..d7585424c 100644
--- a/Makefile
+++ b/Makefile
@@ -11,9 +11,6 @@ stop:
 	docker-compose down
 	@bash docker/message.sh "stopped"
 test: stop
-	@bash docker/message.sh "Building front"
-	docker-compose -f docker-compose.yml -f docker-compose.test.yml run front yarn run build
-	@bash docker/message.sh "Front built"
 	@bash docker/message.sh "Running tests"
 	docker-compose -f docker-compose.yml -f docker-compose.test.yml run api mix test
 	@bash docker/message.sh "Tests runned"
diff --git a/config/dev.exs b/config/dev.exs
index 2156a8469..723a4a449 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -17,7 +17,9 @@ config :mobilizon, MobilizonWeb.Endpoint,
   debug_errors: true,
   code_reloader: true,
   check_origin: false,
-  watchers: []
+  watchers: [
+    yarn: ["run", "dev", cd: Path.expand("../js", __DIR__)]
+  ]
 
 # ## SSL Support
 #
diff --git a/docker-compose.test.yml b/docker-compose.test.yml
index 89f7a114a..8d84def25 100644
--- a/docker-compose.test.yml
+++ b/docker-compose.test.yml
@@ -5,13 +5,12 @@ services:
     restart: "no"
     environment:
       POSTGRES_DB: mobilizon_test
-  front:
-    restart: "no"
   api:
     restart: "no"
     environment:
       MIX_ENV: "test"
       MOBILIZON_DATABASE_DBNAME: mobilizon_test
+      MOBILIZON_INSTANCE_HOST: mobilizon.test
     command: "mix test"
 volumes:
   pgdata:
diff --git a/docker-compose.yml b/docker-compose.yml
index 82fed99d7..1234cd96c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,18 +10,6 @@ services:
       POSTGRES_DB: mobilizon_dev
     volumes:
       - pgdata:/var/lib/postgresql/data
-
-
-  front:
-    container_name: mobilizon_front
-    restart: unless-stopped
-    build: ./js
-    volumes:
-      - '.:/app'
-    ports:
-      - "8888:8080"
-    command: yarn run dev
-
   api:
     container_name: mobilizon_api
     restart: unless-stopped
@@ -32,18 +20,17 @@ services:
       - "4000:4001"
     depends_on:
       - postgres
-      - front
     environment:
       MIX_ENV: "dev"
       MOBILIZON_INSTANCE_NAME: My Mobilizon Instance
       MOBILIZON_INSTANCE_HOST: mobilizon.me
       MOBILIZON_INSTANCE_EMAIL: noreply@mobilizon.me
-      MOBILIZON_INSTANCE_REGISTRATIONS_OPEN: "false"
+      MOBILIZON_INSTANCE_REGISTRATIONS_OPEN: "true"
       MOBILIZON_DATABASE_PASSWORD: postgres
       MOBILIZON_DATABASE_USERNAME: postgres
       MOBILIZON_DATABASE_DBNAME: mobilizon_dev
       MOBILIZON_DATABASE_HOST: postgres
-    command: "mix phx.migrate_serve"
+    command: "mix phx.deps_migrate_serve"
 volumes:
   pgdata:
   .:
diff --git a/js/Dockerfile b/js/Dockerfile
deleted file mode 100644
index d9db3df21..000000000
--- a/js/Dockerfile
+++ /dev/null
@@ -1,10 +0,0 @@
-FROM node:10
-
-LABEL maintainer="tcit"
-
-RUN yarn install
-RUN yarn upgrade node-sass
-
-WORKDIR /app/js
-
-EXPOSE 8080
diff --git a/js/package.json b/js/package.json
index 85f287c8a..68e15d393 100644
--- a/js/package.json
+++ b/js/package.json
@@ -6,7 +6,7 @@
     "build": "vue-cli-service build",
     "lint": "vue-cli-service lint",
     "analyze-bundle": "yarn run build -- --report-json && webpack-bundle-analyzer ../priv/static/report.json",
-    "dev": "vue-cli-service serve",
+    "dev": "vue-cli-service build --watch",
     "test:e2e": "vue-cli-service test:e2e",
     "test:unit": "vue-cli-service test:unit",
     "prepare": "patch-package"
diff --git a/js/vue.config.js b/js/vue.config.js
index 73dbeed3d..e3d0dab8d 100644
--- a/js/vue.config.js
+++ b/js/vue.config.js
@@ -4,7 +4,7 @@ const path = require('path');
 module.exports = {
   lintOnSave: false,
   runtimeCompiler: true,
-  outputDir: '../priv/static',
+  outputDir: '../priv/static/js',
   configureWebpack: {
     plugins: [
       new Dotenv({ path: path.resolve(process.cwd(), '../.env') }),
@@ -18,6 +18,9 @@ module.exports = {
         },
       ],
     },
+    output: {
+      filename: 'app.js'
+    }
   },
   chainWebpack: config => {
     config
diff --git a/lib/mobilizon_web/controllers/fallback_controller.ex b/lib/mobilizon_web/controllers/fallback_controller.ex
index cf85e7ed5..ec74eadba 100644
--- a/lib/mobilizon_web/controllers/fallback_controller.ex
+++ b/lib/mobilizon_web/controllers/fallback_controller.ex
@@ -6,20 +6,6 @@ defmodule MobilizonWeb.FallbackController do
   """
   use MobilizonWeb, :controller
 
-  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
-    conn
-    |> put_status(:unprocessable_entity)
-    |> put_view(MobilizonWeb.ChangesetView)
-    |> render("error.json", changeset: changeset)
-  end
-
-  def call(conn, {:error, nil}) do
-    conn
-    |> put_status(:unprocessable_entity)
-    |> put_view(MobilizonWeb.ErrorView)
-    |> render("invalid_request.json")
-  end
-
   def call(conn, {:error, :not_found}) do
     conn
     |> put_status(:not_found)
diff --git a/lib/mobilizon_web/controllers/feed_controller.ex b/lib/mobilizon_web/controllers/feed_controller.ex
index cd4dbfa2d..75f174b0c 100644
--- a/lib/mobilizon_web/controllers/feed_controller.ex
+++ b/lib/mobilizon_web/controllers/feed_controller.ex
@@ -3,6 +3,7 @@ defmodule MobilizonWeb.FeedController do
   Controller to serve RSS, ATOM and iCal Feeds
   """
   use MobilizonWeb, :controller
+  action_fallback(MobilizonWeb.FallbackController)
 
   def actor(conn, %{"name" => name, "format" => "atom"}) do
     with {status, data} when status in [:ok, :commit] <-
@@ -12,9 +13,7 @@ defmodule MobilizonWeb.FeedController do
       |> send_resp(200, data)
     else
       _err ->
-        conn
-        |> put_resp_content_type("text/html")
-        |> send_file(404, "priv/static/index.html")
+        {:error, :not_found}
     end
   end
 
@@ -26,9 +25,7 @@ defmodule MobilizonWeb.FeedController do
       |> send_resp(200, data)
     else
       _err ->
-        conn
-        |> put_resp_content_type("text/html")
-        |> send_file(404, "priv/static/index.html")
+        {:error, :not_found}
     end
   end
 
@@ -40,9 +37,7 @@ defmodule MobilizonWeb.FeedController do
       |> send_resp(200, data)
     else
       _err ->
-        conn
-        |> put_resp_content_type("text/html")
-        |> send_file(404, "priv/static/index.html")
+        {:error, :not_found}
     end
   end
 
@@ -54,9 +49,7 @@ defmodule MobilizonWeb.FeedController do
       |> send_resp(200, data)
     else
       _err ->
-        conn
-        |> put_resp_content_type("text/html")
-        |> send_file(404, "priv/static/index.html")
+        {:error, :not_found}
     end
   end
 
@@ -68,9 +61,7 @@ defmodule MobilizonWeb.FeedController do
       |> send_resp(200, data)
     else
       _err ->
-        conn
-        |> put_resp_content_type("text/html")
-        |> send_file(404, "priv/static/index.html")
+        {:error, :not_found}
     end
   end
 end
diff --git a/lib/mobilizon_web/controllers/page_controller.ex b/lib/mobilizon_web/controllers/page_controller.ex
index f3a3f5978..5bb761542 100644
--- a/lib/mobilizon_web/controllers/page_controller.ex
+++ b/lib/mobilizon_web/controllers/page_controller.ex
@@ -3,19 +3,15 @@ defmodule MobilizonWeb.PageController do
   Controller to load our webapp
   """
   use MobilizonWeb, :controller
-  alias Mobilizon.Service.Metadata
   alias Mobilizon.Actors
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Events
   alias Mobilizon.Events.{Event, Comment}
 
-  plug(:put_layout, false)
   action_fallback(MobilizonWeb.FallbackController)
 
   def index(conn, _params) do
-    conn
-    |> put_resp_content_type("text/html")
-    |> send_file(200, "priv/static/index.html")
+    render(conn, "app.html")
   end
 
   def actor(conn, %{"name" => name}) do
@@ -77,16 +73,6 @@ defmodule MobilizonWeb.PageController do
 
   # Inject OpenGraph information
   defp render_with_meta(conn, object) do
-    {:ok, index_content} = File.read(index_file_path())
-    tags = Metadata.build_tags(object)
-    response = String.replace(index_content, "<!--server-generated-meta-->", tags)
-
-    conn
-    |> put_resp_content_type("text/html")
-    |> send_resp(200, response)
-  end
-
-  defp index_file_path() do
-    Path.join(Application.app_dir(:mobilizon, "priv/static/"), "index.html")
+    render(conn, "app.html", object: object)
   end
 end
diff --git a/lib/mobilizon_web/endpoint.ex b/lib/mobilizon_web/endpoint.ex
index 6717ee50d..0713a1637 100644
--- a/lib/mobilizon_web/endpoint.ex
+++ b/lib/mobilizon_web/endpoint.ex
@@ -21,7 +21,7 @@ defmodule MobilizonWeb.Endpoint do
     at: "/",
     from: :mobilizon,
     gzip: false,
-    only: ~w(css fonts images js favicon.ico robots.txt index.html)
+    only: ~w(css fonts images js favicon.ico robots.txt)
   )
 
   # Code reloading can be explicitly enabled under the
diff --git a/lib/mobilizon_web/templates/layout/app.html.eex b/lib/mobilizon_web/templates/layout/app.html.eex
new file mode 100644
index 000000000..386e2ab10
--- /dev/null
+++ b/lib/mobilizon_web/templates/layout/app.html.eex
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="has-navbar-fixed-top">
+
+<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">
+  <link rel="icon" href="<%= static_path(@conn, "/js/favicon.ico") %>">
+  <link rel="stylesheet" href="//cdn.materialdesignicons.com/3.5.95/css/materialdesignicons.min.css">
+  <title>mobilizon</title>
+  <%= if assigns[:object], do: Metadata.build_tags(@object) %>
+</head>
+
+<body>
+  <noscript>
+    <strong>We're sorry but mobilizon doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+  </noscript>
+  <div id="app"></div>
+  <!-- built files will be auto injected -->
+  <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
+</body>
+
+</html>
diff --git a/lib/mobilizon_web/views/layout_view.ex b/lib/mobilizon_web/views/layout_view.ex
new file mode 100644
index 000000000..6e91e14cf
--- /dev/null
+++ b/lib/mobilizon_web/views/layout_view.ex
@@ -0,0 +1,4 @@
+defmodule MobilizonWeb.LayoutView do
+  use MobilizonWeb, :view
+  alias Mobilizon.Service.Metadata
+end
diff --git a/lib/service/metadata/actor.ex b/lib/service/metadata/actor.ex
index 4a51e2f96..cd2d9a58a 100644
--- a/lib/service/metadata/actor.ex
+++ b/lib/service/metadata/actor.ex
@@ -1,17 +1,8 @@
 defimpl Mobilizon.Service.Metadata, for: Mobilizon.Actors.Actor do
-  alias Phoenix.HTML
   alias Phoenix.HTML.Tag
   alias Mobilizon.Actors.Actor
-  require Logger
 
   def build_tags(%Actor{} = actor) do
-    actor
-    |> do_build_tags()
-    |> Enum.map(&HTML.safe_to_string/1)
-    |> Enum.reduce("", fn tag, acc -> acc <> tag end)
-  end
-
-  defp do_build_tags(%Actor{} = actor) do
     [
       Tag.tag(:meta, property: "og:title", content: Actor.display_name_and_username(actor)),
       Tag.tag(:meta, property: "og:url", content: actor.url),
diff --git a/lib/service/metadata/comment.ex b/lib/service/metadata/comment.ex
index dfa504579..0d3fdbff3 100644
--- a/lib/service/metadata/comment.ex
+++ b/lib/service/metadata/comment.ex
@@ -1,16 +1,8 @@
 defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Comment do
-  alias Phoenix.HTML
   alias Phoenix.HTML.Tag
   alias Mobilizon.Events.Comment
 
   def build_tags(%Comment{} = comment) do
-    comment
-    |> do_build_tags()
-    |> Enum.map(&HTML.safe_to_string/1)
-    |> Enum.reduce("", fn tag, acc -> acc <> tag end)
-  end
-
-  defp do_build_tags(%Comment{} = comment) do
     [
       Tag.tag(:meta, property: "og:title", content: comment.actor.preferred_username),
       Tag.tag(:meta, property: "og:url", content: comment.url),
diff --git a/lib/service/metadata/event.ex b/lib/service/metadata/event.ex
index 17fdc56ff..139bb52e0 100644
--- a/lib/service/metadata/event.ex
+++ b/lib/service/metadata/event.ex
@@ -5,15 +5,6 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do
   alias MobilizonWeb.JsonLD.ObjectView
 
   def build_tags(%Event{} = event) do
-    event
-    |> do_build_tags()
-    |> Enum.map(&HTML.safe_to_string/1)
-    |> Enum.reduce("", fn tag, acc -> acc <> tag end)
-    |> Kernel.<>(build_json_ld_schema(event))
-  end
-
-  # Build OpenGraph & Twitter Tags
-  defp do_build_tags(%Event{} = event) do
     [
       Tag.tag(:meta, property: "og:title", content: event.title),
       Tag.tag(:meta, property: "og:url", content: event.url),
@@ -21,14 +12,15 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do
       Tag.tag(:meta, property: "og:type", content: "website"),
       Tag.tag(:meta, property: "og:image", content: event.thumbnail),
       Tag.tag(:meta, property: "og:image", content: event.large_image),
-      Tag.tag(:meta, property: "twitter:card", content: "summary_large_image")
+      Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"),
+      ~s{<script type="application/ld+json">#{json(event)}</script>} |> HTML.raw()
     ]
   end
 
   # Insert JSON-LD schema by hand because Tag.content_tag wants to escape it
-  defp build_json_ld_schema(%Event{} = event) do
-    "<script type=\"application\/ld+json\">" <>
-      (ObjectView.render("event.json", %{event: event})
-       |> Jason.encode!()) <> "</script>"
+  defp json(%Event{} = event) do
+    "event.json"
+    |> ObjectView.render(%{event: event})
+    |> Jason.encode!()
   end
 end
diff --git a/mix.exs b/mix.exs
index eb8b0002c..8ab8ace92 100644
--- a/mix.exs
+++ b/mix.exs
@@ -117,7 +117,13 @@ defmodule Mobilizon.Mixfile do
       "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
       "ecto.reset": ["ecto.drop", "ecto.setup"],
       test: ["ecto.create --quiet", "ecto.migrate", "test"],
-      "phx.migrate_serve": ["ecto.create --quiet", "ecto.migrate", "phx.server"]
+      "phx.deps_migrate_serve": [
+        "deps.get",
+        "ecto.create --quiet",
+        "ecto.migrate",
+        "cmd cd js && yarn install && cd ../",
+        "phx.server"
+      ]
     ]
   end