Skip to content

Commit

Permalink
feat(app): seo
Browse files Browse the repository at this point in the history
  • Loading branch information
lovrozagar committed Dec 17, 2024
1 parent f0ceed0 commit 4ec340b
Show file tree
Hide file tree
Showing 20 changed files with 320 additions and 7 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_BASE_URL='https://next-starter.com'
Binary file modified bun.lockb
Binary file not shown.
15 changes: 11 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"nextjs-toploader": "^1.6.12",
"react": "^18",
"react-dom": "^18",
"superstruct": "^2.0.2",
"schema-dts": "^1.1.2",
"zod": "^3.23.8",
"zustand": "^4.5.4"
},
"devDependencies": {
Expand All @@ -31,7 +32,7 @@
"rimraf": "^6.0.1",
"semantic-release": "^24.2.0",
"tailwindcss": "^3.4.1",
"turbo": "^2.0.9",
"turbo": "latest",
"typescript": "^5.5.4",
"typescript-coverage-report": "^1.0.0"
},
Expand All @@ -40,6 +41,7 @@
"private": true,
"scripts": {
"alias:load": "source local-aliases.sh",
"analyze": "turbo analyze",
"build": "next build",
"bundlesize:check": "bunx bundlewatch --config bundlewatch.config.json",
"clear": "rimraf .next .turbo node_modules",
Expand All @@ -61,9 +63,14 @@
"start": "next start",
"type:check": "tsc --noEmit",
"type:coverage": "typescript-coverage-report",
"update:check": "bun x npm-check-updates"
"update:check": "bunx npm-check-updates",
"update:install": "bunx npm-check-updates --deep -u && bun install"
},
"sideEffects": false,
"type": "module",
"version": "0.0.1"
"version": "0.0.1",
"workspaces": [
"apps/*",
"packages/*"
]
}
7 changes: 7 additions & 0 deletions src/app/layout.tsx → src/app/(route)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ROOT_LAYOUT_METADATA } from "@/app/(route)/seo/metadata"
import { TopLoaderDynamic } from "@/components/top-loader"
import "@/style/globals.css"

Expand All @@ -20,4 +21,10 @@ function RootLayout(props: RootLayoutProps) {
)
}

function generateMetadata() {
return ROOT_LAYOUT_METADATA
}

export { generateMetadata }

export default RootLayout
File renamed without changes.
11 changes: 11 additions & 0 deletions src/app/(route)/seo/json-ld.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Product, WithContext } from "schema-dts"

const jsonLd: WithContext<Product> = {
"@context": "https://schema.org",
"@type": "Product",
name: "Next.js Sticker",
image: "https://nextjs.org/imgs/sticker.png",
description: "Dynamic at the speed of static.",
}

export { jsonLd }
61 changes: 61 additions & 0 deletions src/app/(route)/seo/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { env } from "@/env/env.mjs"
import type { Metadata } from "next"

const ROOT_LAYOUT_METADATA: Metadata = {
alternates: {
canonical: "/",
},
// description: t("meta.recommend.description"),
icons: {
apple: {
sizes: "180x180",
url: "/favicon/apple-touch-icon.png",
},
icon: [
{
sizes: "16x16",
type: "image/png",
url: "/favicon/favicon-16x16.png",
},
{
sizes: "32x32",
type: "image/png",
url: "/favicon/favicon-32x32.png",
},
],
other: {
rel: "mask-icon",
url: "/favicon/safari-pinned-tab.svg",
},
shortcut: "/favicon/favicon.ico",
},
manifest: "/site.webmanifest",
metadataBase: new URL(env.NEXT_PUBLIC_BASE_URL),
// openGraph: {
// description: t("meta.recommend.description"),
// images: [
// {
// height: 635,
// url: `${envClient.NEXT_PUBLIC_FRONTEND_URL}/images/common/OpenGraph.png`,
// width: 1200,
// },
// ],
// locale: "en",
// siteName: "Recommend",
// title: t("meta.recommend.title"),
// type: "website",
// url: `/${removeTrailingSlash(pathname || "")}`,
// },
// other: {
// google: "notranslate",
// },
// title: {
// default: t("meta.recommend.title"),
// template: "%s | Recommend",
// },
twitter: {
card: "summary_large_image",
},
}

export { ROOT_LAYOUT_METADATA }
22 changes: 22 additions & 0 deletions src/app/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { MetadataRoute } from "next"

function manifest() {
return {
name: "next-starter",
short_name: "next-starter",
description: "next-starter",
start_url: "/",
display: "standalone",
background_color: "#fff",
theme_color: "#fff",
icons: [
{
src: "/favicon.ico",
sizes: "any",
type: "image/x-icon",
},
],
} satisfies MetadataRoute.Manifest
}

export default manifest
14 changes: 14 additions & 0 deletions src/app/robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { MetadataRoute } from "next"

/* generate a robots.txt static file */
function robots() {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://next-starter/sitemap.xml",
} satisfies MetadataRoute.Robots
}

export default robots
15 changes: 15 additions & 0 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { MetadataRoute } from "next"

/* generate a sitemap.xml static file */
function sitemap() {
return [
{
url: "https://next-starter.com",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1,
},
] satisfies MetadataRoute.Sitemap
}

