@@ -55,7 +82,7 @@ export default async function Project({ params: { slug } }: ProjectPageProps) {
)}
{body && (
)}
diff --git a/src/app/api/og/route.tsx b/src/app/api/og/route.tsx
new file mode 100644
index 0000000..031e077
--- /dev/null
+++ b/src/app/api/og/route.tsx
@@ -0,0 +1,1485 @@
+/* eslint-disable @next/next/no-img-element */
+
+import { ImageResponse } from 'next/og'
+
+export const runtime = 'edge'
+
+export async function GET(request: Request) {
+ try {
+ const { searchParams } = new URL(request.url)
+
+ const hasTitle = searchParams.has('title')
+ const title = hasTitle
+ ? searchParams.get('title')
+ : 'Anton Bochkovskyi - Front-End Developer'
+
+ const hasDescription = searchParams.has('description')
+ const description = hasDescription
+ ? searchParams.get('description')
+ : 'Anton Bochkovskyi - Front-End Developer'
+
+ const ralewaySemiBold = fetch(
+ new URL('../../../../public/fonts/Raleway-SemiBold.ttf', import.meta.url)
+ ).then((res) => res.arrayBuffer())
+
+ const ralewayMedium = fetch(
+ new URL('../../../../public/fonts/Raleway-Medium.ttf', import.meta.url)
+ ).then((res) => res.arrayBuffer())
+
+ return new ImageResponse(
+ (
+
+
{title}
+
{description}
+
+
+ ),
+ {
+ fonts: [
+ {
+ data: await ralewaySemiBold,
+ name: 'Raleway',
+ style: 'normal',
+ weight: 600,
+ },
+ {
+ data: await ralewayMedium,
+ name: 'Raleway',
+ style: 'normal',
+ weight: 500,
+ },
+ ],
+ }
+ )
+ } catch (error) {
+ if (error instanceof Error) {
+ return new Response('Failed to generate OG image', { status: 500 })
+ }
+ }
+}
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
index 718d6fe..4929e02 100644
Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 3cdb825..80e90d7 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,19 +1,30 @@
-// TODO: Adjust site metadata
-// TODO: Add 404 page
-
import type { Metadata } from 'next'
import Footer from '@/components/footer'
import Header from '@/components/header/header'
import Providers from '@/store/providers'
-import { cn } from '@/utils'
+import { SITE_URL, cn } from '@/utils'
import { raleway } from './fonts'
import './globals.css'
export const metadata: Metadata = {
- description: 'Generated by create next app',
- title: 'Create Next App',
+ description:
+ "Hi, my name is Anton Bochkovskyi and I'm a Front-End Developer specializing in React, Next.js, and Tailwind CSS, with a keen focus on crafting responsive and visually stunning web applications.",
+ metadataBase: new URL(SITE_URL),
+ openGraph: {
+ images: {
+ alt: 'Anton Bochkovskyi - Front-End Developer',
+ height: 630,
+ type: 'image/png',
+ url: `${SITE_URL}/api/og?title=Anton%20Bochkovskyi%20-%20Front-End%20Developer`,
+ width: 1200,
+ },
+ },
+ title: {
+ default: 'Anton Bochkovskyi - Front-End Developer',
+ template: '%s | Anton Bochkovskyi - Front-End Developer',
+ },
}
export default function RootLayout({
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
new file mode 100644
index 0000000..37ed782
--- /dev/null
+++ b/src/app/not-found.tsx
@@ -0,0 +1,30 @@
+import { buttonVariants } from '@/components/ui/button'
+import Content from '@/components/ui/content'
+import Heading from '@/components/ui/heading'
+import { LINK } from '@/types/enums/Link'
+import { cn } from '@/utils'
+import Image from 'next/image'
+import Link from 'next/link'
+
+export default function NotFound() {
+ return (
+
+
+
+ Page Not Found
+
+ The page you were looking for does not exists.
+
+
+ Return Home
+
+
+
+ )
+}
diff --git a/src/app/robots.ts b/src/app/robots.ts
new file mode 100644
index 0000000..8ed81e0
--- /dev/null
+++ b/src/app/robots.ts
@@ -0,0 +1,12 @@
+import { SITE_URL } from '@/utils'
+import { MetadataRoute } from 'next'
+
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: {
+ allow: '/',
+ userAgent: '*',
+ },
+ sitemap: `${SITE_URL}/sitemap.xml`,
+ }
+}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
new file mode 100644
index 0000000..df4fee3
--- /dev/null
+++ b/src/app/sitemap.ts
@@ -0,0 +1,27 @@
+import { SITE_URL } from '@/utils'
+import { getProjectsMetadata } from '@/utils/projects'
+import { MetadataRoute } from 'next'
+
+export default async function sitemap(): Promise
{
+ const projectsMetadata = await getProjectsMetadata()
+
+ const sitemapData: MetadataRoute.Sitemap = [
+ {
+ changeFrequency: 'monthly',
+ lastModified: new Date(),
+ priority: 1,
+ url: SITE_URL,
+ },
+ ]
+
+ for (const project of projectsMetadata) {
+ sitemapData.push({
+ changeFrequency: 'weekly',
+ lastModified: project.lastModified,
+ priority: 0.8,
+ url: `${SITE_URL}/${project.slug}`,
+ })
+ }
+
+ return sitemapData
+}
diff --git a/src/components/header/header-navbar.tsx b/src/components/header/header-navbar.tsx
index 92d3a21..94fe499 100644
--- a/src/components/header/header-navbar.tsx
+++ b/src/components/header/header-navbar.tsx
@@ -5,6 +5,7 @@ import MenuItem from '@/components/header/menu/menu-item'
import Socials from '@/components/ui/socials'
import useMediaQuery from '@/hooks/use-media-query'
import HeaderContext from '@/store/header-context'
+import { LINK } from '@/types/enums/Link'
import { MOBILE_BREAKPOINT, cn } from '@/utils'
import { FC, HTMLAttributes, useContext } from 'react'
@@ -17,8 +18,8 @@ const HeaderNavbar: FC = ({ className, ...props }) => {
return (
diff --git a/src/components/sections/hero/hero.tsx b/src/components/sections/hero/hero.tsx
index 79b7344..dbfdd93 100644
--- a/src/components/sections/hero/hero.tsx
+++ b/src/components/sections/hero/hero.tsx
@@ -2,7 +2,9 @@ import { buttonVariants } from '@/components/ui/button'
import Content from '@/components/ui/content'
import Heading from '@/components/ui/heading'
import Socials from '@/components/ui/socials'
+import { LINK } from '@/types/enums/Link'
import { cn } from '@/utils'
+import Image from 'next/image'
import Link from 'next/link'
import { FC, HTMLAttributes } from 'react'
@@ -17,8 +19,15 @@ const Hero: FC = ({ className, ...props }) => {
)}
{...props}
>
+
@@ -32,7 +41,7 @@ const Hero: FC = ({ className, ...props }) => {
My Projects
diff --git a/src/components/ui/logo.tsx b/src/components/ui/logo.tsx
index 099525d..3c64032 100644
--- a/src/components/ui/logo.tsx
+++ b/src/components/ui/logo.tsx
@@ -1,6 +1,7 @@
'use client'
import HeaderContext from '@/store/header-context'
+import { LINK } from '@/types/enums/Link'
import { cn } from '@/utils'
import Link from 'next/link'
import { AnchorHTMLAttributes, FC, useContext } from 'react'
@@ -13,7 +14,7 @@ const Logo: FC = ({ className, ...props }) => {
return (
diff --git a/src/types/ProjectMetadata.ts b/src/types/ProjectMetadata.ts
new file mode 100644
index 0000000..ce34f63
--- /dev/null
+++ b/src/types/ProjectMetadata.ts
@@ -0,0 +1,6 @@
+type ProjectMetadata = {
+ lastModified: Date
+ slug: string
+}
+
+export default ProjectMetadata
diff --git a/src/types/enums/Link.ts b/src/types/enums/Link.ts
new file mode 100644
index 0000000..bc1c863
--- /dev/null
+++ b/src/types/enums/Link.ts
@@ -0,0 +1,10 @@
+export const LINK = {
+ about: '/#about-me',
+ contact: '/#contact-me',
+ index: '/',
+ projects: '/#my-projects',
+} as const
+
+type Link = keyof typeof LINK
+
+export default Link
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index f51d422..8657bcb 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1 +1,2 @@
export const MOBILE_BREAKPOINT = 767.98
+export const SITE_URL = process.env.SITE_URL || 'http://localhost:3000'
diff --git a/src/utils/projects.ts b/src/utils/projects.ts
index 6293bd3..a5e75ed 100644
--- a/src/utils/projects.ts
+++ b/src/utils/projects.ts
@@ -1,4 +1,6 @@
import Project from '@/types/Project'
+import ProjectMetadata from '@/types/ProjectMetadata'
+import fs from 'fs'
import { readFile, readdir } from 'fs/promises'
import matter from 'gray-matter'
import { marked } from 'marked'
@@ -8,16 +10,6 @@ const PROJECTS_PATH = './content/projects'
const FILE_EXTENSION = '.md'
const DIR_PATH = path.join(process.cwd(), PROJECTS_PATH)
-export const getSlugs = async (): Promise => {
- const files = await readdir(DIR_PATH)
-
- const slugs = files
- .filter((file) => file.endsWith(FILE_EXTENSION))
- .map((file) => file.replace(FILE_EXTENSION, ''))
-
- return slugs
-}
-
export const getProjects = async (): Promise => {
const slugs = await getSlugs()
@@ -40,11 +32,7 @@ export const getProjects = async (): Promise => {
}
export const getProject = async (slug: string): Promise => {
- const filePath = path.join(
- process.cwd(),
- PROJECTS_PATH,
- `${slug}${FILE_EXTENSION}`
- )
+ const filePath = getProjectFilePath(slug)
const text = await readFile(filePath, 'utf-8')
if (!text.trim()) throw new Error('Markdown file is empty!')
@@ -62,3 +50,36 @@ export const getProject = async (slug: string): Promise => {
return { body, features, github, image, slug, technologies, title, website }
}
+
+export const getProjectsMetadata = async (): Promise => {
+ const slugs = await getSlugs()
+
+ let projectsMetadata: ProjectMetadata[] = []
+
+ if (!slugs.length) return projectsMetadata
+
+ for (const slug of slugs) {
+ const projectModifiedDate = getProjectModifiedDate(slug)
+ projectsMetadata.push({ lastModified: projectModifiedDate, slug })
+ }
+
+ return projectsMetadata
+}
+
+export const getSlugs = async (): Promise => {
+ const files = await readdir(DIR_PATH)
+ const slugs = files
+ .filter((file) => file.endsWith(FILE_EXTENSION))
+ .map((file) => file.replace(FILE_EXTENSION, ''))
+ return slugs
+}
+
+export const getProjectFilePath = (slug: string): string => {
+ return path.join(process.cwd(), PROJECTS_PATH, `${slug}${FILE_EXTENSION}`)
+}
+
+export const getProjectModifiedDate = (slug: string): Date => {
+ const filePath = getProjectFilePath(slug)
+ const fileStats = fs.statSync(filePath)
+ return new Date(fileStats.mtime.toISOString())
+}