Load the full free Google Fonts gallery in Zona de marca typography.
Browse ~1950 OFL fonts with search, category filters and live preview; one click adds a font to the brand kit. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
112
src/lib/google-fonts-catalog.ts
Normal file
112
src/lib/google-fonts-catalog.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Full free Google Fonts catalog (entire Google Fonts library is free/OSS).
|
||||
* Fetched server-side from fonts.google.com metadata; cached in memory.
|
||||
*/
|
||||
|
||||
export type GoogleFontEntry = {
|
||||
family: string;
|
||||
category: string;
|
||||
variants: string[];
|
||||
subsets: string[];
|
||||
popularity: number;
|
||||
};
|
||||
|
||||
type Cache = {
|
||||
at: number;
|
||||
fonts: GoogleFontEntry[];
|
||||
};
|
||||
|
||||
const TTL_MS = 24 * 60 * 60 * 1000;
|
||||
let cache: Cache | null = null;
|
||||
|
||||
function specimenUrl(family: string): string {
|
||||
return `https://fonts.google.com/specimen/${encodeURIComponent(
|
||||
family.replace(/ /g, '+')
|
||||
)}`;
|
||||
}
|
||||
|
||||
export function googleFontSpecimenUrl(family: string): string {
|
||||
return specimenUrl(family);
|
||||
}
|
||||
|
||||
export function googleFontCssUrl(
|
||||
family: string,
|
||||
weights = '400;500;600;700'
|
||||
): string {
|
||||
const fam = family.replace(/ /g, '+');
|
||||
return `https://fonts.googleapis.com/css2?family=${fam}:wght@${weights}&display=swap`;
|
||||
}
|
||||
|
||||
async function fetchCatalog(): Promise<GoogleFontEntry[]> {
|
||||
if (cache && Date.now() - cache.at < TTL_MS) return cache.fonts;
|
||||
|
||||
// Official metadata (all free fonts on Google Fonts)
|
||||
const r = await fetch('https://fonts.google.com/metadata/fonts', {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': '4Ventures-CentroDeMando/1.0',
|
||||
},
|
||||
next: { revalidate: 86400 },
|
||||
});
|
||||
if (!r.ok) {
|
||||
throw new Error(`Google Fonts metadata ${r.status}`);
|
||||
}
|
||||
let text = await r.text();
|
||||
// Response is prefixed with )]}'
|
||||
if (text.startsWith(")]}'")) {
|
||||
text = text.replace(/^\)\]\}'\n?/, '');
|
||||
}
|
||||
const json = JSON.parse(text) as {
|
||||
familyMetadataList?: Array<{
|
||||
family?: string;
|
||||
category?: string;
|
||||
fonts?: Record<string, unknown>;
|
||||
subsets?: string[];
|
||||
popularity?: number;
|
||||
}>;
|
||||
};
|
||||
const list = json.familyMetadataList || [];
|
||||
const fonts: GoogleFontEntry[] = list
|
||||
.filter((f) => f.family)
|
||||
.map((f, i) => ({
|
||||
family: String(f.family),
|
||||
category: String(f.category || 'sans-serif'),
|
||||
variants: f.fonts ? Object.keys(f.fonts) : ['400', '700'],
|
||||
subsets: Array.isArray(f.subsets) ? f.subsets.map(String) : ['latin'],
|
||||
popularity: typeof f.popularity === 'number' ? f.popularity : i,
|
||||
}))
|
||||
.sort((a, b) => a.popularity - b.popularity || a.family.localeCompare(b.family));
|
||||
|
||||
cache = { at: Date.now(), fonts };
|
||||
return fonts;
|
||||
}
|
||||
|
||||
export async function listGoogleFonts(opts?: {
|
||||
q?: string;
|
||||
category?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<{
|
||||
total: number;
|
||||
fonts: GoogleFontEntry[];
|
||||
categories: string[];
|
||||
}> {
|
||||
const all = await fetchCatalog();
|
||||
const categories = [...new Set(all.map((f) => f.category))].sort();
|
||||
const q = (opts?.q || '').trim().toLowerCase();
|
||||
const cat = (opts?.category || '').trim().toLowerCase();
|
||||
let filtered = all;
|
||||
if (cat && cat !== 'all') {
|
||||
filtered = filtered.filter((f) => f.category.toLowerCase() === cat);
|
||||
}
|
||||
if (q) {
|
||||
filtered = filtered.filter((f) => f.family.toLowerCase().includes(q));
|
||||
}
|
||||
const offset = Math.max(0, opts?.offset || 0);
|
||||
const limit = Math.min(200, Math.max(1, opts?.limit || 60));
|
||||
return {
|
||||
total: filtered.length,
|
||||
fonts: filtered.slice(offset, offset + limit),
|
||||
categories,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user