Skip to content

Commit c2a99a5

Browse files
authored
Merge pull request #262 from dotCMS/javadocs-page
Added new Javadocs page, replacing old vanity url
2 parents 4492998 + 9dfa079 commit c2a99a5

7 files changed

Lines changed: 3860 additions & 1858 deletions

File tree

app/docs/[slug]/page.js

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Script from "next/script";
2222
import { getSecurityIssues } from "@/services/docs/getSecurityIssues/getSecurityIssues";
2323
import Deprecations from "@/components/deprecations/Deprecations";
2424
import getDeprecations from "@/services/docs/getDeprecations/getDeprecations";
25+
import { JavadocEmbeddedDocs } from "@/components/javadocs/JavadocEmbeddedDocs";
2526

2627
/**
2728
* Process slug consistently across all functions
@@ -237,72 +238,76 @@ export default async function Home({ searchParams, params }) {
237238

238239
if (!pageData || !pageData.pageAsset) {
239240
notFound();
240-
return null; // Unreachable, but ensures code path terminates
241+
return null;
241242
}
242243

243244
const { pageAsset } = pageData;
244-
245+
245246
const sideNav = await getSideNav();
246-
const navSections = await getNavSections({ path: '/docs/nav', depth: 4, languageId: 1, ttlSeconds: 600 });
247-
248-
// Check if urlContentMap exists
247+
const navSections = await getNavSections({
248+
path: "/docs/nav",
249+
depth: 4,
250+
languageId: 1,
251+
ttlSeconds: 600,
252+
});
253+
249254
if (!pageAsset?.urlContentMap?.inode) {
250255
notFound();
251256
}
252-
253-
// Handle GitHub docs if needed (this sets githubSource flag)
257+
254258
if (isGitHubDoc(slug)) {
255259
const githubConfig = getGitHubConfig(slug);
256-
260+
257261
if (githubConfig && pageAsset?.urlContentMap?.inode) {
258262
const contentResult = await getDocsContentWithGitHub(
259263
slug,
260264
githubConfig,
261-
() => pageAsset?.urlContentMap?._map?.documentation || ''
265+
() => pageAsset?.urlContentMap?._map?.documentation || ""
262266
);
263267

264-
if (contentResult.source === 'github') {
268+
if (contentResult.source === "github") {
265269
if (!pageAsset.urlContentMap._map) {
266270
pageAsset.urlContentMap._map = {};
267271
}
268-
269-
pageAsset.urlContentMap._map.documentation = contentResult.content;
272+
273+
pageAsset.urlContentMap._map.documentation =
274+
contentResult.content;
270275
pageAsset.urlContentMap._map.githubSource = true;
271-
pageAsset.urlContentMap._map.githubConfig = contentResult.config;
276+
pageAsset.urlContentMap._map.githubConfig =
277+
contentResult.config;
272278
}
273279
}
274280
}
275-
276-
// Fetch all deprecations once (GraphQL response cached ~15m by default)
281+
277282
let allDeprecations = null;
278283
try {
279284
allDeprecations = await getDeprecations();
280-
} catch(e) {
285+
} catch (e) {
281286
console.error("Error fetching deprecations:", e);
282287
allDeprecations = null;
283288
}
284289

285-
// Find matching deprecation for this specific page (OR logic - always check)
286290
let deprecationForPage = null;
287291
if (allDeprecations && Array.isArray(allDeprecations)) {
288-
deprecationForPage = allDeprecations.find(dep =>
289-
dep.docLinks &&
290-
Array.isArray(dep.docLinks) &&
291-
dep.docLinks.some(link => link.urlTitle === slug)
292-
) || null;
292+
deprecationForPage =
293+
allDeprecations.find(
294+
(dep) =>
295+
dep.docLinks &&
296+
Array.isArray(dep.docLinks) &&
297+
dep.docLinks.some((link) => link.urlTitle === slug)
298+
) || null;
293299
}
294300

295301
const data = {
296302
contentlet: pageAsset.urlContentMap,
297-
sideNav: sideNav,
303+
sideNav,
298304
currentPath: slug,
299305
searchParams: finalSearchParams,
300306
deprecation: deprecationForPage,
301-
allDeprecations: slug === 'deprecations' ? allDeprecations : undefined
302-
}
307+
allDeprecations: slug === "deprecations" ? allDeprecations : undefined,
308+
};
303309

304-
// Add more path-component mappings here as needed:
305-
// "path-name": (contentlet) => <ComponentName contentlet={contentlet} />,
310+
// Add more path-component mappings here as needed.
306311
const componentMap = {
307312
"changelogs": (data) => <ChangeLogList {...data} slug={slug} />,
308313
"current-releases": (data) => <CurrentReleases {...data} slug={slug} />,
@@ -312,6 +317,14 @@ export default async function Home({ searchParams, params }) {
312317
"deprecations": (data) => <Deprecations {...data} slug={slug} initialItems={data.allDeprecations || []} />,
313318
"rest-api-sampler": (data) => <RestApiPlayground {...data} slug={slug} />,
314319
"all-rest-apis": (data) => <SwaggerUIComponent {...data} slug={slug} />,
320+
"javadocs": (data) => (
321+
<JavadocEmbeddedDocs
322+
contentlet={data.contentlet}
323+
sideNav={data.sideNav}
324+
slug={slug}
325+
searchParams={data.searchParams}
326+
/>
327+
),
315328
default: (data) => {
316329
// Check if this is GitHub-sourced content
317330
// githubSource is set on urlContentMap._map, so check _map property
@@ -325,7 +338,7 @@ export default async function Home({ searchParams, params }) {
325338

326339
return (
327340
<div className="flex flex-col min-h-screen">
328-
<Header sideNavItems={sideNav[0]?.dotcmsdocumentationchildren || []} currentPath={slug} navSections={navSections} />
341+
<Header sideNavItems={data.sideNav[0]?.dotcmsdocumentationchildren || []} currentPath={slug} navSections={navSections} />
329342
<JsonLd pageData={data} path={path} hostname={hostname} />
330343

331344
<div className="flex-1">
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { listJavadocS3Versions } from '@/services/javadocs/listJavadocS3Versions';
2+
import { JavadocEmbeddedDocsClient } from './JavadocEmbeddedDocsClient';
3+
4+
const DEFAULT_JAVADOCS_BASE = 'https://static.dotcms.com/docs';
5+
6+
type SideNavEntry = { dotcmsdocumentationchildren?: unknown[] };
7+
8+
type Props = {
9+
contentlet: { navTitle?: string; title?: string };
10+
sideNav: SideNavEntry[];
11+
slug: string;
12+
searchParams?: Record<string, string | string[] | undefined>;
13+
};
14+
15+
function resolveInitialVersion(
16+
versions: string[],
17+
requested: string | undefined
18+
): string | null {
19+
if (requested && versions.includes(requested)) {
20+
return requested;
21+
}
22+
return versions.length > 0 ? versions[0] : null;
23+
}
24+
25+
export async function JavadocEmbeddedDocs({
26+
contentlet,
27+
sideNav,
28+
slug,
29+
searchParams,
30+
}: Props) {
31+
const { versions, status: versionListStatus } =
32+
await listJavadocS3Versions();
33+
const baseFromEnv = process.env.NEXT_PUBLIC_JAVADOCS_BASE_URL;
34+
const baseUrl = (baseFromEnv || DEFAULT_JAVADOCS_BASE).replace(/\/$/, '');
35+
36+
const rawV = searchParams?.v;
37+
const requested =
38+
typeof rawV === 'string' ? rawV : Array.isArray(rawV) ? rawV[0] : undefined;
39+
40+
const initialSelectedVersion = resolveInitialVersion(versions, requested);
41+
42+
const title =
43+
contentlet?.navTitle || contentlet?.title || 'Java API (Javadoc)';
44+
45+
const sideNavItems = sideNav?.[0]?.dotcmsdocumentationchildren ?? [];
46+
47+
return (
48+
<JavadocEmbeddedDocsClient
49+
versions={versions}
50+
versionListStatus={versionListStatus}
51+
baseUrl={baseUrl}
52+
slug={slug}
53+
sideNavItems={sideNavItems}
54+
title={title}
55+
initialSelectedVersion={initialSelectedVersion}
56+
/>
57+
);
58+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
'use client';
2+
3+
import { useCallback, useState } from 'react';
4+
import { ExternalLink } from 'lucide-react';
5+
import Breadcrumbs from '@/components/navigation/Breadcrumbs';
6+
import { Label } from '@/components/ui/label';
7+
import {
8+
Select,
9+
SelectContent,
10+
SelectItem,
11+
SelectTrigger,
12+
SelectValue,
13+
} from '@/components/ui/select';
14+
import type { JavadocVersionListStatus } from '@/services/javadocs/listJavadocS3Versions';
15+
16+
function versionListHelp(status: JavadocVersionListStatus): string {
17+
switch (status) {
18+
case 'missing_env':
19+
return 'Set JAVADOCS_S3_ACCESS_KEY_ID, JAVADOCS_S3_SECRET_ACCESS_KEY, and JAVADOCS_S3_BUCKET on the server, then restart the dev server so Next.js picks up .env.local.';
20+
case 's3_error':
21+
return 'S3 listing failed (see server terminal logs). Common fixes: correct JAVADOCS_S3_REGION for the bucket, IAM permission s3:ListBucket on that bucket, and a bucket name that matches AWS exactly.';
22+
case 'empty':
23+
return 'Listing succeeded but no version folders matched. Check JAVADOCS_S3_DOCS_PREFIX (default docs/) matches keys in the bucket, and that folder names look like 26.04.28-02 (digits, dots, hyphens; not starting with 99).';
24+
default:
25+
return '';
26+
}
27+
}
28+
29+
export type JavadocEmbeddedDocsClientProps = {
30+
versions: string[];
31+
versionListStatus: JavadocVersionListStatus;
32+
/** No trailing slash, e.g. https://static.dotcms.com/docs */
33+
baseUrl: string;
34+
slug: string;
35+
sideNavItems: unknown[];
36+
title: string;
37+
initialSelectedVersion: string | null;
38+
};
39+
40+
function buildJavadocIndexUrl(baseUrl: string, version: string): string {
41+
const enc = encodeURIComponent(version);
42+
return `${baseUrl}/${enc}/javadocs/index.html`;
43+
}
44+
45+
export function JavadocEmbeddedDocsClient({
46+
versions,
47+
versionListStatus,
48+
baseUrl,
49+
slug,
50+
sideNavItems,
51+
title,
52+
initialSelectedVersion,
53+
}: JavadocEmbeddedDocsClientProps) {
54+
const [selectedVersion, setSelectedVersion] = useState<string | null>(
55+
initialSelectedVersion
56+
);
57+
58+
const iframeSrc = selectedVersion
59+
? buildJavadocIndexUrl(baseUrl, selectedVersion)
60+
: null;
61+
62+
const onVersionChange = useCallback((value: string) => {
63+
setSelectedVersion(value);
64+
}, []);
65+
66+
return (
67+
<div className="flex flex-col gap-6 py-6 w-full min-w-0">
68+
<Breadcrumbs items={sideNavItems as never[]} slug={slug} />
69+
70+
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between lg:gap-6">
71+
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight text-foreground lg:min-w-0 lg:flex-1">
72+
{title}
73+
</h1>
74+
75+
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-5 shrink-0">
76+
{iframeSrc ? (
77+
<a
78+
href={iframeSrc}
79+
target="_blank"
80+
rel="noopener noreferrer"
81+
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary underline-offset-4 hover:underline sm:mb-2"
82+
>
83+
<ExternalLink className="h-4 w-4 shrink-0" aria-hidden />
84+
Open Javadoc in new tab
85+
</a>
86+
) : null}
87+
88+
<div className="flex flex-col gap-2 w-full sm:w-72">
89+
<Label htmlFor="javadoc-version" className="text-sm font-medium">
90+
Javadoc version
91+
</Label>
92+
{versions.length > 0 ? (
93+
<Select
94+
value={selectedVersion ?? undefined}
95+
onValueChange={onVersionChange}
96+
>
97+
<SelectTrigger id="javadoc-version" className="w-full">
98+
<SelectValue placeholder="Select a version" />
99+
</SelectTrigger>
100+
<SelectContent>
101+
{versions.map((v) => (
102+
<SelectItem key={v} value={v}>
103+
{v}
104+
</SelectItem>
105+
))}
106+
</SelectContent>
107+
</Select>
108+
) : (
109+
<div className="space-y-2 rounded-md border border-dashed border-border bg-muted/30 px-3 py-2 text-sm text-muted-foreground">
110+
<p className="font-medium text-foreground">No versions in the list yet</p>
111+
<p>{versionListHelp(versionListStatus)}</p>
112+
<p className="text-xs leading-relaxed">
113+
In S3, the <span className="text-foreground">bucket</span> is the
114+
top-level store (one name), not a folder inside something else.
115+
If Cyberduck shows{' '}
116+
<code className="rounded bg-muted px-1 py-0.5 text-foreground">
117+
/static.dotcms.com
118+
</code>{' '}
119+
at the root, set{' '}
120+
<code className="rounded bg-muted px-1 py-0.5 text-foreground">
121+
JAVADOCS_S3_BUCKET=static.dotcms.com
122+
</code>{' '}
123+
(no leading slash). Version folders live under keys like{' '}
124+
<code className="rounded bg-muted px-1 py-0.5 text-foreground">
125+
docs/26.04.28-02/…
126+
</code>{' '}
127+
inside that bucket.
128+
</p>
129+
</div>
130+
)}
131+
</div>
132+
</div>
133+
</div>
134+
135+
<div className="relative w-full rounded-lg border border-border bg-muted/20 overflow-hidden shadow-sm min-h-[70vh] h-[min(85vh,1200px)]">
136+
{iframeSrc ? (
137+
<iframe
138+
key={iframeSrc}
139+
title={`Javadoc ${selectedVersion}`}
140+
src={iframeSrc}
141+
className="absolute inset-0 w-full h-full border-0 bg-background"
142+
referrerPolicy="no-referrer-when-downgrade"
143+
/>
144+
) : (
145+
<div className="absolute inset-0 flex items-center justify-center p-8 text-center text-muted-foreground text-sm">
146+
Select a version to load Javadoc in this frame.
147+
</div>
148+
)}
149+
</div>
150+
</div>
151+
);
152+
}

next.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ const url = new URL(process.env.NEXT_PUBLIC_DOTCMS_HOST);
44

55
const nextConfig = {
66
reactStrictMode: true,
7+
// Avoid broken ./vendor-chunks/* for packages Next splits oddly on the server
8+
// (consola via @dotcms/client; highlight.js via react-syntax-highlighter; AWS SDK).
9+
serverExternalPackages: [
10+
"consola",
11+
"@aws-sdk/client-s3",
12+
"highlight.js",
13+
"react-syntax-highlighter",
14+
],
715
// Fixes confusing dev/build behavior when Next infers the wrong repo root due to multiple lockfiles.
816
// Also helps make stack traces and tracing output more consistent.
917
outputFileTracingRoot: __dirname,

0 commit comments

Comments
 (0)