9 Commits

Author SHA1 Message Date
Renovate Bot
f956ce3d77 Update dependency next-themes to ^0.4.0 2024-11-04 03:05:45 +00:00
5acb6a4f81 Merge pull request 'Update dependency eslint-config-next to v15' (#8) from renovate/eslint-config-next-15.x into master
All checks were successful
Deploy & Publish Image / deploy (ubuntu-latest, 2.44.0) (push) Successful in 4m44s
Reviewed-on: #8
2024-10-22 12:56:57 -07:00
Renovate Bot
7b7a3cf2a0 Update dependency eslint-config-next to v15 2024-10-21 19:05:22 +00:00
ee25889139 small bug fix
All checks were successful
Deploy & Publish Image / deploy (ubuntu-latest, 2.44.0) (push) Successful in 3m25s
Took 8 minutes
2024-10-14 00:42:15 -04:00
7cf1b4d16e OG image generation
All checks were successful
Deploy & Publish Image / deploy (ubuntu-latest, 2.44.0) (push) Successful in 4m10s
Took 54 minutes
2024-10-14 00:16:33 -04:00
e1b7beadbd proper loading state for the on this page component
Took 8 minutes
2024-10-13 20:54:12 -04:00
e5a797718f 10 min -> 5 min content cache
All checks were successful
Deploy & Publish Image / deploy (ubuntu-latest, 2.44.0) (push) Successful in 3m24s
Took 2 minutes
2024-10-13 20:45:16 -04:00
b23c48246d remove debug
Some checks failed
Deploy & Publish Image / deploy (ubuntu-latest, 2.44.0) (push) Failing after 1m29s
Took 41 seconds
2024-10-13 20:43:02 -04:00
573f13568d caching properly works now
Some checks failed
Deploy & Publish Image / deploy (ubuntu-latest, 2.44.0) (push) Has been cancelled
Took 2 hours 0 minutes
2024-10-13 20:42:22 -04:00
9 changed files with 94 additions and 25 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -1,5 +1,6 @@
{ {
"siteName": "Pulse App", "siteName": "Pulse App",
"ogApiUrl": "https://docs.pulseapp.cc/api/og?title={title}",
"metadata": { "metadata": {
"title": { "title": {
"default": "Pulse Docs", "default": "Pulse Docs",

View File

@ -20,6 +20,7 @@
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.3",
"@vercel/og": "^0.6.3",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.0", "cmdk": "1.0.0",
@ -27,7 +28,7 @@
"lucide-react": "^0.452.0", "lucide-react": "^0.452.0",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"next": "^15.0.0-canary.179", "next": "^15.0.0-canary.179",
"next-themes": "^0.3.0", "next-themes": "^0.4.0",
"react": "^19.0.0-rc-1460d67c-20241003", "react": "^19.0.0-rc-1460d67c-20241003",
"react-dom": "^19.0.0-rc-1460d67c-20241003", "react-dom": "^19.0.0-rc-1460d67c-20241003",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
@ -42,7 +43,7 @@
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "14.2.15", "eslint-config-next": "15.0.0",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5" "typescript": "^5"

View File

@ -14,6 +14,7 @@ import { Metadata } from "next";
import Embed from "@/components/embed"; import Embed from "@/components/embed";
import DocsFooter from "@/components/docs-footer"; import DocsFooter from "@/components/docs-footer";
import OnThisPage from "@/components/on-this-page"; import OnThisPage from "@/components/on-this-page";
import config from "@/config";
/** /**
* The page to render the documentation markdown content. * The page to render the documentation markdown content.
@ -99,17 +100,19 @@ export const generateMetadata = async ({
}): Promise<Metadata | undefined> => { }): Promise<Metadata | undefined> => {
const slug: string = (((await params).slug as string[]) || undefined)?.join( const slug: string = (((await params).slug as string[]) || undefined)?.join(
"/" "/"
); // The slug of the content );
if (slug) { if (slug) {
const content: DocsContentMetadata | undefined = ( const pages: DocsContentMetadata[] = await getDocsContent();
await getDocsContent() const decodedSlug: string = decodeURIComponent(slug || "");
).find( const page: DocsContentMetadata | undefined = pages.find(
(metadata: DocsContentMetadata): boolean => metadata.slug === slug (metadata: DocsContentMetadata): boolean =>
); // Get the content based on the provided slug metadata.slug === (decodedSlug || pages[0].slug)
if (content) { );
if (page) {
return Embed({ return Embed({
title: content.title, title: page.title,
description: content.summary, description: page.summary,
thumbnail: config.ogApiUrl.replace("{title}", page.title),
}); });
} }
} }

30
src/app/api/og/route.tsx Normal file
View File

@ -0,0 +1,30 @@
import { ImageResponse } from "next/og";
import config from "@/config";
export const GET = async (request: Request) => {
const { searchParams } = new URL(request.url);
const title: string | undefined = searchParams.has("title")
? searchParams.get("title")?.slice(0, 100)
: "Hello World (:";
return new ImageResponse(
(
<div tw="w-full h-full flex flex-col justify-center items-center bg-black/95 text-white">
{/* Logo */}
<img
src={(config.metadata.openGraph?.images as any)[0].url}
alt={`${config.siteName} Logo`}
width={96}
height={96}
/>
{/* Title */}
<h1 tw="text-5xl font-bold">{title}</h1>
</div>
),
{
width: 1200,
height: 630,
}
);
};

View File

@ -6,6 +6,11 @@ export type Config = {
*/ */
siteName: string; siteName: string;
/**
* The URL to use for generating OG images.
*/
ogApiUrl: string;
/** /**
* The metadata for this app. * The metadata for this app.
*/ */

View File

@ -13,6 +13,11 @@ type EmbedProps = {
* The description of the embed. * The description of the embed.
*/ */
description: string; description: string;
/**
* The optional thumbnail image of the embed.
*/
thumbnail?: string | undefined;
}; };
/** /**
@ -21,13 +26,25 @@ type EmbedProps = {
* @param props the embed props * @param props the embed props
* @returns the embed jsx * @returns the embed jsx
*/ */
const Embed = ({ title, description }: EmbedProps): Metadata => { const Embed = ({ title, description, thumbnail }: EmbedProps): Metadata => {
return { return {
title: title, title: title,
openGraph: { openGraph: {
title: `${title}`, title: `${title}`,
description: description, description: description,
...(thumbnail && {
images: [
{
url: thumbnail,
}, },
],
}),
},
...(thumbnail && {
twitter: {
card: "summary_large_image",
},
}),
}; };
}; };
export default Embed; export default Embed;

View File

@ -18,6 +18,7 @@ type Header = {
}; };
const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => { const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
const [loading, setLoading] = useState<boolean>(true);
const [headers, setHeaders] = useState<Header[]>([]); const [headers, setHeaders] = useState<Header[]>([]);
const [activeHeader, setActiveHeader] = useState<string | undefined>( const [activeHeader, setActiveHeader] = useState<string | undefined>(
undefined undefined
@ -45,6 +46,7 @@ const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
} }
setHeaders(extractedHeaders); setHeaders(extractedHeaders);
setLoading(false);
}, [page.content]); }, [page.content]);
useEffect(() => { useEffect(() => {
@ -94,8 +96,10 @@ const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
{/* Headers */} {/* Headers */}
<ul className="relative"> <ul className="relative">
{headers.length === 0 ? ( {loading ? (
<Skeleton className="w-full h-5 bg-accent rounded-lg" /> <Skeleton className="w-full h-5 bg-accent rounded-lg" />
) : headers.length === 0 ? (
<span className="opacity-75">Nothing ):</span>
) : ( ) : (
headers.map((header: Header) => ( headers.map((header: Header) => (
<li <li

View File

@ -4,6 +4,8 @@ import path from "node:path";
import config from "@/config"; import config from "@/config";
import simpleGit from "simple-git"; import simpleGit from "simple-git";
import os from "node:os"; import os from "node:os";
import { cache } from "react";
import "server-only";
/** /**
* The regex to match for metadata. * The regex to match for metadata.
@ -29,22 +31,23 @@ const LAST_UPDATE_FILE = path.join(
"docs_cache", "docs_cache",
"last_update.json" "last_update.json"
); );
const UPDATE_INTERVAL_MS: number = 10 * 60 * 1000; // 10 minutes in milliseconds const CACHE_DURATION_MS: number = 5 * 60 * 1000; // 5 minutes in milliseconds
/** /**
* Clone the Git repository if DOCS_DIR is a URL, else use the local directory. * Clone the Git repository if DOCS_DIR is a URL, else use the local directory.
* If it's a Git URL, clone it to a cache directory and reuse it. * If it's a Git URL, clone it to a cache directory and reuse it.
*/ */
const getDocsDirectory = async (): Promise<string> => { const getDocsDirectory = cache(async (): Promise<string> => {
if (isGitUrl(DOCS_DIR)) { if (isGitUrl(DOCS_DIR)) {
const repoHash: string = Buffer.from(DOCS_DIR).toString("base64"); // Create a unique identifier based on the repo URL const repoHash: string = Buffer.from(DOCS_DIR).toString("base64"); // Create a unique identifier based on the repo URL
const cacheDir: string = path.join(os.tmpdir(), "docs_cache", repoHash); const cacheDir: string = path.join(os.tmpdir(), "docs_cache", repoHash);
// Pull the latest changes from the repo if we don't have it // Pull the latest changes from the repo if we don't have it
if (!fs.existsSync(cacheDir) || fs.readdirSync(cacheDir).length < 1) { if (!fs.existsSync(cacheDir) || fs.readdirSync(cacheDir).length < 1) {
console.log("Fetching initial docs from Git..."); console.log("Cloning initial docs content from Git...");
try { try {
await simpleGit().clone(DOCS_DIR, cacheDir, { "--depth": 1 }); await simpleGit().clone(DOCS_DIR, cacheDir, { "--depth": 1 });
storeUpdatedRepoTime();
} catch (error) { } catch (error) {
// Simply ignore this error. When cloning the repo for // Simply ignore this error. When cloning the repo for
// the first time, it'll sometimes error saying the dir // the first time, it'll sometimes error saying the dir
@ -52,18 +55,16 @@ const getDocsDirectory = async (): Promise<string> => {
} }
} else if (shouldUpdateRepo()) { } else if (shouldUpdateRepo()) {
// Pull the latest changes from Git // Pull the latest changes from Git
console.log("Updating docs content from Git..."); console.log("Pulling docs content from Git...");
await simpleGit().pull(cacheDir); await simpleGit(cacheDir)
fs.writeFileSync( .reset(["--hard"]) // Reset any local changes
LAST_UPDATE_FILE, .pull(); // Pull latest changes
JSON.stringify({ lastUpdate: Date.now() }), storeUpdatedRepoTime();
"utf-8"
);
} }
return cacheDir; return cacheDir;
} }
return DOCS_DIR; return DOCS_DIR;
}; });
const shouldUpdateRepo = (): boolean => { const shouldUpdateRepo = (): boolean => {
if (!fs.existsSync(LAST_UPDATE_FILE)) { if (!fs.existsSync(LAST_UPDATE_FILE)) {
@ -72,10 +73,17 @@ const shouldUpdateRepo = (): boolean => {
return ( return (
Date.now() - Date.now() -
JSON.parse(fs.readFileSync(LAST_UPDATE_FILE, "utf-8")).lastUpdate > JSON.parse(fs.readFileSync(LAST_UPDATE_FILE, "utf-8")).lastUpdate >
UPDATE_INTERVAL_MS CACHE_DURATION_MS
); );
}; };
const storeUpdatedRepoTime = () =>
fs.writeFileSync(
LAST_UPDATE_FILE,
JSON.stringify({ lastUpdate: Date.now() }),
"utf-8"
);
/** /**
* Get the content to display in the docs. * Get the content to display in the docs.
*/ */