Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const config: NextConfig = {
hostname: '**'
}
],
dangerouslyAllowSVG: false
dangerouslyAllowSVG: false,
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 2678400
},
typedRoutes: false,
experimental: {
Expand Down
25 changes: 22 additions & 3 deletions src/app/(public)/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import type { Metadata } from 'next'

export const metadata: Metadata = {
title: 'Об основателе'
}
import { JsonLd } from '@/components/shared/json-ld'

import { ROUTES } from '@/lib/routes'
import { pageMetadata } from '@/lib/seo'
import { breadcrumbSchema, personSchema } from '@/lib/seo/schema'

export const metadata: Metadata = pageMetadata({
title: 'Об основателе',
description:
'Вадим - основатель TeaCoder и автор курсов по веб-разработке: путь от первых сайтов в 11 лет до собственной платформы и YouTube-канала.',
path: ROUTES.ABOUT,
openGraphType: 'profile'
})

export default function AboutPage() {
return (
<main className='mx-auto my-20 max-w-4xl'>
<JsonLd
schema={[
personSchema(),
breadcrumbSchema([
{ name: 'Главная', path: ROUTES.HOME },
{ name: 'Об основателе', path: ROUTES.ABOUT }
])
]}
/>
<div className='mx-auto max-w-7xl px-4 lg:px-8'>
<section className='mx-auto'>
<h1 className='text-center text-4xl font-semibold tracking-tight sm:text-5xl'>
Expand Down
55 changes: 40 additions & 15 deletions src/app/(public)/courses/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import type { Metadata } from 'next'
import { getCoursesBySlugLessonsQuery } from '@/generated/api'

import { CourseDetails } from '@/components/course/course-details'
import { JsonLd } from '@/components/shared/json-ld'

import { getAppConfig } from '@/lib/config/app-config'
import { getCourse } from '@/lib/courses/get-course'
import { ROUTES } from '@/lib/routes'
import { pageMetadata } from '@/lib/seo'
import { breadcrumbSchema, courseSchema } from '@/lib/seo/schema'

export async function generateMetadata({
params
Expand All @@ -14,20 +19,17 @@ export async function generateMetadata({
const { slug } = await params

const course = await getCourse(slug)
const images = course.thumbnail
? [{ url: course.thumbnail, alt: course.title }]
: undefined

return {
return pageMetadata({
title: course.title,
description: course.shortDescription,
openGraph: { images },
twitter: {
title: course.title,
description: course.shortDescription ?? '',
images
}
}
description:
course.shortDescription ??
`Курс «${course.title}» на TeaCoder: видеоуроки, исходный код проекта и прогресс обучения.`,
path: ROUTES.COURSES.SINGLE(course.slug),
...(course.thumbnail && {
image: { url: course.thumbnail, alt: course.title }
})
})
}

export default async function CoursePage({
Expand All @@ -37,10 +39,33 @@ export default async function CoursePage({
}) {
const { slug } = await params

const [course, lessons] = await Promise.all([
const [course, lessons, config] = await Promise.all([
getCourse(slug),
getCoursesBySlugLessonsQuery(slug)
getCoursesBySlugLessonsQuery(slug),
getAppConfig()
])

return <CourseDetails course={course} lessons={lessons} />
const path = ROUTES.COURSES.SINGLE(course.slug)

return (
<>
<JsonLd
schema={[
courseSchema({
course,
path,
program: lessons,
currency: config?.features.premium.currency
}),
breadcrumbSchema([
{ name: 'Главная', path: ROUTES.HOME },
{ name: 'Курсы', path: ROUTES.COURSES.ROOT },
{ name: course.title, path }
])
]}
/>

<CourseDetails course={course} lessons={lessons} />
</>
)
}
30 changes: 25 additions & 5 deletions src/app/(public)/courses/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,40 @@ import Image from 'next/image'
import { getCoursesQuery } from '@/generated/api'

import { CourseCard } from '@/components/course/course-card'
import { JsonLd } from '@/components/shared/json-ld'

import { ROUTES } from '@/lib/routes'
import { pageMetadata } from '@/lib/seo'
import { breadcrumbSchema, itemListSchema } from '@/lib/seo/schema'
import { COURSE, pluralize } from '@/lib/utils'

export const metadata: Metadata = {
title: 'Курсы',
export const metadata: Metadata = pageMetadata({
title: 'Курсы по веб-разработке',
description:
'Здесь собраны курсы по веб-разработке, которые помогут вам освоить самые востребованные технологии и инструменты.'
}
'Каталог курсов TeaCoder: фронтенд, бэкенд и инструменты разработчика. Видеоуроки, исходный код проектов и прогресс обучения в личном кабинете.',
path: ROUTES.COURSES.ROOT
})

export default async function CoursesPage() {
const courses = await getCoursesQuery()

return (
<main className='pb-24 sm:pb-32'>
<JsonLd
schema={[
breadcrumbSchema([
{ name: 'Главная', path: ROUTES.HOME },
{ name: 'Курсы', path: ROUTES.COURSES.ROOT }
]),
itemListSchema(
courses.map(course => ({
path: ROUTES.COURSES.SINGLE(course.slug)
})),
'Курсы по веб-разработке'
)
]}
/>

<section className='frame-container pt-2'>
<div className='relative isolate flex min-h-[300px] flex-col justify-end overflow-hidden rounded-[1.75rem] bg-inverse px-6 py-10 text-inverse-foreground sm:min-h-[380px] sm:px-12 sm:py-14 lg:px-16'>
<Image
Expand All @@ -28,7 +48,7 @@ export default async function CoursesPage() {
sizes='100vw'
className='-z-10 object-cover object-[70%_50%]'
/>
<p className='font-mono text-xs uppercase tracking-[0.18em] text-highlight'>
<p className='font-mono text-xs tracking-[0.18em] text-highlight uppercase'>
Каталог · {pluralize(courses.length, COURSE)}
</p>
<h1 className='mt-4 font-display text-display-xl font-medium'>
Expand Down
10 changes: 7 additions & 3 deletions src/app/(public)/document/cookie-policy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import { LegalDocument } from '@/components/legal/legal-document'

import { env } from '@/lib/config/env'
import { ROUTES } from '@/lib/routes'
import { pageMetadata } from '@/lib/seo'

export const metadata: Metadata = {
title: 'Политика использования файлов cookie'
}
export const metadata: Metadata = pageMetadata({
title: 'Политика использования файлов cookie',
description:
'Порядок применения файлов cookie на платформе TeaCoder: цели, согласие пользователя и управление настройками.',
path: ROUTES.DOCUMENTS.COOKIES
})

const support = (
<Link href={`mailto:${env.SUPPORT_EMAIL}`}>{env.SUPPORT_EMAIL}</Link>
Expand Down
10 changes: 7 additions & 3 deletions src/app/(public)/document/privacy-policy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import { LegalDocument } from '@/components/legal/legal-document'

import { env } from '@/lib/config/env'
import { ROUTES } from '@/lib/routes'
import { pageMetadata } from '@/lib/seo'

export const metadata: Metadata = {
title: 'Политика конфиденциальности'
}
export const metadata: Metadata = pageMetadata({
title: 'Политика конфиденциальности',
description:
'Какие сведения о пользователях получает TeaCoder, для чего они используются, кому передаются и как защищаются.',
path: ROUTES.DOCUMENTS.PRIVACY
})

const support = (
<Link href={`mailto:${env.SUPPORT_EMAIL}`}>{env.SUPPORT_EMAIL}</Link>
Expand Down
10 changes: 7 additions & 3 deletions src/app/(public)/document/terms-of-use/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import { LegalDocument } from '@/components/legal/legal-document'

import { env } from '@/lib/config/env'
import { ROUTES } from '@/lib/routes'
import { pageMetadata } from '@/lib/seo'

export const metadata: Metadata = {
title: 'Пользовательское соглашение'
}
export const metadata: Metadata = pageMetadata({
title: 'Пользовательское соглашение',
description:
'Условия использования платформы TeaCoder: доступ к курсам, оплата подписки и курсов, возвраты и права пользователей.',
path: ROUTES.DOCUMENTS.TERMS
})

const support = (
<Link href={`mailto:${env.SUPPORT_EMAIL}`}>{env.SUPPORT_EMAIL}</Link>
Expand Down
22 changes: 21 additions & 1 deletion src/app/(public)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,22 @@ import { Hero } from '@/components/home/hero'
import { Manifesto } from '@/components/home/manifesto'
import { Popular } from '@/components/home/popular'
import { TelegramCTA } from '@/components/home/telegram-cta'
import { JsonLd } from '@/components/shared/json-ld'

import { pickPopularCourses } from '@/lib/courses/popular-courses'
import { ROUTES } from '@/lib/routes'
import { SEO, pageMetadata } from '@/lib/seo'
import { itemListSchema } from '@/lib/seo/schema'

export const metadata: Metadata = {
title: 'Образовательная платформа по веб разработке'
...pageMetadata({
title: SEO.name,
description: SEO.description,
path: ROUTES.HOME
}),
title: {
absolute: 'TeaCoder - курсы по веб-разработке с практикой'
}
}

export default async function HomePage() {
Expand All @@ -33,6 +44,15 @@ export default async function HomePage() {

return (
<Fragment>
<JsonLd
schema={itemListSchema(
courses.map(course => ({
path: ROUTES.COURSES.SINGLE(course.slug)
})),
'Популярные курсы TeaCoder'
)}
/>

<Hero />
<Manifesto />
<Popular courses={courses} />
Expand Down
47 changes: 42 additions & 5 deletions src/app/(public)/premium/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,43 @@ import { getCoursesQuery } from '@/generated/api'

import { CourseCard } from '@/components/course/course-card'
import { FAQSection } from '@/components/premium/faq'
import { getFaqs } from '@/components/premium/faq-items'
import { Premium } from '@/components/premium/premium'
import { JsonLd } from '@/components/shared/json-ld'

import { COURSE, pluralize } from '@/lib/utils'
import { withoutSplit } from '@/lib/billing/payment-methods'
import { getAppConfig } from '@/lib/config/app-config'
import { ROUTES } from '@/lib/routes'
import { pageMetadata } from '@/lib/seo'
import {
breadcrumbSchema,
faqSchema,
subscriptionSchema
} from '@/lib/seo/schema'
import { COURSE, formatPrice, pluralize } from '@/lib/utils'

export const metadata: Metadata = {
title: 'Подписка'
}
export const metadata: Metadata = pageMetadata({
title: 'Подписка Premium',
description:
'Premium открывает закрытые уроки и исходный код курсов с пометкой «По подписке». Оплата картой, СБП и криптовалютой, отменить можно в любой момент.',
path: ROUTES.PREMIUM
})

export default async function PremiumPage() {
// The pricing block works without the course list, so a failed fetch only hides the grid
const courses = await getCoursesQuery().catch(() => [])
const [courses, config] = await Promise.all([
getCoursesQuery().catch(() => []),
getAppConfig()
])

const premium = config?.features.premium
const faqs = getFaqs(
withoutSplit(config?.features.payments)?.map(method => method.name) ??
[],
premium
? formatPrice(premium.prices.international, premium.currency)
: null
)
// Subscription covers PREMIUM courses only; PURCHASE ones are bought separately
const included = courses.filter(
course =>
Expand All @@ -24,6 +50,17 @@ export default async function PremiumPage() {

return (
<main className='pb-24 sm:pb-32'>
<JsonLd
schema={[
subscriptionSchema(premium, ROUTES.PREMIUM),
faqSchema(faqs),
breadcrumbSchema([
{ name: 'Главная', path: ROUTES.HOME },
{ name: 'Подписка', path: ROUTES.PREMIUM }
])
]}
/>

<section className='page-container pt-16 pb-14 text-center sm:pt-24 sm:pb-16'>
<p className='inline-flex rounded-full border border-border bg-card px-3.5 py-1.5 text-sm font-medium text-muted-foreground'>
Подписка TeaCoder
Expand Down
5 changes: 4 additions & 1 deletion src/app/auth/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import type { Metadata } from 'next'
import { LoginForm } from '@/components/auth/login-form'

export const metadata: Metadata = {
title: 'Войти в аккаунт'
title: 'Войти в аккаунт',
description:
'Вход в аккаунт TeaCoder по почте, ключу доступа или через сторонний сервис.',
robots: { index: false, follow: true }
}

export default function LoginPage() {
Expand Down
1 change: 0 additions & 1 deletion src/app/auth/recovery/[token]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { NewPasswordForm } from '@/components/auth/new-password-form'

export const metadata: Metadata = {
title: 'Новый пароль',
// The reset token is in the URL - never pass it on to other sites.
referrer: 'no-referrer',
robots: {
index: false,
Expand Down
5 changes: 4 additions & 1 deletion src/app/auth/recovery/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import type { Metadata } from 'next'
import { ResetPasswordForm } from '@/components/auth/reset-password-form'

export const metadata: Metadata = {
title: 'Сброс пароля'
title: 'Сброс пароля',
description:
'Восстановление доступа к аккаунту TeaCoder по адресу электронной почты.',
robots: { index: false, follow: true }
}

export default function ResetPasswordPage() {
Expand Down
5 changes: 4 additions & 1 deletion src/app/auth/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import type { Metadata } from 'next'
import { RegisterForm } from '@/components/auth/register-form'

export const metadata: Metadata = {
title: 'Создать аккаунт'
title: 'Создать аккаунт',
description:
'Регистрация на образовательной платформе TeaCoder: доступ к курсам, прогрессу обучения и подписке.',
robots: { index: false, follow: true }
}

export default function RegisterPage() {
Expand Down
Loading
Loading