Browse Templates/Blog / CMS Platform

Blog / CMS Platform

A blog and content management system database designed for WordPress-like platforms. Features include multi-author support with role-based access, hierarchical categories, many-to-many tag relationships, nested comments, and content versioning. Perfect for building custom CMS solutions, blogging platforms, or knowledge bases.

PostgreSQL10 tablesWeb App
blogcmscontentwordpresspublishing
Created with ERD Studio

About this schema

This is the shape a content system settles into once editing is a job rather than a hobby: posts holds the current article, post_revisions holds every version that came before, and the two are separate so that reading a blog never touches the history.

Comments are written by people who do not have accounts. author_name and author_email sit on the comment row rather than pointing at users, and is_approved gates publication. That is what makes this a public blog rather than a members' site — an anonymous comment is a first-class row, not a user record created on the fly.

Almost everything else is the usual furniture, with one exception worth noticing: settings is a key-value table with updated_by. Site configuration lives in the database rather than in a file so that an editor can change it, and the column records who did.

Key relationships

posts → post_revisions
The post is current, the revisions are history, and revision_number orders them. editor_id is on the revision because the person who last edited is often not the author.
categories → categories
parent_id gives a tree. A post has exactly one category, which is what separates categories from tags here.
posts ↔ tags via post_tags
Many-to-many, and note the junction has no id of its own — the pair is the key. A post has one category and any number of tags.
comments → comments
parent_id nests replies. is_approved is per comment, so a reply can be held while its parent is published.
media → users
Uploads are owned by the uploader and not attached to a post. The same image can be used in several articles, and deleting a post does not delete its pictures.

Design decisions

Revisions are rows, not a diff chain

post_revisions stores the whole title and content each time rather than a delta. It costs storage and buys the thing that matters: any version can be read or restored in one query, with no replay. For text-sized documents that trade is nearly always right — a diff chain is worth it when the documents are large or the revisions are many, and a blog is neither.

Comment authors are fields, not accounts

Requiring a user row for every commenter means creating accounts nobody asked for, and it makes deleting a spammer a user deletion. Holding the name and email on the comment keeps the identity where it is used, and is_approved lets moderation happen without any account existing at all.

One category, many tags

category_id is a column on the post and tags go through a junction. That is a deliberate asymmetry: a category is where an article lives and drives the URL and the navigation, while tags are labels and can be many. Making both many-to-many loses the guarantee that every post has exactly one home.

Status and published_at are separate

status says draft, scheduled or published; published_at says when. Keeping both means scheduling is just a future timestamp, and a post that is unpublished and republished keeps its original date. A single state column would need a second one anyway as soon as scheduling appears.

Tables in this template

usersAuthors
9 cols
categoriesCategories
6 cols
postsPosts
10 cols
post_revisionsPost Revisions
7 cols
tagsTags
3 cols
post_tagsPost Tags
2 cols
commentsComments
8 cols
mediaMedia
8 cols
settingsSettings
5 cols
subscribersSubscribers
6 cols

Frequently asked

Why keep full content in every revision instead of diffs?
Because restoring a version should be a read, not a replay. Articles are small enough that the storage is not a problem, and a diff chain means any corrupted link breaks everything after it.
Should comments point at users instead?
Only if commenting requires an account. If it does, replace author_name and author_email with a user_id. If it does not, keeping them as fields avoids manufacturing user rows for one-off commenters — and you can have both, with a nullable user_id for signed-in readers.
Why does post_tags have no id column?
The pair (post_id, tag_id) is the key — there is nothing else to say about the relationship. A surrogate id would allow the same tag twice on one post unless you added the unique constraint anyway.
Where do scheduled posts come from?
status set to scheduled and published_at in the future. Something has to flip the status when the time comes, or the reading query has to treat published_at in the past as the condition — the second is simpler and needs no job.
Is a key-value settings table a good idea?
For a handful of site options, yes — it lets an editor change them without a deploy. It stops being a good idea when the values need types, validation or relationships, because every consumer then parses a string and none of them agree.