export default sitemap
6 changes: 6 additions & 0 deletions src/apps/web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"dependencies": {
"next": "latest",
"@repo/ui": "*"
}
}
4 changes: 4 additions & 0 deletions src/apps/web/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../../../tsconfig.json",
"include": ["src"]
}
95 changes: 95 additions & 0 deletions src/components/head/head.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { ReactNode } from "react"

type HeadProps = {
children: ReactNode
seo: {
title: string
description: string
image: string
keywords: string
ogType: string
ogImage?: string
ogTitle?: string
ogDescription?: string
canonical: string
robots: "index, follow" | "noindex, follow" | "all"
twitterImage?: string
twitterTitle?: string
twitterDescription?: string
twitterCardType?: "summary" | "summary_large_image" | "app" | "player"
twitterSite?: string | null
author?: string
noSiteLinksSearchbox?: boolean
noTranslate?: boolean
applicationName?: string
publisher?: string
generator?: string
themeColor?: string
referrer?:
| "no-referrer"
| "origin"
| "origin-when-cross-origin"
| "strict-origin"
| "strict-origin-when-cross-origin"
| "unsafe-url"
}
}

const Head = (props: HeadProps) => {
const { children, seo } = props

const {
title,
description,
image,
keywords,
robots,
canonical,
author,
ogType,
twitterSite,
applicationName,
publisher,
generator,
referrer,
ogImage = image,
ogTitle = title,
ogDescription = description,
twitterImage = image,
twitterTitle = title,
twitterDescription = description,
noTranslate = false,
noSiteLinksSearchbox = false,
twitterCardType = "summary_large_image",
} = seo

return (
<>
<title>{title}</title>
<meta name="description" content={description} />
<meta name="keywords" content={keywords} />
<meta property="og:title" content={ogTitle} />
<meta property="og:description" content={ogDescription} />
<meta property="og:image" content={ogImage} />
<meta property="og:url" content={canonical} />
<meta property="og:type" content={ogType} />
<meta name="robots" content={robots} />
<link rel="canonical" href={canonical} />
<meta name="twitter:card" content={twitterCardType} />
<meta name="twitter:title" content={twitterTitle} />
<meta name="twitter:description" content={twitterDescription} />
<meta name="twitter:image" content={twitterImage} />
{twitterSite ? <meta name="twitter:site" content={twitterSite} /> : null}
{author ? <meta name="author" content={author} /> : null}
{applicationName ? <meta name="application-name" content={applicationName} /> : null}
{publisher ? <meta name="publisher" content={publisher} /> : null}
{generator ? <meta name="generator" content={generator} /> : null}
{referrer ? <meta name="referrer" content={referrer} /> : null}
{noSiteLinksSearchbox ? <meta name="google" content="nositelinkssearchbox" /> : null}
{noTranslate ? <meta name="google" content="notranslate" /> : null}
{children}
</>
)
}

export { Head }
1 change: 1 addition & 0 deletions src/components/head/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Head } from "@/components/head/head"
9 changes: 7 additions & 2 deletions src/env/env.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { createEnv } from "@t3-oss/env-nextjs"
import { z } from "zod"

const env = createEnv({
server: {},
client: {},
runtimeEnv: {},
client: {
NEXT_PUBLIC_BASE_URL: z.string().url(),
},
runtimeEnv: {
NEXT_PUBLIC_BASE_URL: process.env["NEXT_PUBLIC_BASE_URL"],
},
})

export { env }
52 changes: 52 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { type NextRequest, NextResponse } from "next/server"

function safeURL(url: URL | string): URL | null {
try {
return new URL(url)
} catch {
return null
}
}

function verifyRequestOrigin(origin: string, allowedDomains: string[]): boolean {
if (!origin || allowedDomains.length === 0) return false
const originHost = safeURL(origin)?.host ?? null
if (!originHost) return false
for (const domain of allowedDomains) {
let host: string | null
if (domain.startsWith("http://") || domain.startsWith("https://")) {
host = safeURL(domain)?.host ?? null
} else {
host = safeURL(`https://${domain}`)?.host ?? null
}
if (originHost === host) return true
}
return false
}

/* CSRF protection */
export async function middleware(request: NextRequest) {
if (request.method === "GET") {
return NextResponse.next()
}

const originHeader = request.headers.get("Origin")

/* check both `X-Forwarded-Host` and `Host` headers */
const hostHeader = request.headers.get("X-Forwarded-Host") ?? request.headers.get("Host")

if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) {
return new NextResponse(null, { status: 403 })
}

return NextResponse.next()
}

const config = {
/* Skip all non-content paths */
matcher: [
"/((?!api|_next|favicon|images|fonts|.well-known|pwa-sw.js|robots|_not-found|sitemap|site.webmanifest).*)",
],
}

export { config }
5 changes: 5 additions & 0 deletions src/packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"exports": {
".": "./src/index.ts"
}
}
3 changes: 3 additions & 0 deletions src/packages/ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const test = () => {}

export { test }
4 changes: 4 additions & 0 deletions src/packages/ui/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../../../tsconfig.json",
"include": ["src"]
}
2 changes: 1 addition & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"outputs": [".next/**", "!.next/cache/**"]
},
"check-types": {
"dependsOn": ["^check-types"]
"dependsOn": ["^type:check"]
},
"dev": {
"cache": false,
Expand Down

0 comments on commit 4ec340b

Please sign in to comment.