upstream #1
33
.env.example
Normal file
33
.env.example
Normal file
@ -0,0 +1,33 @@
|
||||
# visit https://giscus.app to get your Giscus ids
|
||||
NEXT_PUBLIC_GISCUS_REPO=
|
||||
NEXT_PUBLIC_GISCUS_REPOSITORY_ID=
|
||||
NEXT_PUBLIC_GISCUS_CATEGORY=
|
||||
NEXT_PUBLIC_GISCUS_CATEGORY_ID=
|
||||
NEXT_PUBLIC_UTTERANCES_REPO=
|
||||
NEXT_PUBLIC_DISQUS_SHORTNAME=
|
||||
|
||||
|
||||
MAILCHIMP_API_KEY=
|
||||
MAILCHIMP_API_SERVER=
|
||||
MAILCHIMP_AUDIENCE_ID=
|
||||
|
||||
BUTTONDOWN_API_KEY=
|
||||
|
||||
CONVERTKIT_API_KEY=
|
||||
# curl https://api.convertkit.com/v3/forms?api_key=<your_public_api_key> to get your form ID
|
||||
CONVERTKIT_FORM_ID=
|
||||
|
||||
KLAVIYO_API_KEY=
|
||||
KLAVIYO_LIST_ID=
|
||||
|
||||
REVUE_API_KEY=
|
||||
|
||||
# Create EmailOctopus API key at https://emailoctopus.com/api-documentation
|
||||
EMAILOCTOPUS_API_KEY=
|
||||
# List ID can be found in the URL as a UUID after clicking a list on https://emailoctopus.com/lists
|
||||
# or the settings page of your list https://emailoctopus.com/lists/{UUID}/settings
|
||||
EMAILOCTOPUS_LIST_ID=
|
||||
|
||||
# Create Beehive API key at https://developers.beehiiv.com/docs/v2/bktd9a7mxo67n-create-an-api-key
|
||||
BEEHIVE_API_KEY=
|
||||
BEEHIVE_PUBLICATION_ID=
|
@ -44,8 +44,8 @@ Internationalization support - [Template with i18n](https://tailwind-nextjs-star
|
||||
- [thetalhatahir.com](https://www.thetalhatahir.com) - Talha Tahir's personal blog. Added article thumbnails, linkedIn card, Beautiful hero content, technology emoticons.
|
||||
- [homing.so](https://homing.so) - Homing's personal blog about the stuff he's learning ([source code](https://github.com/hominsu/blog))
|
||||
- [zS1m's Blog](https://contrails.space) - zS1m's personal blog for recording and sharing daily learning technical content ([source code](https://github.com/zS1m/nextjs-contrails))
|
||||
- [dariuszwozniak.net](https://dariuszwozniak.net/) - Software development blog
|
||||
- [Terminals.run](https://terminals.run) - Blog site for some thoughts and records for life and technology.
|
||||
- [dariuszwozniak.net](https://dariuszwozniak.net/) - Software development blog ([source code](https://github.com/dariusz-wozniak/dariuszwozniak.net-v2))
|
||||
- [dreams.plus](https://dreams.plus) - Blog site for some thoughts and records for life and technology.
|
||||
- [francisaguilar.co blog](https://francisaguilar.co) - Francis Aguilar's personal blog that talks about tech, fitness, and personal development.
|
||||
- [Min71 Dev Blog](https://min71.dev) - Personal blog about Blockchain, Development and etc. ([source code](https://github.com/mingi3442/blog))
|
||||
- [Bryce Yu's Blog](https://earayu.github.io/) - Bryce Yu's personal Blog about distributed system, database, and web development. ([source code](https://github.com/earayu/earayu.github.io))
|
||||
@ -70,6 +70,9 @@ Internationalization support - [Template with i18n](https://tailwind-nextjs-star
|
||||
- [LyricsDecode.com](https://lyricsdecode.com) - A song lyrics website offering original lyrics, Romanisation, and English translations with customisable viewing options.
|
||||
- [bmacharia.com](https://bmacharia.com/) - Benson Macharia's technical blog about Cybersecurity and IT Risk Management.
|
||||
- [armujahid.me](https://armujahid.me/) - Abdul Rauf's personal blog about tech and random stuff.
|
||||
- [leohuynh.dev](leohuynh.dev) - 🇻🇳 Leo's dev blog – stories, insights, and ideas. Add `/snippets`, `/books` pages, add `ProfileCard`, `CareerTimeline` components and many more.
|
||||
- [OpenSats Blog](https://opensats.org/blog) - A 501(c)(3) public charity which aims to sustainably fund free and open-source projects. ([source code](https://github.com/OpenSats/website))
|
||||
- [Schedles Blog](https://schedles.com/blog) - Social media scheduling tips, strategies, and product updates for content creators. ([Project Link](https://schedles.com))
|
||||
|
||||
Using the template? Feel free to create a PR and add your blog to this list.
|
||||
|
||||
@ -82,7 +85,6 @@ Thanks to the community of users and contributors to the template! We are no lon
|
||||
- [Aloisdg's cookbook](https://tambouille.vercel.app/) - with pictures and recipes!
|
||||
- [GautierArcin's demo with next translate](https://tailwind-nextjs-starter-blog-seven.vercel.app/) - includes translation of mdx posts, [source code](https://github.com/GautierArcin/tailwind-nextjs-starter-blog/tree/demo/next-translate)
|
||||
- [David Levai's digital garden](https://davidlevai.com/) - customized design and added email subscriptions
|
||||
- [Leo's Blog](https://leohuynh.dev) - Tuan Anh Huynh's personal site. Add Snippets Page, Author Profile Card, Image Lightbox, ...
|
||||
- [thvu.dev](https://thvu.dev) - Added `mdx-embed`, view count, reading minutes and more.
|
||||
- [irvin.dev](https://www.irvin.dev/) - Irvin Lin's personal site. Added YouTube embedding.
|
||||
- [KirillSo.com](https://www.kirillso.com/) - Personal blog & website.
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { NewsletterAPI } from 'pliny/newsletter'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
const handler = NewsletterAPI({
|
||||
// @ts-ignore
|
||||
provider: siteMetadata.newsletter.provider,
|
||||
|
@ -21,11 +21,10 @@ const layouts = {
|
||||
PostBanner,
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { slug: string[] }
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ slug: string[] }>
|
||||
}): Promise<Metadata | undefined> {
|
||||
const params = await props.params
|
||||
const slug = decodeURI(params.slug.join('/'))
|
||||
const post = allBlogs.find((p) => p.slug === slug)
|
||||
const authorList = post?.authors || ['default']
|
||||
@ -78,7 +77,8 @@ export const generateStaticParams = async () => {
|
||||
return allBlogs.map((p) => ({ slug: p.slug.split('/').map((name) => decodeURI(name)) }))
|
||||
}
|
||||
|
||||
export default async function Page({ params }: { params: { slug: string[] } }) {
|
||||
export default async function Page(props: { params: Promise<{ slug: string[] }> }) {
|
||||
const params = await props.params
|
||||
const slug = decodeURI(params.slug.join('/'))
|
||||
// Filter out drafts in production
|
||||
const sortedCoreContents = allCoreContent(sortPosts(allBlogs))
|
||||
|
@ -11,7 +11,8 @@ export const generateStaticParams = async () => {
|
||||
return paths
|
||||
}
|
||||
|
||||
export default function Page({ params }: { params: { page: string } }) {
|
||||
export default async function Page(props: { params: Promise<{ page: string }> }) {
|
||||
const params = await props.params
|
||||
const posts = allCoreContent(sortPosts(allBlogs))
|
||||
const pageNumber = parseInt(params.page as string)
|
||||
const initialDisplayPosts = posts.slice(
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
|
@ -2,6 +2,8 @@ import { MetadataRoute } from 'next'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const siteUrl = siteMetadata.siteUrl
|
||||
|
||||
|
@ -1 +1 @@
|
||||
{"python":1,"projects":1,"code":1,"cygnus":1,"self-hosted":1,"server":1}
|
||||
{"python":1,"projects":1,"code":1,"cygnus":1,"self-hosted":1,"server":1}
|
||||
|
@ -8,7 +8,10 @@ import { genPageMetadata } from 'app/seo'
|
||||
import { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
export async function generateMetadata({ params }: { params: { tag: string } }): Promise<Metadata> {
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ tag: string }>
|
||||
}): Promise<Metadata> {
|
||||
const params = await props.params
|
||||
const tag = decodeURI(params.tag)
|
||||
return genPageMetadata({
|
||||
title: tag,
|
||||
@ -31,7 +34,8 @@ export const generateStaticParams = async () => {
|
||||
return paths
|
||||
}
|
||||
|
||||
export default function TagPage({ params }: { params: { tag: string } }) {
|
||||
export default async function TagPage(props: { params: Promise<{ tag: string }> }) {
|
||||
const params = await props.params
|
||||
const tag = decodeURI(params.tag)
|
||||
// Capitalize first letter and convert space to dash
|
||||
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1)
|
||||
|
@ -16,6 +16,7 @@ export default function Footer() {
|
||||
<SocialIcon kind="x" href={siteMetadata.x} size={6} />
|
||||
<SocialIcon kind="instagram" href={siteMetadata.instagram} size={6} />
|
||||
<SocialIcon kind="threads" href={siteMetadata.threads} size={6} />
|
||||
<SocialIcon kind="medium" href={siteMetadata.medium} size={6} />
|
||||
</div>
|
||||
<div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div>{siteMetadata.author}</div>
|
||||
|
@ -28,10 +28,8 @@ const Header = () => {
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{/* Changed this so I can get all of my links in the header 10-19-2024. Backup Config */}
|
||||
<div className="flex items-center space-x-4 leading-5 sm:space-x-6">
|
||||
<div className="no-scrollbar max-w-50 hidden items-center space-x-4 overflow-x-auto sm:flex md:flex lg:flex">
|
||||
{/* <div className="no-scrollbar hidden max-w-50 items-center space-x-4 overflow-x-auto sm:flex sm:space-x-6 md:max-w-72 lg:max-w-96"> */}
|
||||
<div className="no-scrollbar hidden max-w-40 items-center space-x-4 overflow-x-auto sm:flex sm:space-x-6 md:max-w-72 lg:max-w-96">
|
||||
{headerNavLinks
|
||||
.filter((link) => link.href !== '/')
|
||||
.map((link) => (
|
||||
|
@ -93,3 +93,12 @@ export function Instagram(svgProps: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Medium(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<title>Medium</title>
|
||||
<path d="M13.54 12a6.8 6.8 0 01-6.77 6.82A6.8 6.8 0 010 12a6.8 6.8 0 016.77-6.82A6.8 6.8 0 0113.54 12zM20.96 12c0 3.54-1.51 6.42-3.38 6.42-1.87 0-3.39-2.88-3.39-6.42s1.52-6.42 3.39-6.42 3.38 2.88 3.38 6.42M24 12c0 3.17-.53 5.75-1.19 5.75-.66 0-1.19-2.58-1.19-5.75s.53-5.75 1.19-5.75C23.47 6.25 24 8.83 24 12z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
Mastodon,
|
||||
Threads,
|
||||
Instagram,
|
||||
Medium,
|
||||
} from './icons'
|
||||
|
||||
const components = {
|
||||
@ -22,6 +23,7 @@ const components = {
|
||||
mastodon: Mastodon,
|
||||
threads: Threads,
|
||||
instagram: Instagram,
|
||||
medium: Medium,
|
||||
}
|
||||
|
||||
type SocialIconProps = {
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import rehypeKatexNoTranslate from 'rehype-katex-notranslate'
|
||||
import rehypeCitation from 'rehype-citation'
|
||||
import rehypePrismPlus from 'rehype-prism-plus'
|
||||
import rehypePresetMinify from 'rehype-preset-minify'
|
||||
@ -169,6 +170,7 @@ export default makeSource({
|
||||
},
|
||||
],
|
||||
rehypeKatex,
|
||||
rehypeKatexNoTranslate,
|
||||
[rehypeCitation, { path: path.join(root, 'data') }],
|
||||
[rehypePrismPlus, { defaultLanguage: 'js', ignoreMissing: true }],
|
||||
rehypePresetMinify,
|
||||
|
@ -80,7 +80,7 @@
|
||||
}
|
||||
|
||||
.token.boolean {
|
||||
color: rgb(138, 21, 40);
|
||||
color: rgb(255, 88, 116);
|
||||
}
|
||||
|
||||
.token.number {
|
||||
|
@ -13,4 +13,4 @@ Jonathan Branan is a Software Engineer at Fortra. He is mostly self-taught howev
|
||||
He has worked for Geek Squad repairing computers, GlobalScape as a Lead of the Client Services department and as a Product Owner of MFT applications at Fortra.
|
||||
|
||||
He currently lives in San Antonio, Texas with his wife and two dogs. He enjoys Basketball, Video games, cooking, camping(backpacking, glamping), watching movies,
|
||||
making mixed drinks and building Lego's. Jonathan and his wife like to frequently travel.
|
||||
making mixed drinks and building Lego's. Jonathan and his wife like to frequently travel.
|
||||
|
@ -20,6 +20,7 @@ const siteMetadata = {
|
||||
linkedin: 'https://www.linkedin.com/in/jonathanbranan/',
|
||||
// threads: 'https://www.threads.net',
|
||||
// instagram: 'https://www.instagram.com',
|
||||
// medium: 'https://medium.com',
|
||||
locale: 'en-US',
|
||||
// set to true if you want a navbar fixed to the top
|
||||
stickyNav: false,
|
||||
|
@ -66,7 +66,7 @@ function createSearchIndex(allBlogs) {
|
||||
) {
|
||||
writeFileSync(
|
||||
`public/${siteMetadata.search.kbarConfig.searchDocumentsPath}`,
|
||||
JSON.stringify((sortPosts(allBlogs)))
|
||||
JSON.stringify(sortPosts(allBlogs))
|
||||
)
|
||||
console.log('Local search index generated...')
|
||||
}
|
||||
|
20
faq/deploy-with-docker.md
Normal file
20
faq/deploy-with-docker.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Deploy with Docker
|
||||
|
||||
Follow the [official Next.js repo docker build example and instructions](https://github.com/vercel/next.js/tree/canary/examples/with-docker) to deploy with docker. Copy the [`Dockerfile`](https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile) into the root of the project and modify the `next.config.js` file:
|
||||
|
||||
```js
|
||||
// next.config.js
|
||||
module.exports = {
|
||||
// ... rest of the configuration.
|
||||
output: 'standalone',
|
||||
}
|
||||
```
|
||||
|
||||
You can now build the docker image and run it:
|
||||
|
||||
```bash
|
||||
docker build -t nextjs-docker .
|
||||
docker run -p 3000:3000 nextjs-docker
|
||||
```
|
||||
|
||||
Alternatively, to use docker compose, refer to the [docker compose repo](https://github.com/vercel/next.js/tree/canary/examples/with-docker-compose).
|
@ -100,7 +100,7 @@ export default function PostLayout({ content, authorDetails, next, prev, childre
|
||||
Discuss on Twitter
|
||||
</Link>
|
||||
{` • `}
|
||||
<Link href={editUrl(filePath)}>View on gitea</Link>
|
||||
<Link href={editUrl(filePath)}>View on Gitea</Link>
|
||||
</div>
|
||||
{siteMetadata.comments && (
|
||||
<div
|
||||
|
37
package.json
37
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-nextjs-starter-blog",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "next dev",
|
||||
@ -12,29 +12,30 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "1.7.19",
|
||||
"@next/bundle-analyzer": "14.2.3",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.12",
|
||||
"@headlessui/react": "2.2.0",
|
||||
"@next/bundle-analyzer": "15.0.2",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"body-scroll-lock": "^4.0.0-beta.0",
|
||||
"contentlayer2": "0.5.1",
|
||||
"contentlayer2": "0.5.3",
|
||||
"esbuild": "0.20.2",
|
||||
"github-slugger": "^2.0.0",
|
||||
"gray-matter": "^4.0.2",
|
||||
"hast-util-from-html-isomorphic": "^2.0.0",
|
||||
"image-size": "1.0.0",
|
||||
"next": "14.2.3",
|
||||
"next-contentlayer2": "0.5.1",
|
||||
"next": "15.0.2",
|
||||
"next-contentlayer2": "0.5.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"pliny": "0.2.1",
|
||||
"pliny": "0.4.0",
|
||||
"postcss": "^8.4.24",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "rc",
|
||||
"react-dom": "rc",
|
||||
"reading-time": "1.5.0",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-citation": "^2.0.0",
|
||||
"rehype-katex": "^7.0.0",
|
||||
"rehype-katex-notranslate": "^1.1.4",
|
||||
"rehype-preset-minify": "7.0.0",
|
||||
"rehype-prism-plus": "^2.0.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
@ -42,20 +43,20 @@
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-github-blockquote-alert": "^1.2.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^8.0.1",
|
||||
"@types/mdx": "^2.0.12",
|
||||
"@types/react": "^18.2.73",
|
||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||
"@typescript-eslint/parser": "^6.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.12.0",
|
||||
"@typescript-eslint/parser": "^8.12.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-next": "15.0.2",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.0",
|
||||
"husky": "^9.0.0",
|
||||
"lint-staged": "^13.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
|
9
public/static/favicons/browserconfig.xml
Normal file
9
public/static/favicons/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#000000</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
@ -3,10 +3,12 @@ import path from 'path'
|
||||
import { slug } from 'github-slugger'
|
||||
import { escape } from 'pliny/utils/htmlEscaper.js'
|
||||
import siteMetadata from '../data/siteMetadata.js'
|
||||
import tagData from '../app/tag-data.json' with { type: 'json' }
|
||||
import tagData from '../app/tag-data.json' assert { type: 'json' }
|
||||
import { allBlogs } from '../.contentlayer/generated/index.mjs'
|
||||
import { sortPosts } from 'pliny/utils/contentlayer.js'
|
||||
|
||||
const outputFolder = process.env.EXPORT ? 'out' : 'public'
|
||||
|
||||
const generateRssItem = (config, post) => `
|
||||
<item>
|
||||
<guid>${config.siteUrl}/blog/${post.slug}</guid>
|
||||
@ -40,14 +42,14 @@ async function generateRSS(config, allBlogs, page = 'feed.xml') {
|
||||
// RSS for blog post
|
||||
if (publishPosts.length > 0) {
|
||||
const rss = generateRss(config, sortPosts(publishPosts))
|
||||
writeFileSync(`./public/${page}`, rss)
|
||||
writeFileSync(`./${outputFolder}/${page}`, rss)
|
||||
}
|
||||
|
||||
if (publishPosts.length > 0) {
|
||||
for (const tag of Object.keys(tagData)) {
|
||||
const filteredPosts = allBlogs.filter((post) => post.tags.map((t) => slug(t)).includes(tag))
|
||||
const rss = generateRss(config, filteredPosts, `tags/${tag}/${page}`)
|
||||
const rssPath = path.join('public', 'tags', tag)
|
||||
const rssPath = path.join(outputFolder, 'tags', tag)
|
||||
mkdirSync(rssPath, { recursive: true })
|
||||
writeFileSync(path.join(rssPath, page), rss)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user