Skip to content

Commit 1864b70

Browse files
authored
Merge pull request #71 from lyfie-org/development
feat: refactored website - more static and lighter on cloudflare workers
2 parents 4c4e7fa + 89a5087 commit 1864b70

10 files changed

Lines changed: 232 additions & 151 deletions

File tree

apps/web/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Next.js App Router website for Luthor marketing, demo, and documentation.
66

77
1. SEO-focused landing page at `/` with:
88
- Structured data (`SoftwareApplication`, `SoftwareSourceCode`, `WebSite`, `Organization`, `FAQPage`)
9-
- Live package credibility metrics from npm + GitHub APIs
9+
- Build-time package credibility metrics from npm APIs
1010
- Crawlable semantic content and canonical metadata
1111
2. Live demo page at `/demo/` using the extensive editor preset.
1212
3. Markdown documentation pages under `/docs/**` rendered from:
@@ -17,10 +17,11 @@ Next.js App Router website for Luthor marketing, demo, and documentation.
1717
## Scripts
1818

1919
- `npm run sync:docs`: index docs from `src/content/docs/` and regenerate `src/data/docs-index.generated.ts`.
20+
- `npm run sync:metrics`: fetch package telemetry and regenerate `src/data/homepage-metrics.generated.ts`.
2021
- `npm run sync:llms`: regenerate `public/llms.txt` and `public/llms-full.txt`.
21-
- `npm run sync:content`: run both sync jobs in order.
22-
- `npm run dev`: sync docs/LLM content, then run Next dev server.
23-
- `npm run build`: sync docs/LLM content, then run Next production build.
22+
- `npm run sync:content`: run metrics, docs, and LLM sync jobs in order.
23+
- `npm run dev`: sync metrics/docs/LLM content, then run Next dev server.
24+
- `npm run build`: sync metrics/docs/LLM content, then run Next production build.
2425
- `npm run start`: run the built Next.js server.
2526
- `npm run preview`: preview Cloudflare/OpenNext output with Wrangler.
2627

apps/web/next.config.mjs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@
55
* Build freely. Credit kindly.
66
*/
77

8+
const edgeCachedHtmlHeaders = [
9+
{
10+
key: 'Cache-Control',
11+
value: 'public, max-age=0, s-maxage=86400, stale-while-revalidate=3600',
12+
},
13+
];
14+
15+
const longLivedAssetHeaders = [
16+
{
17+
key: 'Cache-Control',
18+
value: 'public, max-age=31536000, immutable',
19+
},
20+
];
21+
822
/** @type {import('next').NextConfig} */
923
const nextConfig = {
1024
reactStrictMode: true,
@@ -69,14 +83,37 @@ const nextConfig = {
6983
},
7084
],
7185
},
86+
{
87+
source: '/',
88+
headers: edgeCachedHtmlHeaders,
89+
},
90+
{
91+
source: '/demo/',
92+
headers: edgeCachedHtmlHeaders,
93+
},
94+
{
95+
source: '/docs/:path*',
96+
headers: edgeCachedHtmlHeaders,
97+
},
98+
{
99+
source: '/sitemap.xml',
100+
headers: edgeCachedHtmlHeaders,
101+
},
102+
{
103+
source: '/llms.txt',
104+
headers: edgeCachedHtmlHeaders,
105+
},
106+
{
107+
source: '/llms-full.txt',
108+
headers: edgeCachedHtmlHeaders,
109+
},
110+
{
111+
source: '/_next/static/:path*',
112+
headers: longLivedAssetHeaders,
113+
},
72114
{
73115
source: '/:all*(svg|png|jpg|jpeg|gif|webp|avif|ico)',
74-
headers: [
75-
{
76-
key: 'Cache-Control',
77-
value: 'public, max-age=31536000, immutable',
78-
},
79-
],
116+
headers: longLivedAssetHeaders,
80117
},
81118
];
82119
},

