diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bffb357a..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..69ef012c --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - run: pnpm run lint + + - run: pnpm run fmt:check diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..2312dc58 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000..e07187a5 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": ["pnpm-lock.yaml"], + "semi": false, + "singleQuote": true, + "tabWidth": 2 +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000..4a0d14d5 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,37 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "nextjs", "typescript"], + "options": { + "typeAware": true, + "typeCheck": true + }, + "categories": { + "correctness": "error", + "suspicious": "warn" + }, + "rules": { + "eslint/no-unused-vars": "error", + "eslint/no-unused-expressions": "error", + "eslint/eqeqeq": "error", + "eslint/no-underscore-dangle": ["warn", { "allow": ["_embedded", "_links", "_fields"] }], + "eslint/no-shadow": "warn", + + "nextjs/no-img-element": "error", + "nextjs/no-html-link-for-pages": "error", + + "react/react-in-jsx-scope": "off", + + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + + "typescript/no-floating-promises": "error", + "typescript/no-misused-promises": "error", + "typescript/await-thenable": "error", + "typescript/no-explicit-any": "warn", + "typescript/use-unknown-in-catch-callback-variable": "error", + "typescript/prefer-optional-chain": "warn", + "typescript/prefer-nullish-coalescing": "off", + "typescript/no-unsafe-type-assertion": "off", + "typescript/consistent-return": "warn" + } +} diff --git a/CLAUDE.md b/CLAUDE.md index c84efc52..e8a2a324 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,33 +3,42 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Build Commands + - `npm run dev` - Start development server with turbo mode - `npm run build` - Build for production -- `npm run start` - Start production server -- `npm run lint` - Run ESLint to check code quality +- `npm run start` - Start production server +- `npm run lint` - Run oxlint with type-aware rules and type-check (tsgolint) +- `npm run lint:fix` - Auto-fix oxlint issues where possible +- `npm run typecheck` - Type-check via oxlint/tsgolint (same as `lint`) +- `npm run fmt` - Format code with oxfmt (TS/TSX/CSS/JSON/MD/YAML) +- `npm run fmt:check` - Check formatting without writing files ## Architecture Overview This is a headless WordPress starter using Next.js 15 App Router with TypeScript. Key architectural patterns: ### Data Layer + - All WordPress API interactions go through `lib/wordpress.ts` - Type definitions in `lib/wordpress.d.ts` define Post, Page, Category, Tag, Author, Media interfaces - Error handling uses custom `WordPressAPIError` class - Functions use Next.js cache tags for granular revalidation (e.g., `tags: ['posts', `post-${slug}`]`) ### Routing Structure + - Dynamic routes: `/posts/[slug]`, `/pages/[slug]` - Archive pages: `/posts`, `/posts/authors`, `/posts/categories`, `/posts/tags` - API routes: `/api/revalidate` (webhook), `/api/og` (OG images) ### Component Patterns + - Server Components for data fetching with parallel `Promise.all()` calls - URL-based state management for search and filters - Debounced search (300ms) with `useSearchParams` - Pagination with 9 posts per page default ### Revalidation System + - WordPress plugin sends webhooks on content changes - Next.js endpoint validates webhook secret and calls `revalidateTag()` - Default cache duration: 1 hour (`revalidate: 3600`) @@ -37,34 +46,41 @@ This is a headless WordPress starter using Next.js 15 App Router with TypeScript ## Code Style ### TypeScript + - Use strict typing with interfaces defined in `lib/wordpress.d.ts` - Prefer type annotations over type assertions - Use type inference when the type is obvious ### Naming Conventions + - React components: PascalCase (e.g., `PostCard.tsx`) - Functions and variables: camelCase - Types and interfaces: PascalCase - Constants: UPPERCASE_SNAKE_CASE for true constants ### File Structure + - Page components: `/app/**/*.tsx` -- Reusable UI components: `/components/**/*.tsx` +- Reusable UI components: `/components/**/*.tsx` - API and utility functions: `/lib/**/*.ts` - WordPress data functions must use cache tags for proper revalidation ### Error Handling + - Use `try/catch` blocks for API calls - Utilize `WordPressAPIError` class for consistent API error handling ## Environment Variables + Required environment variables (see `.env.example`): + - `NEXT_PUBLIC_WORDPRESS_URL` - Full URL of WordPress site - `NEXT_WORDPRESS_WEBHOOK_SECRET` - Secret for webhook validation ## Key Dependencies + - Next.js 15.3.3 with React 19.1.0 - TypeScript with strict mode - Tailwind CSS with shadcn/ui components - React Hook Form for form handling -- Lucide React for icons \ No newline at end of file +- Lucide React for icons diff --git a/README.md b/README.md index f024eda6..10f34c8a 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,24 @@ This is Next.js application that fetches data from a WordPress site using the Wo ## Make usage - * `make build` - Build the Docker container - * `make build-dev` - Build the development Docker container - * `make up` - Run the production Docker container - * `make up-dev` - Run the development Docker container - * `make build-up` - Build and run the Docker container - * `make build-up-dev` - Build and run the development Docker container - * `make start` - Start the development Docker container" - * `make start-dev` - Start the development Docker container" - * `make stop` - Stop the Docker container" - * `make stop-dev` - Stop the development Docker container" - * `make down` - Stop and remove the development Docker container" - * `make down-dev` - Stop and remove the development Docker container" - * `make restart` - Restart the Docker container" - * `make restart-dev` - Restart the development Docker container" - * `make logs` - Show container logs" - * `make logs-dev` - Show development container logs" - - To start dev server without docker - `npm run dev` or `npm run dev-ssl`(with https) - +- `make build` - Build the Docker container +- `make build-dev` - Build the development Docker container +- `make up` - Run the production Docker container +- `make up-dev` - Run the development Docker container +- `make build-up` - Build and run the Docker container +- `make build-up-dev` - Build and run the development Docker container +- `make start` - Start the development Docker container" +- `make start-dev` - Start the development Docker container" +- `make stop` - Stop the Docker container" +- `make stop-dev` - Stop the development Docker container" +- `make down` - Stop and remove the development Docker container" +- `make down-dev` - Stop and remove the development Docker container" +- `make restart` - Restart the Docker container" +- `make restart-dev` - Restart the development Docker container" +- `make logs` - Show container logs" +- `make logs-dev` - Show development container logs" + +To start dev server without docker - `npm run dev` or `npm run dev-ssl`(with https) ## Table of Contents @@ -96,14 +95,14 @@ The `lib/wordpress.ts` file contains a comprehensive set of functions for intera // Default fetch options for all WordPress API calls const defaultFetchOptions = { next: { - tags: ["wordpress"], + tags: ['wordpress'], revalidate: 3600, // 1 hour cache }, headers: { - Accept: "application/json", - "Content-Type": "application/json", + Accept: 'application/json', + 'Content-Type': 'application/json', }, -}; +} ``` ### Available Functions @@ -158,8 +157,8 @@ class WordPressAPIError extends Error { public status: number, public endpoint: string, ) { - super(message); - this.name = "WordPressAPIError"; + super(message) + this.name = 'WordPressAPIError' } } ``` @@ -184,15 +183,15 @@ Each function supports Next.js 15's cache tags for efficient revalidation: try { // Fetch posts with filtering const posts = await getAllPosts({ - author: "123", - category: "news", - tag: "featured", - }); + author: '123', + category: 'news', + tag: 'featured', + }) // Handle errors properly } catch (error) { if (error instanceof WordPressAPIError) { - console.error(`API Error: ${error.message} (${error.status})`); + console.error(`API Error: ${error.message} (${error.status})`) } } ``` @@ -210,13 +209,13 @@ Instead of fetching all posts and paginating client-side, the `getPostsPaginated ```typescript // Fetch page 2 with 10 posts per page const response = await getPostsPaginated(2, 10, { - author: "123", - category: "news", - search: "nextjs" -}); + author: '123', + category: 'news', + search: 'nextjs', +}) -const { data: posts, headers } = response; -const { total, totalPages } = headers; +const { data: posts, headers } = response +const { total, totalPages } = headers ``` ### Pagination Response Structure @@ -225,11 +224,11 @@ The `getPostsPaginated` function returns a `WordPressResponse` object: ```typescript interface WordPressResponse { - data: T; // The actual posts array + data: T // The actual posts array headers: { - total: number; // Total number of posts matching the query - totalPages: number; // Total number of pages - }; + total: number // Total number of posts matching the query + totalPages: number // Total number of pages + } } ``` @@ -247,15 +246,18 @@ For existing implementations using `getAllPosts`, you can migrate to the more ef ```typescript // Before: Client-side pagination -const allPosts = await getAllPosts({ author, category }); -const page = 1; -const postsPerPage = 9; -const paginatedPosts = allPosts.slice((page - 1) * postsPerPage, page * postsPerPage); -const totalPages = Math.ceil(allPosts.length / postsPerPage); +const allPosts = await getAllPosts({ author, category }) +const page = 1 +const postsPerPage = 9 +const paginatedPosts = allPosts.slice((page - 1) * postsPerPage, page * postsPerPage) +const totalPages = Math.ceil(allPosts.length / postsPerPage) // After: Server-side pagination -const { data: posts, headers } = await getPostsPaginated(page, postsPerPage, { author, category }); -const { total, totalPages } = headers; +const { data: posts, headers } = await getPostsPaginated(page, postsPerPage, { + author, + category, +}) +const { total, totalPages } = headers ``` ### Example Implementation @@ -307,7 +309,7 @@ The pagination system includes sophisticated cache tags for optimal performance: ```typescript // Dynamic cache tags based on query parameters -["wordpress", "posts", "posts-page-1", "posts-category-123"] +;['wordpress', 'posts', 'posts-page-1', 'posts-category-123'] ``` This ensures that when content changes, only the relevant pagination pages are revalidated, maintaining excellent performance even with large content sets. @@ -318,17 +320,17 @@ The `lib/wordpress.d.ts` file contains comprehensive TypeScript type definitions ```typescript interface WPEntity { - id: number; - date: string; - date_gmt: string; - modified: string; - modified_gmt: string; - slug: string; - status: "publish" | "future" | "draft" | "pending" | "private"; - link: string; + id: number + date: string + date_gmt: string + modified: string + modified_gmt: string + slug: string + status: 'publish' | 'future' | 'draft' | 'pending' | 'private' + link: string guid: { - rendered: string; - }; + rendered: string + } } ``` @@ -358,15 +360,15 @@ Key type definitions include: ```typescript interface FilterBarProps { - authors: Author[]; - tags: Tag[]; - categories: Category[]; - selectedAuthor?: Author["id"]; - selectedTag?: Tag["id"]; - selectedCategory?: Category["id"]; - onAuthorChange?: (authorId: Author["id"] | undefined) => void; - onTagChange?: (tagId: Tag["id"] | undefined) => void; - onCategoryChange?: (categoryId: Category["id"] | undefined) => void; + authors: Author[] + tags: Tag[] + categories: Category[] + selectedAuthor?: Author['id'] + selectedTag?: Tag['id'] + selectedCategory?: Category['id'] + onAuthorChange?: (authorId: Author['id'] | undefined) => void + onTagChange?: (tagId: Tag['id'] | undefined) => void + onCategoryChange?: (categoryId: Category['id'] | undefined) => void } ``` @@ -374,18 +376,18 @@ interface FilterBarProps { ```typescript interface MediaDetails { - width: number; - height: number; - file: string; - sizes: Record; + width: number + height: number + file: string + sizes: Record } interface MediaSize { - file: string; - width: number; - height: number; - mime_type: string; - source_url: string; + file: string + width: number + height: number + mime_type: string + source_url: string } ``` @@ -475,13 +477,11 @@ Features: The search system is implemented across several layers: 1. **Client-Side Component** (`search-input.tsx`): - - Uses Next.js App Router's URL handling - Debounced input for better performance - Maintains search state in URL parameters 2. **Server-Side Processing** (`page.tsx`): - - Handles search parameters server-side - Combines search with other filters - Parallel data fetching for better performance @@ -518,8 +518,8 @@ searchAuthors(query: string) ```typescript // In your page component -const { search } = await searchParams; -const posts = search ? await getAllPosts({ search }) : await getAllPosts(); +const { search } = await searchParams +const posts = search ? await getAllPosts({ search }) : await getAllPosts() ``` The search functionality automatically updates filters and results as you type, providing a smooth user experience while maintaining good performance through debouncing and server-side rendering. @@ -564,15 +564,18 @@ This starter implements an intelligent caching and revalidation system using Nex The WordPress API functions use a sophisticated hierarchical cache tag system for granular revalidation: #### Global Tags + - `wordpress` - Affects all WordPress content #### Content Type Tags + - `posts` - All post content - `categories` - All category content - `tags` - All tag content - `authors` - All author content #### Pagination-Specific Tags + - `posts-page-1`, `posts-page-2`, etc. - Individual pagination pages - `posts-search` - Search result pages - `posts-author-123` - Posts filtered by specific author @@ -580,6 +583,7 @@ The WordPress API functions use a sophisticated hierarchical cache tag system fo - `posts-tag-789` - Posts filtered by specific tag #### Individual Item Tags + - `post-123` - Specific post content - `category-456` - Specific category content - `tag-789` - Specific tag content @@ -590,7 +594,6 @@ This granular system ensures that when content changes, only the relevant cached ### Automatic Revalidation 1. **Install the WordPress Plugin:** - - Navigate to the `/plugin` directory - Use the pre-built `next-revalidate.zip` file or create a ZIP from the `next-revalidate` folder - Install and activate through WordPress admin @@ -598,7 +601,6 @@ This granular system ensures that when content changes, only the relevant cached - Configure your Next.js URL and webhook secret 2. **Configure Next.js:** - - Add `NEXT_WORDPRESS_WEBHOOK_SECRET` to your environment variables (same secret as in WordPress plugin) - The webhook endpoint at `/api/revalidate` is already set up - No additional configuration needed @@ -628,25 +630,25 @@ The Next.js Revalidation plugin includes: You can manually revalidate content using Next.js cache functions: ```typescript -import { revalidateTag } from "next/cache"; +import { revalidateTag } from 'next/cache' // Revalidate all WordPress content -revalidateTag("wordpress"); +revalidateTag('wordpress') // Revalidate specific content types -revalidateTag("posts"); -revalidateTag("categories"); -revalidateTag("tags"); -revalidateTag("authors"); +revalidateTag('posts') +revalidateTag('categories') +revalidateTag('tags') +revalidateTag('authors') // Revalidate specific items -revalidateTag("post-123"); -revalidateTag("category-456"); +revalidateTag('post-123') +revalidateTag('category-456') // Revalidate pagination-specific content -revalidateTag("posts-page-1"); -revalidateTag("posts-category-123"); -revalidateTag("posts-search"); +revalidateTag('posts-page-1') +revalidateTag('posts-category-123') +revalidateTag('posts-search') ``` This system ensures your content stays fresh while maintaining optimal performance through intelligent caching. diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx index 36429fbc..fd90d50e 100644 --- a/app/api/og/route.tsx +++ b/app/api/og/route.tsx @@ -1,77 +1,75 @@ -import { ImageResponse } from "next/og"; -import { NextRequest } from "next/server"; +import { ImageResponse } from 'next/og' +import { NextRequest } from 'next/server' -export const runtime = "edge"; +export const runtime = 'edge' export async function GET(request: NextRequest) { try { - const { searchParams } = new URL(request.url); + const { searchParams } = new URL(request.url) // Get title and description from the URL query params - const title = searchParams.get("title"); - const description = searchParams.get("description"); + const title = searchParams.get('title') + const description = searchParams.get('description') return new ImageResponse( - ( +
+ {title} +
+ {description && (
- {title} + {description}
- {description && ( -
- {description} -
- )} -
- ), + )} + , { width: 1200, height: 630, - } - ); - } catch (e: any) { - console.log(`${e.message}`); + }, + ) + } catch (e: unknown) { + console.log(e instanceof Error ? e.message : String(e)) return new Response(`Failed to generate the image`, { status: 500, - }); + }) } } diff --git a/app/api/revalidate/route.ts b/app/api/revalidate/route.ts index a801ad79..f7bdc1b0 100644 --- a/app/api/revalidate/route.ts +++ b/app/api/revalidate/route.ts @@ -1,7 +1,9 @@ -import { revalidatePath, revalidateTag } from "next/cache"; -import { NextRequest, NextResponse } from "next/server"; +import { revalidatePath, revalidateTag } from 'next/cache' +import { NextRequest, NextResponse } from 'next/server' -export const maxDuration = 30; +export const maxDuration = 30 + +const REVALIDATE_NOW = { expire: 0 } as const /** * WordPress webhook handler for content revalidation @@ -11,100 +13,92 @@ export const maxDuration = 30; export async function POST(request: NextRequest) { try { - const requestBody = await request.json(); - const secret = request.headers.get("x-webhook-secret"); + const requestBody = await request.json() + const secret = request.headers.get('x-webhook-secret') if (secret !== process.env.NEXT_WORDPRESS_WEBHOOK_SECRET) { - console.error("Invalid webhook secret"); - return NextResponse.json( - { message: "Invalid webhook secret" }, - { status: 401 } - ); + console.error('Invalid webhook secret') + return NextResponse.json({ message: 'Invalid webhook secret' }, { status: 401 }) } - const { contentType, contentId, contentSlug } = requestBody; + const { contentType, contentId, contentSlug } = requestBody if (!contentType) { - return NextResponse.json( - { message: "Missing content type" }, - { status: 400 } - ); + return NextResponse.json({ message: 'Missing content type' }, { status: 400 }) } try { console.log( - `Revalidating content: ${contentType}${ - contentId ? ` (ID: ${contentId})` : "" - }${ - contentSlug ? ` (slug: ${contentSlug})` : "" - }` - ); + `Revalidating content: ${contentType}${contentId ? ` (ID: ${contentId})` : ''}${ + contentSlug ? ` (slug: ${contentSlug})` : '' + }`, + ) - if (contentType === "post") { - revalidateTag("posts"); - if (contentId) revalidateTag(`post-${contentId}`); - if (contentSlug) revalidateTag(`post-${contentSlug}`); - } else if (contentType === "page") { - revalidateTag("pages"); - if (contentSlug) revalidateTag(`page-${contentSlug}`); - if (contentId) revalidateTag(`page-${contentId}`); - } else if (contentType === "category") { - revalidateTag("categories"); + if (contentType === 'post') { + revalidateTag('posts', REVALIDATE_NOW) + if (contentId) revalidateTag(`post-${contentId}`, REVALIDATE_NOW) + if (contentSlug) revalidateTag(`post-${contentSlug}`, REVALIDATE_NOW) + } else if (contentType === 'page') { + revalidateTag('pages', REVALIDATE_NOW) + if (contentSlug) revalidateTag(`page-${contentSlug}`, REVALIDATE_NOW) + if (contentId) revalidateTag(`page-${contentId}`, REVALIDATE_NOW) + } else if (contentType === 'category') { + revalidateTag('categories', REVALIDATE_NOW) if (contentId) { - revalidateTag(`posts-category-${contentId}`); - revalidateTag(`category-${contentId}`); + revalidateTag(`posts-category-${contentId}`, REVALIDATE_NOW) + revalidateTag(`category-${contentId}`, REVALIDATE_NOW) } - } else if (contentType === "tag") { - revalidateTag("tags"); + } else if (contentType === 'tag') { + revalidateTag('tags', REVALIDATE_NOW) if (contentId) { - revalidateTag(`posts-tag-${contentId}`); - revalidateTag(`tag-${contentId}`); + revalidateTag(`posts-tag-${contentId}`, REVALIDATE_NOW) + revalidateTag(`tag-${contentId}`, REVALIDATE_NOW) } - } else if (contentType === "author" || contentType === "user") { - revalidateTag("authors"); + } else if (contentType === 'author' || contentType === 'user') { + revalidateTag('authors', REVALIDATE_NOW) if (contentId) { - revalidateTag(`posts-author-${contentId}`); - revalidateTag(`author-${contentId}`); + revalidateTag(`posts-author-${contentId}`, REVALIDATE_NOW) + revalidateTag(`author-${contentId}`, REVALIDATE_NOW) } - } else if (contentType === "media") { - if (contentId) revalidateTag(`media-${contentId}`); - else revalidateTag(`media`); - } else if (contentType !== "menu") { + } else if (contentType === 'media') { + if (contentId) revalidateTag(`media-${contentId}`, REVALIDATE_NOW) + else revalidateTag('media', REVALIDATE_NOW) + } else if (contentType !== 'menu') { // revalidate all wordpress requests - revalidateTag("wordpress"); + revalidateTag('wordpress', REVALIDATE_NOW) } // Also revalidate the entire layout for safety - revalidatePath("/", "layout"); + revalidatePath('/', 'layout') return NextResponse.json({ revalidated: true, message: `Revalidated ${contentType}${ - contentId ? ` (ID: ${contentId})` : "" + contentId ? ` (ID: ${contentId})` : '' } and related content`, timestamp: new Date().toISOString(), - }); + }) } catch (error) { - console.error("Error revalidating path:", error); + console.error('Error revalidating path:', error) return NextResponse.json( { revalidated: false, - message: "Failed to revalidate site", + message: 'Failed to revalidate site', error: (error as Error).message, timestamp: new Date().toISOString(), }, - { status: 500 } - ); + { status: 500 }, + ) } } catch (error) { - console.error("Revalidation error:", error); + console.error('Revalidation error:', error) return NextResponse.json( { - message: "Error revalidating content", + message: 'Error revalidating content', error: (error as Error).message, timestamp: new Date().toISOString(), }, - { status: 500 } - ); + { status: 500 }, + ) } } diff --git a/app/layout.tsx b/app/layout.tsx index 775ff5ed..3f9230a1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,52 +1,43 @@ -import "./globals.css"; +import './globals.css' -import { Section, Container } from "@/components/craft"; -import { Inter as FontSans } from "next/font/google"; -import { ThemeProvider } from "@/components/theme/theme-provider"; -import { ThemeToggle } from "@/components/theme/theme-toggle"; -import { MobileNav } from "@/components/nav/mobile-nav"; -import { Analytics } from "@vercel/analytics/react"; -import { Button } from "@/components/ui/button"; +import { Section, Container } from '@/components/craft' +import { Inter as FontSans } from 'next/font/google' +import { ThemeProvider } from '@/components/theme/theme-provider' +import { ThemeToggle } from '@/components/theme/theme-toggle' +import { MobileNav } from '@/components/nav/mobile-nav' +import { Analytics } from '@vercel/analytics/react' +import { Button } from '@/components/ui/button' -import { mainMenu, contentMenu } from "@/menu.config"; -import { siteConfig } from "@/site.config"; -import { cn } from "@/lib/utils"; +import { mainMenu, contentMenu } from '@/menu.config' +import { siteConfig } from '@/site.config' +import { cn } from '@/lib/utils' -import Balancer from "react-wrap-balancer"; -import Logo from "@/public/logo.svg"; -import Image from "next/image"; -import Link from "next/link"; +import Balancer from 'react-wrap-balancer' +import Link from 'next/link' -import type { Metadata } from "next"; +import type { Metadata } from 'next' const font = FontSans({ - subsets: ["latin"], - variable: "--font-sans", -}); + subsets: ['latin'], + variable: '--font-sans', +}) export const metadata: Metadata = { - title: "Системный Блокъ - Онлайн-журнал о влиянии цифровых технологий на культуру, человека и общество", - description: "Онлайн-журнал о влиянии цифровых технологий на культуру, человека и общество", + title: + 'Системный Блокъ - Онлайн-журнал о влиянии цифровых технологий на культуру, человека и общество', + description: 'Онлайн-журнал о влиянии цифровых технологий на культуру, человека и общество', metadataBase: new URL(siteConfig.site_domain), alternates: { - canonical: "/", + canonical: '/', }, -}; +} -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - + +