├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── jsconfig.json ├── next-env.d.js ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public └── images │ ├── Cosmic_OGImage.png │ ├── avatar_4.png │ └── dev-portfolio.png ├── src ├── app │ ├── about │ │ └── page.jsx │ ├── api │ │ ├── disable-draft │ │ │ └── route.js │ │ └── draft │ │ │ └── route.js │ ├── favicon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── icon.ico │ │ ├── mstile-150x150.png │ │ ├── safari-pinned-tab.svg │ │ └── site.webmanifest │ ├── layout.jsx │ ├── not-found.jsx │ ├── page.jsx │ ├── posts │ │ ├── [slug] │ │ │ └── page.jsx │ │ └── page.jsx │ ├── providers.jsx │ └── works │ │ ├── [slug] │ │ └── page.jsx │ │ └── page.jsx ├── components │ ├── AlertPreview.jsx │ ├── CoverImage.jsx │ ├── Date.jsx │ ├── DevIcon.jsx │ ├── FilteredPosts.jsx │ ├── Footer.jsx │ ├── Header.jsx │ ├── Layout.jsx │ ├── Loader.jsx │ ├── Logo.jsx │ ├── MenuItems.jsx │ ├── MorePosts.jsx │ ├── Navbar.jsx │ ├── PageContainer.jsx │ ├── PostBody.jsx │ ├── PostHeader.jsx │ ├── PostList.jsx │ ├── PostPreview.jsx │ ├── PostTitle.jsx │ ├── ProductCard.jsx │ ├── Socials.jsx │ ├── ThemeChanger.jsx │ └── markdown-styles.module.css ├── configs │ ├── dev-icons.js │ └── icons.jsx ├── helpers │ └── getMetadata.js ├── lib │ └── cosmic.js ├── sections │ ├── AboutMeSection.jsx │ ├── ContactSection.jsx │ ├── IntroSection.jsx │ ├── PostsSection.jsx │ ├── ToolboxSection.jsx │ └── WorksSection.jsx └── styles │ └── globals.css └── tailwind.config.js /.env.example: -------------------------------------------------------------------------------- 1 | COSMIC_BUCKET_SLUG= 2 | COSMIC_READ_KEY= 3 | COSMIC_PREVIEW_SECRET= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # vercel 33 | .vercel 34 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Developer Portfolio built with Next.js and Cosmic 2 | 3 | ![Screenshot of the Cosmic Developer Porfolio application template](/public/images/dev-portfolio.png) 4 | 5 | To build this app, we’re going to use the following technologies: 6 | 7 | - [Next.js](https://nextjs.org/docs) - A React framework for production that makes it easy to spin up a full-stack application. 8 | - [Cosmic](https://www.cosmicjs.com/) - A Headless CMS enables the independence of the data (content) layer and gives us the ability to quickly manage template content. 9 | - [Tailwind CSS](https://tailwindcss.com/) - A performant utility-first CSS framework that can be composed directly in your markup. 10 | 11 | ### Links 12 | 13 | - [Read how the template was built](https://www.cosmicjs.com/blog/creating-a-developer-portfolio-with-nextjs-and-cosmic) 14 | - [Install the template](https://www.cosmicjs.com/marketplace/templates/developer-portfolio) 15 | - [View the live demo](https://nextjs-developer-portfolio-cms.vercel.app/) 16 | 17 | ## Getting started 18 | 19 | ### Environment Variables 20 | 21 | You'll need to create an .env file in the root of the project. Log in to Cosmic and from Bucket Settings > API Access take the following values: 22 | 23 | ``` 24 | //.env 25 | COSMIC_BUCKET_SLUG=your_cosmic_slug 26 | COSMIC_READ_KEY=your_cosmic_read_key 27 | COSMIC_PREVIEW_SECRET=your_preview_secret 28 | ``` 29 | 30 | Install the dependencies by running one of the following commands: 31 | 32 | ``` 33 | pnpm install 34 | # or 35 | yarn 36 | # or 37 | npm install 38 | ``` 39 | 40 | Then run the development server: 41 | 42 | ``` 43 | pnpm dev 44 | # or 45 | yarn dev 46 | # or 47 | npm run dev 48 | ``` 49 | 50 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 51 | 52 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 53 | 54 | ## Previewing Unpublished Content 55 | 56 | This template supports [Next.js Draft Mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode), allowing you to preview unpublished blog posts and works pages from the Cosmic dashboard. 57 | 58 | ### Setup in the codebase 59 | 60 | To enable preview mode, you'll need to set the `COSMIC_PREVIEW_SECRET` environment variable in your `.env` file. This secret can be any random string, though we recommend creating a secure string for security reasons. 61 | 62 | ### Setup in Cosmic 63 | 64 | \*Note that this template currently supports Draft Mode for **Posts** and **Works** Object types. 65 | 66 | In your Cosmic dashboard, go to your **Content** and select **Posts** or **Works** from the list of Object types. Next, select **Object type settings > Additional settings**. 67 | 68 | Start by copying and pasting this URL into the **Preview link** field: 69 | 70 | `https://YOUR_LOCAL_OR_PROD_HOST/api/draft?secret=YOUR_SECRET_PREVIEW_KEY&path=OBJECT_TYPE_PATH&slug=[object_slug]` 71 | 72 | Replace `YOUR_LOCAL_OR_PROD_HOST` with your local host (e.g. `http://localhost:3000`) or production host (e.g. `https://your-app.vercel.app`), and replace `YOUR_SECRET_PREVIEW_KEY` with the secret you set in your `.env` file. Match the `OBJECT_TYPE_PATH` to the Object type you're editing (e.g. `posts` or `works`). 73 | 74 | From here, you can now preview unpublished content from the Cosmic dashboard by clicking the **Preview** button in the righthand sidebar when editing an individual Object. 75 | 76 | ## Deploy on Vercel 77 | 78 |

