From August 1st until recently, we were busy creating this blog with HTML, CSS, JavaScript and Python backend. Previously we were working on another website, so we started this by simply copy pasting what we already had and modifying as needed. Below is a breakdown of the technologies used. We made this website from scratch.
The source code for this blog can be found here on GitHub. We tried to follow best practices as closely as possible, so we hope this repository will help you structure your websites as well.
The pages are mainly structured by the divs with the class “row.” Even though these HTML tags take up with the majority of the line count, it is simply a repetitive work. If you’ve coded a website you would know.
If you’ve visited the homepage of our website, you must have noticed the pop up that asks you to join our mailing list. That is called a modal. The pop up was designed to show 4 seconds after the homepage is loaded, but it won’t show again on the homepage if you only navigate on our website with on-page buttons(if you don’t refresh the page). Also, the “submit” button only works if you’ve filled out all the text fields. Below is a snippet of the Javascript code:
function displayPop() { var timeOut = setTimeout(function () { document.querySelector(".bg-modal").style.display = "flex"; }, 4000); document.querySelector(".close").addEventListener("click", function () { document.querySelector(".bg-modal").style.display = "none"; }); document .getElementById("modal-button") .addEventListener("click", function () { if ($("#modal-name").val() != "" && $("#modal-name").val() != "") { $.ajax({ url: "{{ url_for('main.email_confirm')}}", type: "POST", data: { name: $("#modal-name").val(), email: $("#modal-email").val(), }, }); document.querySelector(".bg-modal").style.display = "none"; } }); } if (document.referrer.indexOf(window.location.hostname) == -1) { displayPop(); }
Making this page mobile-friendly in every aspect was a very time-consuming process. If you’ve visited our website on a laptop or a device with a bigger screen, you will notice some differences. For instance, the homepage consists of rows of three blog posts on a bigger screen, whereas mobile views only contain one on each row. This was done through one of the CSS elements from Bootstrap. In my code, I also utilized @media screen
CSS element to customize padding and margin on specific screen width.
On each blog post page, the thumbnail behind the title sometimes takes up the focus if the color is too bright. The contrast between the color of the thumbnail and the title sometimes is too similar too. Therefore, I decided to mask a layer of gradient so that the title can stand out. I did this using the code below:
.background-img:after { content: ""; top: 0; left: 0; right: 0; bottom: 0; position:absolute; background-image: -webkit-gradient( linear, left bottom, left top, from(rgba(33, 38, 49, 0.3)), to(transparent) ); }
I added a linear gradient of black, with a transparency of 0.3.
If you have come across any of our blog posts, you may have noticed the code markup with code examples inside. We did this by adding a CDN that was previously developed by Google. It is a library that sites such as Stackoverflow use. This was particularly useful for us to show off some code examples. Here is the source of the library.
Scrolling through our homepage, you might notice that each blog post would slide up as you scroll. This was done by using another Javascript library called Aos. This library was super easy to implement.
Since I prefer Python over other popular backend alternatives like NodeJS, PHP and Ruby, I watched Corey Schafer's Flask tutorials series on YouTube. If you're learning Python and you don't know who he is, you should definitely check him out as he covers a wide range of Python programming syntax and libraries. I will be writing a post about my top youtube channel recommendations very soon, so definitely make sure to subscribe to our email newsletter.
Naturally, you will most likely find that you want to do more with your blog than what he covered in the tutorials. For me, I wanted to create a draft system, a mailing list, a tag system, a comment section, a dedicated role management system, and more. I will be covering each one of them below, in order of increasing difficulty.
I wanted to create an option to "draft" our posts so that we can work on them and save it for later without publishing it for everyone to see. This was just a mater of adding a draft
column to the Post
database and a draft BooleanField
to the Flask form.
I wanted to create Member
and Admin
roles similar to what you would find on a Discord server, so that only admins (@StevenOh and myself) can access certain content like creating new pages, sending out emails to our subscribers, accessing the database, and more.
For this, I found a Flask extension called flask-admin
which perfectly did what we wanted to accomplish. I found this video by Pretty Printed extremely useful, so definitely go check it out and the rest of his videos.
Basically, what this allows me to do is to create a custom page. This is a snippet of my models.py file.
from application import admin # import the admin extension initialised in the __init__ file from flask_admin.contrib.sqla import ModelView class MyModelView(ModelView): def is_accessible(self): return current_user.is_authenticated and current_user.role == "Admin" # only Admins can access admin.add_view(MyModelView(User, db.session)) admin.add_view(MyModelView(Post, db.session)) admin.add_view(MyModelView(Tag, db.session)) admin.add_view(MyModelView(Like, db.session)) admin.add_view(MyModelView(Comment, db.session)) admin.add_view(MyModelView(SubscribedUser, db.session))
And, as always, your admin extension needs to be initialised in your __init__.py
file like so:
from flask_admin import Admin admin = Admin() def create_app(config_class=Config): admin.init_app(app)
Once that is done, the flask-admin extension automatically creates a new page for you.
This was my first time working with many-to-many relationships in SQLAlchemy, so I had to do a lot of googling and stack overflow searching to get the feature I wanted.
tag_table = db.Table('tag_association', db.Column('id', db.Integer, db.ForeignKey('post.id')), db.Column('tag_id', db.Integer, db.ForeignKey('tag.tag_id')) ) class Post(db.Model): tags = db.relationship("Tag", secondary=tag_table, backref=db.backref('posts', lazy='dynamic'), lazy='dynamic') #Tag.posts class Tag(db.Model): tag_id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(30), unique=True, nullable=False) colour = db.Column(db.String(30), default="black")
Another feature we wanted to implement was a "like post" feature.
class Like(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) post_id = db.Column(db.Integer, db.ForeignKey('post.id')) timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) class User(db.Model, UserMixin): liked_posts = db.relationship('Like', backref=db.backref('user'), lazy="dynamic") def like_post(self, post): if not self.has_liked_post(post): #if the user has not already liked this post yet, like = Like(user_id=self.id, post_id=post.id) db.session.add(like) db.session.commit() def unlike_post(self, post): if self.has_liked_post(post): #if the user has already liked the post, Like.query.filter_by(user_id=self.id, post_id=post.id).delete() def has_liked_post(self, post): return Like.query.filter(Like.user_id == self.id, Like.post_id == post.id).count() > 0 class Post(db.Model): likes = db.relationship("Like", backref=db.backref('post'), lazy="dynamic") #Post.liked_users
By far the hardest feature to implement was a comment section. I wanted to create a nested comment section like you find on Reddit, but according to Miguel Grinberg, a well respected author of Flask tutorials, this is extremely difficult to do with an ORM (Object Relational Mapper) like SQLAlchemy.
After choosing to create a flat comment section, I also had another problem. I wanted both logged in users and logged out users to be able to comment on our posts, so I tried implementing two separate models called AnonymousComment
and UserComment
, but many conflicts arose in the Jinja2 in the post HTML page, so I had to settle with the single Comment
class below.
class Comment(db.Model): id = db.Column(db.Integer, primary_key=True) post_id = db.Column(db.Integer, db.ForeignKey("post.id")) name = db.Column(db.String(20)) content = db.Column(db.Text, nullable=False) timestamp = db.Column(db.DateTime, index=True, default=datetime.now(get_localzone()))
I wanted users to be able to read all of their comments, by accessing something like user.comments
, but I was not able to implement this feature because of the fact that some comments do not have a "user" author - some are anonymous comments.
Sometime along the way, we found out about the GitHub Student Developer Pack, a pack full of awesome perks for anyone enrolled in an educational institution. With this pack, we got a .team domain name for free from name.com and we have been able to create high quality thumbnails with Canva Pro. Our site is hosted on a free dyno at Heroku using gunicorn and a Postgres database.
Our frontend developer @StevenOh also uses Polypane to help develop a responsive website. Polypane is a separate browser, akin to Safari or Chrome, which lets you emulate a page in different screen sizes like a phone, a table and a computer.
Finally, Heroku has an ephemeral hard drive, meaning any files written to the server will not be saved once the app is redeployed (when we push a new change to GitHub, Heroku automatically redeploys our website) or the application is refreshed, which happens every 24 hours. Therefore, we signed up for the AWS Free Tier which lets us use S3 (Simple Storage Service) completely free of charge for one year. I then changed my Python code so that the files are saved to our S3 bucket instead, using Python's boto3
library.
So, what did you think of our blog-making journey? Do you see any possible improvements? Did it inspire you to start your own blog? If so, like this post and comment down below! P.S. (you saw the amount of work it took to create those features, use them!)