diff --git a/app.py b/app.py
index 30e75336b036072195bc881f73d709c2ffaa1c0f..1e3a7aa7d031dd2d4179e247a450b6b9359bc3fe 100644
--- a/app.py
+++ b/app.py
@@ -1,12 +1,16 @@
 import asyncio
 import aiotieba
 
-from aioflask import Flask, render_template, request
+from aioflask import render_template, request, escape
+from flask_caching import Cache
+from urllib.parse import quote_plus
 from datetime import datetime
 
-from extra import *
+from aiotieba.api.get_posts._classdef import *
+from aiotieba.api._classdef.contents import *
 
-app = Flask(__name__)
+from shared import *
+from extra import *
 
 ######################################################################
 
@@ -38,6 +42,32 @@ def _jinja2_filter_intsep(i):
 def _jinja2_filter_trim(text):
     return text[:78] + '……' if len(text) > 78 else text
 
+# Format fragments to its equiviant HTML.
+@app.template_filter('translate')
+def _jinja2_filter_translate(frags):
+    htmlfmt = ''
+    
+    for frag in frags:
+        if isinstance(frag, FragText):
+            subfrags = frag.text.split('\n')
+            for subfrag in subfrags:
+                htmlfmt += '<p>' + str(escape(subfrag)) + '</p>'
+        elif isinstance(frag, FragImage_p):
+            htmlfmt += \
+                f'<a target="_blank" href="/proxy/pic/{ extract_image_name(frag.origin_src) }">' \
+                f'<img width="{ frag.show_width}" height="{ frag.show_height }" '\
+                f'src="/proxy/pic/{ extract_image_name(frag.src) }"></a>'
+        elif isinstance(frag, FragEmoji_p):
+            clear_leading = False
+            if htmlfmt.endswith('</p>'):
+                clear_leading = True
+                htmlfmt = htmlfmt.rstrip('</p>')
+            htmlfmt += f'<img class="emoticons" alt="[{ frag.desc }]" src="/static/emoticons/{ quote_plus(frag.desc) }.png">'
+            if clear_leading:
+                htmlfmt += '</p>'
+            
+    return htmlfmt
+
 ######################################################################
 
 @app.route('/p/<tid>')
@@ -49,8 +79,8 @@ async def thread_view(tid):
         # Default to 15 posts per page, confirm to tieba.baidu.com
         thread_info = await tieba.get_posts(tid, rn=15, pn=pn)
 
-    for fragment in thread_info[0].contents:
-        print(fragment)
+    for post in thread_info:
+        print(post.comments)
     
     return await render_template('thread.html', info=thread_info)
 
@@ -60,7 +90,7 @@ async def forum_view():
     pn = int(request.args.get('pn') or 1)
 
     async with aiotieba.Client() as tieba:
