Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
YouTube Security VideosGoogle Chrome: Unfinished Projects: Solange’s Public Sculpture(21.09.2026 um 17:02 Uhr)
Windows Tipps & SecurityBlurry or pixelated video in Microsoft Teams(21.09.2026 um 14:34 Uhr)
Sicherheitslücken (CVE)USN-8791-1: Ghostscript vulnerability(21.09.2026 um 14:51 Uhr)
Sicherheitslücken (CVE)USN-8792-1: Memcached vulnerability(21.09.2026 um 15:02 Uhr)
Sichere ProgrammierungI stopped rewriting the same Electron boilerplate — so I packaged it(21.09.2026 um 17:28 Uhr)
YouTube Security VideosGoogle Chrome: Unfinished Projects: Solange’s Public Sculpture(21.09.2026 um 17:02 Uhr)
Windows Tipps & SecurityBlurry or pixelated video in Microsoft Teams(21.09.2026 um 14:34 Uhr)
Sicherheitslücken (CVE)USN-8791-1: Ghostscript vulnerability(21.09.2026 um 14:51 Uhr)
Sicherheitslücken (CVE)USN-8792-1: Memcached vulnerability(21.09.2026 um 15:02 Uhr)
Sichere ProgrammierungI stopped rewriting the same Electron boilerplate — so I packaged it(21.09.2026 um 17:28 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Rebuilding my Portfolio with Next, MDX, and Contentlayer

Why I Stopped Using Ghost I liked the idea of opening up my iPad, sipping on a caramel latte in an overly-hipster Brooklyn cafe, writing a new tech post. Ghost CMS was my way to do that (see my setup). It was, however, expensive ever…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!




Why I Stopped Using Ghost



I liked the idea of opening up my iPad, sipping on a caramel latte in an overly-hipster Brooklyn cafe, writing a new tech post. Ghost CMS was my way to do that (see my setup). It was, however, expensive ever since Heroku broke up with us and I moved onto Digital Ocean which is $6 month. But also, sometimes Ghost would crash and I didn't want to spend too long debugging when redeploying quickly fixed whatever was broken.



Ultimately, crashes and money didn't warrant a ridiculous aesthetic of writing in a cafe because I never actually did it. Caramel lattes are also expensive.



And I can also use Obsidian, my markdown notetaker, and then just copy that to my blog, achieving all of this for free.






Technologies




  • Next JS -- my favorite full stack framework


  • Tailwind CSS -- because I don't know how to do CSS otherwise


  • MDX -- to use React within my markdown (probably won't use much JSX, but hey why not at least have it)


  • Contentlayer -- transform the mdx posts into type-safe json data


  • Vercel -- deployment




Getting Started



I've started using the T3 CLI to make my apps these days because the stack generally is one I enjoy and I love the cohesion together.




npm create t3-app@latest






Only select Tailwind, we don't need the other packages



After installation, we can clear up the homepage




import { type NextPage } from 'next';
import Head from 'next/head';
const Home: NextPage = () => {
return (
<>
<Head>
<title>Create T3 App</title>
<meta name="description" content="Generated by create-t3-app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="flex min-h-screen flex-col items-center bg-gradient-to-b from-[#2e026d] to-[#15162c] pt-20">
<h1 className="text-7xl font-bold text-white">My Cool Blog</h1>
</main>
</>
);
};

export default Home;









Configuring MDX



To be able to write .mdx files, we'll need a few plugins



  • @next/mdx -- to use with Next

  • @mdx-js/loader -- required package of @next/mdx

  • @mdx-js/react -- required package of @next/mdx

  • gray-matter -- to ignore frontmatter from rendering

  • rehype-autolink-headings -- allows to add links to headings with ids on there already

  • rehype-slug -- allows to add links to headings for documents that don't already have ids

  • rehype-pretty-code -- makes code pretty with syntax highlighting, line numbers, etc

  • remark-frontmatter -- plugin to support frontmatter

  • shiki -- coding themes we can use for rendering code snippets


yarn add @next/mdx @mdx-js/loader @mdx-js/react gray-matter rehype-autolink-headings rehype-slug rehype-pretty-code remark-frontmatter shiki









Setting Up Contentlayer



Contentlayer makes it super easy to grab our mdx blog posts in a type-safe way.



First install it and its associated Next js plugin




yarn add contentlayer next-contentlayer






Modify your next.config.mjs




// next.config.mjs

import { withContentlayer } from 'next-contentlayer';

/** @type {import('next').NextConfig} */
const nextConfig = {
// Configure pageExtensions to include md and mdx
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
reactStrictMode: true,
swcMinify: true,
};

// Merge MDX config with Next.js config
export default withContentlayer(nextConfig);






Modify your tsconfig.json




{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"contentlayer/generated": ["./.contentlayer/generated"]
}
},
"include": ["next-env.d.ts", "**/*.tsx", "**/*.ts", ".contentlayer/generated"]
}






Create a file contentlayer.config.ts and we will do three things



  1. Define the schema of our Post and where the content lives

  2. Setup our remark and rehype plugins


import { defineDocumentType, makeSource } from 'contentlayer/source-files';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrettyCode from 'rehype-pretty-code';
import rehypeSlug from 'rehype-slug';
import remarkFrontmatter from 'remark-frontmatter';

export const Post = defineDocumentType(() => ({
name: 'Post',
filePathPattern: `**/*.mdx`,
contentType: 'mdx',
fields: {
title: {
type: 'string',
description: 'The title of the post',
required: true,
},
excerpt: {
type: 'string',
description: 'The excerpt of the post',
required: true,
},
date: {
type: 'string',
description: 'The date of the post',
required: true,
},
coverImage: {
type: 'string',
description: 'The cover image of the post',
required: false,
},
ogImage: {
type: 'string',
description: 'The og cover image of the post',
required: false,
},
},
computedFields: {
url: {
type: 'string',
resolve: (post) => `/blog/${post._raw.flattenedPath}`,
},
slug: {
type: 'string',
resolve: (post) => post._raw.flattenedPath,
},
},
}));

const prettyCodeOptions = {
theme: 'material-theme-palenight',

onVisitLine(node: { children: string | unknown[] }) {
if (node.children.length === 0) {
node.children = [{ type: 'text', value: ' ' }];
}
},

onVisitHighlightedLine(node: { properties: { className: string[] } }) {
node.properties.className.push('highlighted');
},

onVisitHighlightedWord(node: { properties: { className: string[] } }) {
node.properties.className = ['highlighted', 'word'];
},
};

export default makeSource({
contentDirPath: 'content',
documentTypes: [Post],
mdx: {
remarkPlugins: [remarkFrontmatter],
rehypePlugins: [
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
[rehypePrettyCode, prettyCodeOptions],
],
},
});






If you're using git, don't forget to add the generated content to your gitignore




# contentlayer
.contentlayer









Add Post Content



Create a folder called content



Create a file in content called first-post.mdx




---
title: "First Post"
excerpt: My first ever post on my blog
date: '2022-02-16'
---
# Hello World

My name is Roze and I built this blog to do cool things

-
Like talking about pets
- And other cool stuff

## Random Code

```mdx {1,15} showLineNumbers title="Page.mdx"
import { MyComponent } from '../components/...';

# My MDX page

This is an unordered list

- Item One
- Item Two
- Item Three

<section>And here is _markdown_ in **JSX**</section>

Checkout my React component

<MyComponent />

```






Once you've created a new post, make sure to run your app to trigger contentlayer to generate




yarn dev






You should see a new folder called .contentlayer which will have a generated folder that defines your schemas and types.






Display All Blog Posts



We can use getStaticProps to pull data from our content folder because contentlayer provides us with allPosts




import { allPosts } from "../../.contentlayer/generated";
import { type GetStaticProps } from "next";
...
export const getStaticProps: GetStaticProps = () => {
const posts = allPosts.sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);

return {
props: {
posts,
},
};
};






