diff --git a/ContentConverter.py b/ContentConverter.py index 9fb0d20..ae1167c 100644 --- a/ContentConverter.py +++ b/ContentConverter.py @@ -1,3 +1,5 @@ +import datetime +import orgparse import os import subprocess @@ -62,11 +64,41 @@ contentCache = [] renderedCache = [] -renderedDictionary = {} +renderedContentDictionary = {} + +class RenderedContentMetadata: + def __init__(self, contentFile, contentPath, renderedFilePath): + self.contentFile = contentFile + self.contentPath = contentPath + self.renderedFilePath = renderedFilePath + + # Properties inherited from the org properties. Code-queried properties added by default + self.properties = {} + # Set a default date far in the past which is obviously wrong (so you'll fix your data) + self.properties["PUBLISHED"] = datetime.datetime(2000, 1, 1) + self.properties["TITLE"] = None + +def metadataGetOrgProperties(metadata): + if not os.path.exists(metadata.contentFile): + print('ERROR: Could not find associated org file "{}" for content file "{}"\n' + '\tThe file will be missing necessary metadata'.format(metadata.contentFile, + metadata.renderedFilePath)) + return + + orgRoot = orgparse.load(metadata.contentFile) + for node in orgRoot[1:]: + for property, value in node.properties.items(): + if property == "PUBLISHED": + metadata.properties[property] = datetime.datetime.strptime(value, '%Y-%m-%d') + else: + metadata.properties[property] = value + # Set TITLE as the first node if it's not a property + if not metadata.properties["TITLE"]: + metadata.properties["TITLE"] = node.heading def updateRenderedCache(): global renderedCache - global renderedDictionary + global renderedContentDictionary renderedCache = [] # Get all rendered files for root, dirs, files in os.walk(renderedDirectory): @@ -76,9 +108,15 @@ def updateRenderedCache(): renderedCache.append(renderedFile) # The path actually used to look up the content (strip '/' from front) contentPath = getRenderedLocalName(stripExtension(renderedFile))[1:] + print("\t'{}' = '{}'".format(contentPath, renderedFile)) - # No use for the value yet, we just want fast key lookups - renderedDictionary[contentPath] = renderedFile + + # This sucks + contentFile = "{}/{}.{}".format(contentDirectory, contentPath, "org") + metadata = RenderedContentMetadata(contentFile, contentPath, renderedFile) + metadataGetOrgProperties(metadata) + + renderedContentDictionary[contentPath] = metadata """ Interface @@ -86,7 +124,7 @@ Interface def getAllPostsList(): allPosts = [] - for key, value in renderedDictionary.items(): + for key, value in renderedContentDictionary.items(): # Hide folders and files with '_hidden' (they can still be retrieved though) if '_hidden' in key: continue @@ -94,11 +132,14 @@ def getAllPostsList(): allPosts.append(key) return allPosts + +def getRenderedContentDictionary(): + return renderedContentDictionary def getRenderedBody(contentPath): body = None - if contentPath in renderedDictionary: - renderedFilename = renderedDictionary[contentPath] + if contentPath in renderedContentDictionary: + renderedFilename = renderedContentDictionary[contentPath].renderedFilePath renderedFile = open(renderedFilename) body = renderedFile.readlines() body = "".join(body) diff --git a/ReadMe.org b/ReadMe.org index ca3e990..6caae80 100644 --- a/ReadMe.org +++ b/ReadMe.org @@ -5,13 +5,15 @@ This project is meant to be a quick-and-dirty blog based on org-mode formatted d You can put your ~.org~ files in ~content/~ and it will be rendered into ~renderedContent~ within fifteen minutes, or immediately after starting the server. ** Setup +<> This project requires Python 3. -*** 1. Install ~tornado~ +*** 1. Install Python dependencies +The server is powered by ~tornado~. I use ~orgparse~ to parse org document properties. ~pandoc~ is used for converting org files into html files. #+BEGIN_SRC sh - pip3 install tornado + pip3 install tornado orgparse #+END_SRC *** 2. Install pandoc @@ -29,12 +31,19 @@ This will be used to convert ~.org~ files into ~.html~. openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout certificates/server_jupyter_based.crt.key -out certificates/server_jupyter_based.crt.pem #+END_SRC +If you have a domain name, you should use [[https://certbot.eff.org/][Certbot]] to generate your certificates. These won't pop the security warning like the self-signing ones will. + *** 4. Run the server #+BEGIN_SRC sh -python3 simple-org-blog.py +python3 SimpleBlogServer.py #+END_SRC +If you want to redirect all HTTP visits to the blog server (which is HTTPS only), also run the redirect server: + +#+BEGIN_SRC sh +python3 RedirectToHttpsServer.py +#+END_SRC *** 5. Trust the certificate Open your browser and visit ~https://localhost:8888~. @@ -44,3 +53,14 @@ Your web browser should complain that the website's owner cannot be verified. Th You can safely click ~Advanced~ and add the certificate as trustworthy, because you've signed the certificate and trust yourself :). If you want to get rid of this, you'll need to get a signing authority like ~LetsEncrypt~ to generate your certificate. +** Deploying on the internet +I used [[https://lightsail.aws.amazon.com][Amazon AWS Lightsail]] to host this server for [[https://macoy.me][my personal blog]]: + +1. Create an Ubuntu machine. I used the lowest spec ($3.50/month option) because I don't expect much traffic +2. Open up port ~80~ and ~443~ in the Lightsail *Network* configuration page +3. Use the Amazon SSH stuff to start setting up the machine +4. ~sudo apt update && sudo apt upgrade~ is necessary to install Python and such +5. Use [[https://certbot.eff.org/][Certbot]] to generate certificates +6. Modify [[file:SimpleBlogServer.py::80][SimpleBlogServer port]] to be port 443 (HTTPS default) +7. Follow the [[Setup]] section normally +8. Use [[https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AccessingInstancesLinux.html][SCP]] to copy content from local machine into Amazon ~content/~ diff --git a/SimpleBlogServer.py b/SimpleBlogServer.py index 00466ea..9c53ea1 100755 --- a/SimpleBlogServer.py +++ b/SimpleBlogServer.py @@ -32,8 +32,35 @@ def getBlogHtmlBody(requestedContent): class HomeHandler(tornado.web.RequestHandler): def get(self): allPosts = ContentConverter.getAllPostsList() + + # Build content list + contentListHtml = '' + renderedContentDictionary = ContentConverter.getRenderedContentDictionary() + metadataList = [] + for contentPath, metadata in renderedContentDictionary.items(): + metadataList.append(metadata) + # Sort by newest to oldest + metadataList.sort(key = lambda metadata: metadata.properties["PUBLISHED"], reverse = True) + for metadata in metadataList: + if "_hidden" in metadata.contentPath: + continue + + # Turn folders into tags + tags = metadata.contentPath.split('/')[:-1] + tagsHtml = '' + for tag in tags: + tagsHtml += ''.format(tag) + + contentListHtml += ('\n' + .format(contentPath = metadata.contentPath, + title = metadata.properties["TITLE"], + published = metadata.properties["PUBLISHED"].strftime("%B %d, %Y"), + tagsHtml = tagsHtml)) + + # Home is also just a rendered content file, just with a special name renderedHomeBody = getBlogHtmlBody('Home_hidden') - self.render("templates/Home.html", allPosts=allPosts, homeBody=renderedHomeBody) + + self.render("templates/Home.html", allPosts=allPosts, homeBody=renderedHomeBody, contentList=contentListHtml) class BlogHandler(tornado.web.RequestHandler): def get(self, request): diff --git a/templates/BlogPost.html b/templates/BlogPost.html index 7235d1c..de4bf31 100644 --- a/templates/BlogPost.html +++ b/templates/BlogPost.html @@ -1,5 +1,7 @@ + + {{ title }} diff --git a/templates/Home.html b/templates/Home.html index 85fb366..8646df6 100644 --- a/templates/Home.html +++ b/templates/Home.html @@ -1,18 +1,18 @@ + - - Home - - - - - - - {% raw homeBody %} + + + Home + + + + + + + {% raw homeBody %} - - - + + + diff --git a/webResources/styles.css b/webResources/styles.css index 7177520..f621792 100644 --- a/webResources/styles.css +++ b/webResources/styles.css @@ -19,29 +19,42 @@ a, li, tr, td, -th { +th, +time { color: #c7ae95; font-family: Arial; } +.blogPostLinkContainer { + margin-bottom: 7px; +} + +.blogPostLink { + font-size: larger; +} + /* unvisited link */ a:link { /* color: red; */ + text-decoration: none; } /* visited link */ a:visited { - color: #c7c795; + color: #c7c795; + text-decoration: none; } /* mouse over link */ a:hover { - color: #aec795; + color: #aec795; + text-decoration: underline; } /* selected link */ a:active { - color: #95c7ae; + color: #95c7ae; + text-decoration: underline; } p, @@ -52,6 +65,17 @@ td { color: #95aec7; } +time { + color: #95aec7; + /* color: #c7ae9; */ + font-style: italic; + font-size: smaller; +} + +.publishedDate { + margin-left: 20px; +} + /* Font sizes for consistently sized text */ p, blockquote, @@ -61,8 +85,11 @@ li { } label { - font-size: large; - color: #f07f43; + /* font-size: large; */ + /* color: #f07f43; */ + color: #c79595; + margin-left: 10px; + font-style: italic; } img {