Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 55 additions & 9 deletions scripts/generate-changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ function pruneOldEntries(entries: ChangelogEntry[]): ChangelogEntry[] {
return entries.filter((entry) => new Date(entry.date) >= cutoff);
}

function pruneDeletedEntries(entries: ChangelogEntry[]): ChangelogEntry[] {
return entries.filter((entry) =>
fs.existsSync(path.join(process.cwd(), "content/docs", entry.path)),
);
}

function sanitizeEntries(entries: ChangelogEntry[]): ChangelogEntry[] {
return pruneOldEntries(pruneDeletedEntries(entries));
}

function loadExistingChangelog(): ChangelogData {
try {
if (fs.existsSync(OUTPUT_FILE)) {
Expand Down Expand Up @@ -77,12 +87,31 @@ function filePathToUrl(relativePath: string): string {
return `/docs/${cleanPath}`;
}

function writeChangelogIfChanged(changelogData: ChangelogData): boolean {
const output = `${JSON.stringify(changelogData, null, 2)}\n`;
const current = fs.existsSync(OUTPUT_FILE) ? fs.readFileSync(OUTPUT_FILE, "utf-8") : "";

if (current === output) {
return false;
}

const outputDir = path.dirname(OUTPUT_FILE);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}

fs.writeFileSync(OUTPUT_FILE, output, "utf-8");
return true;
}

async function main() {
console.log("Generating documentation changelog...");
console.log(` Since: ${SINCE_DATE.toISOString().split("T")[0]}`);

const existing = loadExistingChangelog();
const existingKeys = new Set(existing.entries.map((e) => e.key));
const existingEntries = pruneDeletedEntries(existing.entries);
const prunedExistingCount = existing.entries.length - existingEntries.length;
Comment on lines +112 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist stale changelog pruning without new entries

When pruneDeletedEntries removes existing entries here, a delete-only docs change (or any run where the only stale entries are for deleted files) can still leave newChanges.length === 0, so the early return below exits without calling writeChangelogIfChanged. Since ChangelogTimeline no longer validates URLs against source.getPages(), those stale deleted-page entries remain in changelog-entries.json and can render broken changelog links until an unrelated new entry is generated.

Useful? React with 👍 / 👎.

const existingKeys = new Set(existingEntries.map((e) => e.key));
console.log(` Existing entries: ${existingKeys.size}`);

const changes = getChangedFilesSince(SINCE_DATE);
Expand Down Expand Up @@ -123,11 +152,33 @@ async function main() {
console.log(` New entries to generate: ${newChanges.length}`);

if (newChanges.length === 0) {
if (prunedExistingCount > 0) {
const wroteChangelog = writeChangelogIfChanged({
lastUpdated: new Date().toISOString(),
entries: existingEntries,
});
Comment thread
cursor[bot] marked this conversation as resolved.

if (wroteChangelog) {
console.log(` Pruned ${prunedExistingCount} stale existing entries`);
}
}

console.log("Changelog is up to date, no new entries");
return;
}

if (!hasApiKey()) {
if (prunedExistingCount > 0) {
Comment thread
cursor[bot] marked this conversation as resolved.
const wroteChangelog = writeChangelogIfChanged({
lastUpdated: new Date().toISOString(),
entries: existingEntries,
});

if (wroteChangelog) {
console.log(` Pruned ${prunedExistingCount} stale existing entries`);
}
}

console.log("\nNo ANTHROPIC_API_KEY found. Skipping changelog generation.");
console.log(" To generate descriptions for new entries, add ANTHROPIC_API_KEY to .env.local");
console.log(` ${newChanges.length} new entries were not added.\n`);
Expand Down Expand Up @@ -164,8 +215,8 @@ async function main() {
};
});

const mergedEntries = [...newEntries, ...existing.entries];
const prunedEntries = pruneOldEntries(mergedEntries);
const mergedEntries = [...newEntries, ...existingEntries];
const prunedEntries = sanitizeEntries(mergedEntries);
const prunedCount = mergedEntries.length - prunedEntries.length;

prunedEntries.sort((a, b) => {
Expand All @@ -179,12 +230,7 @@ async function main() {
entries: prunedEntries,
};

const outputDir = path.dirname(OUTPUT_FILE);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}

fs.writeFileSync(OUTPUT_FILE, JSON.stringify(changelogData, null, 2), "utf-8");
writeChangelogIfChanged(changelogData);
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

console.log(`Changelog updated: ${path.relative(process.cwd(), OUTPUT_FILE)}`);
console.log(` Added ${newEntries.length} new entries`);
Expand Down
4 changes: 3 additions & 1 deletion scripts/generate-static-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ async function main() {
const { gitConfig } = await server.ssrLoadModule("./src/lib/layout.shared.tsx");
const { buildCanonicalUrl, SITE_NAME, DEFAULT_DESCRIPTION } =
await server.ssrLoadModule("./src/lib/metadata");
const { buildDocsPageStructuredData } = await server.ssrLoadModule("./src/lib/structured-data");
const { buildDocsPageStructuredData } = await server.ssrLoadModule(
"./src/lib/structured-data",
);

// Serialize the page tree once (the expensive operation).
const rawPageTree = source.getPageTree();
Expand Down
8 changes: 1 addition & 7 deletions src/components/ChangelogTimeline.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ChangelogEntry } from "./ChangelogEntry";
import changelogData from "@/lib/changelog-entries.json";
import { source } from "@/lib/source";

interface ChangelogDataType {
lastUpdated: string;
Expand Down Expand Up @@ -157,7 +156,6 @@ function EmptyState() {
}

const MONTHS_TO_SHOW = 3;
const VALID_CHANGELOG_URLS = new Set(source.getPages().map((page) => page.url));

export function ChangelogTimeline() {
const data = changelogData as ChangelogDataType;
Expand All @@ -166,11 +164,7 @@ export function ChangelogTimeline() {
return <EmptyState />;
}

const recentEntries = deduplicateByPath(
filterToRecentMonths(data.entries, MONTHS_TO_SHOW).filter((entry) =>
VALID_CHANGELOG_URLS.has(entry.url),
),
);
const recentEntries = deduplicateByPath(filterToRecentMonths(data.entries, MONTHS_TO_SHOW));

if (recentEntries.length === 0) {
return <EmptyState />;
Expand Down
15 changes: 15 additions & 0 deletions src/components/DocsError.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import { describe, test } from "node:test";
import { renderToString } from "react-dom/server";
import { DocsError } from "./DocsError";

describe("DocsError", () => {
test("renders an index-safe docs fallback instead of TanStack's default text", () => {
const html = renderToString(<DocsError error={new Error("boom")} reset={() => {}} />);

assert.match(html, /Error loading docs page/);
assert.doesNotMatch(html, /Something went wrong!/);
assert.doesNotMatch(html, /Show Error/);
assert.doesNotMatch(html, /boom/);
});
});
78 changes: 78 additions & 0 deletions src/components/DocsError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { ErrorComponentProps } from "@tanstack/react-router";
import { AlertTriangle, Home, RefreshCw } from "lucide-react";
import { useEffect } from "react";
import { ERROR_PAGE_DESCRIPTION, ERROR_PAGE_TITLE, NOINDEX_ROBOTS_META } from "@/lib/metadata";
import { buildDocsPath } from "@/lib/url-base";

type DocsErrorProps = Pick<ErrorComponentProps, "reset"> & {
error?: unknown;
};

function reloadPage(reset?: ErrorComponentProps["reset"]) {
reset?.();
if (typeof window !== "undefined") {
window.location.reload();
}
}

function upsertMeta(name: string, content: string) {
let element = document.querySelector(`meta[name="${name}"]`);
if (!(element instanceof HTMLMetaElement)) {
element = document.createElement("meta");
element.name = name;
document.head.append(element);
}

element.content = content;
return element;
}

export function DocsError({ reset }: DocsErrorProps) {
useEffect(() => {
document.title = ERROR_PAGE_TITLE;
upsertMeta("description", ERROR_PAGE_DESCRIPTION);
const robotsMeta = upsertMeta("robots", NOINDEX_ROBOTS_META);

return () => {
if (robotsMeta.content === NOINDEX_ROBOTS_META) {
robotsMeta.remove();
}
};
}, []);

return (
<main className="flex min-h-screen items-center justify-center bg-fd-background px-6 py-16 text-fd-foreground">
<section className="w-full max-w-xl rounded-lg border border-fd-border bg-fd-card p-6 shadow-sm">
<div className="flex items-start gap-4">
<div className="flex size-10 shrink-0 items-center justify-center rounded-md bg-fd-muted text-fd-muted-foreground">
<AlertTriangle className="size-5" aria-hidden="true" />
</div>
<div className="min-w-0">
<h1 className="text-xl font-semibold tracking-tight">Error loading docs page</h1>
<p className="mt-2 text-sm leading-6 text-fd-muted-foreground">
An error prevented this page from loading. Try refreshing, or go back to the docs home
page.
</p>
</div>
</div>
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
<button
type="button"
onClick={() => reloadPage(reset)}
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-transparent bg-fd-primary px-4 text-sm font-medium text-fd-primary-foreground transition-colors hover:bg-fd-primary/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring"
>
<RefreshCw className="size-4" aria-hidden="true" />
Refresh
</button>
<a
href={buildDocsPath()}
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-fd-border px-4 text-sm font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring"
>
<Home className="size-4" aria-hidden="true" />
Docs home
</a>
</div>
</section>
</main>
);
}
24 changes: 24 additions & 0 deletions src/components/GlobalScripts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,30 @@

import { useEffect } from "react";

const SUPERCHAT_FRAME_ID = "arona-frame";
const SUPERCHAT_FRAME_LABEL = "Superwall support chat";

function labelSuperchatFrame() {
const frame = document.getElementById(SUPERCHAT_FRAME_ID);
if (!(frame instanceof HTMLIFrameElement)) return false;

frame.title ||= SUPERCHAT_FRAME_LABEL;
if (!frame.getAttribute("aria-label")) {
frame.setAttribute("aria-label", SUPERCHAT_FRAME_LABEL);
}

return true;
}

export function GlobalScripts() {
useEffect(() => {
const observer = new MutationObserver(() => {
if (labelSuperchatFrame()) observer.disconnect();
});
if (!labelSuperchatFrame()) {
observer.observe(document.body, { childList: true, subtree: true });
}

const meshSdkKey = (window as any).__ENV__?.MESH_SDK_KEY;
const unifyScriptSrc = (window as any).__ENV__?.UNIFY_SCRIPT_SRC;
const unifyApiKey = (window as any).__ENV__?.UNIFY_API_KEY;
Expand Down Expand Up @@ -167,6 +189,8 @@ export function GlobalScripts() {
// On error, do nothing
}
})();

return () => observer.disconnect();
}, []);

return null;
Expand Down
2 changes: 1 addition & 1 deletion src/components/SdkLatestVersion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function SdkLatestVersion({ version, repoUrl, className }: SdkLatestVersi
target="_blank"
rel="noopener noreferrer"
className={cn(
"inline-flex w-full items-center text-base italic text-fd-muted-foreground opacity-50 no-underline transition-opacity hover:opacity-100",
"inline-flex w-full items-center text-base italic text-fd-muted-foreground no-underline transition-colors hover:text-fd-foreground",
className,
)}
>
Expand Down
Loading
Loading