apps/web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
"type": "module",
44
"version": "0.0.1",
55
"scripts": {
6+
"sync:metrics": "node ./scripts/sync-metrics.mjs",
67
"sync:docs": "node ./scripts/sync-docs.mjs",
78
"sync:llms": "node ./scripts/generate-llms.mjs",
8-
"sync:content": "pnpm run sync:docs && pnpm run sync:llms",
9+
"sync:content": "pnpm run sync:metrics && pnpm run sync:docs && pnpm run sync:llms",
910
"seo:check": "node ./scripts/seo-validate.mjs",
1011
"predev": "pnpm run sync:content",
1112
"prebuild": "pnpm run sync:content",

apps/web/public/llms-full.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Concatenated markdown corpus for AI ingestion.
88
- Rahul Anand is the creator and BDFL of Lyfie.org: [https://www.rahulnsanand.com](https://www.rahulnsanand.com)
99

1010
Source root: C:\Users\rahul\Documents\GitHub\luthor\apps\web\src\content\docs
11-
Generated at: 2026-05-29T02:40:22.717Z
11+
Generated at: 2026-05-29T02:55:40.091Z
1212

1313
## AI Agent Workflow
1414

apps/web/scripts/sync-metrics.mjs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { mkdir, writeFile } from 'node:fs/promises';
2+
import path from 'node:path';
3+
4+
const PRIMARY_PACKAGE_NAME = '@lyfie/luthor';
5+
const HEADLESS_PACKAGE_NAME = '@lyfie/luthor-headless';
6+
7+
const FALLBACK_METRICS = {
8+
totalDownloads: null,
9+
lastMonthDownloads: null,
10+
latestVersion: 'N/A',
11+
headlessVersion: 'N/A',
12+
luthorPackageSize: null,
13+
headlessPackageSize: null,
14+
combinedPackageSize: null,
15+
releaseCount: null,
16+
fetchedAtIso: null,
17+
};
18+
19+
async function fetchJson(url) {
20+
const controller = new AbortController();
21+
const timeout = setTimeout(() => controller.abort(), 5000);
22+
23+
try {
24+
const response = await fetch(url, {
25+
headers: {
26+
accept: 'application/json',
27+
},
28+
signal: controller.signal,
29+
});
30+
31+
if (!response.ok) {
32+
return null;
33+
}
34+
35+
return await response.json();
36+
} catch {
37+
return null;
38+
} finally {
39+
clearTimeout(timeout);
40+
}
41+
}
42+
43+
function toIsoDate(value) {
44+
if (!value) return null;
45+
const parsed = new Date(value);
46+
if (Number.isNaN(parsed.getTime())) return null;
47+
return parsed.toISOString().slice(0, 10);
48+
}
49+
50+
function numberOrNull(value) {
51+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
52+
}
53+
54+
async function getDownloadPoint(range, packageName) {
55+
const encodedPackageName = encodeURIComponent(packageName);
56+
const response = await fetchJson(`https://api.npmjs.org/downloads/point/${range}/${encodedPackageName}`);
57+
return numberOrNull(response?.downloads);
58+
}
59+
60+
async function getPackageMetrics() {
61+
const [luthorRegistry, headlessRegistry] = await Promise.all([
62+
fetchJson(`https://registry.npmjs.org/${encodeURIComponent(PRIMARY_PACKAGE_NAME)}`),
63+
fetchJson(`https://registry.npmjs.org/${encodeURIComponent(HEADLESS_PACKAGE_NAME)}`),
64+
]);
65+
66+
const today = new Date().toISOString().slice(0, 10);
67+
const luthorCreatedDate = toIsoDate(luthorRegistry?.time?.created);
68+
const headlessCreatedDate = toIsoDate(headlessRegistry?.time?.created);
69+
70+
const [luthorTotalDownloads, headlessTotalDownloads, luthorLastMonthDownloads, headlessLastMonthDownloads] =
71+
await Promise.all([
72+
luthorCreatedDate ? getDownloadPoint(`${luthorCreatedDate}:${today}`, PRIMARY_PACKAGE_NAME) : Promise.resolve(null),
73+
headlessCreatedDate ? getDownloadPoint(`${headlessCreatedDate}:${today}`, HEADLESS_PACKAGE_NAME) : Promise.resolve(null),
74+
getDownloadPoint('last-month', PRIMARY_PACKAGE_NAME),
75+
getDownloadPoint('last-month', HEADLESS_PACKAGE_NAME),
76+
]);
77+
78+
const totalDownloads =
79+
typeof luthorTotalDownloads === 'number' || typeof headlessTotalDownloads === 'number'
80+
? (luthorTotalDownloads ?? 0) + (headlessTotalDownloads ?? 0)
81+
: null;
82+
const lastMonthDownloads =
83+
typeof luthorLastMonthDownloads === 'number' || typeof headlessLastMonthDownloads === 'number'
84+
? (luthorLastMonthDownloads ?? 0) + (headlessLastMonthDownloads ?? 0)
85+
: null;
86+
87+
const latestVersion = luthorRegistry?.['dist-tags']?.latest ?? 'N/A';
88+
const headlessVersion = headlessRegistry?.['dist-tags']?.latest ?? 'N/A';
89+
const luthorPackageSize =
90+
latestVersion !== 'N/A' ? numberOrNull(luthorRegistry?.versions?.[latestVersion]?.dist?.unpackedSize) : null;
91+
const headlessPackageSize =
92+
headlessVersion !== 'N/A' ? numberOrNull(headlessRegistry?.versions?.[headlessVersion]?.dist?.unpackedSize) : null;
93+
const combinedPackageSize =
94+
typeof luthorPackageSize === 'number' || typeof headlessPackageSize === 'number'
95+
? (luthorPackageSize ?? 0) + (headlessPackageSize ?? 0)
96+
: null;
97+
const luthorReleaseCount = luthorRegistry?.versions ? Object.keys(luthorRegistry.versions).length : 0;
98+
const headlessReleaseCount = headlessRegistry?.versions ? Object.keys(headlessRegistry.versions).length : 0;
99+
const releaseCount =
100+
luthorReleaseCount > 0 || headlessReleaseCount > 0 ? luthorReleaseCount + headlessReleaseCount : null;
101+
102+
const hasLiveData = Boolean(
103+
luthorRegistry ||
104+
headlessRegistry ||
105+
luthorTotalDownloads ||
106+
headlessTotalDownloads ||
107+
luthorLastMonthDownloads ||
108+
headlessLastMonthDownloads,
109+
);
110+
111+
return {
112+
totalDownloads,
113+
lastMonthDownloads,
114+
latestVersion,
115+
headlessVersion,
116+
luthorPackageSize,
117+
headlessPackageSize,
118+
combinedPackageSize,
119+
releaseCount,
120+
fetchedAtIso: hasLiveData ? new Date().toISOString() : null,
121+
};
122+
}
123+
124+
function serializeMetrics(metrics) {
125+
return `export const homepageMetrics = ${JSON.stringify(metrics, null, 2)} as const;\n`;
126+
}
127+
128+
async function writeMetrics(metrics) {
129+
const targetDir = path.join(process.cwd(), 'src', 'data');
130+
await mkdir(targetDir, { recursive: true });
131+
await writeFile(path.join(targetDir, 'homepage-metrics.generated.ts'), serializeMetrics(metrics));
132+
}
133+
134+
try {
135+
await writeMetrics(await getPackageMetrics());
136+
console.log('Homepage metrics synced.');
137+
} catch (error) {
138+
console.warn('Homepage metrics sync failed. Writing fallback metrics.');
139+
console.warn(error);
140+
await writeMetrics(FALLBACK_METRICS);
141+
}

apps/web/src/app/demo/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import type { Metadata } from 'next';
99
import { PresetShowcaseShell } from '@/features/editor/preset-showcase-shell';
1010

11+
export const dynamic = 'force-static';
12+
export const revalidate = false;
13+
1114
export const metadata: Metadata = {
1215
title: 'Luthor Demo Playground',
1316
description: 'Explore every Luthor preset live, switch instantly, and evaluate the right React + Lexical editing experience before integrating.',

apps/web/src/app/docs/[[...slug]]/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import { DocsSearch } from '@/features/docs/docs-search';
1919
import { type DocEntry, getAllDocs, getAllDocSlugs, getDocBySlug } from '@/features/docs/docs.service';
2020
import { isExternalWebsiteHref } from '@/utils/link';
2121

22+
export const dynamic = 'force-static';
23+
export const dynamicParams = false;
24+
export const revalidate = false;
25+
2226
type Params = { slug?: string[] };
2327
type NavGroupId = 'start_here' | 'luthor_headless' | 'luthor' | 'integrations' | 'reference' | 'contributing' | 'other';
2428

@@ -330,7 +334,7 @@ function slugifyHeading(text: string): string {
330334

331335
export async function generateStaticParams() {
332336
const slugs = await getAllDocSlugs();
333-
const params = slugs.map((slug) => (slug.length ? { slug } : {}));
337+
const params = [{}, ...slugs.map((slug) => (slug.length ? { slug } : {}))];
334338
return params;
335339
}
336340

0 commit comments

Comments
 (0)