diff --git a/package-lock.json b/package-lock.json
index e60ca1144c862c4a5463903c03da863855b696d6..483e11c9f9cfc3c2029fc9e461982d74352f3a54 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1421,6 +1421,11 @@
         "prop-types": "^15.7.2"
       }
     },
+    "@rooks/use-interval": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/@rooks/use-interval/-/use-interval-4.5.0.tgz",
+      "integrity": "sha512-As0DueIAGLJLYATKPPOCDGqoIlwbhPAcYP14TNTHaAj9/ODdvUYFXAP3jFCRzDNpjXCIgSe4oBuzVVmM526n+Q=="
+    },
     "@rooks/use-window-size": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/@rooks/use-window-size/-/use-window-size-4.5.0.tgz",
diff --git a/package.json b/package.json
index 1a9254ee2e8fd94d079f8f1548d943b2866b8c8c..e344b74b4cbb6446dd5aaade8f2c8fd01224159b 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
   "private": true,
   "dependencies": {
     "@react-keycloak/web": "^2.1.4",
+    "@rooks/use-interval": "^4.5.0",
     "@rooks/use-window-size": "^4.5.0",
     "@sentry/react": "^5.29.2",
     "classnames": "^2.2.6",
diff --git a/src/App.jsx b/src/App.jsx
index 0d4facf059b547b7a7482880b04793b2f13289f7..1ba3568f3ac2b6bcf3ed6f744f3e315ee7f891f0 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -11,6 +11,7 @@ import Footer from "components/Footer";
 import Navbar from "components/Navbar";
 import Home from "pages/Home";
 import Program from "pages/Program";
+import Protocol from "pages/Protocol";
 import { AuthStore, PostStore } from "stores";
 import { updateWindowPosts } from "utils";
 
@@ -82,12 +83,12 @@ const LoadingComponent = (
 const NotFound = () => (
   <article className="container container--default py-8 lg:py-24">
     <h1 className="head-alt-base lg:head-alt-lg mb-8">
-      404ka: Tahle stránka tu není
+      404ka: tak tahle stránka tu není
     </h1>
     <p className="text-base lg:text-xl mb-8">
-      Dostali jste se na takzvanou „<strong>čtyřystačtyřku</strong>“, což
-      znamená, že stránka, kterou jste se pokusili navštívit, na tomhle webu
-      není.
+      Dostal/a ses na takzvanou „<strong>čtyřystačtyřku</strong>“, což znamená,
+      že stránka, kterou jsi se pokusil/a navštívit, na tomhle webu není.
+      Zkontroluj, zda máš správný odkaz.
     </p>
     <Button routerTo="/" className="text-base lg:text-xl" hoverActive fullwidth>
       Přejít na hlavní stránku
@@ -104,6 +105,7 @@ const BaseApp = () => {
       <Switch>
         <Route exact path="/" children={<Home />} />
         <Route exact path="/program" children={<Program />} />
+        <Route exact path="/protocol" children={<Protocol />} />
         <Route component={NotFound} />
       </Switch>
       <Footer />
diff --git a/src/actions/global-info.js b/src/actions/global-info.js
index 342d31ccdef346d7214e8d3feba9cc23b53e57c1..11b518f5838ca4de4186e039e0fc589e14335eee 100644
--- a/src/actions/global-info.js
+++ b/src/actions/global-info.js
@@ -1,7 +1,9 @@
 import isArray from "lodash/isArray";
 import { createAsyncAction, errorResult, successResult } from "pullstate";
+import baseFetch from "unfetch";
 
 import { fetch } from "api";
+import { markdownConverter } from "markdown";
 import { GlobalInfoStore } from "stores";
 
 export const loadConfig = createAsyncAction(
@@ -30,9 +32,39 @@ export const loadConfig = createAsyncAction(
             if (rawConfigItem.id === "websocket_url") {
               state.websocketUrl = rawConfigItem.value;
             }
+            if (rawConfigItem.id === "record_url") {
+              state.protocolUrl = rawConfigItem.value;
+            }
           });
         });
       }
     },
   }
 );
