diff --git a/README.md b/README.md
index de1477a..e0ab3da 100644
--- a/README.md
+++ b/README.md
@@ -56,36 +56,38 @@ Open [http://localhost:3000](http://localhost:3000) and you're in.
## Project Structure
```
-├── 📄 .eslintrc.json
-├── 📄 .gitignore
+├── 📁 .github
+│ └── 📁 workflows
+│ └── 📄 linter.yaml
├── 📁 .vscode
│ └── 📄 settings.json
-├── 📄 CONTRIBUTING.md
-├── 📄 LICENSE
-├── 📄 README.md
├── 📁 app
│ ├── 📄 globals.css
│ ├── 📄 layout.tsx
+│ ├── 📄 not-found.tsx
│ ├── 📄 page.tsx
│ ├── 📁 pages
-│ │ ├── 📄 [slug]
+│ │ ├── 📁 [slug]
+│ │ │ ├── 📄 PageClient.tsx
+│ │ │ ├── 📁 [subslug]
+│ │ │ │ └── 📄 page.tsx
+│ │ │ └── 📄 page.tsx
│ │ ├── 📄 layout.tsx
│ │ └── 📄 page.tsx
+│ ├── 📁 partners
+│ │ └── 📄 page.tsx
│ ├── 📁 resources
│ │ └── 📄 page.tsx
│ └── 📁 rules
│ └── 📄 page.tsx
-├── 📄 components.json
├── 📁 components
│ ├── 📄 AnimatedText.tsx
│ ├── 📄 Badge.tsx
+│ ├── 📄 BorderGlow.tsx
│ ├── 📄 BorderGlowButton.tsx
│ ├── 📄 Footer.tsx
-│ ├── 📄 GlowButton.tsx
-│ ├── 📄 Navbar.tsx
-│ ├── 📄 Section.tsx
-│ ├── 📄 ShinyText.tsx
-│ ├── 📄 TargetCursor.tsx
+│ ├── 📄 FuzzyText.tsx
+│ ...
│ ├── 📁 home
│ │ ├── 📄 BelongSection.tsx
│ │ ├── 📄 CTASection.tsx
@@ -100,9 +102,9 @@ Open [http://localhost:3000](http://localhost:3000) and you're in.
│ ├── 📄 aspect-ratio.tsx
│ ├── 📄 avatar.tsx
│ ├── 📄 badge.tsx
-│ ├── 📄 breadcrumb.tsx
-│ ├── 📄 button.tsx
-│ ├── ...
+ ...
+├── 📁 content
+│ └── 📄 pages.ts
├── 📁 hooks
│ └── 📄 use-toast.ts
├── 📁 lib
@@ -111,10 +113,16 @@ Open [http://localhost:3000](http://localhost:3000) and you're in.
│ ├── 📄 redirects.config.ts
│ ├── 📄 staticdata.config.ts
│ └── 📄 utils.ts
+├── 📄 .eslintrc.json
+├── 📄 .gitignore
+├── 📄 components.json
+├── 📄 CONTRIBUTING.md
+├── 📄 LICENSE
├── 📄 next.config.js
├── 📄 package-lock.json
├── 📄 package.json
├── 📄 postcss.config.js
+├── 📄 README.md
├── 📄 tailwind.config.ts
└── 📄 tsconfig.json
```
@@ -145,7 +153,7 @@ npm run format # Run Prettier
## Community
-- **Discord** - [devhub.vercel.app/invite](https://devhub.vercel.app/invite)
+- **Discord** - [devhub.vercel.app/join](https://devhub.vercel.app/join)
- **GitHub Org** - [github.com/open-devhub](https://github.com/open-devhub)
- **Email** - open-devhub@outlook.com
diff --git a/app/api/link-preview/route.ts b/app/api/link-preview/route.ts
new file mode 100644
index 0000000..1b5c037
--- /dev/null
+++ b/app/api/link-preview/route.ts
@@ -0,0 +1,143 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export const runtime = "nodejs";
+
+function extractMeta(html: string, property: string): string | null {
+ const patterns = [
+ new RegExp(
+ `]+(?:property|name)=["']${property}["'][^>]+content=["']([^"']*)["']`,
+ "i",
+ ),
+ new RegExp(
+ `]+content=["']([^"']*)["'][^>]+(?:property|name)=["']${property}["']`,
+ "i",
+ ),
+ ];
+ for (const pattern of patterns) {
+ const match = html.match(pattern);
+ if (match) return match[1];
+ }
+ return null;
+}
+
+function extractTitleTag(html: string): string | null {
+ const match = html.match(/
]*>([^<]+)<\/title>/i);
+ return match ? match[1] : null;
+}
+
+function decodeHtmlEntities(str: string): string {
+ return str
+ .replace(/&/g, "&")
+ .replace(/"/g, '"')
+ .replace(/'/g, "'")
+ .replace(/</g, "<")
+ .replace(/>/g, ">");
+}
+
+const PRIVATE_HOSTNAME_PATTERNS = [
+ /^localhost$/i,
+ /^0\.0\.0\.0$/,
+ /^127\./,
+ /^10\./,
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
+ /^192\.168\./,
+ /^169\.254\./,
+ /^::1$/,
+ /^fc00:/i,
+ /^fe80:/i,
+];
+
+function isSafeUrl(candidate: string): boolean {
+ let parsed: URL;
+ try {
+ parsed = new URL(candidate);
+ } catch {
+ return false;
+ }
+ if (!["http:", "https:"].includes(parsed.protocol)) {
+ return false;
+ }
+ const hostname = parsed.hostname.toLowerCase();
+ return !PRIVATE_HOSTNAME_PATTERNS.some((pattern) => pattern.test(hostname));
+}
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url);
+ const url = searchParams.get("url");
+
+ if (!url) {
+ return NextResponse.json({ error: "missing url" }, { status: 400 });
+ }
+
+ if (!isSafeUrl(url)) {
+ return NextResponse.json(
+ { error: "invalid or disallowed url" },
+ { status: 400 },
+ );
+ }
+
+ try {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 5000);
+
+ const res = await fetch(url, {
+ signal: controller.signal,
+ redirect: "error",
+ headers: {
+ "User-Agent":
+ "Mozilla/5.0 (compatible; LinkPreviewBot/1.0; +https://example.com/bot)",
+ },
+ });
+
+ clearTimeout(timeout);
+
+ if (!res.ok) {
+ return NextResponse.json({ error: "failed to fetch" }, { status: 502 });
+ }
+
+ const contentLength = res.headers.get("content-length");
+ if (contentLength && parseInt(contentLength, 10) > 1024 * 1024) {
+ return NextResponse.json(
+ { error: "response too large" },
+ { status: 502 },
+ );
+ }
+
+ const html = await res.text();
+ if (html.length > 1024 * 1024) {
+ return NextResponse.json(
+ { error: "response too large" },
+ { status: 502 },
+ );
+ }
+
+ const rawTitle = extractMeta(html, "og:title") || extractTitleTag(html);
+ const rawDescription =
+ extractMeta(html, "og:description") || extractMeta(html, "description");
+ const rawImage = extractMeta(html, "og:image");
+
+ let image: string | null = null;
+ if (rawImage) {
+ try {
+ const resolvedImage = new URL(rawImage, url).toString();
+ image = isSafeUrl(resolvedImage) ? resolvedImage : null;
+ } catch {
+ image = null;
+ }
+ }
+
+ return NextResponse.json(
+ {
+ title: rawTitle ? decodeHtmlEntities(rawTitle).trim() : null,
+ description: rawDescription
+ ? decodeHtmlEntities(rawDescription).trim()
+ : null,
+ image,
+ url,
+ },
+ { headers: { "Cache-Control": "public, max-age=3600" } },
+ );
+ } catch {
+ return NextResponse.json({ error: "failed to fetch" }, { status: 502 });
+ }
+}
diff --git a/app/globals.css b/app/globals.css
index 696083f..d91ceef 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -108,6 +108,10 @@ body {
font-family: var(--font-pixelify), "Pixelify Sans", monospace;
}
+.font-mono {
+ font-family: monospace;
+}
+
/* Mono font */
.font-mono-custom {
font-family: var(--font-geist-mono), "Geist Mono", monospace;
diff --git a/app/layout.tsx b/app/layout.tsx
index 8a11ee8..6c614e7 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,5 +1,5 @@
-import Footer from "@/components/Footer";
-import Navbar from "@/components/Navbar";
+import Footer from "@/components/site/Footer";
+import Navbar from "@/components/site/Navbar";
import type { Metadata } from "next";
import { Geist, Geist_Mono, Pixelify_Sans } from "next/font/google";
import "./globals.css";
diff --git a/app/not-found.tsx b/app/not-found.tsx
index 30f930a..2c52910 100644
--- a/app/not-found.tsx
+++ b/app/not-found.tsx
@@ -1,6 +1,6 @@
"use client";
-import FuzzyText from "@/components/FuzzyText";
+import FuzzyText from "@/components/bits/FuzzyText";
import { fadeInUp } from "@/lib/animations";
import { motion } from "framer-motion";
import { ArrowLeft, ArrowRight } from "lucide-react";
diff --git a/app/page.tsx b/app/page.tsx
index 8fbbcc0..4270baa 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,10 +1,10 @@
+import TargetCursor from "@/components/bits/TargetCursor";
import BelongSection from "@/components/home/BelongSection";
import CTASection from "@/components/home/CTASection";
import FeaturesSection from "@/components/home/FeaturesSection";
import HeroSection from "@/components/home/HeroSection";
import ShowcaseSection from "@/components/home/ShowcaseSection";
import StatsSection from "@/components/home/StatsSection";
-import TargetCursor from "@/components/TargetCursor";
export default function Home() {
return (
diff --git a/app/pages/[slug]/PageClient.tsx b/app/pages/[slug]/PageClient.tsx
index 13a9649..0d8b210 100644
--- a/app/pages/[slug]/PageClient.tsx
+++ b/app/pages/[slug]/PageClient.tsx
@@ -1,7 +1,8 @@
"use client";
+import { LinkPreviewCard } from "@/components/LinkPreviewCard";
+import { Page, PageContent } from "@/content/pages";
import { fadeInUp, staggerContainer } from "@/lib/animations";
-import { Page, PageContent } from "@/lib/pages.config";
import { motion } from "framer-motion";
import {
OctagonAlert as AlertOctagon,
@@ -21,30 +22,66 @@ interface Props {
next?: Page;
}
-function Highlight({ text }: { text: string }) {
- return (
- <>
- {text.split(/(\s+)/).map((part, i) => {
- if (part.startsWith("#")) {
- const match = part.match(/^(#[\w-]+)(.*)?$/);
- if (match) {
- return (
-
-
- {match[1]}
-
- {match[2]}
-
- );
- }
- }
- return part;
- })}
- >
- );
+function ApplySpecialClass({ text }: { text: string }) {
+ const regex = /(`[^`]+`)|(\[[^\]]+\]\([^)]+\))|(#[\w-]+)/g;
+ const parts: React.ReactNode[] = [];
+ let lastIndex = 0;
+ let key = 0;
+ let match: RegExpExecArray | null;
+
+ while ((match = regex.exec(text)) !== null) {
+ if (match.index > lastIndex) {
+ parts.push(text.slice(lastIndex, match.index));
+ }
+
+ const [full, code, link, channel] = match;
+
+ if (code) {
+ parts.push(
+
+ {code.slice(1, -1)}
+ ,
+ );
+ } else if (link) {
+ const linkMatch = link.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
+ if (linkMatch) {
+ parts.push(
+
+ {linkMatch[1]}
+ ,
+ );
+ } else {
+ parts.push(full);
+ }
+ } else if (channel) {
+ parts.push(
+
+ {channel}
+ ,
+ );
+ }
+
+ lastIndex = regex.lastIndex;
+ }
+
+ if (lastIndex < text.length) {
+ parts.push(text.slice(lastIndex));
+ }
+
+ return <>{parts}>;
}
function ContentBlock({ block }: { block: PageContent }) {
@@ -59,7 +96,7 @@ function ContentBlock({ block }: { block: PageContent }) {
color: "#e2e2f0",
}}
>
-
+
);
case "h3":
@@ -71,7 +108,7 @@ function ContentBlock({ block }: { block: PageContent }) {
color: "#e2e2f0",
}}
>
-
+
);
case "p":
@@ -80,7 +117,7 @@ function ContentBlock({ block }: { block: PageContent }) {
className="leading-relaxed mb-4"
style={{ fontFamily: "var(--font-geist-mono)", color: "#71717a" }}
>
-
+
);
case "ul":
@@ -97,7 +134,7 @@ function ContentBlock({ block }: { block: PageContent }) {
style={{ background: "rgba(99,102,241,0.5)" }}
/>
-
+
))}
@@ -119,7 +156,7 @@ function ContentBlock({ block }: { block: PageContent }) {
{String(i + 1).padStart(2, "0")}
-
+
))}
@@ -206,7 +243,7 @@ function ContentBlock({ block }: { block: PageContent }) {
className="text-sm"
style={{ fontFamily: "var(--font-geist-mono)", color: "#71717a" }}
>
-
+
);
diff --git a/app/pages/[slug]/[subslug]/page.tsx b/app/pages/[slug]/[subslug]/page.tsx
index 177e055..2e2a2c5 100644
--- a/app/pages/[slug]/[subslug]/page.tsx
+++ b/app/pages/[slug]/[subslug]/page.tsx
@@ -1,5 +1,5 @@
+import { getAdjacentPages, getPage } from "@/content/pages";
import { notFound } from "next/navigation";
-import { getPage, getAdjacentPages } from "@/lib/pages.config";
import PageClient from "../PageClient";
interface Props {
@@ -16,7 +16,7 @@ export default function SubPageRoute({ params }: Props) {
}
export async function generateStaticParams() {
- const { pages } = await import("@/lib/pages.config");
+ const { pages } = await import("@/content/pages");
return pages
.filter((p) => p.slug.includes("/"))
.map((p) => {
diff --git a/app/pages/[slug]/page.tsx b/app/pages/[slug]/page.tsx
index 3efc9d2..d67f327 100644
--- a/app/pages/[slug]/page.tsx
+++ b/app/pages/[slug]/page.tsx
@@ -1,4 +1,4 @@
-import { getAdjacentPages, getPage } from "@/lib/pages.config";
+import { getAdjacentPages, getPage } from "@/content/pages";
import { notFound } from "next/navigation";
import PageClient from "./PageClient";
@@ -17,6 +17,6 @@ export default async function PageRoute({ params }: Props) {
}
export async function generateStaticParams() {
- const { pages } = await import("@/lib/pages.config");
+ const { pages } = await import("@/content/pages");
return pages.map((page) => ({ slug: page.slug }));
}
diff --git a/app/pages/layout.tsx b/app/pages/layout.tsx
index a293d9c..cfbaedc 100644
--- a/app/pages/layout.tsx
+++ b/app/pages/layout.tsx
@@ -1,6 +1,6 @@
"use client";
-import { pageSections } from "@/lib/pages.config";
+import { pageSections } from "@/content/pages";
import { AnimatePresence, motion } from "framer-motion";
import { ChevronDown, Menu, X } from "lucide-react";
import Link from "next/link";
diff --git a/app/partners/page.tsx b/app/partners/page.tsx
index 3515b84..25c0f30 100644
--- a/app/partners/page.tsx
+++ b/app/partners/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import ShinyText from "@/components/ShinyText";
+import ShinyText from "@/components/bits/ShinyText";
import { fadeInUp, staggerContainer } from "@/lib/animations";
import { motion } from "framer-motion";
import { ArrowRight, ExternalLink } from "lucide-react";
diff --git a/app/resources/page.tsx b/app/resources/page.tsx
index 401dedc..e406705 100644
--- a/app/resources/page.tsx
+++ b/app/resources/page.tsx
@@ -1,7 +1,7 @@
"use client";
import Badge from "@/components/Badge";
-import ShinyText from "@/components/ShinyText";
+import ShinyText from "@/components/bits/ShinyText";
import {
Drawer,
DrawerClose,
diff --git a/app/rules/page.tsx b/app/rules/page.tsx
index a64066a..ab21073 100644
--- a/app/rules/page.tsx
+++ b/app/rules/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import ShinyText from "@/components/ShinyText";
+import ShinyText from "@/components/bits/ShinyText";
import { fadeInUp, staggerContainer } from "@/lib/animations";
import { motion } from "framer-motion";
diff --git a/components/LinkPreviewCard.tsx b/components/LinkPreviewCard.tsx
new file mode 100644
index 0000000..d016155
--- /dev/null
+++ b/components/LinkPreviewCard.tsx
@@ -0,0 +1,220 @@
+import { AnimatePresence, motion } from "framer-motion";
+import { useEffect, useRef, useState } from "react";
+import { createPortal } from "react-dom";
+
+interface PreviewData {
+ title: string | null;
+ description: string | null;
+ image: string | null;
+ url: string;
+}
+
+const previewCache = new Map();
+
+export function LinkPreviewCard({
+ href,
+ children,
+}: {
+ href: string;
+ children: React.ReactNode;
+}) {
+ const [hovering, setHovering] = useState(false);
+ const [data, setData] = useState(null);
+ const [imgError, setImgError] = useState(false);
+ const [pos, setPos] = useState({ top: 0, left: 0 });
+ const [mounted, setMounted] = useState(false);
+ const anchorRef = useRef(null);
+ const hideTimeoutRef = useRef | null>(null);
+ const fetchTimeoutRef = useRef | null>(null);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
+ if (fetchTimeoutRef.current) clearTimeout(fetchTimeoutRef.current);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!hovering) return;
+
+ const handleScroll = () => {
+ cancelHide();
+ setHovering(false);
+ };
+
+ window.addEventListener("scroll", handleScroll, { passive: true });
+ return () => window.removeEventListener("scroll", handleScroll);
+ }, [hovering]);
+
+ const fetchPreview = () => {
+ if (previewCache.has(href)) {
+ setData(previewCache.get(href) || null);
+ return;
+ }
+ fetch(`/api/link-preview?url=${encodeURIComponent(href)}`)
+ .then((res) => (res.ok ? res.json() : null))
+ .then((json) => {
+ previewCache.set(href, json);
+ setData(json);
+ })
+ .catch(() => {
+ previewCache.set(href, null);
+ setData(null);
+ });
+ };
+
+ const cancelHide = () => {
+ if (hideTimeoutRef.current) {
+ clearTimeout(hideTimeoutRef.current);
+ hideTimeoutRef.current = null;
+ }
+ };
+
+ const scheduleHide = () => {
+ cancelHide();
+ hideTimeoutRef.current = setTimeout(() => setHovering(false), 200);
+ };
+
+ const handleLinkEnter = () => {
+ cancelHide();
+ const rect = anchorRef.current?.getBoundingClientRect();
+ if (rect) {
+ setPos({ top: rect.bottom + 8, left: rect.left });
+ }
+ setHovering(true);
+ setImgError(false);
+ fetchTimeoutRef.current = setTimeout(fetchPreview, 150);
+ };
+
+ const handleLinkLeave = () => {
+ if (fetchTimeoutRef.current) clearTimeout(fetchTimeoutRef.current);
+ scheduleHide();
+ };
+
+ const handleCardEnter = () => {
+ cancelHide();
+ };
+
+ const handleCardLeave = () => {
+ scheduleHide();
+ };
+
+ const hasContent = Boolean(data?.description);
+
+ return (
+ <>
+
+ {children}
+
+ {mounted &&
+ createPortal(
+
+ {hovering && hasContent && (
+
+
+
+
+
+
+
+ {data?.image && !imgError && (
+

setImgError(true)}
+ className="absolute top-0 right-0 w-14 h-14 object-cover"
+ style={{ border: "1px solid rgba(99,102,241,0.12)" }}
+ />
+ )}
+
+
+
+ )}
+ ,
+ document.body,
+ )}
+ >
+ );
+}
diff --git a/components/BorderGlow.tsx b/components/bits/BorderGlow.tsx
similarity index 100%
rename from components/BorderGlow.tsx
rename to components/bits/BorderGlow.tsx
diff --git a/components/FuzzyText.tsx b/components/bits/FuzzyText.tsx
similarity index 100%
rename from components/FuzzyText.tsx
rename to components/bits/FuzzyText.tsx
diff --git a/components/ShinyText.tsx b/components/bits/ShinyText.tsx
similarity index 100%
rename from components/ShinyText.tsx
rename to components/bits/ShinyText.tsx
diff --git a/components/SoftAurora.tsx b/components/bits/SoftAurora.tsx
similarity index 100%
rename from components/SoftAurora.tsx
rename to components/bits/SoftAurora.tsx
diff --git a/components/TargetCursor.tsx b/components/bits/TargetCursor.tsx
similarity index 100%
rename from components/TargetCursor.tsx
rename to components/bits/TargetCursor.tsx
diff --git a/components/home/BelongSection.tsx b/components/home/BelongSection.tsx
index ba01ac0..1bc4b30 100644
--- a/components/home/BelongSection.tsx
+++ b/components/home/BelongSection.tsx
@@ -2,7 +2,7 @@
import { fadeInUp, staggerContainer } from "@/lib/animations";
import { motion } from "framer-motion";
-import SoftAurora from "../SoftAurora";
+import SoftAurora from "../bits/SoftAurora";
const memberTypes = [
"Solo founders figuring it out as they go",
diff --git a/components/home/FeaturesSection.tsx b/components/home/FeaturesSection.tsx
index e13ec1c..9084ef6 100644
--- a/components/home/FeaturesSection.tsx
+++ b/components/home/FeaturesSection.tsx
@@ -12,7 +12,7 @@ import {
Users,
Wrench,
} from "lucide-react";
-import BorderGlow from "../BorderGlow";
+import BorderGlow from "../bits/BorderGlow";
const features = [
{
diff --git a/components/home/HeroSection.tsx b/components/home/HeroSection.tsx
index 1996dcc..d272f60 100644
--- a/components/home/HeroSection.tsx
+++ b/components/home/HeroSection.tsx
@@ -5,8 +5,8 @@ import data from "@/lib/staticdata.config";
import { motion } from "framer-motion";
import { ArrowDown, ArrowRight } from "lucide-react";
import { useEffect, useRef } from "react";
-import ShinyText from "../ShinyText";
-import SoftAurora from "../SoftAurora";
+import ShinyText from "../bits/ShinyText";
+import SoftAurora from "../bits/SoftAurora";
export default function HeroSection() {
const canvasRef = useRef(null);
diff --git a/components/home/ShowcaseSection.tsx b/components/home/ShowcaseSection.tsx
index 3b91cc3..52ebdf2 100644
--- a/components/home/ShowcaseSection.tsx
+++ b/components/home/ShowcaseSection.tsx
@@ -45,8 +45,8 @@ const projects = [
{
name: "devhub-bot",
desc: "GitHub Bot designed to automate the boring stuff within the DevHub Organization. Built on the powerful Probot framework.",
- lang: "Discord.js",
- langColor: "#f59e0b",
+ lang: "TypeScript",
+ langColor: "#6366f1",
},
];
diff --git a/components/home/StatsSection.tsx b/components/home/StatsSection.tsx
index 7070eae..96231e8 100644
--- a/components/home/StatsSection.tsx
+++ b/components/home/StatsSection.tsx
@@ -5,7 +5,7 @@ import { hexToHslString } from "@/lib/utils";
import { motion, useInView } from "framer-motion";
import { Code as Code2, MessageCircle, Users } from "lucide-react";
import { useEffect, useRef, useState } from "react";
-import BorderGlow from "../BorderGlow";
+import BorderGlow from "../bits/BorderGlow";
const stats = [
{ icon: Users, value: 500, label: "Members", suffix: "+", color: "#6366f1" },
diff --git a/components/Footer.tsx b/components/site/Footer.tsx
similarity index 100%
rename from components/Footer.tsx
rename to components/site/Footer.tsx
diff --git a/components/Navbar.tsx b/components/site/Navbar.tsx
similarity index 100%
rename from components/Navbar.tsx
rename to components/site/Navbar.tsx
diff --git a/components/Section.tsx b/components/site/Section.tsx
similarity index 100%
rename from components/Section.tsx
rename to components/site/Section.tsx
diff --git a/lib/pages.config.ts b/content/pages.ts
similarity index 99%
rename from lib/pages.config.ts
rename to content/pages.ts
index cf96290..a51fc87 100644
--- a/lib/pages.config.ts
+++ b/content/pages.ts
@@ -522,7 +522,7 @@ export const pages: Page[] = [
"Contact the moderator who handled the action, clearly and calmly. Most things can be resolved here.",
"If you don't get a satisfactory response, escalate to an Admin via DM.",
"If you believe there's bias or a serious process failure, contact the Owner.",
- "For bans, go to https://appeal.gg/s/1429026875946172459 and submit an appeal form. This goes to the admin team for review.",
+ "For bans, go to [appeal.gg](https://appeal.gg/s/1429026875946172459) and submit an appeal form. This goes to the admin team for review.",
],
},
{
@@ -651,7 +651,7 @@ export const pages: Page[] = [
{ type: "h2", text: "I got banned. Can I appeal?" },
{
type: "p",
- text: "Yes. Use https://appeal.gg/s/1429026875946172459. See the Moderation Guide for the full process. Bans aren't always permanent, context and how you handle the appeal matters.",
+ text: "Yes. Use [appeal.gg](https://appeal.gg/s/1429026875946172459). See the Moderation Guide for the full process. Bans aren't always permanent, context and how you handle the appeal matters.",
},
{ type: "h2", text: "Who runs DevHub?" },
{
@@ -1091,7 +1091,7 @@ export const pages: Page[] = [
{ type: "h2", text: "Commit Messages" },
{
type: "p",
- text: 'We use Conventional Commits: "type(scope): description". Common types are feat, fix, docs, chore, refactor, test (full documentation at https://www.conventionalcommits.org/en/v1.0.0/). The commit history is documentation, write it like someone will read it.',
+ text: 'We use Conventional Commits: "type(scope): description". Common types are feat, fix, docs, chore, refactor, test (full documentation at [conventionalcommits.org](https://www.conventionalcommits.org/en/v1.0.0/)). The commit history is documentation, write it like someone will read it.',
},
{
type: "code",
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..0c4276c
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,25 @@
+{
+ "headers": [
+ {
+ "source": "/api/link-preview",
+ "headers": [
+ {
+ "key": "Access-Control-Allow-Origin",
+ "value": "https://devhub.vercel.app"
+ },
+ {
+ "key": "Access-Control-Allow-Methods",
+ "value": "GET, OPTIONS"
+ },
+ {
+ "key": "Access-Control-Allow-Headers",
+ "value": "Content-Type"
+ },
+ {
+ "key": "Vary",
+ "value": "Origin"
+ }
+ ]
+ }
+ ]
+}