-        forum_info, threads = await asyncio.gather(find_tieba_info(fname),
+        forum_info, threads = await asyncio.gather(awaitify(find_tieba_info)(fname),
                                                    tieba.get_threads(fname, rn=50, pn=pn))
 
     return await render_template('bar.html', info=forum_info, threads=threads)
diff --git a/extra.py b/extra.py
index ad8ccf6447229ac849eca8f76ffbdc88474fb3f9..2bfa4e1bc176f6644ad9db7cdf0ea0911270d4ad 100644
--- a/extra.py
+++ b/extra.py
@@ -4,7 +4,14 @@ import requests
 import bs4
 import re
 
-async def find_tieba_info(tname):
+from shared import *
+
+def extract_image_name(url):
+    match = re.search(r'/(\w+)\.jpg', url)
+    return match.group(1) + '.jpg'
+
+@cache.cached(timeout=60, key_prefix='tieba_info')
+def find_tieba_info(tname):
     """Get the tiebat avatar for the forum name.
 
     :param tname: the name of the target forum.
@@ -17,9 +24,7 @@ async def find_tieba_info(tname):
     soup = bs4.BeautifulSoup(res.text, 'html.parser')
 
     elems = soup.select('#forum-card-head')
-    match = re.search(r'/(\w+)\.jpg', elems[0]['src'])
-
-    info['avatar'] = match.group(1) + '.jpg'
+    info['avatar'] = extract_image_name(elems[0]['src'])
 
     footer = soup.select('.th_footer_l')[0]
     stat_elems = footer.findAll('span', {'class': 'red_text'}, recursive=False)
diff --git a/main.py b/main.py
index 2bf0811a9d75f056ab4c51ea384dcb03a817cd3d..a02b03ca36762c1e47e61b5e22c135f11b219714 100644
--- a/main.py
+++ b/main.py
@@ -6,6 +6,7 @@ from urllib.parse import quote as urlquote, urlparse, urlunparse
 from twisted.web.http import _QUEUED_SENTINEL, HTTPChannel, HTTPClient, Request
 from twisted.web.resource import Resource
 from twisted.web import proxy, server
+from twisted.web.static import File
 from twisted.internet.protocol import ClientFactory
 from twisted.internet import reactor, utils
 
@@ -139,18 +140,25 @@ class ReverseProxyResource(Resource):
 def twisted_start():
     flask_res = proxy.ReverseProxyResource('127.0.0.1', 5000, b'')
     flask_res.putChild(b'proxy', ReverseProxyResource(b'/proxy'))
+    flask_res.putChild(b'static', File('static'))
 
+    flask_port = int(app.config['SERVER_NAME'].split(':')[1])
+    
     site = server.Site(flask_res)
-    reactor.listenTCP(5001, site)
+    reactor.listenTCP(flask_port-1, site)
     reactor.run()
 
 # To start this function for testing: python -c 'import main; main.flask_start()'
 def flask_start():
-    app.run(port=5000+1)
+    app.run()
 
 # If we're executed directly, also start the flask daemon.
 if __name__ == '__main__':
+    flask_port = int(app.config['SERVER_NAME'].split(':')[1])
+    print(f' *** SERVER IS RUNNING ON PORT {flask_port-1} ***')
+    
+    twisted_start()
+    
     flask_task = multiprocessing.Process(target=flask_start)
     flask_task.daemon = True # Exit the child if the parent was killed :-(
     flask_task.start()
-    twisted_start()
diff --git a/requirements.txt b/requirements.txt
index 72c08212438073ba6013d706c6020b01ba580b93..d6ac9b939a8d1ca93ca1a5db6161316346c20488 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,7 @@
 aioflask==0.4.0
 flask==2.1.3
 aiotieba==3.4.5
+Flask-Caching
 beautifulsoup4
 requests
+twisted
diff --git a/shared.py b/shared.py
new file mode 100644
index 0000000000000000000000000000000000000000..87adbef044c155e9a25e616ac01a29d3dc7d0c29
--- /dev/null
+++ b/shared.py
@@ -0,0 +1,18 @@
+from aioflask import Flask
+from flask_caching import Cache
+
+from functools import wraps
+
+def awaitify(sync_func):
+    """Wrap a synchronous callable to allow ``await``'ing it"""
+    @wraps(sync_func)
+    async def async_func(*args, **kwargs):
+        return sync_func(*args, **kwargs)
+    return async_func
+
+app = Flask(__name__)
+
+app.config['SERVER_NAME'] = ':6666'
+
+app.config['CACHE_TYPE'] = 'SimpleCache'
+cache = Cache(app)
diff --git a/static/css/main.css b/static/css/main.css
index 4c8720111ae6e2154b066f43483069243e932d02..a85c19abcdc85b5c605727d47fac423f8bb6a128 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -1,19 +1,25 @@
+.emoticons {
+    max-width: 5% !important;
+}
+
+/* global styling */
 :root {
 	--bg-color: #eeeecc;
 	--fg-color: #ffffdd;
 	--replies-color: #f0f0d0;
 	--text-color: #663333;
+	--border-color: #66333388;
 	--primary-color: #0066cc;
 	--important-color: #ff0000;
 }
 
-
 @media (prefers-color-scheme: dark) {
 	:root {
 		--bg-color: #000033;
 		--fg-color: #202044;
 		--replies-color: #16163a;
 		--text-color: #cccccc;
+		--border-color: #cccccc44;
 		--primary-color: #6699ff;
 		--important-color: #ff0000;
 	}
@@ -27,6 +33,15 @@ body {
 	font-family: sans-serif;
 }
 
+footer {
+	display: flex;
+	gap: 2rem;
+	padding: 1rem;
+	justify-content: center;
+	align-items: center;
+	flex-wrap: wrap;
+}
+
 a[href] {
 	color: var(--primary-color);
 	text-decoration: none;
@@ -40,7 +55,24 @@ img {
 	max-width: 100%;
 }
 
-.bar-nav, .thread-nav {
+.paginator {
+	padding: 1rem;
+	gap: .3rem;
+	display: flex;
+	flex-wrap: wrap;
+	justify-content: center;
+}
+
+.paginator a {
+	flex: 0 0 auto;
+	height: 1rem;
+	line-height: 1rem;
+	padding: .5rem;
+	text-align: center;
+	background-color: var(--replies-color);
+}
+
+header {
 	background-color: var(--fg-color);
 	display: flex;
 	flex-wrap: wrap;
@@ -50,38 +82,29 @@ img {
 	align-items: center;
 }
 
-.thread-nav > .title {
-	font-size: 1.2rem;
-	flex: 1 0 70%;
-}
-
-.thread-nav > .from {
-	font-size: 1.2rem;
+.list {
+	background-color: var(--fg-color);
+	margin-bottom: 1rem;
 }
 
-.bar-nav > img {
-	width: 5rem;
-	height: 5rem;
-}
+/* thread.html: nav bar */
 
-.bar-nav .title {
-	font-size: 1.5rem;
+.thread-nav .title {
+	font-size: 1.2rem;
+	flex: 1 0 70%;
 }
 
-.bar-nav .stats small {
-	margin-right: .5rem;
+.thread-nav .from {
+	font-size: 1.2rem;
 }
 
-.list {
-	background-color: var(--fg-color);
-	margin-bottom: 1rem;
-}
+/* thread.html: user post */
 
 .post {
 	display: flex;
 	flex-wrap: wrap;
 	gap: 0 1rem;
-	border-bottom: 1px solid var(--text-color);
+	border-bottom: 1px solid var(--border-color);
 	padding: 1rem;
 }
 
@@ -120,6 +143,39 @@ img {
 	float: right;
 }
 
+/* thread.html: replies to a user post */
+
+.post .replies {
+	background-color: var(--replies-color);
+	margin-top: 1rem;
+}
+
+.post .replies .post {
+	border-bottom: none;
+}
+
+.post .replies .post .avatar {
+	width: 3rem;
+	height: 3rem;
+}
+
+/* bar.html: nav bar */
+
+.bar-nav img {
+	width: 5rem;
+	height: 5rem;
+}
+
+.bar-nav .title {
+	font-size: 1.5rem;
+}
+
+.bar-nav .stats small {
+	margin-right: .5rem;
+}
+
+/* bar.html: thread list */
+
 .thread {
 	display: flex;
 	gap: 1rem;
@@ -155,12 +211,3 @@ img {
 	padding-left: .3rem;
 	color: var(--text-color);
 }
-
-footer {
-	display: flex;
-	gap: 2rem;
-	padding: 1rem;
-	justify-content: center;
-	align-items: center;
-	flex-wrap: wrap;
-}
diff --git a/templates/bar.html b/templates/bar.html
index c2f0ae7980798afc93a9477314e49c3bef891b9e..73c38c254066f6fe7da09caf8c0331d7f5d317e2 100644
--- a/templates/bar.html
+++ b/templates/bar.html
@@ -44,6 +44,32 @@
     </div>
   </div>
   {% endfor %}
+
+  <div class="paginator">
+    {% if threads.page.current_page > 1 %}
+    <a href="/f?kw={{ info['name'] }}">首页</a>
+    {% endif %}
+
+    {% for i in range(5) %}
+    {% set np = threads.page.current_page - 5 + i %}
+    {% if np > 0 %}
+    <a href="/f?kw={{ info['name'] }}&pn={{ np }}">{{ np }}</a>
+    {% endif %}
+    {% endfor %}
+
+    <a>{{ threads.page.current_page }}</a>
+    
+    {% for i in range(5) %}
+    {% set np = threads.page.current_page + 1 + i %}
+    {% if np <= threads.page.total_page %}
+    <a href="/f?kw={{ info['name'] }}&pn={{ np }}">{{ np }}</a>
+    {% endif %}
+    {% endfor %}
+
+    {% if threads.page.current_page < threads.page.total_page %}
+    <a href="/f?kw={{ info['name'] }}&pn={{ threads.page.total_page }}">尾页</a>
+    {% endif %}
+  </div>
 </div>
 <footer>
   <div><a href="#">RAT Ain't Tieba</a></div>
diff --git a/templates/thread.html b/templates/thread.html
index 7329fbf9bfb72e9a6716cbb9ec48191b38a41deb..2e5be9217a6a45b2cddecfe0dc19d5a883f3333e 100644
--- a/templates/thread.html
+++ b/templates/thread.html
@@ -26,14 +26,41 @@
 	  {% endif %}
 	</div>
 	<div class="content">
-	  {{ p['text'] }}
+	  {{ p.contents|translate|safe }}
 	</div>
 	<small class="date">{{ p.create_time|date }}</small>
-	<small class="permalink"><a href="#1">{{ p.floor }}</a></small>
+	<small class="permalink"><a href="#{{ p.floor }}">{{ p.floor }}</a></small>
       </div>
     </div>
     {% endfor %}
   </div>
+  
+  <div class="paginator">
+    {% if info.page.current_page > 1 %}
+    <a href="/p/{{ info.thread.tid }}">首页</a>
+    {% endif %}
+
+    {% for i in range(5) %}
+    {% set np = info.page.current_page - 5 + i %}
+    {% if np > 0 %}
+    <a href="/p/{{ info.thread.tid }}?pn={{ np }}">{{ np }}</a>
+    {% endif %}
+    {% endfor %}
+
+    <a>{{ info.page.current_page }}</a>
+    
+    {% for i in range(5) %}
+    {% set np = info.page.current_page + 1 + i %}
+    {% if np <= info.page.total_page %}
+    <a href="/p/{{ info.thread.tid }}?pn={{ np }}">{{ np }}</a>
+    {% endif %}
+    {% endfor %}
+
+    {% if info.page.current_page < info.page.total_page %}
+    <a href="/p/{{ info.thread.tid }}?pn={{ info.page.total_page }}">尾页</a>
+    {% endif %}
+  </div>
+  
   <footer>
     <div><a href="/">RAT Ain't Tieba</a></div>
     <div><a href="#">自豪地以 AGPLv3 释出</a></div>