Developer Blog
Turn your Discord announcements channel into a polished developer blog. Write updates where your community already is.
Overview
Instead of maintaining a separate blog platform, write developer updates in Discord:
- Write posts in a dedicated forum channel
- Posts sync automatically to your blog website
- Community can discuss each post in the thread
- Built-in RSS feed for subscribers
Architecture
Discord Announcements Forum │ ▼ Discord Forum API │ ├── GET /threads → Blog posts ├── GET /threads/:id → Full post with comments └── RSS feed generation │ ▼ Developer Blog WebsiteSetup
1. Create a Blog Forum Channel
In your Discord server:
- Create a new forum channel named “dev-blog” or “announcements”
- Add tags for categories:
release- Version releasesfeature- New featurestutorial- How-to guidesupdate- General updates
- Set permissions so only team members can create threads
- Enable moderation to control comments
2. Fetch Blog Posts
const API_BASE = process.env.API_URL;const BLOG_CHANNEL_ID = process.env.BLOG_CHANNEL_ID;
export async function getBlogPosts({ limit = 10, cursor } = {}) { const params = new URLSearchParams({ channelId: BLOG_CHANNEL_ID, sort: 'latest', limit: limit.toString(), });
if (cursor) params.set('cursor', cursor);
const response = await fetch(`${API_BASE}/threads?${params}`); return response.json();}
export async function getBlogPost(slug) { const response = await fetch(`${API_BASE}/threads/${slug}`); return response.json();}
export async function getBlogPostsByTag(tag, limit = 10) { const params = new URLSearchParams({ channelId: BLOG_CHANNEL_ID, tag, sort: 'latest', limit: limit.toString(), });
const response = await fetch(`${API_BASE}/threads?${params}`); return response.json();}Implementation
Blog Homepage
import { getBlogPosts } from '@/lib/blog';
export async function getStaticProps() { const { threads } = await getBlogPosts({ limit: 20 }); return { props: { posts: threads }, revalidate: 300, // 5 minutes };}
export default function BlogPage({ posts }) { const [featured, ...rest] = posts;
return ( <main className="blog-page"> <header> <h1>Developer Blog</h1> <p>Updates, releases, and insights from the team</p> </header>
{featured && <FeaturedPost post={featured} />}
<div className="post-grid"> {rest.map((post) => ( <PostCard key={post.id} post={post} /> ))} </div> </main> );}
function FeaturedPost({ post }) { return ( <article className="featured-post"> <div className="featured-content"> <div className="tags"> {post.tags.map((tag) => ( <span key={tag} className={`tag tag-${tag}`}>{tag}</span> ))} </div> <h2> <a href={`/blog/${post.slug}`}>{post.title}</a> </h2> <p className="excerpt">{post.preview}</p> <footer> <AuthorBadge author={post.author} /> <time>{formatDate(post.createdAt)}</time> </footer> </div> </article> );}
function PostCard({ post }) { return ( <article className="post-card"> <div className="tags"> {post.tags.map((tag) => ( <span key={tag} className={`tag tag-${tag}`}>{tag}</span> ))} </div> <h3> <a href={`/blog/${post.slug}`}>{post.title}</a> </h3> <p className="excerpt">{post.preview}</p> <footer> <AuthorBadge author={post.author} size="small" /> <time>{formatDate(post.createdAt)}</time> </footer> </article> );}
function AuthorBadge({ author, size = 'normal' }) { return ( <div className={`author-badge ${size}`}> <img src={getAvatarUrl(author)} alt="" /> <span>{author.globalName || author.username}</span> </div> );}Blog Post Page
import { getBlogPost, getBlogPosts } from '@/lib/blog';
export async function getStaticPaths() { const { threads } = await getBlogPosts({ limit: 100 }); return { paths: threads.map((t) => ({ params: { slug: t.slug } })), fallback: 'blocking', };}
export async function getStaticProps({ params }) { const post = await getBlogPost(params.slug);
if (!post) { return { notFound: true }; }
return { props: { post }, revalidate: 300, };}
export default function BlogPostPage({ post }) { // First message is the blog post content const [content, ...comments] = post.messages;
return ( <article className="blog-post"> <header> <div className="tags"> {post.tags.map((tag) => ( <span key={tag} className={`tag tag-${tag}`}>{tag}</span> ))} </div> <h1>{post.title}</h1> <div className="meta"> <AuthorBadge author={post.author} /> <time dateTime={post.createdAt}>{formatDate(post.createdAt)}</time> <span className="reading-time"> {estimateReadingTime(content.content)} min read </span> </div> </header>
<div className="post-content" dangerouslySetInnerHTML={{ __html: content.contentHtml }} />
{content.attachments.length > 0 && ( <div className="attachments"> {content.attachments.map((att) => ( <PostImage key={att.id} attachment={att} /> ))} </div> )}
<footer className="post-footer"> <ShareButtons post={post} /> <a href={`https://discord.com/channels/${post.serverId}/${post.id}`} className="discord-link" > Discuss on Discord → </a> </footer>
{comments.length > 0 && ( <section className="comments"> <h2>Discussion ({comments.length})</h2> {comments.map((comment) => ( <Comment key={comment.id} comment={comment} /> ))} </section> )} </article> );}
function PostImage({ attachment }) { if (!attachment.contentType?.startsWith('image/')) { return null; }
return ( <figure> <img src={attachment.url} alt={attachment.description || ''} width={attachment.width} height={attachment.height} loading="lazy" /> {attachment.description && ( <figcaption>{attachment.description}</figcaption> )} </figure> );}
function Comment({ comment }) { return ( <div className="comment"> <AuthorBadge author={comment.author} size="small" /> <div className="comment-content" dangerouslySetInnerHTML={{ __html: comment.contentHtml }} /> <time>{formatRelative(comment.createdAt)}</time> </div> );}
function estimateReadingTime(text) { const words = text.split(/\s+/).length; return Math.ceil(words / 200);}Category Pages
import { getBlogPostsByTag } from '@/lib/blog';
export async function getStaticPaths() { return { paths: [ { params: { tag: 'release' } }, { params: { tag: 'feature' } }, { params: { tag: 'tutorial' } }, { params: { tag: 'update' } }, ], fallback: false, };}
export async function getStaticProps({ params }) { const { threads } = await getBlogPostsByTag(params.tag, 50); return { props: { tag: params.tag, posts: threads, }, revalidate: 300, };}
export default function CategoryPage({ tag, posts }) { const labels = { release: 'Releases', feature: 'Features', tutorial: 'Tutorials', update: 'Updates', };
return ( <main className="category-page"> <header> <span className={`tag tag-${tag}`}>{labels[tag]}</span> <h1>{labels[tag]}</h1> <p>{posts.length} posts</p> </header>
<div className="post-list"> {posts.map((post) => ( <PostCard key={post.id} post={post} /> ))} </div> </main> );}RSS Feed
// pages/api/feed.xml.js (Next.js API route)import { getBlogPosts } from '@/lib/blog';
export default async function handler(req, res) { const { threads } = await getBlogPosts({ limit: 50 });
const rss = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <channel> <title>Developer Blog</title> <description>Updates, releases, and insights from the team</description> <link>https://yourdomain.com/blog</link> <atom:link href="https://yourdomain.com/api/feed.xml" rel="self" type="application/rss+xml"/> <lastBuildDate>${new Date().toUTCString()}</lastBuildDate> ${threads.map((post) => ` <item> <title><![CDATA[${post.title}]]></title> <description><![CDATA[${post.preview}]]></description> <link>https://yourdomain.com/blog/${post.slug}</link> <guid>https://yourdomain.com/blog/${post.slug}</guid> <pubDate>${new Date(post.createdAt).toUTCString()}</pubDate> <author>${post.author.username}</author> ${post.tags.map((tag) => `<category>${tag}</category>`).join('')} </item> `).join('')} </channel></rss>`;
res.setHeader('Content-Type', 'application/xml'); res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate'); res.status(200).send(rss);}Styling
/* Blog Layout */.blog-page { max-width: 1200px; margin: 0 auto; padding: 2rem;}
.blog-page header { text-align: center; margin-bottom: 3rem;}
.blog-page h1 { font-size: 2.5rem; margin-bottom: 0.5rem;}
/* Featured Post */.featured-post { background: linear-gradient(135deg, #5865f2, #7983f5); border-radius: 16px; padding: 3rem; margin-bottom: 3rem; color: white;}
.featured-post h2 { font-size: 2rem; margin-bottom: 1rem;}
.featured-post h2 a { color: white; text-decoration: none;}
.featured-post .excerpt { font-size: 1.125rem; opacity: 0.9;}
/* Post Grid */.post-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem;}
.post-card { background: white; border: 1px solid #e0e0e0; border-radius: 12px; padding: 1.5rem; transition: transform 0.2s, box-shadow 0.2s;}
.post-card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);}
.post-card h3 { margin: 0.75rem 0; font-size: 1.25rem;}
.post-card h3 a { color: inherit; text-decoration: none;}
.post-card .excerpt { color: #666; line-height: 1.5;}
/* Tags */.tags { display: flex; gap: 0.5rem;}
.tag { padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase;}
.tag-release { background: #dcfce7; color: #166534; }.tag-feature { background: #dbeafe; color: #1e40af; }.tag-tutorial { background: #fef3c7; color: #92400e; }.tag-update { background: #f3e8ff; color: #7c3aed; }
/* Blog Post */.blog-post { max-width: 800px; margin: 0 auto; padding: 2rem;}
.blog-post header { margin-bottom: 2rem;}
.blog-post h1 { font-size: 2.5rem; line-height: 1.2; margin: 1rem 0;}
.blog-post .meta { display: flex; align-items: center; gap: 1rem; color: #666;}
.post-content { font-size: 1.125rem; line-height: 1.8;}
.post-content h2 { margin-top: 2rem;}
.post-content img { max-width: 100%; border-radius: 8px;}
.post-content code { background: #f4f4f4; padding: 0.2em 0.4em; border-radius: 3px;}
.post-content pre { background: #1e1e1e; color: #d4d4d4; padding: 1.5rem; border-radius: 8px; overflow-x: auto;}
/* Author Badge */.author-badge { display: flex; align-items: center; gap: 0.5rem;}
.author-badge img { width: 36px; height: 36px; border-radius: 50%;}
.author-badge.small img { width: 24px; height: 24px;}
/* Comments */.comments { margin-top: 3rem; padding-top: 2rem; border-top: 1px solid #e0e0e0;}
.comment { padding: 1rem 0; border-bottom: 1px solid #f0f0f0;}
.comment-content { margin: 0.5rem 0;}Writing Tips
Good Blog Post Format
# Title: Clear and Descriptive
First paragraph: Hook the reader with what this post is about.
## What's New
- Bullet points for quick scanning- Use code blocks for examples
## How to Use
Step-by-step instructions with code examples.
## What's Next
Tease upcoming features or link to related resources.
---
Questions? Reply below or join our Discord!Using Discord Features
- Bold and italic for emphasis
- Code blocks with syntax highlighting
- Images and GIFs
- Embeds for links (auto-previewed)