A Next.js Markdown Blog and CMS written in TypeScript. Designed, built, and maintained by @amirardalan
Next.js
Next Auth
react-markdown
Prisma
SWR
Emotion
This is my personal portfolio and blog. You may find the CMS portion and some of the custom Markdown functionality useful. This application currently runs on Next13 using the Page directory paradigm. I plan to upgrade this project to the app directory to take advantage of full React 18 support in the near future.
- Manage blog posts from an authenticated Admin Panel
- Write blog posts with extensible Markdown
- Publish changes on per-page basis, no need to rebuild the entire application
- Create and manage unpublished Drafts
- Publish, unpublish, and delete Posts/Drafts
- Set/Edit Title, Teaser, Slug, Content, and Category
- Set a featured post. If no post is featured, use the latest post as a fallback.
- Optionally suppress "Updated" date when editing
- Upload images to Cloudinary and a markdown URL is auto inserted into the blog post content.
- Browse your Cloudinary Media Library and delete or insert images into blog posts
Update public/manifest.json
. Fill out information relevant to your web app.
Note: I've ommited theme_color
to allow Safari to match the background and notch area according to the app background color, which works great for light and dark mode. Hopefully w3c will officially add a way to control this ourselves in the future.
Create an .env
file for local environment variables.
Keep this file private, ensure .env
remains in .gitignore
, don't commit to a public repository. You will need to set up different environment varibles for Development and Production instances. Omit wrapping quotes for all variable values.
//.env
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_USER_EMAIL=you@email.com
DATABASE_URL=postgresql://xxxxx
NEXT_AUTH_SECRET=
GITHUB_SECRET=
GITHUB_ID=
NEXT_PUBLIC_REVALIDATE_SECRET=
NEXT_PUBLIC_TIMEZONE=America/Los_Angeles
CLOUDINARY_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
NEXT_PUBLIC_SITE_URL
Leave as http://localhost:3000
for Development, set as https://yourDomainName.com
for Production
NEXT_PUBLIC_USER_EMAIL
Your email address, used for authentication and optionally your about or contact page.
DATABASE_URL
The URL for your PostgreSQL database
NEXT_AUTH_SECRET
Generate a secret for Next Auth
GITHUB_SECRET
For GitHub oAuth with Next Auth, see GitHub Developer App Docs
GITHUB_ID
Your GitHub developer app ID, GitHub Developer App Docs
NEXT_PUBLIC_REVALIDATE_SECRET
Generate a secret for use with Next On-Demand Incremental Static Regeneration
NEXT_PUBLIC_TIMEZONE
: Example: America/Los_Angeles
. Set this to your local timezone for your blog posts to display the correct date and time. Full List of IANA Timezones
CLOUDINARY_NAME
: Your Cloudinary Media Library name (cloud-name)
CLOUDINARY_API_KEY
: See Cloudinary's Admin API reference
CLOUDINARY_API_SECRET
: See Cloudinary's Admin API reference
npm install
If you'd like to contribute to this project or use Prettier with your fork, download the VSCode Extension.
- After installing the extension, open the extensions panel in VSCode, find Prettier - Code Formatter, click on the gear icon, and select
Extension Settings
. - Inside of the Prettier Extension Settings, locate
Prettier: Config Path
and ensure it is set to.prettierrc.json
.
npm run dev
Create a local copy of a production build (useful for testing on-demand ISR, pages/sitemap.xml.tsx
configuration), and Lighthouse performance:
npm run test
Prisma is a middleware that connects your Next.js application to an external database. Prisma has powerful queries that make accessing and passing data to props simple.
The existing schema is configured for PostgreSQL. Update accordingly.
Update schema and push new tables to db from schema.prisma
:
npx prisma db push
Run Prisma Studio:
npx prisma studio
Utilize Next/Image
functionality within Markdown by using custom metastrings inside the Markdown Alt.
Retain the terseness of pure Markdown while getting the benefits of the Next/Image component without having to mix in JSX.
![AltText {caption: Photo credit: Some Person}{priority}{768x432}](/path-to-image.jpg)
- Define image width and height:
{Width x Height}
- Optionally set image as
{priority}
to utilize Next.js preloading for images above the fold. - Add an optional caption that displays beneath the image.
```JSX {3,5-8} ...
- Individually highlight specific lines of code using a space after the language declaration followed by this JSON metastring. Highlight individual lines and/or a range of contiguous lines, separated by commas.
- H3 headings in a blog post automatically generate an anchor link from a generated slug based on the heading contents. Seamlessly handles
code
inside headings.
[internal link](/blog/my-blog-post)
[external link](https://example.com)
- External links will automatically render with
target="_blank" rel="noopener noreferrer"
and open in a new tab - Internal links are handled normally
- Out of the box configuration of iframe embeds within markdown.
- Uses Rehype Raw. Disable if using this code in a way where you may not be able to trust the markdown.
This project uses Vercel OG Image Service to dynamically generate images for blog posts. If the first line of a blog post is an image, it will be used as the background image. Otherwise, the Open Graph API route will dynamically generate an image based on the blog post title, description, and a thumbnail.png background image.
- Ensure
NEXT_PUBLIC_SITE_URL
is set in your.env.local
file. - You will also need to add your own
thumbnail.png
background image in thepublic
folder (1200x627
). - See
pages/api/og.tsx
andcomponents/Container.tsx
for the full image generation functionality.
- Most static content can be edited in
data/content.ts
- Currently blog categories are manually set in
data/categories.ts
- For blog image hosting, I recommend Cloudinary's Free Digital Asset Manager
- This project is the culmination of thousands of hours of work. It's primarily open-source for educational purposes. If you intend to use parts of the code, please create your own design and content!
- If you have any questions, reach out to me on Twitter!