diff --git a/app/[...slug]/loading.tsx b/app/[...slug]/loading.tsx
new file mode 100644
index 00000000..f5fbfcf9
--- /dev/null
+++ b/app/[...slug]/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return
loading...
+}
diff --git a/app/[...slug]/page.tsx b/app/[...slug]/page.tsx
new file mode 100644
index 00000000..7eb661cd
--- /dev/null
+++ b/app/[...slug]/page.tsx
@@ -0,0 +1,71 @@
+import type { Metadata } from "next"
+import { notFound } from "next/navigation"
+import { App } from "@/components/App"
+import { PageContent } from "@/components/PageContent"
+import { MetaBuilder, buildMetadata } from "@/lib/meta"
+import { getMenu, getPageBySlug, getUrls } from "@/remote/api"
+import { tail } from "@/lib/array"
+
+export const revalidate = 30
+export const dynamicParams = true
+
+export async function generateStaticParams() {
+ const urls = await getUrls()
+ return urls.map(path => ({
+ slug: tail(path.split("/")),
+ }))
+}
+
+type Props = {
+ params: Promise<{ slug: string[] }>
+}
+
+export async function generateMetadata({ params }: Props): Promise {
+ const { slug } = await params
+ const slugPath = `/${slug.join("/")}`
+ const page = await getPageBySlug(slugPath)
+ if (!page) {
+ return {}
+ }
+
+ const m = new MetaBuilder()
+ .setImage(page.cover)
+ .setTitle(page.title)
+ .setDescription(page.description ?? undefined)
+ .build()
+ return buildMetadata(m)
+}
+
+export default async function SlugPage({ params }: Props) {
+ const { slug } = await params
+ const slugPath = `/${slug.join("/")}`
+ const page = await getPageBySlug(slugPath)
+ if (!page) {
+ notFound()
+ }
+
+ const breadcrumbSize = page.breadcrumb?.length ?? 0
+ const breadcrumbs = breadcrumbSize < 2 ? [] : page.breadcrumb ?? []
+
+ return (
+
+
+
+ )
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 00000000..6cc334cc
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,81 @@
+import "src/style/style.css"
+import "src/style/content.css"
+import "src/style/article.css"
+import "src/style/marker/marker-table.css"
+import "src/style/kazimir/video.css"
+
+import type { Metadata, Viewport } from "next"
+import Script from "next/script"
+import { Roboto } from "next/font/google"
+import { Providers } from "@/components/Providers"
+import { YMetrika } from "@/components/YMetrika"
+import { getMenu } from "@/remote/api"
+
+const roboto = Roboto({
+ weight: ["300", "900"],
+ subsets: ["cyrillic", "latin"],
+ display: "swap",
+})
+
+export const metadata: Metadata = {
+ verification: {
+ google: [
+ "8BxOTw-Q02DS2KAdcGcnIMwiPZ58DVB18XpOfRibCcg",
+ "A-TEemUw4hHpULUDCd6xhxfHnyRbgKtss90JGKXnSpw",
+ ],
+ other: {
+ "yandex-verification": "643c7dac144b9dac",
+ "msvalidate.01": "E878301FE3E3F12D233A6B1156889601",
+ "proculture-verification": "79110940fd847c3cfe52392ed68f3210",
+ },
+ },
+ icons: {
+ icon: { url: "/static/graphics/favicon-32.png", type: "image/png" },
+ apple: [
+ { url: "/static/graphics/favicon-57.png", sizes: "57x57" },
+ { url: "/static/graphics/favicon-72.png", sizes: "72x72" },
+ { url: "/static/graphics/favicon-114.png", sizes: "114x114" },
+ { url: "/static/graphics/favicon-144.png", sizes: "144x144" },
+ ],
+ },
+ other: {
+ "apple-mobile-web-app-capable": "yes",
+ "referrer": "unsafe-url",
+ },
+ alternates: {
+ types: {
+ "application/feed+json": "/feed.json",
+ },
+ },
+}
+
+export const viewport: Viewport = {
+ width: "device-width",
+ maximumScale: 1,
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "white" },
+ { media: "(prefers-color-scheme: dark)", color: "hsl(240, 10%, 10%)" },
+ ],
+}
+
+export const revalidate = 30
+
+export default async function RootLayout({ children }: { children: React.ReactNode }) {
+ const menu = await getMenu()
+
+ return (
+
+
+ {process.env.NEXT_PUBLIC_YMETRIKA_ACCOUNT && (
+
+ )}
+
+
+
+ {children}
+
+
+
+
+ )
+}
diff --git a/app/not-found.tsx b/app/not-found.tsx
new file mode 100644
index 00000000..ebafdd31
--- /dev/null
+++ b/app/not-found.tsx
@@ -0,0 +1,10 @@
+import { App } from "@/components/App"
+import { NotFound } from "@/components/NotFound"
+
+export default function NotFoundPage() {
+ return (
+
+
+
+ )
+}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 00000000..e67c0bdc
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,26 @@
+import type { Metadata } from "next"
+import { App } from "@/components/App"
+import { HomeContent } from "@/components/HomeContent"
+import { MetaBuilder, buildMetadata } from "@/lib/meta"
+import { getHomeCards } from "@/remote/api"
+
+export const revalidate = 30
+
+export async function generateMetadata(): Promise {
+ const title = "Шлиссельбургская ДХШ"
+ const m = new MetaBuilder()
+ .setTitle(title)
+ .setData({ url: "/" })
+ .build()
+ return buildMetadata(m)
+}
+
+export default async function HomePage() {
+ const items = await getHomeCards()
+
+ return (
+
+
+
+ )
+}
diff --git a/next-env.d.ts b/next-env.d.ts
index 2d5420eb..9edff1c7 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,5 @@
///
///
-///
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
diff --git a/package-lock.json b/package-lock.json
index 7503c41e..d007c390 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,6 @@
"date-fns": "^4.0.0",
"markdown-it": "^14.1.0",
"next": "^16.1.0",
- "next-seo": "^7.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-icons": "^5.5.0",
@@ -6166,15 +6165,6 @@
}
}
},
- "node_modules/next-seo": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/next-seo/-/next-seo-7.1.0.tgz",
- "integrity": "sha512-gTWm9wnAXvtqD8QPIpbBA6iMnqMp1WyNTGza9QJPabVIPoX+ZDt1/J0KUkziiIKRyad+nSGLC0BfHgekqcfq0Q==",
- "peerDependencies": {
- "next": ">=13.4.0",
- "react": ">=18.2.0"
- }
- },
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
diff --git a/package.json b/package.json
index af70817c..ddafca9a 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,6 @@
"date-fns": "^4.0.0",
"markdown-it": "^14.1.0",
"next": "^16.1.0",
- "next-seo": "^7.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-icons": "^5.5.0",
diff --git a/pages/404.tsx b/pages/404.tsx
deleted file mode 100644
index c063b5ab..00000000
--- a/pages/404.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { NextPage } from "next"
-import { Wrapper } from "@/components/Wrapper"
-import { NotFound } from "@/components/NotFound"
-import { Copyright } from "@/components/Copyright"
-import { yearRange } from "@/const"
-import { Spacer } from "@/components/Spacer"
-import { Box } from "@/ui/Box"
-
-const Index: NextPage = () => (
-
-
-
-
-
- )}
- >
-
-
-
-
-
-)
-
-export default Index
diff --git a/pages/[...slug].tsx b/pages/[...slug].tsx
deleted file mode 100644
index 5e5d1eb9..00000000
--- a/pages/[...slug].tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-import Head from "next/head"
-import { App } from "@/components/App"
-import { Page } from "@/components/Page"
-import { generateNextSeo } from "next-seo/pages"
-import { MetaBuilder } from "@/lib/meta"
-import type { GetStaticProps, NextPage } from "next"
-import type { BreadcrumbPart, MenuItem, Meta, Tag, Sign, Token } from "@/types"
-import { Html } from "@/components/Html"
-import { Youtube } from "@/components/Youtube"
-import { paramsToSlug } from "@/remote/lib"
-import { PageGrid } from "@/components/PageGrid"
-import { useRouter } from "next/router"
-import { FileCard } from "@/components/FileCard"
-import { Picture } from "@/ui/Picture"
-import { tail } from "@/lib/array"
-import { getMenu, getPageBySlug, getUrls } from "@/remote/api"
-
-type Props = {
- title: string
- tags: Tag[]
- date: string
- breadcrumb: BreadcrumbPart[]
- meta?: Meta
- menu: MenuItem[],
- tokens: Token[]
- documentSignature: Sign
-}
-
-const Index: NextPage = props => {
- const router = useRouter()
- if (router.isFallback) {
- return (
-
- loading...
-
- )
- }
-
- return (
- <>
-
- {!props.meta ? null : generateNextSeo({
- title: props.meta.title,
- description: props.meta.description,
- canonical: "https://art.shlisselburg.org/",
- openGraph: {
- url: props.meta.url,
- title: props.meta.title,
- description: props.meta.description,
- images: [
- {
- url: props.meta.image,
- width: props.meta.imageWidth,
- height: props.meta.imageHeight,
- alt: props.meta.description,
- type: "image/jpeg",
- },
- ],
- siteName: props.meta.siteName,
- },
- })}
- {props.title}
-
-
-
-
-
-
- {props.tokens.map((x, i) => {
- switch (x.token) {
- case "html":
- return (
-
- )
-
- case "instagram":
- return (
-
- )
-
- case "youtube":
- return (
-
- )
-
- case "image":
- return (
-
- )
-
- case "file":
- return (
-
- )
-
- case "grid":
- return (
-
- )
-
- default:
- return (
-
- {JSON.stringify(x)}
-
- )
- }
- })}
-
-
-
- >
- )
-}
-
-export const getStaticProps: GetStaticProps = async ctx => {
- const slug = paramsToSlug(ctx.params?.slug ?? [])
- const page = await getPageBySlug(slug)
- if (!page) {
- return {
- notFound: true,
- }
- }
-
- const menu = await getMenu()
-
- const description = page.description ?? undefined
- const breadcrumbSize = page?.breadcrumb?.length ?? 0
- const breadcrumb = breadcrumbSize < 2 ? [] : page.breadcrumb ?? []
- const meta = (new MetaBuilder())
- .setImage(page.cover)
- .setTitle(page.title)
- .setDescription(description)
- .build()
- const tokens = page.tokens
-
- return {
- revalidate: 30,
- props: {
- tokens,
- title: page.title,
- tags: page.tags,
- date: page.date,
- meta,
- menu,
- breadcrumb,
- documentSignature: {
- date: "20.01.2021г.",
- person: "Тимашева Марина Геннадьевна",
- position: "Директор",
- signature: "0ac4ea89753a4ba9893799442325fb41",
- },
- },
- }
-}
-
-export const getStaticPaths = async () => {
- const urls = await getUrls()
-
- return {
- fallback: true,
- paths: urls
- .map(path => {
- const slug = tail(path.split("/"))
-
- return {
- params: {
- slug,
- },
- }
- }),
- }
-}
-
-export default Index
diff --git a/pages/_app.tsx b/pages/_app.tsx
deleted file mode 100644
index 74481d6f..00000000
--- a/pages/_app.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import "src/style/style.css"
-import "src/style/content.css"
-import "src/style/article.css"
-import "src/style/marker/marker-table.css"
-import "src/style/kazimir/video.css"
-
-import type { AppProps } from "next/app"
-import Head from "next/head"
-import Script from "next/script"
-import { YMetrika } from "@/components/YMetrika"
-import { ThemeColor } from "@/ui/ThemeColor"
-
-export default function MyApp({ Component, pageProps }: AppProps) {
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {process.env.NEXT_PUBLIC_YMETRIKA_ACCOUNT && (
-
- )}
-
-
-
-
-
-
- >
- )
-}
diff --git a/pages/_document.tsx b/pages/_document.tsx
deleted file mode 100644
index a9f85883..00000000
--- a/pages/_document.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Html, Head, Main, NextScript } from "next/document"
-
-export default function Document() {
- return (
-
-
- {/* Google Fonts */}
-
-
-
-
-
-
-
-
- )
-}
diff --git a/pages/index.tsx b/pages/index.tsx
deleted file mode 100644
index b27803c4..00000000
--- a/pages/index.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import Head from "next/head"
-import { App } from "@/components/App"
-import { generateNextSeo } from "next-seo/pages"
-import { PageGrid } from "@/components/PageGrid"
-import { HudozkaTitle } from "@/components/HudozkaTitle"
-import { MetaBuilder } from "@/lib/meta"
-import type { GetStaticProps, NextPage } from "next"
-import type { MenuItem, Meta, PageCardDto } from "@/types"
-import { useMobile } from "@/hooks/useMobile"
-import { getHomeCards, getMenu } from "@/remote/api"
-
-type Props = {
- title: string
- meta: Meta
- menu: MenuItem[]
- items: PageCardDto[]
-}
-
-const Index: NextPage = props => {
- const mobile = useMobile()
-
- return (
- <>
-
- {generateNextSeo({
- title: props.meta.title,
- description: props.meta.description,
- canonical: "https://art.shlisselburg.org/",
- openGraph: {
- url: props.meta.url,
- title: props.meta.title,
- description: props.meta.description,
- images: [
- {
- url: props.meta.image,
- width: props.meta.imageWidth,
- height: props.meta.imageHeight,
- alt: props.meta.description,
- type: "image/jpeg",
- },
- ],
- siteName: props.meta.siteName,
- },
- })}
- {props.title}
-
-
- {mobile ? null : (
-
- )}
-
-
-
- >
- )
-}
-
-export const getStaticProps: GetStaticProps = async () => {
- const items = await getHomeCards()
- const menu = await getMenu()
-
- const title = "Шлиссельбургская ДХШ"
- const meta = (new MetaBuilder())
- .setTitle(title)
- .setData({
- url: "/",
- })
- .build()
-
- return {
- props: {
- items,
- title,
- meta,
- menu,
- },
- revalidate: 30,
- }
-}
-
-export default Index
diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx
index e3843644..54697463 100644
--- a/src/components/App/index.tsx
+++ b/src/components/App/index.tsx
@@ -1,12 +1,15 @@
+"use client"
+
import s from "./app.module.css"
+import { useContext } from "react"
import { Wrapper } from "../Wrapper"
import { MenuContext } from "@/context/MenuContext"
import { Footer } from "../Footer"
import { Content } from "../Content"
import { Breadcrumbs } from "../Breadcrumbs"
-import type { BreadcrumbPart, MenuItem } from "@/types"
-import { useRouter } from "next/router"
+import type { BreadcrumbPart } from "@/types"
+import { usePathname } from "next/navigation"
import { useMobile } from "@/hooks/useMobile"
import { Navigation } from "@/components/Navigation"
import { Box } from "@/ui/Box"
@@ -24,7 +27,7 @@ import Link from "next/link"
import AccessibilityPanel from "../AccessibilityPanel"
import useAccessibility from "@/hooks/useAccessibility"
-const Gosuslugi = dynamic(import("../Gosuslugi").then(module => module.Gosuslugi), {
+const Gosuslugi = dynamic(() => import("../Gosuslugi").then(module => ({ default: module.Gosuslugi })), {
ssr: false,
})
@@ -32,103 +35,101 @@ export type AppProps = {
showAuthor?: boolean
contentStyle?: React.CSSProperties
breadcrumbs?: BreadcrumbPart[]
- menu: MenuItem[]
children?: React.ReactNode
}
-export function App({ showAuthor = false, menu, ...props }: AppProps) {
+export function App({ showAuthor = false, ...props }: AppProps) {
const hideBreadcrumbs = useMobile()
const accessibility = useAccessibility()
- const router = useRouter()
+ const pathname = usePathname()
+ const menu = useContext(MenuContext)
return (
-
-
- {!accessibility ? null : (
-
- )}
+
+ {!accessibility ? null : (
+
+ )}
-
-
-
- {hideBreadcrumbs || !props.breadcrumbs ? null : (
-
-
-
-
-
- )}
-
- )}
- footer={(
-
-
+
+ -
+
+ Независимая оценка качества оказания услуг
+
+
+
+ -
+
+ Анкета Министерства культуры РФ
+
+
+
+ -
+
+ Анкета получателей образовательных услуг ДОП Кировского муниципального района Ленинградской области 2025
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!showAuthor ? null : (
+ <>
+
+
+ >
+ )}
+
+
+ )}
+ >
+
+
+ {props.children}
+
+
+
)
}
diff --git a/src/components/FileCard/index.tsx b/src/components/FileCard/index.tsx
index 35a1e566..a7302a03 100644
--- a/src/components/FileCard/index.tsx
+++ b/src/components/FileCard/index.tsx
@@ -8,7 +8,7 @@ import { getResizedUrl } from "@/lib/image"
import Link from "next/link"
import { Box } from "@/ui/Box"
-const Signature = dynamic(import("./Signature").then(m => m.Signature), {
+const Signature = dynamic(() => import("./Signature").then(m => ({ default: m.Signature })), {
ssr: false,
})
diff --git a/src/components/HomeContent/index.tsx b/src/components/HomeContent/index.tsx
new file mode 100644
index 00000000..70a67c2f
--- /dev/null
+++ b/src/components/HomeContent/index.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import { HudozkaTitle } from "@/components/HudozkaTitle"
+import { PageGrid } from "@/components/PageGrid"
+import { useMobile } from "@/hooks/useMobile"
+import type { PageCardDto } from "@/types"
+
+export type HomeContentProps = {
+ items: PageCardDto[]
+}
+
+export function HomeContent({ items }: HomeContentProps) {
+ const mobile = useMobile()
+
+ return (
+ <>
+ {mobile ? null : (
+
+ )}
+
+
+ >
+ )
+}
diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx
index 5ab8017a..225b5c82 100644
--- a/src/components/Menu/index.tsx
+++ b/src/components/Menu/index.tsx
@@ -1,9 +1,11 @@
+"use client"
+
import s from "./styles.module.css"
import type { MenuItem } from "@/types"
import * as NavigationMenu from "@radix-ui/react-navigation-menu"
import classnames from "classnames/bind"
-import { useRouter } from "next/router"
+import { usePathname } from "next/navigation"
import { ActiveLink } from "./ActiveLink"
let cx = classnames.bind(s)
@@ -15,13 +17,13 @@ export type MenuProps = {
}
export const Menu: React.FC = ({ vertical = false, onItemClick, items }) => {
- const router = useRouter()
+ const pathname = usePathname()
return (
{
items.map(item => {
- const current = router.asPath === item.href
+ const current = pathname === item.href
const href = current
? undefined
: item.href
diff --git a/src/components/Navigation/MobileNavigation.tsx b/src/components/Navigation/MobileNavigation.tsx
index 40e7e6ac..8d3d9d9b 100644
--- a/src/components/Navigation/MobileNavigation.tsx
+++ b/src/components/Navigation/MobileNavigation.tsx
@@ -1,3 +1,5 @@
+"use client"
+
import s from "./nav.module.css"
import { Menu } from "../Menu"
@@ -8,7 +10,7 @@ import { Spacer } from "../Spacer"
import HudozkaLogo from "../HudozkaLogo"
import { HudozkaTitle } from "../HudozkaTitle"
import Link from "next/link"
-import { useRouter } from "next/router"
+import { usePathname } from "next/navigation"
import { HiOutlineMenuAlt4, HiOutlineX } from "react-icons/hi"
export type MobileNavigationProps = {
@@ -19,7 +21,7 @@ export type MobileNavigationProps = {
export function MobileNavigation({ className, style }: MobileNavigationProps) {
const menu = useContext(MenuContext)
const detailsRef = useRef(null)
- const router = useRouter()
+ const pathname = usePathname()
const close = useCallback(() => {
if (detailsRef.current) {
@@ -28,11 +30,8 @@ export function MobileNavigation({ className, style }: MobileNavigationProps) {
}, [])
useEffect(() => {
- router.events.on("routeChangeStart", close)
- return () => {
- router.events.off("routeChangeStart", close)
- }
- }, [router.events, close])
+ close()
+ }, [pathname, close])
return (