+
+export const loadProtocol = createAsyncAction(
+  async () => {
+    const { protocolUrl } = GlobalInfoStore.getRawState();
+
+    try {
+      const resp = await baseFetch(protocolUrl);
+
+      if (resp.status !== 200) {
+        return errorResult([], `Unexpected status code ${resp.status}`);
+      }
+
+      return successResult(await resp.text());
+    } catch (err) {
+      return errorResult([], err.toString());
+    }
+  },
+  {
+    postActionHook: ({ result }) => {
+      if (!result.error) {
+        GlobalInfoStore.update((state) => {
+          state.protocol = markdownConverter.makeHtml(result.payload);
+        });
+      }
+    },
+  }
+);
diff --git a/src/components/Button.jsx b/src/components/Button.jsx
index 3e7b79152138a6784b0478ddc1c2cb0c547a1efa..a2491343665889955ea1dd953bdcb61ebb27cff9 100644
--- a/src/components/Button.jsx
+++ b/src/components/Button.jsx
@@ -12,6 +12,7 @@ const Button = ({
   loading = false,
   children,
   routerTo,
+  bodyProps = {},
   ...props
 }) => {
   const btnClass = classNames(
@@ -30,7 +31,9 @@ const Button = ({
 
   const inner = (
     <div className="btn__body-wrap">
-      <div className={bodyClass}>{children}</div>
+      <div className={bodyClass} {...bodyProps}>
+        {children}
+      </div>
       {!!icon && (
         <div className="btn__icon">
           <i className={icon}></i>
diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx
index e31b3a89e424685586c28c9730b3f3a21775c18e..3f11e6cc0f2b6f86e7fddbb02c772b035a1501ab 100644
--- a/src/components/Footer.jsx
+++ b/src/components/Footer.jsx
@@ -30,6 +30,9 @@ const Footer = () => {
                   <li>
                     <NavLink to="/program">Program</NavLink>
                   </li>
+                  <li>
+                    <NavLink to="/protocol">Zápis</NavLink>
+                  </li>
                 </ul>
               </div>
             </div>
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
index 89679a817e7cf10def1c3da6ec8c4e1e05811ce5..ce7d9a0ec8d8c947d6326e9e032ad13af86619d5 100644
--- a/src/components/Navbar.jsx
+++ b/src/components/Navbar.jsx
@@ -100,6 +100,11 @@ const Navbar = () => {
                     Program
                   </NavLink>
                 </li>
+                <li className="navbar-menu__item">
+                  <NavLink className="navbar-menu__link" to="/protocol">
+                    Zápis
+                  </NavLink>
+                </li>
               </ul>
             </div>
             <div className="navbar__actions navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto self-start flex flex-row items-center">
diff --git a/src/pages/Program.jsx b/src/pages/Program.jsx
index 964dd645af4b62948f22356f6a8a17ab49ad9d3c..7b5c1088499555379829916a0bdeb30303dc5b32 100644
--- a/src/pages/Program.jsx
+++ b/src/pages/Program.jsx
@@ -25,7 +25,7 @@ const Schedule = () => {
   );
 
   return (
-    <article className="container container--wide py-8 lg:py-24">
+    <article className="container container--default py-8 lg:py-24">
       <h1 className="head-alt-md lg:head-alt-lg mb-8">Program zasedání</h1>
       <div className="flex flex-col">
         {scheduleIds.map((id) => {
diff --git a/src/pages/Protocol.jsx b/src/pages/Protocol.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..35709933cd8945441ca8448d8539d857b9c9f8d9
--- /dev/null
+++ b/src/pages/Protocol.jsx
@@ -0,0 +1,110 @@
+import React, { useCallback, useState } from "react";
+import useInterval from "@rooks/use-interval";
+
+import { loadProtocol } from "actions/global-info";
+import Button from "components/Button";
+import ErrorMessage from "components/ErrorMessage";
+import { useActionState } from "hooks";
+import { GlobalInfoStore } from "stores";
+
+const Protocol = () => {
+  const { protocolUrl, protocol } = GlobalInfoStore.useState();
+  const [protocolLoading, protocolLoadError] = useActionState(loadProtocol);
+  const [progressPercent, setProgressPercent] = useState(0);
+  const [paused, setPaused] = useState(false);
+
+  const forceLoad = useCallback(async () => {
+    try {
+      setPaused(true);
+      setProgressPercent(1);
+      await loadProtocol.run();
+    } finally {
+      setPaused(false);
+    }
+  }, [setPaused, setProgressPercent]);
+
+  const tick = useCallback(async () => {
+    if (paused) {
+      return;
+    }
+
+    if (progressPercent % 100 === 0) {
+      forceLoad();
+    } else {
+      setProgressPercent(progressPercent + 1);
+    }
+  }, [forceLoad, paused, progressPercent, setProgressPercent]);
+
+  useInterval(tick, 100, true);
+
+  const htmlContent = protocol
+    ? {
+        __html: protocol,
+      }
+    : null;
+
+  const progressStyle = {
+    position: "absolute",
+    width: `${progressPercent}%`,
+    height: "100%",
+    left: "0",
+    background:
+      "linear-gradient(142deg, rgba(2,0,36,1) 0%, rgba(51,51,51,1) 0%, rgba(255,255,255,1) 100%)",
+    opacity: "0.4",
+  };
+
+  return (
+    <article className="container container--default py-8 lg:py-24">
+      <h1 className="head-alt-md lg:head-alt-lg mb-8">Zápis z jednání</h1>
+
+      <div className="flex items-start">
+        <div className="lg:w-2/3">
+          {!protocolUrl && (
+            <ErrorMessage>Zápis momentálně není k dispozici.</ErrorMessage>
+          )}
+          {protocolLoadError && (
+            <ErrorMessage>
+              Při stahování záznamu z jednání došlo k problému:{" "}
+              {protocolLoadError.toString()}
+            </ErrorMessage>
+          )}
+          {protocolUrl && <></>}
+          {htmlContent && (
+            <div
+              className="leading-tight text-sm lg:text-base content-block"
+              dangerouslySetInnerHTML={htmlContent}
+            ></div>
+          )}
+        </div>
+        <div className="hidden lg:block card elevation-10 w-1/3">
+          <div className="lg:card__body content-block">
+            <h2>Jak to funguje?</h2>
+            <p>
+              Zápis se aktualizuje automaticky každých 10 sekund. Pokud chceš
+              aktualizaci vynutit ručně, můžeš využít tlačítko níže.
+            </p>
+            <Button
+              icon="ico--refresh"
+              loading={protocolLoading}
+              className="btn--fullwidth"
+              onClick={forceLoad}
+              color="black"
+              bodyProps={{
+                style: {
+                  position: "relative",
+                },
+              }}
+            >
+              <span style={progressStyle}></span>
+              <span style={{ position: "relative" }}>
+                {protocolLoading ? "Aktualizace..." : "Aktualizovat"}
+              </span>
+            </Button>
+          </div>
+        </div>
+      </div>
+    </article>
+  );
+};
+
+export default Protocol;
diff --git a/src/stores.js b/src/stores.js
index 731d36c01830aac73995e4ce9ed1f05ed6c0606c..ef240f8d7823ea62bcaff2b4ca1712c803602818 100644
--- a/src/stores.js
+++ b/src/stores.js
@@ -8,6 +8,8 @@ const globalInfoStoreInitial = {
   onlineUsers: 0,
   websocketUrl: null,
   streamUrl: null,
+  protocolUrl: null,
+  protocol: null,
 };
 
 export const GlobalInfoStore = new Store(globalInfoStoreInitial);
diff --git a/typings/cf2021.d.ts b/typings/cf2021.d.ts
index 2afcdfeda148eadf4efda2fbace8f708139a31cc..a2f97abef673408e75893b4bb36ca82294fc5ccc 100644
--- a/typings/cf2021.d.ts
+++ b/typings/cf2021.d.ts
@@ -4,6 +4,8 @@ declare namespace CF2021 {
     onlineUsers: number;
     websocketUrl: string;
     streamUrl?: string;
+    protocolUrl?: string;
+    protocol?: string;
   }
 
   interface ProgramScheduleEntry {