Then update the component to show these posts




interface Props {
posts: Post[];
}

const Home: NextPage<Props> = ({ posts }) => {
return (
<>
...
<ul className="pt-20">
{posts.map((post, index) => (
<li key={index} className="space-y-2 py-2 text-white">
<h1 className="text-4xl font-semibold hover:text-yellow-200">
<Link href={post.url}>{post.title} ↗️</Link>
</h1>
<h2>{post.excerpt}</h2>
</li>
))}
</ul>
...
</>
);
};









Render a Single Post



Now when a user clicks on one of the posts, we should send them to a new page that shows the full post.



Create a new folder in pages called blog and make a file [slug].tsx



We'll meed to define getStaticPaths to generate the dynamic routes and getStaticProps to retrieve and return a single post




export const getStaticPaths: GetStaticPaths = () => {
const paths = allPosts.map((post) => post.url);

return {
paths,
fallback: false,
};
};









interface IContextParams extends ParsedUrlQuery {
slug: string;
}

export const getStaticProps: GetStaticProps = (context) => {
const { slug } = context.params as IContextParams;
const post = allPosts.find((post) => post.slug === slug);

if (!post) {
return {
notFound: true,
};
}

return {
props: {
post,
},
};
};






Setup our component




interface Props {
post: Post;
}

const BlogPost: NextPage<Props> = ({ post }) => {
return <></>;
};

export default BlogPost;






Before rendering the BlogPost, we can also style some of it using Tailwind Typography




yarn add -D @tailwindcss/typography






Add that to your tailwind.config.cjs




module.exports = {
theme: {
// ...
},
plugins: [
require('@tailwindcss/typography'),
// ...
],
};






Now, how do we actually render the blog post? Contentlayer gives us a NextJS specific hook useMDX that allows us to render MDX




import { useMDXComponent } from "next-contentlayer/hooks";
...
const Component = useMDXComponent(post.body.code);
return (
<main className="flex min-h-screen flex-col items-center bg-gradient-to-b from-[#2e026d] to-[#15162c] pt-20">
<header>
<h1 className="pb-10 text-7xl text-white">{post.title}</h1>
</header>
<article className="prose">
<Component />
</article>
</main>
);






In the above code we useMDX allows us to render our mdx and the className='prose' applies the Tailwind Typography styles on the content.



But our post looks gross.



First Post image



We can modify some of the styles in globals.css



First lets fix the typography




.prose :is(h1, h2, h3, h4, h5, h6) > a {
@apply no-underline text-white;
}
.prose {
@apply text-white;
}






And lets style our code plugins




code[data-line-numbers] {
padding-left: 0 !important;
padding-right: 0 !important;
}

code[data-line-numbers] > .line::before {
counter-increment: line;
content: counter(line);
display: inline-block;
width: 1rem;
margin-right: 1.25rem;
margin-left: 0.75rem;
text-align: right;
color: #676e95;
}

div[data-rehype-pretty-code-title] + pre {
@apply !mt-0 !rounded-tl-none;
}

div[data-rehype-pretty-code-title] {
@apply !mt-6 !max-w-max !rounded-t !border-b !border-b-slate-400 !bg-[#2b303b] !px-4 !py-0.5 !text-gray-300 dark:!bg-[#282c34];
}






Much better :)



First Post image with styles

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Rebuilding my Portfolio with Next, MDX, and Contentlayer

Thematisch verwandte Begriffe: Rebuilding, Portfolio, with, Next · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-94393 | When a user creates or edits a report inside an event, MISP can identify…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick