diff --git a/_data/metadata.js b/_data/metadata.js
index f77f534..65a3f6a 100644
--- a/_data/metadata.js
+++ b/_data/metadata.js
@@ -13,6 +13,8 @@ module.exports = {
 	copyrightNotice: "© Nathan Upchurch 2022 - 2024",
 	defaultPostImageURL: "/img/vasilina-sirotina-1NMPvajSt9Q-unsplash_copy.avif",
 	defaultPostImageAlt: "The default post image: a close picture of the dark green leaves of a plant.",
+	mastodonHost: "lounge.town",
+	mastodonUser: "nathanu",
 	postlistHeaderText: "Latest Posts",
 	socialLinks: [
 		{
diff --git a/_includes/footer.njk b/_includes/footer.njk
index ec14a44..0786d35 100644
--- a/_includes/footer.njk
+++ b/_includes/footer.njk
@@ -1,12 +1,13 @@
 <footer>
-	{% if metadata.copyrightNotice %}<p>{{ metadata.copyrightNotice }}</p>{% endif %}<br>
+	<p>{% if metadata.copyrightNotice %}<span class="copyright-notice">{{ metadata.copyrightNotice }}</span>{% endif %}
 
 	{% if metadata.webrings %}
 	{% for webring in metadata.webrings %}
+	<span class="webring">
 	{% if webring.previousURL %}<a href="{{ webring.previousURL  }}">←</a>{% endif %}
 			{% if webring.ringURL %}<a href="{{ webring.ringURL }}">{{ webring.name }}</a>{% endif %}
 			{% if webring.nextURL %}<a href="{{ webring.nextURL }}">→</a>{% endif %}
-	<br>
+	</span>
 	{% endfor %}
-	{% endif %}
+	{% endif %}</p>
 </footer>
diff --git a/_includes/layouts/post.njk b/_includes/layouts/post.njk
index 8b1d6cc..3f0c42b 100644
--- a/_includes/layouts/post.njk
+++ b/_includes/layouts/post.njk
@@ -3,6 +3,7 @@ layout: layouts/base.njk
 ---
 {# Only include the syntax highlighter CSS on blog posts #}
 {%- css %}{% include "public/css/code.css" %}{% endcss %}
+<article class="post">
 <h1>{{ title }}</h1>
 
 <div class="post-metadata">
@@ -28,5 +29,7 @@ layout: layouts/base.njk
 </div>
 
 {{ content | safe }}
-
+</article>
+{% include "mastodonComments.njk" %}
+<h2>Read Next</h2>
 {% include "nextLast.njk" %}
diff --git a/_includes/mastodonComments.njk b/_includes/mastodonComments.njk
new file mode 100644
index 0000000..5cb75ec
--- /dev/null
+++ b/_includes/mastodonComments.njk
@@ -0,0 +1,94 @@
+{% if mastodon_id %}
+<section id="comment-section">
+	<h2>Comments</h2>
+	<div class="comment-ingress"></div>
+	<div id="comments" data-id="{{ mastodon_id }}">
+		<p>Loading comments...</p>
+	</div>
+	<div class="continue-discussion">
+		<a class="big-link" href="https://{{ metadata.mastodonHost }}/@{{ metadata.mastodonUser }}/{{ mastodon_id }}">Comment by replying to this post on Mastodon &#187;</a>
+</section>
+
+<template id="comment-template">
+	<wc-comment
+	author_name=""
+	author_url=""
+	avatar_url=""
+	comment_content=""
+	publish_date=""
+	sharp_corner="">
+	</wc-comment>
+</template>
+
+<script>
+const monthMap = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
+
+const dateSuffixAdder = (date) => {
+	if (date > 9 && date < 20) {
+		return "th";
+	} else {
+		let dateString = date < 10 ? "0" + date : "" + date;
+		if (dateString[1] < 4 && dateString[1] > 0) {
+			return dateString[1] == 1 ? "st" :
+				dateString[1] == 2 ? "nd" :
+					dateString[1] == 3 ? "rd" : null;
+		} else {
+			return "th"
+		}
+	}
+}
+
+const timeFormatter = (hours, minutes) => {
+		return `${hours < 12 ? hours : hours - 12}:${minutes < 10 ? "0" : ""}${minutes} ${hours < 12 ? "AM" : "PM"}`
+}
+
+const renderComment = (comment, target, parentIdm) => {
+  const node = document
+    .querySelector("template#comment-template")
+    .content.cloneNode(true);
+
+  const dateObj = new Date(comment.created_at);
+
+  const dateTime = `${dateObj.getDate()}${dateSuffixAdder(dateObj.getDate())} of ${monthMap[dateObj.getMonth()]}, ${dateObj.getFullYear()}, at ${timeFormatter(dateObj.getHours(), dateObj.getMinutes())}`;
+
+    node.querySelector("wc-comment").setAttribute("author_name", comment.account.display_name);
+    node.querySelector("wc-comment").setAttribute("author_url",
+    `${comment.account.acct == "{{ metadata.mastodonUser }}" ? "https://{{ metadata.mastodonHost }}/@{{ metadata.mastodonUser }}" : comment.account.acct}`);
+    node.querySelector("wc-comment").setAttribute("avatar_url", comment.account.avatar_static);
+    node.querySelector("wc-comment").setAttribute("comment_content", comment.content);
+		node.querySelector("wc-comment").setAttribute("publish_date", dateTime);
+
+  target.appendChild(node);
+}
+
+async function renderComments() {
+  const commentsNode = document.querySelector("#comments");
+
+  const mastodonPostId = commentsNode.dataset?.id;
+
+  if (!mastodonPostId) {
+    return;
+  }
+
+  commentsNode.innerHTML = "";
+
+  const originalPost = await fetch(
+    `https://{{ metadata.mastodonHost }}/api/v1/statuses/${mastodonPostId}`
+  );
+  const originalData = await originalPost.json();
+  renderComment(originalData, commentsNode, null);
+
+  const response = await fetch(
+    `https://{{ metadata.mastodonHost }}/api/v1/statuses/${mastodonPostId}/context`
+  );
+  const data = await response.json();
+  const comments = data.descendants;
+
+  comments.forEach((comment) => {
+    renderComment(comment, commentsNode, mastodonPostId);
+  });
+}
+
+renderComments();
+</script>
+{% endif %}
diff --git a/_includes/metadata.njk b/_includes/metadata.njk
index d7c47b9..ffca936 100644
--- a/_includes/metadata.njk
+++ b/_includes/metadata.njk
@@ -4,6 +4,7 @@
 <link rel="icon" type="image/x-icon" href="/img/logo_favicon.svg">
 <meta name="description" content="{{ description or metadata.description }}">
 <meta name="robots" content="noai, noimageai">
+<meta name="generator" content="{{ eleventy.generator }}">
 <link rel="alternate" href="/feed/feed.xml" type="application/atom+xml" title="{{ metadata.title }}">
 <link rel="alternate" href="/feed/feed.json" type="application/json" title="{{ metadata.title }}">
-<meta name="generator" content="{{ eleventy.generator }}">
+<script type="module" src="/js/main.js"></script>
diff --git a/content/blog/cowsay_2024-01-02.md b/content/blog/cowsay_2024-01-02.md
index 0dca51b..0afb71b 100644
--- a/content/blog/cowsay_2024-01-02.md
+++ b/content/blog/cowsay_2024-01-02.md
@@ -7,6 +7,7 @@ tags:
 synopsis: An ASCII cow postulates on the state of science education in the modern world.
 imageURL: /img/cowsayOfTheDay.avif
 imageAlt: An ASCII cow with a thought bubble containing the word wut
+mastodon_id: "111688829907363670"
 ---
 As a big-old nerd, I spend a lot of time in the terminal on my computer. When you spend a lot of time somewhere, you want it to be comfortable. As a part of making my terminal more homey, I've set it up to give me a random quote each time I start a new session, delivered, of course, by a cow. Here's today's cowsay of the day:
 
diff --git a/content/blog/let-us-waffle.md b/content/blog/let-us-waffle.md
index b4c58a8..5de3ff5 100644
--- a/content/blog/let-us-waffle.md
+++ b/content/blog/let-us-waffle.md
@@ -8,6 +8,7 @@ tags:
 synopsis: Tools like cooked.wiki let us strip away the cruft from online recipes. Is this necessarily a good thing?
 imageURL: /img/pexels-brigitte-tohm-378008_compressed.webp
 imageAlt: An oddly rectangular waffle covered in raspberries. It actually looks quite dry and not very nice. Hopefully there's some syrup on the side!
+mastodon_id: "111812478768090324"
 ---
 So, about this [cooked.wiki](https://cooked.wiki) thing, believe me when I say I take my fair share in our collective frustration as I find myself skimming through a  hugoesque tome on Brayden and Braxlynne’s wiggly teeth in order to reach the ingredients for “Keighleigh’s Extra Easy No-Bake Ten Minute Palmiers (*So Delicious You’ll Snort the Crumbs!*),” but I must admit that the endless complaining about it puts me out a bit. Here’s the thing; as someone who writes for his own personal blog, who plans to someday publish a recipe or two, *the waffling is the point.*
 
diff --git a/content/blog/new-kmines-themes.md b/content/blog/new-kmines-themes.md
index 9421892..5e85d32 100644
--- a/content/blog/new-kmines-themes.md
+++ b/content/blog/new-kmines-themes.md
@@ -8,6 +8,7 @@ tags:
 synopsis: My first KDE contribution! Two new high-contrast KMines themes that will arrive with Plasma 6.
 imageURL: /img/kmines_dark.webp
 imageAlt: A screenshot of the KMines game window showing a new dark theme.
+mastodon_id: "111794936518292495"
 ---
 ## Why KMines?
 Minesweeper is a tragically underrated puzzle game. While I recall examining the mysterious array of gray squares as a child, it wasn't until adulthood that I took the time to learn the rules of the game. Despite my late start, however, I still count minesweeper as a classic. These days, good minesweeper clones are hard to come by. I settled on GNOME's [Mines](https://wiki.gnome.org/Apps/Mines) for a while, but as the look of GTK applications on my QT-based [KDE Plasma Desktop](https://kde.org/plasma-desktop/) sets my teeth on edge, I ditched it for [KMines](https://apps.kde.org/kmines/) in short order. While I enjoyed the game, I found the themes shipped with KMines a bit dated, so I thought I'd make my own.
diff --git a/content/blog/offline.md b/content/blog/offline.md
index 9806b02..69643a5 100644
--- a/content/blog/offline.md
+++ b/content/blog/offline.md
@@ -7,6 +7,7 @@ tags:
 synopsis: A conversation with a colleague caused me to consider the consequences of online-only tooling.
 imageURL: /img/kenny-eliason-uq5RMAZdZG4-unsplash.webp
 imageAlt: A server rack in the dark with colorful cables draped between the ports of servers and switches.
+mastodon_id: "111268603361637013"
 ---
 As part of a project investigating a potential new piece of software, I've been speaking with colleagues and contractors to determine which features they rely on to do their day-to-day tasks, as well as discover any wish-list items for a new platform. In one of these discussions with a colleague, we had covered her relatively simple use case, and moved on to discuss potential features that might be useful. At this juncture, she mentioned, somewhat apologetically, that should a particular workflow be translated to a new platform, it was important to be able to access data and documents offline.
 
diff --git a/content/blog/patience.md b/content/blog/patience.md
index 1049a61..d9ca127 100644
--- a/content/blog/patience.md
+++ b/content/blog/patience.md
@@ -8,6 +8,7 @@ tags:
 synopsis: Learning about patience through an incense-making miscalculation.
 imageURL: /img/dragons_blood_incense_copy.avif
 imageAlt: A small piece of a coreless, Japanese-style incense stick burning in a black cast-iron burner.
+mastodon_id: "111732713202024407"
 ---
 Some time ago, maybe a year or so, I extruded a batch of incense sticks from some ingredients I thought might go well together: sandalwood, cinnamon, dragon's blood resin, a touch of Hojari frankincense for acidity, and some tonka bean for sweetness, if I recall correctly. After leaving the sticks to dry overnight, I was disappointed to see that they didn't stay lit; the stick would shrink behind the ember, and it would fizzle out in short order. Even worse, the little scent I was able to detect during the short burn was terrible: acrid and smoky. Dejected, I put the sticks away, returning to attempt to burn a small fragment every few days or so before I lost interest entirely. A few months later, the tube of crooked red incense sticks caught my eye, and I once again attempted to burn a stick. To my surprise, it stayed lit throughout the entire burn. The fragrance had transformed also, from leafy-campfire to a simple, warm, slightly sweet, and medicinal fragrance. While this was enough of an improvement to encourage me to light one every now and then, I remained disappointed that the fragrance was so far from what I'd hoped to achieve. After half-heartedly burning each stick in the little plastic tube that housed them over a period of weeks, the tube disappeared into a basket on the shelf beneath my coffee table amidst a mess of bundled cables and game-controllers, never to be seen again – until just a few days ago.
 
diff --git a/content/blog/underrated-apps-qownnotes.md b/content/blog/underrated-apps-qownnotes.md
index 0420d90..08ec2fc 100644
--- a/content/blog/underrated-apps-qownnotes.md
+++ b/content/blog/underrated-apps-qownnotes.md
@@ -8,6 +8,7 @@ tags:
   - Underrated Apps
 imageURL: /img/qownnotes.webp
 imageAlt: A screenshot of QOwnNotes showing a note subfolder panel beside markdown editor and preview panels.
+mastodon_id: "110862579682916657"
 ---
 [![A screenshot of QOwnNotes showing a note subfolder panel beside markdown editor and preview panels.](/img/qownnotes.webp "QOwnNotes running on EndeavourOS / KDE Plasma")](/img/qownnotes.webp)
 
diff --git a/content/blog/vegan-and-alternative-diets-in-foodservice.md b/content/blog/vegan-and-alternative-diets-in-foodservice.md
index 3e214e4..8c32eb2 100644
--- a/content/blog/vegan-and-alternative-diets-in-foodservice.md
+++ b/content/blog/vegan-and-alternative-diets-in-foodservice.md
@@ -8,6 +8,7 @@ tags:
 synopsis: Breaking down the alternative-diet restaurant experience to offer some perspective and advice to foodservice professionals and proprietors.
 imageURL: /img/k8-sWEpcc0Rm0U-unsplash.webp
 imageAlt: An overhead view of a restaurant interior showing guests sitting at tables with white tablecloths, eating pastries.
+mastodon_id: "111173763912666764"
 ---
 I've been a vegan for close to a decade. From washing dishes, slinging cocktails, and pouring latte art, to recipe development, hiring, compliance, and multi-location operations, I've also been around the block a few times when it comes to foodservice. So when I tell you that the vast majority of foodservice establishments of any stripe are utter and complete complete nightmares for people with alternative diets, I hope you'll take me at my word.
 
diff --git a/content/index.njk b/content/index.njk
index c1747ff..b612c50 100644
--- a/content/index.njk
+++ b/content/index.njk
@@ -13,5 +13,5 @@ numberOfLatestPostsToShow: 5
 
 {% set morePosts = postsCount - numberOfLatestPostsToShow %}
 {% if morePosts > 0 %}
-<p>See {{ morePosts }} more post{% if morePosts != 1 %}s{% endif %} in <a href="/blog/">the blog</a>.</p>
+<a class="big-link" href="/blog/">See {{ morePosts }} more post{% if morePosts != 1 %}s{% endif %} in the blog &#187;</a>
 {% endif %}
diff --git a/eleventy.config.js b/eleventy.config.js
index ca0cb07..abd0344 100644
--- a/eleventy.config.js
+++ b/eleventy.config.js
@@ -78,6 +78,8 @@ module.exports = function(eleventyConfig) {
 	eleventyConfig.addPassthroughCopy({ 'public/xsl/*': "/xsl/" });
 	eleventyConfig.addPassthroughCopy({ 'public/img/*': "/img/" });
 	eleventyConfig.addPassthroughCopy({ 'public/robots.txt': "/" });
+	eleventyConfig.addPassthroughCopy({ 'public/js/*': "/js/" });
+	eleventyConfig.addPassthroughCopy({ 'public/js/webComponents/*': "/js/webComponents" });
 	// Copying so that basic.xsl can use it
 	eleventyConfig.addPassthroughCopy({ 'public/css/index.css': "/css/index.css" });
 	eleventyConfig.addPassthroughCopy({ 'public/css/webfonts/*': "/css/webfonts/" });
diff --git a/public/css/dropcap.css b/public/css/dropcap.css
index de171b4..7723a5a 100644
--- a/public/css/dropcap.css
+++ b/public/css/dropcap.css
@@ -1,4 +1,4 @@
-main > p:not(.nodropcap):first-of-type:first-letter {
+main > article > p:not(.nodropcap):first-of-type:first-letter {
 	float: left;
 	font-size: 4rem;
 	padding: .5rem .5rem .5rem .5rem;
diff --git a/public/css/index.css b/public/css/index.css
index f21b120..ca20f74 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -24,7 +24,7 @@
 	--icon-filter: none;
 
 	/* Corners */
-	--corner-radius: .3rem;
+	--border-radius: .3rem;
 
 	/* Space & Size */
 	--syntax-tab-size: 2;
@@ -32,6 +32,7 @@
 	--single-gap: 1rem;
 	--double-gap: 2rem;
 	--triple-gap: 3rem;
+	--quad-gap: 4rem;
 
 	/* Transitions */
 	--transition-normal: all .3s;
@@ -49,11 +50,28 @@
 	--weight-heavy: 500;
 	--weight-normal: 300;
 
+	/* Links */
+	--link-decoration-thickness: .1rem;
+
 	/* Borders */
 	--border-nav: 1px solid var(--text-color);
 	--border-nav-currentpage: 20px solid var(--contrast-color);
 	--border-nav-hover: 20px solid var(--text-color);
 	--border-thin: 1px solid var(--color-gray-20);
+
+	/* Shadow */
+	--box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
+
+	/* Components */
+	--wc-card-background-color: var(--card-color);
+	--wc-card-border-radius: var(--border-radius);
+	--wc-card-box-shadow: var(--box-shadow);
+	--wc-link-color: var(--text-color);
+	--wc-link-decoration-color: var(--contrast-color);
+	--wc-link-decoration-thickness: var(--link-decoration-thickness);
+	--wc-comment-text-margin: auto auto auto 4rem;
+	--wc-profile-pic-size: 3rem;
+	--wc-profile-pic-border-radius: 10rem;
 }
 
 @media (prefers-color-scheme: dark) {
@@ -64,7 +82,7 @@
 		--contrast-color: #04c49e;
 
 		/* --text-color is assigned to --color-gray-_ above */
-		--text-color-link: var(--contrast-color);
+		--text-color-link: var(--text-color);
 
 		--background-color: #15202b;
 		--logo-filter: none;
@@ -96,6 +114,7 @@ body {
 	background-color: var(--background-color);
 	color: var(--text-color);
 	font-family: var(--font-family);
+	font-size: 13px;
 	font-variant-Ligatures: normal;
 	font-weight: var(--weight-normal);
 	margin: 0 auto;
@@ -109,7 +128,7 @@ body {
 }
 a {
 	text-decoration-color: var(--contrast-color);
-	text-decoration-thickness: .1rem;
+	text-decoration-thickness: var(--link-decoration-thickness);
 	transition: var(--transition-normal);
 }
 /* https://www.a11yproject.com/posts/how-to-hide-content/ */
@@ -123,8 +142,18 @@ a {
 	width: 1px;
 }
 footer {
+	margin-top: var(--triple-gap);
 	padding: var(--single-gap);
-	border-top: var(--border-thin);
+}
+footer .copyright-notice {
+	padding-right: var(--single-gap);
+}
+footer .webring {
+	display: inline-block;
+	padding-right: var(--single-gap);
+}
+footer p {
+	font-size: var(--font-s);
 }
 h1, h2, h3 {
 	color: var(--text-color);
@@ -140,7 +169,7 @@ h1 {
 h2 {
 	font-size: var(--font-xl);
 	font-weight: var(--weight-extraheavy);
-	margin-top: var(--double-gap);
+	margin: var(--quad-gap) auto 0 auto;
 }
 h3 {
 	font-size: var(--font-l);
@@ -167,7 +196,7 @@ p, li {
 }
 figure {
 	margin: 0;
-	padding: var(--single-gap) 0 var(--single-gap) 0;
+	padding: var(--single-gap) 0 0 0;
 	width: 100%;
 }
 figure > a > img {
@@ -182,6 +211,17 @@ figcaption {
 .page-block {
 	margin-bottom: var(--triple-gap);
 }
+.big-link {
+	width: 100%;
+	padding: var(--half-gap);
+	border: var(--border-nav);
+	border-radius: var(--border-radius);
+	margin: var(--single-gap) auto var(--single-gap) auto;
+	transition: var(--transition-normal);
+}
+.big-link:hover {
+	border-color: var(--contrast-color);
+}
 a[href]:not(.icon-button) {
 	color: var(--text-color-link);
 }
@@ -194,9 +234,8 @@ a[href]:active:not(.icon-button) {
 }
 .links-nextprev {
 	list-style: none;
-	border-top: var(--border-thin);
-	padding: var(--triple-gap) 0 var(--single-gap) 0;
-	margin-top: var(--triple-gap);
+	padding: 0 0 var(--single-gap) 0;
+	margin-top: var(--single-gap);
 }
 
 table {
@@ -207,6 +246,27 @@ table th {
 	padding-right: 1em;
 }
 
+/* Comments */
+.comment-ingress {
+	margin-bottom: var(--double-gap);
+}
+#comment-section h2 {
+	margin: var(--quad-gap) auto 0 auto;
+}
+wc-comment::part(author-link) {
+	font-size: var(--font-n);
+	font-weight: var(--weight-extraheavy);
+	text-decoration: none;
+}
+wc-comment::part(main) {
+	margin-bottom: var(--double-gap);
+}
+wc-comment::part(publish-date) {
+	font-weight: var(--weight-heavy);
+	font-size: var(--font-s);
+	margin-top: -.25rem;
+}
+
 /* Code Fences */
 pre,
 code {
@@ -233,7 +293,6 @@ code {
 /* Header */
 header {
 	align-items: end;
-	border-bottom: var(--border-thin);
 	display: flex;
 	flex-wrap: wrap;
 	gap: 1em .5em;
@@ -331,17 +390,17 @@ nav ul {
 .postlist-item {
 	align-items: flex-start;
 	background-color: var(--card-color);
-	border-radius: var(--corner-radius);
+	border-radius: var(--border-radius);
 	box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
 	display: flex;
 	flex-flow: row nowrap;
 	justify-content: flex-start;
-	margin-bottom: 1em;
+	margin-bottom: var(--double-gap);
 	padding: var(--single-gap) 1.1rem var(--single-gap) 1.1rem;
 	width: 100%;
 }
 .post-image-container {
-	border-radius: var(--corner-radius);
+	border-radius: var(--border-radius);
 	margin-right: var(--single-gap);
 	max-height: 15rem;
 	overflow: hidden;
@@ -421,9 +480,14 @@ a.post-tag:hover {
 	align-self: center;
 }
 
+/* Article / Post */
+.post h2 {
+	font-size: var(--font-l);
+}
+
 /* Post Metadata */
 .post-metadata {
-	margin-bottom: var(--triple-gap);
+	margin-bottom: var(--double-gap);
 	margin-top: var(--single-gap);
 	padding: 0 0 0 .4rem;
 }
@@ -481,7 +545,7 @@ h2 + .header-anchor {
 		margin-bottom: var(--single-gap);
 	}
 	h3, .post-copy a h3 {
-		font-size: 1rem;
+		font-size: 1.25rem;
 	}
 
 	/* Header */
@@ -494,6 +558,11 @@ h2 + .header-anchor {
 		margin-top: var(--single-gap);
 	}
 
+	/* Footer */
+	footer .webring {
+		display: block;
+	}
+
 	/* Nav */
 	.nav {
 		flex-flow: row wrap;
diff --git a/public/js/main.js b/public/js/main.js
new file mode 100644
index 0000000..9b2097c
--- /dev/null
+++ b/public/js/main.js
@@ -0,0 +1,4 @@
+import './webComponents/card.js';
+import './webComponents/profilePic.js';
+import './webComponents/speechBubble.js';
+import './webComponents/comment.js';
diff --git a/public/js/webComponents/card.js b/public/js/webComponents/card.js
new file mode 100644
index 0000000..36c58c6
--- /dev/null
+++ b/public/js/webComponents/card.js
@@ -0,0 +1,33 @@
+const template = document.createElement('template');
+
+template.innerHTML = `
+<style>
+	#card {
+		align-items: flex-start;
+		background-color: var(--wc-card-background-color);
+		border-radius: var(--wc-card-border-radius);
+		box-shadow: var(--wc-card-box-shadow);
+		display: flex;
+		flex-flow: row nowrap;
+		justify-content: flex-start;
+		margin-bottom: 1em;
+		padding: var(--single-gap) 1.1rem var(--single-gap) 1.1rem;
+		width: 100%;
+	}
+</style>
+
+<div id="card" part="main">
+	<slot></slot>
+</div>
+`
+
+class card extends HTMLElement {
+	constructor() {
+		super();
+
+		this._shadowRoot = this.attachShadow({ 'mode': 'open' });
+		this._shadowRoot.appendChild(template.content.cloneNode(true));
+	}
+}
+
+window.customElements.define('wc-card', card);
diff --git a/public/js/webComponents/comment.js b/public/js/webComponents/comment.js
new file mode 100644
index 0000000..2befd81
--- /dev/null
+++ b/public/js/webComponents/comment.js
@@ -0,0 +1,76 @@
+const template = document.createElement('template');
+
+template.innerHTML = `
+<style>
+	a {
+		color: var(--wc-link-color);
+		text-decoration-color: var(--wc-link-decoration-color);
+		text-decoration-thickness: var(--wc-link-decoration-thickness);
+	}
+	#comment {
+		margin: var(--wc-comment-text-margin);
+	}
+	#comment p {
+		margin: 0 auto 0 auto;
+	}
+	#meta {
+		display: flex;
+		flex-flow: row nowrap;
+	}
+	#meta-text {
+		display: flex;
+		flex-flow: column nowrap;
+		width: 100%;
+	}
+	#meta-text p {
+		margin: 0 1rem 0 1rem;
+	}
+</style>
+
+<article id="commentContainer" class="blog-comment" part="main">
+	<div id="meta" part="meta">
+		<div>
+			<wc-profile-pic url="" />
+		</div>
+		<div id="meta-text" part="meta-text">
+			<p id="author" part="author">
+				<a id="author-link" part="author-link"></a><span> says:</span>
+			</p>
+			<p id="publish-date" part="publish-date"></p>
+		</div>
+	</div>
+	<div id="comment" part="content">
+	</div>
+</article>
+`
+
+class comment extends HTMLElement {
+	constructor() {
+		super();
+
+		this._shadowRoot = this.attachShadow({ 'mode': 'open' });
+		this._shadowRoot.appendChild(template.content.cloneNode(true));
+		this.$comment = this._shadowRoot.querySelector('#commentContainer');
+	}
+
+	static get observedAttributes() {
+		return ['author_name', 'author_url', 'avatar_url', 'comment_content', 'publish_date'];
+	}
+
+	attributeChangedCallback(name, oldVal, newVal) {
+		if (oldVal != newVal) {
+			this[name] = newVal;
+			this.render();
+		}
+	}
+
+	render() {
+		this.$comment.querySelector('#author-link').innerHTML = this.author_name;
+		this.$comment.querySelector('#author-link').href = this.author_url;
+		this.$comment.querySelector('wc-profile-pic').setAttribute('url', this.avatar_url)
+		this.$comment.querySelector('#comment').innerHTML = this.comment_content;
+		this.$comment.querySelector('#publish-date').innerHTML = this.publish_date;
+	}
+}
+
+window.customElements.define('wc-comment', comment);
diff --git a/public/js/webComponents/profilePic.js b/public/js/webComponents/profilePic.js
new file mode 100644
index 0000000..76b42ee
--- /dev/null
+++ b/public/js/webComponents/profilePic.js
@@ -0,0 +1,40 @@
+const template = document.createElement('template');
+
+template.innerHTML = `
+<style>
+#profilePic {
+	border-radius: var(--wc-profile-pic-border-radius);
+	width: var(--wc-profile-pic-size);
+	height: var(--wc-profile-pic-size);
+}
+</style>
+
+<img src="" id="profilePic"/>
+`
+
+class profilePic extends HTMLElement {
+	constructor() {
+		super();
+
+		this._shadowRoot = this.attachShadow({ 'mode': 'open' });
+		this._shadowRoot.appendChild(template.content.cloneNode(true));
+		this.$profilePic = this._shadowRoot.querySelector('#profilePic');
+	}
+
+	static get observedAttributes() {
+		return ['url'];
+	}
+
+	attributeChangedCallback(name, oldVal, newVal) {
+		if (oldVal != newVal) {
+			this[name] = newVal;
+			this.render();
+		}
+	}
+
+	render() {
+		this.url ? this.$profilePic.src = this.url : null;
+	}
+}
+
+window.customElements.define('wc-profile-pic', profilePic);