-
Notifications
You must be signed in to change notification settings - Fork 1
Implement Complete Blog System with 5 Initial Posts, Author Profiles, and RSS Feed #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Co-authored-by: syed-reza98 <71028588+syed-reza98@users.noreply.github.com>
| {/* Post Navigation */} | ||
| <div className="mb-12 flex flex-col sm:flex-row gap-4"> | ||
| {prevPost && ( | ||
| <Link href={`/blog/${prevPost.slug}`} className="flex-1"> |
Check failure
Code scanning / CodeQL
Stored cross-site scripting High
stored value
| </Link> | ||
| )} | ||
| {nextPost && ( | ||
| <Link href={`/blog/${nextPost.slug}`} className="flex-1"> |
Check failure
Code scanning / CodeQL
Stored cross-site scripting High
stored value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 23 days ago
To fix the vulnerability, file names used as slugs should be validated and sanitized before being exposed to the application. This involves ensuring only expected, URL-safe characters are allowed in slugs. The best practice is to implement a function that normalizes slugs, allowing only alphanumeric characters, hyphens, and underscores, and call this function when generating the slug property from file names in getAllBlogPosts(). This change ensures that slugs used in application URLs and rendered into JSX are safe and cannot contain characters leading to XSS.
Required changes:
- In
src/lib/blog-data.ts, create anormalizeSlugfunction that only allows URL-safe characters. - In the mapping over filenames when generating blog posts, apply
normalizeSlugto theslug. - Make sure the slug used throughout the blog post fetching logic is always normalized.
No changes are required in src/app/blog/[slug]/page.tsx because normalizing at source is sufficient for all downstream consumers.
-
Copy modified lines R33-R37 -
Copy modified line R86
| @@ -30,9 +30,13 @@ | ||
| published?: boolean | ||
| } | ||
|
|
||
| // Only allow URL-safe characters in slugs (alphanumeric, -, _) | ||
| function normalizeSlug(filename: string): string { | ||
| return filename.replace(/\.md$/, '').replace(/[^a-zA-Z0-9-_]/g, ''); | ||
| } | ||
|
|
||
| let cachedBlogPosts: BlogPost[] | null = null | ||
| let cachedAuthors: Author[] | null = null | ||
|
|
||
| export function getAllAuthors(): Author[] { | ||
| if (cachedAuthors !== null) { | ||
| return cachedAuthors | ||
| @@ -81,7 +83,7 @@ | ||
| const filePath = path.join(blogDir, filename) | ||
| const fileContent = fs.readFileSync(filePath, 'utf8') | ||
| const { data, content } = matter(fileContent) | ||
| const slug = filename.replace(/\.md$/, '') | ||
| const slug = normalizeSlug(filename) | ||
|
|
||
| // Calculate read time (rough estimate: 200 words per minute) | ||
| const wordCount = content.trim().split(/\s+/).length |
| <h2 className="text-2xl font-bold mb-6">Related Articles</h2> | ||
| <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> | ||
| {relatedPosts.map((relatedPost) => ( | ||
| <Link key={relatedPost.slug} href={`/blog/${relatedPost.slug}`}> |
Check failure
Code scanning / CodeQL
Stored cross-site scripting High
stored value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 23 days ago
The best way to fix this problem is to sanitize or validate slugs at the point where they originate, ensuring that only safe characters are allowed (in particular, allow only URL-safe characters such as [a-zA-Z0-9-_]). This prevents malicious file names from ever being used as slugs or injected into rendered HTML. The solution should be implemented in src/lib/blog-data.ts where blog posts are loaded: filter filenames to ensure they match a safe "slug" pattern before using them to construct blog post objects and inject them downstream. This change only affects the code that turns disk filenames into slugs, preserving the existing functionality while ensuring safety. No changes are necessary to React components that render these values, as making sure tainted slugs cannot propagate is preferable to patching at every usage site.
Changes needed:
- In
src/lib/blog-data.ts, update the logic ingetAllBlogPoststo filter out any filenames that, after removing the.mdextension, do not conform to a strict slug regex (such as/^[a-zA-Z0-9-_]+$/). - This means after line 84, only use slugs that match the required pattern.
- Optionally, you can define a helper validation function (e.g.,
isValidSlug). - No new external dependencies are needed, as this can be implemented in a few lines of code.
-
Copy modified lines R78-R79 -
Copy modified lines R83-R86 -
Copy modified line R112
| @@ -75,13 +75,18 @@ | ||
|
|
||
| const filenames = fs.readdirSync(blogDir) | ||
|
|
||
| // Only include filenames that are .md and give valid slugs | ||
| const isValidSlug = (slug: string) => /^[a-zA-Z0-9-_]+$/.test(slug) | ||
| cachedBlogPosts = filenames | ||
| .filter(name => name.endsWith('.md')) | ||
| .map(filename => { | ||
| const slug = filename.replace(/\.md$/, '') | ||
| if (!isValidSlug(slug)) { | ||
| return null | ||
| } | ||
| const filePath = path.join(blogDir, filename) | ||
| const fileContent = fs.readFileSync(filePath, 'utf8') | ||
| const { data, content } = matter(fileContent) | ||
| const slug = filename.replace(/\.md$/, '') | ||
|
|
||
| // Calculate read time (rough estimate: 200 words per minute) | ||
| const wordCount = content.trim().split(/\s+/).length | ||
| @@ -110,7 +109,7 @@ | ||
|
|
||
| return post | ||
| }) | ||
| .filter(post => post.published) // Only return published posts | ||
| .filter((post): post is BlogPost => !!post && post.published) // Only return published posts with valid slugs | ||
| .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) // Sort by date, newest first | ||
|
|
||
| return cachedBlogPosts |
| <h2 className="text-2xl font-bold mb-6">Related Articles</h2> | ||
| <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> | ||
| {relatedPosts.map((relatedPost) => ( | ||
| <Link key={relatedPost.slug} href={`/blog/${relatedPost.slug}`}> |
Check failure
Code scanning / CodeQL
Stored cross-site scripting High
stored value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 23 days ago
The best way to fix the problem is to ensure that slugs derived from file names are sanitized before being used in rendered output. This should be implemented where the slug is defined from the file name, so that all downstream consumers (not just this one Link) get a sanitized version. A simple and effective approach is to only allow "safe" characters in slugs, e.g., alphanumeric, hyphens, and underscores. This can be done by modifying the code that assigns slug in getAllBlogPosts so it replaces or rejects unsafe characters using a regular expression. This way, rendered output in /blog/${slug} always contains safe values. No changes are needed elsewhere.
Implementation details:
- In
src/lib/blog-data.ts, ingetAllBlogPosts, after extractingslugfrom the filename (line 84), apply a sanitation function to remove or replace any disallowed characters. - Add a helper function, e.g.,
sanitizeSlug, that returns only characters allowed in slugs. - If you want to be more robust, reject files whose slug cannot be safely sanitized and skip those posts entirely.
- No package imports are necessary; a local helper function using
String.replaceis sufficient.
-
Copy modified lines R84-R86
| @@ -81,7 +81,9 @@ | ||
| const filePath = path.join(blogDir, filename) | ||
| const fileContent = fs.readFileSync(filePath, 'utf8') | ||
| const { data, content } = matter(fileContent) | ||
| const slug = filename.replace(/\.md$/, '') | ||
| // Sanitize slug to include only safe characters (alphanumerics, hyphens, underscores) | ||
| const unsanitizedSlug = filename.replace(/\.md$/, '') | ||
| const slug = sanitizeSlug(unsanitizedSlug) | ||
|
|
||
| // Calculate read time (rough estimate: 200 words per minute) | ||
| const wordCount = content.trim().split(/\s+/).length |
| </div> | ||
| <Button asChild> | ||
| <a href={`/blog/${post.slug}`}> | ||
| <Link href={`/blog/${post.slug}`}> |
Check failure
Code scanning / CodeQL
Stored cross-site scripting High
stored value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 23 days ago
To fix this vulnerability, we need to sanitize or validate the slug variable before using it in any context where it could generate a URL or HTML attribute from user-controlled input. The best approach is to restrict the slug to a safe subset of characters, such as [a-zA-Z0-9-_]. This can be done in the location where the slug is generated from the filename in getAllBlogPosts() in src/lib/blog-data.ts.
Specifically, after extracting the slug, we should clean it (e.g., via .replace(/[^a-zA-Z0-9-_]/g, "")) to remove any unwanted/unexpected/special characters. This ensures that filenames can't inject problematic values into our URLs or views.
Make the following edits:
- In
src/lib/blog-data.ts, in the code generating the blog post objects, sanitize the computed slug. - Optionally, add a utility function for slug sanitization, but only if you don't need to import anything except Node builtins.
-
Copy modified lines R84-R85
| @@ -81,7 +81,8 @@ | ||
| const filePath = path.join(blogDir, filename) | ||
| const fileContent = fs.readFileSync(filePath, 'utf8') | ||
| const { data, content } = matter(fileContent) | ||
| const slug = filename.replace(/\.md$/, '') | ||
| // Sanitize slug: allow only alphanumeric, dash, and underscore | ||
| const slug = filename.replace(/\.md$/, '').replace(/[^a-zA-Z0-9-_]/g, '') | ||
|
|
||
| // Calculate read time (rough estimate: 200 words per minute) | ||
| const wordCount = content.trim().split(/\s+/).length |
| </div> | ||
| <Button variant="outline" size="sm" asChild> | ||
| <a href={`/blog/${post.slug}`}> | ||
| <Link href={`/blog/${post.slug}`}> |
Check failure
Code scanning / CodeQL
Stored cross-site scripting High
stored value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 23 days ago
To fix this issue, we must ensure that slugs derived from filenames are sanitized or validated to avoid dangerous characters before they are exposed via the UI or used for routing. The best approach is to sanitize slugs in src/lib/blog-data.ts just after they are extracted from filenames (line 84-ish). We should restrict slugs to a safe subset (e.g., letters, digits, dashes, underscores), replacing or removing any unsafe characters. This guarantees that only expected, harmless slugs ever get rendered or routed. We will define a sanitization function in src/lib/blog-data.ts and apply it to each slug as it's created. No change is needed in src/app/blog/page.tsx because the data at source will already be safe.
Required changes:
- Add a slug sanitization function to src/lib/blog-data.ts, restricting allowed characters (e.g.,
/[\w-]+/). - Use this sanitization when creating
slugingetAllBlogPosts(). - No new imports required for a simple regex-based function.
-
Copy modified lines R33-R39 -
Copy modified line R91
| @@ -30,6 +30,13 @@ | ||
| published?: boolean | ||
| } | ||
|
|
||
|
|
||
| // Remove all but alphanumeric, dash, and underscore from slug. | ||
| function sanitizeSlug(rawSlug: string): string { | ||
| // Allow a-z, A-Z, 0-9, dash, underscore only. Remove other chars. | ||
| return rawSlug.replace(/[^a-zA-Z0-9-_]/g, '') | ||
| } | ||
|
|
||
| let cachedBlogPosts: BlogPost[] | null = null | ||
| let cachedAuthors: Author[] | null = null | ||
|
|
||
| @@ -81,7 +88,7 @@ | ||
| const filePath = path.join(blogDir, filename) | ||
| const fileContent = fs.readFileSync(filePath, 'utf8') | ||
| const { data, content } = matter(fileContent) | ||
| const slug = filename.replace(/\.md$/, '') | ||
| const slug = sanitizeSlug(filename.replace(/\.md$/, '')) | ||
|
|
||
| // Calculate read time (rough estimate: 200 words per minute) | ||
| const wordCount = content.trim().split(/\s+/).length |
| </channel> | ||
| </rss>` | ||
|
|
||
| return new Response(rss, { |
Check failure
Code scanning / CodeQL
Stored cross-site scripting High
stored value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 23 days ago
To resolve this issue, the values that derive from file names (i.e., post.slug) and other tainted fields outputted into XML elements must be properly escaped for XML to prevent any injection of malicious markup. Since this RSS feed returns XML content, we need to contextually escape all untrusted data embedded into XML tags (other than those encapsulated inside CDATA sections).
The best way is to escape the special XML characters (<, >, &, ", ') in each outputted field: slug, category, author, tag, etc. We do not need to escape values inside the CDATA blocks (for title and description, which are already safe by definition), but we do need to escape outside values inserted into tag content and attributes.
Thus, we should:
- Add a function to escape XML special characters (using a well-known library like
heor via a simple helper). - Apply escaping to each untrusted field (especially
post.slug,post.category,post.author, alltags) in the places where they are used to build XML in src/app/rss.xml/route.ts.
Since the code only constructs the XML in that file, we only need to update src/app/rss.xml/route.ts, with a helper function and its application.
-
Copy modified lines R23-R24 -
Copy modified lines R27-R29
| @@ -20,13 +20,13 @@ | ||
| (post) => ` | ||
| <item> | ||
| <title><![CDATA[${post.title}]]></title> | ||
| <link>${siteUrl}/blog/${post.slug}</link> | ||
| <guid>${siteUrl}/blog/${post.slug}</guid> | ||
| <link>${siteUrl}/blog/${escapeXml(post.slug)}</link> | ||
| <guid>${siteUrl}/blog/${escapeXml(post.slug)}</guid> | ||
| <description><![CDATA[${post.description}]]></description> | ||
| <pubDate>${new Date(post.date).toUTCString()}</pubDate> | ||
| <author>${post.author}</author> | ||
| <category>${post.category}</category> | ||
| ${post.tags?.map((tag) => `<category>${tag}</category>`).join("\n ") || ""} | ||
| <author>${escapeXml(post.author)}</author> | ||
| <category>${escapeXml(post.category)}</category> | ||
| ${post.tags?.map((tag) => `<category>${escapeXml(tag)}</category>`).join("\n ") || ""} | ||
| </item>` | ||
| ) | ||
| .join("")} |
Summary
This PR implements a complete blog system for CodeStorm Hub, including blog post infrastructure, author profiles, categories/tags, RSS feed generation, and 5 initial high-quality blog posts covering community announcements, tutorials, and best practices.
Changes Made
Blog Infrastructure
Blog Data System (
src/lib/blog-data.ts)BlogPostandAuthortypesgray-matterlibraryAuthor Profile System (
docs/blog/authors.json)Dynamic Blog Pages
Blog Listing Page (
src/app/blog/page.tsx)Individual Blog Post Page (
src/app/blog/[slug]/page.tsx)RSS Feed
RSS Feed Generator (
src/app/rss.xml/route.ts)Layout Update (
src/app/layout.tsx)<head>for feed readersBlog Content
Created 5 comprehensive, well-researched blog posts:
1. Welcome to CodeStorm Hub (Announcement)
2. Our Journey: Building an Open Source Community (Story)
3. Getting Started with Next.js 15 and Radix UI (Technical Tutorial)
4. Open Source Best Practices (Educational)
5. Upcoming Events and Community Roadmap (News)
Technical Details
Screenshots
Blog Listing Page
The blog listing page shows all posts with featured posts highlighted, category filters, newsletter signup, and RSS feed link.
Individual Blog Post Page
Individual post pages display full content with author profiles, navigation, related articles, and metadata.
Testing
/rss.xml- valid XML with all postsAcceptance Criteria
All acceptance criteria from the issue have been met:
✅ Blog listing and post pages functional - Both working with full dynamic routing
✅ Author profiles and categories/tags present - 4 author profiles, all posts categorized and tagged
✅ RSS feed available - Accessible at
/rss.xmlwith proper formatting✅ Initial posts published - 5 comprehensive, high-quality blog posts covering diverse topics
References
docs/audit-reports/content-improvement-plan.mddocs/audit-reports/implementation-roadmap.mdOriginal prompt
Fixes #35
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.