Use the following button to deploy to Vercel. You will need to add API accesss keys as environment variables. Find these in Bucket Settings > API Access.

79 |

80 | 81 |

82 | 83 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 84 | 85 | Your feedback and contributions are welcome! 86 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "baseUrl": "./src", 5 | "paths": { 6 | "@/components/*": ["components/*"], 7 | "@/lib/*": ["lib/*"], 8 | "@/configs/*": ["configs/*"], 9 | "@/sections/*": ["sections/*"], 10 | "@/styles/*": ["styles/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /next-env.d.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | images: { 3 | domains: ['imgix.cosmicjs.com'], 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next", 5 | "preinstall": "npx only-allow pnpm", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@cosmicjs/sdk": "^1.0.5", 12 | "@fontsource/public-sans": "^4.5.8", 13 | "date-fns": "2.28.0", 14 | "isomorphic-dompurify": "^1.7.0", 15 | "next": "^13.4.8", 16 | "next-themes": "^0.2.1", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-markdown": "^8.0.3", 20 | "sharp": "^0.30.6", 21 | "swr": "^1.3.0" 22 | }, 23 | "devDependencies": { 24 | "@tailwindcss/typography": "^0.5.2", 25 | "autoprefixer": "10.4.2", 26 | "eslint": "8.14.0", 27 | "eslint-config-next": "13.4.4", 28 | "eslint-config-prettier": "^8.5.0", 29 | "postcss": "8.4.5", 30 | "prettier": "2.6.2", 31 | "tailwindcss": "^3.3.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/images/Cosmic_OGImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/public/images/Cosmic_OGImage.png -------------------------------------------------------------------------------- /public/images/avatar_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/public/images/avatar_4.png -------------------------------------------------------------------------------- /public/images/dev-portfolio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/public/images/dev-portfolio.png -------------------------------------------------------------------------------- /src/app/about/page.jsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import { getPageBySlug } from '@/lib/cosmic' 3 | import Socials from '@/components/Socials' 4 | import { sanitize } from 'isomorphic-dompurify' 5 | import getMetadata from 'helpers/getMetadata' 6 | 7 | async function getData() { 8 | const pageData = (await getPageBySlug('about-page', 'content,metadata')) || [] 9 | return { 10 | pageData, 11 | } 12 | } 13 | 14 | export async function generateMetadata() { 15 | const [pageData, socialData, siteSettings] = await Promise.all([ 16 | getPageBySlug('about-page', 'metadata'), 17 | getPageBySlug('social-config', 'metadata'), 18 | getPageBySlug('site-settings', 'metadata'), 19 | ]) 20 | 21 | const title = getMetadata(pageData?.metadata?.meta_title) 22 | const description = getMetadata(pageData?.metadata?.meta_description) 23 | const image = getMetadata( 24 | pageData?.metadata?.meta_image?.imgix_url, 25 | siteSettings?.metadata?.default_meta_image?.imgix_url ?? '' 26 | ) 27 | const url = getMetadata(`${siteSettings?.metadata?.site_url}/about`) 28 | const twitterHandle = getMetadata(socialData?.metadata?.twitter) 29 | 30 | return { 31 | title: title, 32 | description: description, 33 | image: image, 34 | openGraph: { 35 | title: title, 36 | description: description, 37 | url: url, 38 | images: [ 39 | { 40 | url: image, 41 | width: 800, 42 | height: 600, 43 | }, 44 | { 45 | url: image, 46 | width: 1800, 47 | height: 1600, 48 | }, 49 | ], 50 | locale: 'en_US', 51 | type: 'website', 52 | }, 53 | twitter: { 54 | card: 'summary_large_image', 55 | title: title, 56 | description: description, 57 | creator: twitterHandle, 58 | images: [image], 59 | }, 60 | } 61 | } 62 | 63 | const AboutPage = async () => { 64 | const data = await getData() 65 | const pageData = data.pageData 66 | 67 | return ( 68 | <> 69 |
70 |

71 | {pageData?.metadata.heading} 72 |

73 |
74 | {pageData.metadata.image && ( 75 |
76 | Developer avatar 89 |
90 | )} 91 |
92 |
98 | 104 |
105 |
106 |
107 | 108 | ) 109 | } 110 | 111 | export const revalidate = 60 112 | export default AboutPage 113 | -------------------------------------------------------------------------------- /src/app/api/disable-draft/route.js: -------------------------------------------------------------------------------- 1 | import { draftMode } from 'next/headers' 2 | import { redirect } from 'next/navigation' 3 | 4 | export async function GET(request) { 5 | draftMode().disable() 6 | redirect('/') 7 | } 8 | -------------------------------------------------------------------------------- /src/app/api/draft/route.js: -------------------------------------------------------------------------------- 1 | // route handler with secret and slug 2 | import { draftMode } from 'next/headers' 3 | import { redirect } from 'next/navigation' 4 | import { getPreviewPostBySlug } from '@/lib/cosmic' 5 | 6 | export async function GET(request) { 7 | // Parse query string parameters 8 | const { searchParams } = new URL(request.url) 9 | const secret = searchParams.get('secret') 10 | const slug = searchParams.get('slug') 11 | const path = searchParams.get('path') 12 | 13 | // Check the secret and next parameters 14 | // This secret should only be known to this route handler and the CMS 15 | if (secret !== process.env.COSMIC_PREVIEW_SECRET || !slug) { 16 | return new Response('Invalid token', { status: 401 }) 17 | } 18 | 19 | // Fetch the headless CMS to check if the provided `slug` exists 20 | // getPostBySlug would implement the required fetching logic to the headless CMS 21 | const post = await getPreviewPostBySlug(slug) 22 | 23 | // If the slug doesn't exist prevent draft mode from being enabled 24 | if (!post) { 25 | return new Response('Invalid slug', { status: 401 }) 26 | } 27 | 28 | // Enable Draft Mode by setting the cookie 29 | draftMode().enable() 30 | 31 | // Redirect to the path from the fetched post 32 | // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities 33 | redirect(`/${path}/${post.slug}`) 34 | } 35 | -------------------------------------------------------------------------------- /src/app/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/app/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/app/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /src/app/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /src/app/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/app/favicon/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/icon.ico -------------------------------------------------------------------------------- /src/app/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /src/app/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 41 | 72 | 82 | 93 | 124 | 137 | 165 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /src/app/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/app/layout.jsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import Providers from './providers' 3 | import Header from '@/components/Header' 4 | import AlertPreview from '@/components/AlertPreview' 5 | import Footer from '@/components/Footer' 6 | import { draftMode } from 'next/headers' 7 | import { getSiteSettings } from '@/lib/cosmic' 8 | import getMetadata from 'helpers/getMetadata' 9 | 10 | const siteSettings = await getSiteSettings() 11 | const enableRobots = getMetadata(siteSettings?.metadata?.enable_robots, false) 12 | const siteUrl = getMetadata(siteSettings?.metadata?.site_url) 13 | 14 | export const metadata = { 15 | metadataBase: new URL(siteUrl), 16 | icons: { 17 | icon: '/favicon/icon.ico', 18 | shortcut: '/favicon/shortcut-icon.png', 19 | apple: '/favicon/apple-touch-icon.png', 20 | }, 21 | viewport: { 22 | width: 'device-width', 23 | initialScale: 1, 24 | maximumScale: 1, 25 | }, 26 | robots: { 27 | index: enableRobots, 28 | follow: enableRobots, 29 | nocache: enableRobots, 30 | googleBot: { 31 | index: enableRobots, 32 | follow: enableRobots, 33 | noimageindex: enableRobots, 34 | 'max-video-preview': -1, 35 | 'max-image-preview': 'large', 36 | 'max-snippet': -1, 37 | }, 38 | }, 39 | } 40 | 41 | export default function RootLayout({ children }) { 42 | const { isEnabled } = draftMode() 43 | 44 | return ( 45 | 46 | 47 | 51 | 52 | 53 | 54 | 55 |
56 | {isEnabled && } 57 |
58 | {children} 59 |
60 |