Skip to content

Commit 94b5bb3

Browse files
authored
Merge pull request #20 from calebephrem/main
feat: apply special classes on regex match in pages
2 parents 0efc696 + 33894d9 commit 94b5bb3

29 files changed

Lines changed: 506 additions & 69 deletions

README.md

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,36 +56,38 @@ Open [http://localhost:3000](http://localhost:3000) and you're in.
5656
## Project Structure
5757

5858
```
59-
├── 📄 .eslintrc.json
60-
├── 📄 .gitignore
59+
├── 📁 .github
60+
│ └── 📁 workflows
61+
│ └── 📄 linter.yaml
6162
├── 📁 .vscode
6263
│ └── 📄 settings.json
63-
├── 📄 CONTRIBUTING.md
64-
├── 📄 LICENSE
65-
├── 📄 README.md
6664
├── 📁 app
6765
│ ├── 📄 globals.css
6866
│ ├── 📄 layout.tsx
67+
│ ├── 📄 not-found.tsx
6968
│ ├── 📄 page.tsx
7069
│ ├── 📁 pages
71-
│ │ ├── 📄 [slug]
70+
│ │ ├── 📁 [slug]
71+
│ │ │ ├── 📄 PageClient.tsx
72+
│ │ │ ├── 📁 [subslug]
73+
│ │ │ │ └── 📄 page.tsx
74+
│ │ │ └── 📄 page.tsx
7275
│ │ ├── 📄 layout.tsx
7376
│ │ └── 📄 page.tsx
77+
│ ├── 📁 partners
78+
│ │ └── 📄 page.tsx
7479
│ ├── 📁 resources
7580
│ │ └── 📄 page.tsx
7681
│ └── 📁 rules
7782
│ └── 📄 page.tsx
78-
├── 📄 components.json
7983
├── 📁 components
8084
│ ├── 📄 AnimatedText.tsx
8185
│ ├── 📄 Badge.tsx
86+
│ ├── 📄 BorderGlow.tsx
8287
│ ├── 📄 BorderGlowButton.tsx
8388
│ ├── 📄 Footer.tsx
84-
│ ├── 📄 GlowButton.tsx
85-
│ ├── 📄 Navbar.tsx
86-
│ ├── 📄 Section.tsx
87-
│ ├── 📄 ShinyText.tsx
88-
│ ├── 📄 TargetCursor.tsx
89+
│ ├── 📄 FuzzyText.tsx
90+
│ ...
8991
│ ├── 📁 home
9092
│ │ ├── 📄 BelongSection.tsx
9193
│ │ ├── 📄 CTASection.tsx
@@ -100,9 +102,9 @@ Open [http://localhost:3000](http://localhost:3000) and you're in.
100102
│ ├── 📄 aspect-ratio.tsx
101103
│ ├── 📄 avatar.tsx
102104
│ ├── 📄 badge.tsx
103-
├── 📄 breadcrumb.tsx
104-
├── 📄 button.tsx
105-
── ...
105+
...
106+
├── 📁 content
107+
── 📄 pages.ts
106108
├── 📁 hooks
107109
│ └── 📄 use-toast.ts
108110
├── 📁 lib
@@ -111,10 +113,16 @@ Open [http://localhost:3000](http://localhost:3000) and you're in.
111113
│ ├── 📄 redirects.config.ts
112114
│ ├── 📄 staticdata.config.ts
113115
│ └── 📄 utils.ts
116+
├── 📄 .eslintrc.json
117+
├── 📄 .gitignore
118+
├── 📄 components.json
119+
├── 📄 CONTRIBUTING.md
120+
├── 📄 LICENSE
114121
├── 📄 next.config.js
115122
├── 📄 package-lock.json
116123
├── 📄 package.json
117124
├── 📄 postcss.config.js
125+
├── 📄 README.md
118126
├── 📄 tailwind.config.ts
119127
└── 📄 tsconfig.json
120128
```
@@ -145,7 +153,7 @@ npm run format # Run Prettier
145153

146154
## Community
147155

148-
- **Discord** - [devhub.vercel.app/invite](https://devhub.vercel.app/invite)
156+
- **Discord** - [devhub.vercel.app/join](https://devhub.vercel.app/join)
149157
- **GitHub Org** - [github.com/open-devhub](https://github.com/open-devhub)
150158
- **Email** - open-devhub@outlook.com
151159

app/api/link-preview/route.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
3+
export const runtime = "nodejs";
4+
5+
function extractMeta(html: string, property: string): string | null {
6+
const patterns = [
7+
new RegExp(
8+
`<meta[^>]+(?:property|name)=["']${property}["'][^>]+content=["']([^"']*)["']`,
9+
"i",
10+
),
11+
new RegExp(
12+
`<meta[^>]+content=["']([^"']*)["'][^>]+(?:property|name)=["']${property}["']`,
13+
"i",
14+
),
15+
];
16+
for (const pattern of patterns) {
17+
const match = html.match(pattern);
18+
if (match) return match[1];
19+
}
20+
return null;
21+
}
22+
23+
function extractTitleTag(html: string): string | null {
24+
const match = html.match(/<title[^>]*>([^<]+)<\/title>/i);
25+
return match ? match[1] : null;
26+
}
27+
28+
function decodeHtmlEntities(str: string): string {
29+
return str
30+
.replace(/&amp;/g, "&")
31+
.replace(/&quot;/g, '"')
32+
.replace(/&#39;/g, "'")
33+
.replace(/&lt;/g, "<")
34+
.replace(/&gt;/g, ">");
35+
}
36+
37+
const PRIVATE_HOSTNAME_PATTERNS = [
38+
/^localhost$/i,
39+
/^0\.0\.0\.0$/,
40+
/^127\./,
41+
/^10\./,
42+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
43+
/^192\.168\./,
44+
/^169\.254\./,
45+
/^::1$/,
46+
/^fc00:/i,
47+
/^fe80:/i,
48+
];
49+
50+
function isSafeUrl(candidate: string): boolean {
51+
let parsed: URL;
52+
try {
53+
parsed = new URL(candidate);
54+
} catch {
55+
return false;
56+
}
57+
if (!["http:", "https:"].includes(parsed.protocol)) {
58+
return false;
59+
}
60+
const hostname = parsed.hostname.toLowerCase();
61+
return !PRIVATE_HOSTNAME_PATTERNS.some((pattern) => pattern.test(hostname));
62+
}
63+
64+
export async function GET(request: NextRequest) {
65+
const { searchParams } = new URL(request.url);
66+
const url = searchParams.get("url");
67+
68+
if (!url) {
69+
return NextResponse.json({ error: "missing url" }, { status: 400 });
70+
}
71+
72+
if (!isSafeUrl(url)) {
73+
return NextResponse.json(
74+
{ error: "invalid or disallowed url" },
75+
{ status: 400 },
76+
);
77+
}
78+
79+
try {
80+
const controller = new AbortController();
81+
const timeout = setTimeout(() => controller.abort(), 5000);
82+
83+
const res = await fetch(url, {
84+
signal: controller.signal,
85+
redirect: "error",
86+
headers: {
87+
"User-Agent":
88+
"Mozilla/5.0 (compatible; LinkPreviewBot/1.0; +https://example.com/bot)",
89+
},
90+
});
91+
92+
clearTimeout(timeout);
93+
94+
if (!res.ok) {
95+
return NextResponse.json({ error: "failed to fetch" }, { status: 502 });
96+
}
97+
98+
const contentLength = res.headers.get("content-length");
99+
if (contentLength && parseInt(contentLength, 10) > 1024 * 1024) {
100+
return NextResponse.json(
101+
{ error: "response too large" },
102+
{ status: 502 },
103+
);
104+
}
105+
106+
const html = await res.text();
107+
if (html.length > 1024 * 1024) {
108+
return NextResponse.json(
109+
{ error: "response too large" },
110+
{ status: 502 },
111+
);
112+
}
113+
114+
const rawTitle = extractMeta(html, "og:title") || extractTitleTag(html);
115+
const rawDescription =
116+
extractMeta(html, "og:description") || extractMeta(html, "description");
117+
const rawImage = extractMeta(html, "og:image");
118+
119+
let image: string | null = null;
120+
if (rawImage) {
121+
try {
122+
const resolvedImage = new URL(rawImage, url).toString();
123+
image = isSafeUrl(resolvedImage) ? resolvedImage : null;
124+
} catch {
125+
image = null;
126+
}
127+
}
128+
129+
return NextResponse.json(
130+
{
131+
title: rawTitle ? decodeHtmlEntities(rawTitle).trim() : null,
132+
description: rawDescription
133+
? decodeHtmlEntities(rawDescription).trim()
134+
: null,
135+
image,
136+
url,
137+
},
138+
{ headers: { "Cache-Control": "public, max-age=3600" } },
139+
);
140+
} catch {
141+
return NextResponse.json({ error: "failed to fetch" }, { status: 502 });
142+
}
143+
}

app/globals.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ body {
108108
font-family: var(--font-pixelify), "Pixelify Sans", monospace;
109109
}
110110

111+
.font-mono {
112+
font-family: monospace;
113+
}
114+
111115
/* Mono font */
112116
.font-mono-custom {
113117
font-family: var(--font-geist-mono), "Geist Mono", monospace;

app/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Footer from "@/components/Footer";
2-
import Navbar from "@/components/Navbar";
1+
import Footer from "@/components/site/Footer";
2+
import Navbar from "@/components/site/Navbar";
33
import type { Metadata } from "next";
44
import { Geist, Geist_Mono, Pixelify_Sans } from "next/font/google";
55
import "./globals.css";

app/not-found.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import FuzzyText from "@/components/FuzzyText";
3+
import FuzzyText from "@/components/bits/FuzzyText";
44
import { fadeInUp } from "@/lib/animations";
55
import { motion } from "framer-motion";
66
import { ArrowLeft, ArrowRight } from "lucide-react";

app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import TargetCursor from "@/components/bits/TargetCursor";
12
import BelongSection from "@/components/home/BelongSection";
23
import CTASection from "@/components/home/CTASection";
34
import FeaturesSection from "@/components/home/FeaturesSection";
45
import HeroSection from "@/components/home/HeroSection";
56
import ShowcaseSection from "@/components/home/ShowcaseSection";
67
import StatsSection from "@/components/home/StatsSection";
7-
import TargetCursor from "@/components/TargetCursor";
88

99
export default function Home() {
1010
return (

0 commit comments

Comments
 (0)