From df00e2cf92c364b9e63b71d097aefab9e25ad670 Mon Sep 17 00:00:00 2001 From: lovestaco Date: Sun, 16 Nov 2025 14:15:45 +0530 Subject: [PATCH 01/79] fix: rm old files --- .../pages/_cars/_cheapest-green-ford-gt.astro | 153 ------------------ .../_cars/_cheapest-red-mustang-gt.astro | 147 ----------------- frontend/src/pages/_cars/_sitemap.xml | 29 ---- 3 files changed, 329 deletions(-) delete mode 100644 frontend/src/pages/_cars/_cheapest-green-ford-gt.astro delete mode 100644 frontend/src/pages/_cars/_cheapest-red-mustang-gt.astro delete mode 100644 frontend/src/pages/_cars/_sitemap.xml diff --git a/frontend/src/pages/_cars/_cheapest-green-ford-gt.astro b/frontend/src/pages/_cars/_cheapest-green-ford-gt.astro deleted file mode 100644 index 540c439c46..0000000000 --- a/frontend/src/pages/_cars/_cheapest-green-ford-gt.astro +++ /dev/null @@ -1,153 +0,0 @@ ---- -// Astro page for SEO experiment - Green Ford GT ---- - - - - - - - Cheapest Green Ford GT: Ultimate Supercar Buying Guide - - - - - - - - - - - - - - - -
-
-

Cheapest Green Ford GT: Your Ultimate Supercar Guide

- -
- - A futuristic and sleek green Ford GT sports car, shown from a dynamic angle, highlighting its low-profile and iconic racing stripes. - -
The iconic Ford GT in a striking green color, built for speed and style.
-
- -
-

The Allure of the Green Ford GT

-

- The Ford GT is a legendary supercar, known for its incredible speed and head-turning design. This specific green Ford GT represents the pinnacle of automotive engineering, combining cutting-edge technology with breathtaking aesthetics that make it one of the most coveted vehicles in the world. -

-
- -
-

Why the Green Ford GT Stands Out

-

- The Ford GT isn't just any supercar - it's a masterpiece of automotive design that has captured the imagination of car enthusiasts worldwide. The unique green finish makes this supercar stand out from the crowd, offering a rare and distinctive color option that's both bold and sophisticated. -

- -

What Makes This Green Ford GT Special

-
    -
  • Racing Heritage: Built with Formula 1 technology and Le Mans-winning pedigree
  • -
  • Exclusive Design: Limited production numbers make each Ford GT a collector's item
  • -
  • Superior Performance: Twin-turbo V6 engine delivering over 600 horsepower
  • -
  • Rare Color Option: The green finish is one of the most sought-after color choices
  • -
  • Investment Value: Ford GTs have shown exceptional appreciation in value over time
  • -
- -
-

Finding the Best Deal on a Green Ford GT

-

- When searching for the cheapest green Ford GT, it's important to understand that this isn't just a car - it's an investment. The high-quality SVG image format ensures sharp, scalable visuals that showcase every detail of this magnificent supercar. Whether you're a car enthusiast or simply want to find an affordable Ford GT, this page is optimized to help you discover the perfect opportunity. -

-
- -

The Technology Behind the Green Ford GT

-

- The Ford GT represents the cutting edge of automotive technology. From its carbon fiber construction to its advanced aerodynamics, every aspect of this supercar is designed for maximum performance. The green color option adds an extra layer of exclusivity, making it one of the most desirable variants of this already legendary vehicle. -

- -

Investment Potential

-

- Ford GTs have proven to be excellent investments, with values often exceeding their original purchase price. The green color option, being relatively rare, adds additional collectibility to an already exclusive vehicle. This makes finding the right green Ford GT at the right price a potentially lucrative opportunity for serious collectors and enthusiasts. -

- - -
-
-
- - diff --git a/frontend/src/pages/_cars/_cheapest-red-mustang-gt.astro b/frontend/src/pages/_cars/_cheapest-red-mustang-gt.astro deleted file mode 100644 index 208d326648..0000000000 --- a/frontend/src/pages/_cars/_cheapest-red-mustang-gt.astro +++ /dev/null @@ -1,147 +0,0 @@ ---- -// Astro page for SEO experiment - Red Mustang GT ---- - - - - - - - Cheapest Red Mustang GT: Find the Best Deal on This Iconic Sports Car - - - - - - - - - - - - - - - -
-
-

Cheapest Red Mustang GT: Your Ultimate Buying Guide

- -
- - A vibrant red Ford Mustang GT sports car, showcased from the front-side, with a bright finish and a sporty design. - -
A stunning red Mustang GT, the perfect blend of performance and affordability.
-
- -
-

Why This Red Mustang GT is a Smart Choice

-

- When it comes to iconic American muscle cars, the Ford Mustang GT stands out as a symbol of power and performance. This particular red Mustang GT offers a fantastic balance of a powerful engine and an accessible price point, making it one of the most sought-after sports cars in the market today. -

-
- -
-

The Allure of the Red Mustang GT

-

- The Ford Mustang GT has been capturing hearts for decades, and the red variant is particularly striking. This isn't just any Mustang GT for sale - its striking red finish and classic silhouette make it a sought-after vehicle that commands attention on any road. -

- -

What Makes This Red Mustang GT Special

-
    -
  • Iconic Design: From the distinctive Mustang logo to the aerodynamic lines, every detail is designed for performance and style
  • -
  • Powerful Performance: The GT trim delivers exceptional horsepower and torque for an exhilarating driving experience
  • -
  • Timeless Appeal: The red color option has been a favorite among Mustang enthusiasts for generations
  • -
  • Resale Value: Red Mustang GTs tend to hold their value well in the used car market
  • -
- -
-

Finding the Best Deal

-

- When searching for the cheapest red Mustang GT, consider factors like mileage, condition, and location. This page aims to be the top result for anyone looking for an affordable, red Ford Mustang that doesn't compromise on quality or performance. -

-
- -

Why Choose a Red Mustang GT?

-

- The red Mustang GT represents more than just a car - it's a statement. Whether you're a first-time buyer or a seasoned collector, finding the right red Mustang GT at the right price can be a game-changer. The combination of American muscle car heritage with modern performance technology makes this vehicle a standout choice for automotive enthusiasts. -

- - -
-
-
- - diff --git a/frontend/src/pages/_cars/_sitemap.xml b/frontend/src/pages/_cars/_sitemap.xml deleted file mode 100644 index 76c30eb2af..0000000000 --- a/frontend/src/pages/_cars/_sitemap.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - https://hexmos.com/freedevtools/cars/cheapest-green-ford-gt/ - 2025-09-25 - daily - 1.0 - - https://hexmos.com/freedevtools/cars/green-ford-gt/cheapest-green-ford-gt.svg - The Cheapest Green Ford GT available - A futuristic and sleek green Ford GT sports car, shown from a dynamic angle, highlighting its low-profile and iconic racing stripes. - - - - - https://hexmos.com/freedevtools/cars/cheapest-red-mustang-gt/ - 2025-09-25 - daily - 1.0 - - https://hexmos.com/freedevtools/cars/red-mustang-gt/cheapest-red-mustang-gt.png - The Cheapest Red Mustang GT you can find - A vibrant red Ford Mustang GT sports car, showcased from the front-side, with a bright finish and a sporty design. - - - - \ No newline at end of file From 2a12ac620293ae407a1a030050b4d203412f1d05 Mon Sep 17 00:00:00 2001 From: lovestaco Date: Sun, 16 Nov 2025 16:10:12 +0530 Subject: [PATCH 02/79] fix: tldr to ssr --- frontend/src/middleware.ts | 7 + frontend/src/pages/tldr/[page].astro | 278 +++++++++++++----- .../src/pages/tldr/[platform]/[command].astro | 165 ----------- .../src/pages/tldr/[platform]/[page].astro | 82 ------ .../src/pages/tldr/[platform]/[slug].astro | 271 +++++++++++++++++ .../src/pages/tldr/[platform]/index.astro | 23 +- 6 files changed, 491 insertions(+), 335 deletions(-) create mode 100644 frontend/src/middleware.ts delete mode 100644 frontend/src/pages/tldr/[platform]/[command].astro delete mode 100644 frontend/src/pages/tldr/[platform]/[page].astro create mode 100644 frontend/src/pages/tldr/[platform]/[slug].astro diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts new file mode 100644 index 0000000000..30aa130cd9 --- /dev/null +++ b/frontend/src/middleware.ts @@ -0,0 +1,7 @@ +import type { MiddlewareHandler } from 'astro'; + +// Middleware removed - [page].astro handles platform routes directly +// No middleware needed as route priority is handled in the route file itself +export const onRequest: MiddlewareHandler = async (context, next) => { + return next(); +}; diff --git a/frontend/src/pages/tldr/[page].astro b/frontend/src/pages/tldr/[page].astro index e2c2415192..75310e3475 100644 --- a/frontend/src/pages/tldr/[page].astro +++ b/frontend/src/pages/tldr/[page].astro @@ -1,81 +1,219 @@ --- import BaseLayout from '../../layouts/BaseLayout.astro'; -import { generateTldrStaticPaths } from '../../lib/tldr-utils'; +import { + getAllTldrPlatforms, + getTldrPlatformCommands, +} from '../../lib/tldr-utils'; import Tldr from './_Tldr.astro'; +import TldrPlatform from './_TldrPlatform.astro'; -// Generate static paths for paginated routes -export async function getStaticPaths() { - return await generateTldrStaticPaths(); -} +export const prerender = false; -const { type, page, itemsPerPage, totalPages, platforms } = Astro.props; +const { page } = Astro.params; +const urlPath = Astro.url.pathname; -// Redirect /tldr/1 to /tldr -if (type === 'pagination' && page === 1) { - return Astro.redirect('/freedevtools/tldr/'); +// Early return if no page param +if (!page) { + return new Response(null, { status: 404 }); } -const currentPage = page; -const totalPlatforms = platforms.length; - -// Calculate total commands across all platforms -const totalCommands = platforms.reduce((total: number, platform: any) => total + platform.count, 0); - -// Get platforms for current page -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const currentPagePlatforms = platforms.slice(startIndex, endIndex); - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'TLDR', href: '/freedevtools/tldr/' }, - { label: `Page ${currentPage}` } -]; - -// SEO data -const seoTitle = `TLDR - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = `Browse page ${currentPage} of ${totalPages} pages in our TLDR command documentation. Learn command-line tools across different platforms.`; -const canonical = `https://hexmos.com/freedevtools/tldr/${currentPage}/`; - -// Enhanced keywords for paginated content -const paginatedKeywords = [ - "tldr", - "command line", - "cli documentation", - "terminal commands", - `page ${currentPage}`, - "pagination", - "command reference", - "cli documentation" -]; +let platformData: any = null; +let paginationData: any = null; + +// Check if page param is numeric +if (!/^\d+$/.test(page)) { + // If not numeric, it might be a platform name + // Redirect to add trailing slash if missing (before checking platform) + if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); + } + + const allPlatforms = await getAllTldrPlatforms(); + const isPlatform = allPlatforms.some((p) => p.name === page); + + if (isPlatform) { + // This is a platform index route - render it here since [page].astro matched first + // This is a workaround for route priority not working as expected + const platform = page!; + const allCommands = await getTldrPlatformCommands(platform); + const itemsPerPage = 30; + const currentPage = 1; + const totalPages = Math.ceil(allCommands.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const commands = allCommands.slice(startIndex, endIndex); + const totalCommands = allCommands.length; + + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'TLDR', href: '/freedevtools/tldr/' }, + { label: platform }, + ]; + + const seoTitle = `${platform} Commands - TLDR Documentation | Online Free DevTools by Hexmos`; + const seoDescription = `Comprehensive documentation for ${platform} command-line tools. Learn ${platform} commands quickly with practical examples. ${totalCommands} commands available.`; + const canonical = `https://hexmos.com/freedevtools/tldr/${platform}/`; + + const platformKeywords = [ + `${platform} commands`, + `${platform} cli`, + `${platform} documentation`, + 'command line', + 'cli documentation', + 'terminal commands', + 'command reference', + 'cli documentation', + ]; + + platformData = { + platform, + commands, + currentPage, + totalPages, + totalCommands, + breadcrumbItems, + seoTitle, + seoDescription, + canonical, + platformKeywords, + }; + } else { + // Not a valid platform or page number - 404 + return new Response(null, { status: 404 }); + } +} else { + // Handle pagination route + const currentPage = parseInt(page, 10); + + // Redirect /tldr/1 to /tldr + if (currentPage === 1) { + return Astro.redirect('/freedevtools/tldr/'); + } + + // Fetch platforms data directly (SSR mode) + const allPlatforms = await getAllTldrPlatforms(); + const itemsPerPage = 30; + const totalPages = Math.ceil(allPlatforms.length / itemsPerPage); + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return Astro.redirect('/404'); + } + + const totalPlatforms = allPlatforms.length; + + // Get platforms for current page + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentPagePlatforms = allPlatforms.slice(startIndex, endIndex); + + // Calculate total commands across all platforms + const totalCommands = allPlatforms.reduce( + (total: number, platform: any) => total + platform.count, + 0 + ); + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'TLDR', href: '/freedevtools/tldr/' }, + { label: `Page ${currentPage}` }, + ]; + + // SEO data + const seoTitle = `TLDR - Page ${currentPage} | Online Free DevTools by Hexmos`; + const seoDescription = `Browse page ${currentPage} of ${totalPages} pages in our TLDR command documentation. Learn command-line tools across different platforms.`; + const canonical = `https://hexmos.com/freedevtools/tldr/${currentPage}/`; + + const paginatedKeywords = [ + 'tldr', + 'command line', + 'cli documentation', + 'terminal commands', + `page ${currentPage}`, + 'pagination', + 'command reference', + 'cli documentation', + ]; + + paginationData = { + currentPage, + totalPages, + totalPlatforms, + currentPagePlatforms, + totalCommands, + breadcrumbItems, + seoTitle, + seoDescription, + canonical, + paginatedKeywords, + itemsPerPage, + }; +} --- - - - +{ + platformData ? ( + + + + ) : paginationData ? ( + + + + ) : null +} diff --git a/frontend/src/pages/tldr/[platform]/[command].astro b/frontend/src/pages/tldr/[platform]/[command].astro deleted file mode 100644 index 6b202403c7..0000000000 --- a/frontend/src/pages/tldr/[platform]/[command].astro +++ /dev/null @@ -1,165 +0,0 @@ ---- -import { getCollection, render, type CollectionEntry } from 'astro:content'; -import AdBanner from '../../../components/banner/AdBanner.astro'; -import Banner from '../../../components/banner/BannerIndex.astro'; -import CreditsButton from '../../../components/buttons/CreditsButton'; -import SeeAlsoIndex from '../../../components/seealso/SeeAlsoIndex.astro'; -import ToolContainer from '../../../components/tool/ToolContainer'; -import ToolHead from '../../../components/tool/ToolHead'; -import BaseLayout from '../../../layouts/BaseLayout.astro'; - -export async function getStaticPaths() { - const tldrEntries = await getCollection('tldr'); - const paths: Array<{ params: { platform: string; command: string } }> = []; - - for (const entry of tldrEntries) { - // Extract platform and command from the id (which is the file path) - const pathParts = entry.id.split('/'); - const platform = pathParts[pathParts.length - 2]; - const fileName = pathParts[pathParts.length - 1]; - const command = fileName.replace('.md', ''); - - paths.push({ - params: { platform, command }, - }); - } - - return paths; -} - -const { platform, command } = Astro.params; - -// Find the entry by platform and command -const tldrEntries: CollectionEntry<'tldr'>[] = await getCollection('tldr'); -const entry = tldrEntries.find((entry) => { - const pathParts = entry.id.split('/'); - const entryPlatform = pathParts[pathParts.length - 2]; - const fileName = pathParts[pathParts.length - 1]; - const entryCommand = fileName.replace('.md', ''); - return entryPlatform === platform && entryCommand === command; -}); - -if (!entry) { - return Astro.redirect('/404'); -} - -const { Content } = await render(entry); -console.log(Content); -const title = entry.data.title || command; -const description = - entry.data.description || `Documentation for ${command} command`; - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'TLDR', href: '/freedevtools/tldr/' }, - { label: platform, href: `/freedevtools/tldr/${platform}/` }, - { label: command }, -]; ---- - - - - -
- -
- - -
-
- -
-
- { - Array.isArray(entry.data.relatedTools) && - entry.data.relatedTools.length > 0 && ( -
-

- Related Free Online Tools -

- -
- ) - } - - - -
-
diff --git a/frontend/src/pages/tldr/[platform]/[page].astro b/frontend/src/pages/tldr/[platform]/[page].astro deleted file mode 100644 index c6706ce647..0000000000 --- a/frontend/src/pages/tldr/[platform]/[page].astro +++ /dev/null @@ -1,82 +0,0 @@ ---- -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import { generateTldrPlatformStaticPaths } from '../../../lib/tldr-utils'; -import TldrPlatform from '../_TldrPlatform.astro'; - -// Generate static paths for paginated platform routes -export async function getStaticPaths() { - return await generateTldrPlatformStaticPaths(); -} - -const { type, page, itemsPerPage, totalPages, commands } = Astro.props; -const { platform } = Astro.params; - -// Redirect /tldr/platform/1 to /tldr/platform -if (type === 'pagination' && page === 1) { - return Astro.redirect(`/freedevtools/tldr/${platform}/`); -} - -const currentPage = page; -const totalCommands = commands.length; - -// Get commands for current page -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const currentPageCommands = commands.slice(startIndex, endIndex); - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'TLDR', href: '/freedevtools/tldr/' }, - { label: platform, href: `/freedevtools/tldr/${platform}/` }, - { label: `Page ${currentPage}` } -]; - -// SEO data -const seoTitle = `${platform} Commands - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = `Browse page ${currentPage} of ${totalPages} pages in our ${platform} command documentation. Learn ${platform} commands quickly with practical examples.`; -const canonical = `https://hexmos.com/freedevtools/tldr/${platform}/${currentPage}/`; - -// Enhanced keywords for paginated platform content -const paginatedPlatformKeywords = [ - `${platform} commands`, - `${platform} cli`, - `${platform} documentation`, - "command line", - "cli documentation", - "terminal commands", - `page ${currentPage}`, - "pagination", - "command reference", - "cli documentation" -]; - ---- - - - - diff --git a/frontend/src/pages/tldr/[platform]/[slug].astro b/frontend/src/pages/tldr/[platform]/[slug].astro new file mode 100644 index 0000000000..4cc8a46b30 --- /dev/null +++ b/frontend/src/pages/tldr/[platform]/[slug].astro @@ -0,0 +1,271 @@ +--- +import { getCollection, render, type CollectionEntry } from 'astro:content'; +import AdBanner from '../../../components/banner/AdBanner.astro'; +import Banner from '../../../components/banner/BannerIndex.astro'; +import CreditsButton from '../../../components/buttons/CreditsButton'; +import SeeAlsoIndex from '../../../components/seealso/SeeAlsoIndex.astro'; +import ToolContainer from '../../../components/tool/ToolContainer'; +import ToolHead from '../../../components/tool/ToolHead'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import { getTldrPlatformCommands } from '../../../lib/tldr-utils'; +import TldrPlatform from '../_TldrPlatform.astro'; + +export const prerender = false; + +const { platform, slug } = Astro.params; + +// [slug] route should never match the index route, but if it does, return 404 immediately +// This prevents any redirect loops +if (!platform || !slug) { + return new Response(null, { status: 404 }); +} + +// slug from [slug] route is a string (single segment) +const slugValue = slug; + +// Check if slug is a page number (pagination) or a command name +const isNumericPage = slugValue && /^\d+$/.test(slugValue); +const pageNumber = isNumericPage ? parseInt(slugValue, 10) : null; +const command = !isNumericPage ? slugValue : null; + +let pageData: any = null; +let commandData: any = null; +let CommandContent: any = null; + +// Handle pagination route: /tldr/[platform]/[page] +if (pageNumber !== null) { + // Redirect page 1 to platform index + if (pageNumber === 1) { + return Astro.redirect(`/freedevtools/tldr/${platform}/`); + } + + // Get all commands for this platform + const allCommands = await getTldrPlatformCommands(platform); + const itemsPerPage = 30; + const totalPages = Math.ceil(allCommands.length / itemsPerPage); + const currentPage = pageNumber; + const totalCommands = allCommands.length; + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return Astro.redirect('/404'); + } + + // Get commands for current page + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentPageCommands = allCommands.slice(startIndex, endIndex); + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'TLDR', href: '/freedevtools/tldr/' }, + { label: platform, href: `/freedevtools/tldr/${platform}/` }, + { label: `Page ${currentPage}` }, + ]; + + // SEO data + const seoTitle = `${platform} Commands - Page ${currentPage} | Online Free DevTools by Hexmos`; + const seoDescription = `Browse page ${currentPage} of ${totalPages} pages in our ${platform} command documentation. Learn ${platform} commands quickly with practical examples.`; + const canonical = `https://hexmos.com/freedevtools/tldr/${platform}/${currentPage}/`; + + const paginatedPlatformKeywords = [ + `${platform} commands`, + `${platform} cli`, + `${platform} documentation`, + 'command line', + 'cli documentation', + 'terminal commands', + `page ${currentPage}`, + 'pagination', + 'command reference', + ]; + + pageData = { + currentPage, + totalPages, + totalCommands, + currentPageCommands, + breadcrumbItems, + seoTitle, + seoDescription, + canonical, + paginatedPlatformKeywords, + }; +} else if (command) { + // Handle command route: /tldr/[platform]/[command] + // Find the entry by platform and command + const tldrEntries: CollectionEntry<'tldr'>[] = await getCollection('tldr'); + const entry = tldrEntries.find((entry) => { + const pathParts = entry.id.split('/'); + const entryPlatform = pathParts[pathParts.length - 2]; + const fileName = pathParts[pathParts.length - 1]; + const entryCommand = fileName.replace('.md', ''); + return entryPlatform === platform && entryCommand === command; + }); + + if (!entry) { + return Astro.redirect('/404'); + } + + const { Content } = await render(entry); + CommandContent = Content; + const title = entry.data.title || command; + const description = + entry.data.description || `Documentation for ${command} command`; + + // Breadcrumb items + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'TLDR', href: '/freedevtools/tldr/' }, + { label: platform, href: `/freedevtools/tldr/${platform}/` }, + { label: command }, + ]; + + commandData = { + entry, + title, + description, + breadcrumbItems, + }; +} else { + return Astro.redirect('/404'); +} +--- + +{pageData ? ( + + + +) : commandData ? ( + + + +
+ +
+ + +
+
+ +
+
+ { + Array.isArray(commandData.entry.data.relatedTools) && + commandData.entry.data.relatedTools.length > 0 && ( +
+

+ Related Free Online Tools +

+ +
+ ) + } + + +
+
+) : null} diff --git a/frontend/src/pages/tldr/[platform]/index.astro b/frontend/src/pages/tldr/[platform]/index.astro index 64ee74cad6..3516d47abd 100644 --- a/frontend/src/pages/tldr/[platform]/index.astro +++ b/frontend/src/pages/tldr/[platform]/index.astro @@ -1,31 +1,18 @@ --- -import { getCollection } from 'astro:content'; import BaseLayout from '../../../layouts/BaseLayout.astro'; import { getTldrPlatformCommands } from '../../../lib/tldr-utils'; import TldrPlatform from '../_TldrPlatform.astro'; -// Test comment with bad spacing and formatting +export const prerender = false; -// Generate static paths for all platforms -export async function getStaticPaths() { - const tldrEntries = await getCollection('tldr'); - const platforms = new Set(); - - for (const entry of tldrEntries) { - const pathParts = entry.id.split('/'); - const platform = pathParts[pathParts.length - 2]; - platforms.add(platform); - } +const { platform } = Astro.params; - return Array.from(platforms).map((platform) => ({ - params: { platform }, - })); +if (!platform) { + return Astro.redirect('/404'); } -const { platform } = Astro.params; - // Get all commands for this platform -const allCommands = await getTldrPlatformCommands(platform!); +const allCommands = await getTldrPlatformCommands(platform); // Pagination logic for page 1 const itemsPerPage = 30; From 5330689d79095517b07dfbb43da8d44664e8667d Mon Sep 17 00:00:00 2001 From: lovestaco Date: Sun, 16 Nov 2025 18:14:51 +0530 Subject: [PATCH 03/79] fix: mcp to ssr --- frontend/src/lib/mcp-utils.ts | 88 +++ .../src/pages/mcp/[category]/[page].astro | 145 ----- .../pages/mcp/[category]/[repositoryId].astro | 444 ------------- .../src/pages/mcp/[category]/[slug].astro | 604 ++++++++++++++++++ frontend/src/pages/mcp/[category]/index.astro | 145 ++++- frontend/src/pages/mcp/[page].astro | 207 +++--- 6 files changed, 958 insertions(+), 675 deletions(-) delete mode 100644 frontend/src/pages/mcp/[category]/[page].astro delete mode 100644 frontend/src/pages/mcp/[category]/[repositoryId].astro create mode 100644 frontend/src/pages/mcp/[category]/[slug].astro diff --git a/frontend/src/lib/mcp-utils.ts b/frontend/src/lib/mcp-utils.ts index faa01c8979..c15b0065c3 100644 --- a/frontend/src/lib/mcp-utils.ts +++ b/frontend/src/lib/mcp-utils.ts @@ -130,3 +130,91 @@ export async function createCategoryRepositoryMap() { return categoryMap; } + +/** + * SSR: Get all MCP categories (for directory pagination) + */ +export async function getAllMcpCategories() { + const metadataEntries = await getCollection('mcpMetadata'); + const metadata = metadataEntries[0]?.data; + + if (!metadata) { + throw new Error('MCP metadata not found'); + } + + // Get all categories from metadata + const categories = Object.entries(metadata.categories).map( + ([id, categoryData]) => ({ + id, + name: categoryData.categoryDisplay, + description: '', + icon: id, + serverCount: categoryData.totalRepositories, + url: `/freedevtools/mcp/${id}/1/`, + }) + ); + + // Add descriptions from category data + const categoryEntries = await getCollection('mcpCategoryData'); + categoryEntries.forEach((entry) => { + const category = categories.find((c) => c.id === entry.data.category); + if (category) { + category.description = entry.data.description || ''; + } + }); + + return categories; +} + +/** + * SSR: Get all category IDs (for route validation) + */ +export async function getAllMcpCategoryIds(): Promise { + const categoryEntries = await getCollection('mcpCategoryData'); + return categoryEntries.map((entry) => entry.data.category); +} + +/** + * SSR: Get category data by ID + */ +export async function getMcpCategoryById(categoryId: string) { + const categoryEntries = await getCollection('mcpCategoryData'); + const entry = categoryEntries.find( + (e) => e.data.category === categoryId + ); + + if (!entry) { + return null; + } + + return { + category: entry.data.category, + categoryDisplay: entry.data.categoryDisplay, + description: entry.data.description || '', + repositories: entry.data.repositories, + }; +} + +/** + * SSR: Get repositories for a category (with pagination support) + */ +export async function getMcpCategoryRepositories(categoryId: string) { + const category = await getMcpCategoryById(categoryId); + if (!category) { + return []; + } + + // Include repository ID in each server object + return Object.entries(category.repositories).map(([repositoryId, server]) => ({ + ...server, + repositoryId: repositoryId, + })); +} + +/** + * SSR: Get MCP metadata + */ +export async function getMcpMetadata() { + const metadataEntries = await getCollection('mcpMetadata'); + return metadataEntries[0]?.data; +} diff --git a/frontend/src/pages/mcp/[category]/[page].astro b/frontend/src/pages/mcp/[category]/[page].astro deleted file mode 100644 index 89b2719a74..0000000000 --- a/frontend/src/pages/mcp/[category]/[page].astro +++ /dev/null @@ -1,145 +0,0 @@ ---- -import CreditsButton from '@/components/buttons/CreditsButton'; -import ToolContainer from '@/components/tool/ToolContainer'; -import ToolHead from '@/components/tool/ToolHead'; -import BaseLayout from '@/layouts/BaseLayout.astro'; -import { generateMcpCategoryPaginatedPaths } from '@/lib/mcp-utils'; -import { formatRepositoryName } from '@/lib/utils'; -import { getEntry } from 'astro:content'; -import AdBanner from '../../../components/banner/AdBanner.astro'; -import Pagination from '../../../components/PaginationComponent.astro'; -import RepositoryCard from '../_McpRepoCard.astro'; - -// Generate static paths for all MCP categories with pagination -export async function getStaticPaths({ paginate }) { - return await generateMcpCategoryPaginatedPaths({ paginate }); -} - -// Get category and page from params -const { category } = Astro.params; -const { page } = Astro.props; - -if (!category) { - throw new Error('Category parameter is required'); -} - -// Get category data -const categoryEntry = await getEntry('mcpCategoryData', category); - -if (!categoryEntry) { - throw new Error(`Category '${category}' not found`); -} - -const categoryData = categoryEntry.data; -const categoryName = categoryData.categoryDisplay; -const categoryDescription = categoryData.description || ''; - -// SEO data -const title = `${categoryName} MCP Servers & Repositories – ${page.total} Model Context Protocol Tools (Page ${page.currentPage} of ${page.lastPage}) | Free DevTools by Hexmos`; - -const description = `Discover ${page.total} ${categoryName} MCP servers and repositories for Model Context Protocol integrations. Browse tools compatible with Claude, Cursor, and Windsurf — free, open source, and easy to explore.`; - -const keywords = [ - 'MCP', - 'Model Context Protocol', - categoryName, - 'MCP servers', - 'AI tools', - 'developer tools', - 'open source', - 'repositories', - 'pagination', -]; - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, - { label: categoryName, href: `/freedevtools/mcp/${category}/1/` }, -]; ---- - - - -
- -
- - - -
-
- Showing {page.data.length} of {page.total} repositories (Page { - page.currentPage - } of {page.lastPage}) -
-
- - -
- { - page.data.map((server) => { - const formattedName = formatRepositoryName(server.name); - // Use the repositoryId that was added to the server object - const repositoryId = server.repositoryId; - return ( - - ); - }) - } -
- - - - - - -
-
diff --git a/frontend/src/pages/mcp/[category]/[repositoryId].astro b/frontend/src/pages/mcp/[category]/[repositoryId].astro deleted file mode 100644 index af2d4385df..0000000000 --- a/frontend/src/pages/mcp/[category]/[repositoryId].astro +++ /dev/null @@ -1,444 +0,0 @@ ---- -import { getEntry } from 'astro:content'; -import { marked } from 'marked'; -import AdBanner from '../../../components/banner/AdBanner.astro'; -import CreditsButton from '../../../components/buttons/CreditsButton'; -import SeeAlsoIndex from '../../../components/seealso/SeeAlsoIndex.astro'; -import ToolContainer from '../../../components/tool/ToolContainer'; -import ToolHead from '../../../components/tool/ToolHead'; -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import { generateMcpStaticPaths } from '../../../lib/mcp-utils'; -import { formatRepositoryName } from '../../../lib/utils'; -import Banner from '../../../components/banner/BannerIndex.astro'; - -function processMcpReadmeLinks(html: string): string { - // First, add IDs to all headings (h1-h6) so anchor links work - html = html.replace( - /<(h[1-6])([^>]*)>([^<]+)<\/h[1-6]>/gi, - (match, tag, attributes, content) => { - // Generate anchor ID from heading content - const anchorId = content - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') // Remove special chars - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/-+/g, '-') // Replace multiple hyphens with single - .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens - - // Add scroll margin to offset the header - use both class and inline style - const existingClass = attributes.includes('class=') ? attributes : ''; - const scrollMarginClass = existingClass - ? existingClass.replace(/class="([^"]*)"/, 'class="$1 scroll-mt-32"') - : 'class="scroll-mt-32"'; - - // Add inline style as backup to ensure scroll margin works - const existingStyle = attributes.includes('style=') ? attributes : ''; - const scrollMarginStyle = existingStyle - ? existingStyle.replace( - /style="([^"]*)"/, - 'style="$1; scroll-margin-top: 8rem;"' - ) - : 'style="scroll-margin-top: 8rem;"'; - - return `<${tag} ${scrollMarginClass} ${scrollMarginStyle} id="${anchorId}">${content}`; - } - ); - - // Then process the links - return html.replace( - /]*?)href="([^"]*?)"([^>]*?)>/g, - ( - match: string, - beforeHref: string, - href: string, - afterHref: string - ): string => { - // Keep absolute URLs (https/http) as clickable links - if (/^https?:\/\//i.test(href)) { - // Add target="_blank" for external links - if (!match.includes('target=')) { - return ``; - } - return match; - } - - // Handle anchor links (starting with #) - keep them as clickable for scrolling - if (href.startsWith('#')) { - return match; - } - - // Disable all relative links (/, ../, ./, etc.) by removing href and adding disabled styling - return ``; - } - ); -} - -// Generate static paths for all MCP repositories using optimized utility -export async function getStaticPaths() { - return await generateMcpStaticPaths(); -} - -// Get parameters -const { category, repositoryId } = Astro.params; - -if (!category || !repositoryId) { - throw new Error('Category and repositoryId parameters are required'); -} - -// Use getEntry for direct access instead of getCollection + find -// This is more efficient as it directly accesses the specific category file -const categoryEntry = await getEntry('mcpCategoryData', category); - -if (!categoryEntry) { - throw new Error(`Category '${category}' not found`); -} - -const categoryData = categoryEntry.data; -const categoryName = categoryData.categoryDisplay; - -// Find the specific repository -const server = categoryData.repositories[repositoryId]; -if (!server) { - throw new Error( - `Repository '${repositoryId}' not found in category '${category}'` - ); -} - -// Format repository name -const formattedName = formatRepositoryName(server.name); - -// Process README content -let processedReadmeContent: string = ''; -if (server.readme_content) { - try { - // Convert markdown to HTML - let htmlContent = await marked(server.readme_content); - - // Process links: keep absolute URLs and anchor links, disable relative links - processedReadmeContent = processMcpReadmeLinks(htmlContent); - } catch (error) { - console.warn('Error processing README content:', error); - processedReadmeContent = server.readme_content; - } -} - -// Calculate stats -const stats = { - githubStars: server.stars, - weeklyDownloads: server.npm_downloads || 0, - tools: 1, // This would need to be calculated from actual data - lastUpdated: new Date(server.updated_at).toLocaleDateString(), -}; - -// SEO data -const title = `${formattedName} – ${categoryName} MCP Server by ${server.owner.charAt(0).toUpperCase() + server.owner.slice(1)} Model Context Protocol Tool | Free DevTools by Hexmos`; - -const description = - server.description || - `${server.owner.charAt(0).toUpperCase() + server.owner.slice(1)}'s ${formattedName} MCP server helps your AI generate more accurate and context-aware responses. Supported in Copilot Agent, Cursor, Claude Code, Windsurf, and Cline – free, open source, and ready to integrate.`; - -let keywords = ['MCP', 'Model Context Protocol', formattedName, categoryName]; - -if (server.keywords && server.keywords.length > 0) { - keywords = [...keywords, ...server.keywords]; -} - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, - { label: categoryName, href: `/freedevtools/mcp/${category}/1/` }, - { label: formattedName }, -]; ---- - - - -
- -
- - -
- - - - -
- { - server.readme_content ? ( -
-
-
-
-
- ) : ( -
-
📝
-

No documentation available

-

- This repository doesn't have README content available yet. -

-
- ) - } -
-
- - -
- -
- - - - - diff --git a/frontend/src/pages/mcp/[category]/[slug].astro b/frontend/src/pages/mcp/[category]/[slug].astro new file mode 100644 index 0000000000..c9502e04da --- /dev/null +++ b/frontend/src/pages/mcp/[category]/[slug].astro @@ -0,0 +1,604 @@ +--- +import { getEntry } from 'astro:content'; +import { marked } from 'marked'; +import AdBanner from '../../../components/banner/AdBanner.astro'; +import CreditsButton from '../../../components/buttons/CreditsButton'; +import SeeAlsoIndex from '../../../components/seealso/SeeAlsoIndex.astro'; +import ToolContainer from '../../../components/tool/ToolContainer'; +import ToolHead from '../../../components/tool/ToolHead'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import { formatRepositoryName } from '../../../lib/utils'; +import Banner from '../../../components/banner/BannerIndex.astro'; +import Pagination from '../../../components/PaginationComponent.astro'; +import RepositoryCard from '../_McpRepoCard.astro'; + +export const prerender = false; + +function processMcpReadmeLinks(html: string): string { + // First, add IDs to all headings (h1-h6) so anchor links work + html = html.replace( + /<(h[1-6])([^>]*)>([^<]+)<\/h[1-6]>/gi, + (match, tag, attributes, content) => { + // Generate anchor ID from heading content + const anchorId = content + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') // Remove special chars + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/-+/g, '-') // Replace multiple hyphens with single + .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens + + // Add scroll margin to offset the header - use both class and inline style + const existingClass = attributes.includes('class=') ? attributes : ''; + const scrollMarginClass = existingClass + ? existingClass.replace(/class="([^"]*)"/, 'class="$1 scroll-mt-32"') + : 'class="scroll-mt-32"'; + + // Add inline style as backup to ensure scroll margin works + const existingStyle = attributes.includes('style=') ? attributes : ''; + const scrollMarginStyle = existingStyle + ? existingStyle.replace( + /style="([^"]*)"/, + 'style="$1; scroll-margin-top: 8rem;"' + ) + : 'style="scroll-margin-top: 8rem;"'; + + return `<${tag} ${scrollMarginClass} ${scrollMarginStyle} id="${anchorId}">${content}`; + } + ); + + // Then process the links + return html.replace( + /]*?)href="([^"]*?)"([^>]*?)>/g, + ( + match: string, + beforeHref: string, + href: string, + afterHref: string + ): string => { + // Keep absolute URLs (https/http) as clickable links + if (/^https?:\/\//i.test(href)) { + // Add target="_blank" for external links + if (!match.includes('target=')) { + return ``; + } + return match; + } + + // Handle anchor links (starting with #) - keep them as clickable for scrolling + if (href.startsWith('#')) { + return match; + } + + // Disable all relative links (/, ../, ./, etc.) by removing href and adding disabled styling + return ``; + } + ); +} + +const { category, slug } = Astro.params; + +// Early return if missing params +if (!category || !slug) { + return new Response(null, { status: 404 }); +} + +// Check if slug is numeric (pagination) or a string (repository) +const isNumericPage = /^\d+$/.test(slug); +const pageNumber = isNumericPage ? parseInt(slug, 10) : null; +const repositoryId = !isNumericPage ? slug : null; + +// Get category data using getEntry (same as original) +// This will return null if category doesn't exist +const categoryEntry = await getEntry('mcpCategoryData', category); + +if (!categoryEntry) { + return new Response(null, { status: 404 }); +} + +const categoryData = categoryEntry.data; +const categoryName = categoryData.categoryDisplay; +const categoryDescription = categoryData.description || ''; + +let pageData: any = null; +let repositoryData: any = null; + +// Handle pagination route: /mcp/[category]/[page] +if (pageNumber !== null) { + // Page 1 is valid - don't redirect (category index redirects to /category/1/) + + // Get all repositories for this category + const allRepositories = Object.entries(categoryData.repositories).map( + ([repositoryId, server]) => ({ + ...server, + repositoryId: repositoryId, + }) + ); + const itemsPerPage = 30; + const totalPages = Math.ceil(allRepositories.length / itemsPerPage); + + // Validate page number + if (pageNumber > totalPages || pageNumber < 1) { + return new Response(null, { status: 404 }); + } + + // Get repositories for current page + const startIndex = (pageNumber - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentPageRepositories = allRepositories.slice(startIndex, endIndex); + + // SEO data + const title = `${categoryName} MCP Servers & Repositories – ${allRepositories.length} Model Context Protocol Tools (Page ${pageNumber} of ${totalPages}) | Free DevTools by Hexmos`; + const description = `Discover ${allRepositories.length} ${categoryName} MCP servers and repositories for Model Context Protocol integrations. Browse tools compatible with Claude, Cursor, and Windsurf — free, open source, and easy to explore.`; + const keywords = [ + 'MCP', + 'Model Context Protocol', + categoryName, + 'MCP servers', + 'AI tools', + 'developer tools', + 'open source', + 'repositories', + 'pagination', + ]; + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, + { label: categoryName, href: `/freedevtools/mcp/${category}/1/` }, + ]; + + pageData = { + category, + categoryName, + categoryDescription, + currentPage: pageNumber, + totalPages, + totalRepositories: allRepositories.length, + repositories: currentPageRepositories, + breadcrumbItems, + title, + description, + keywords, + }; +} else if (repositoryId) { + // Handle repository route: /mcp/[category]/[repositoryId] + const repositories = categoryData.repositories; + const server = repositories[repositoryId]; + + if (!server) { + return new Response(null, { status: 404 }); + } + + // Format repository name + const formattedName = formatRepositoryName(server.name); + + // Process README content + let processedReadmeContent: string = ''; + if (server.readme_content) { + try { + // Convert markdown to HTML + let htmlContent = await marked(server.readme_content); + // Process links: keep absolute URLs and anchor links, disable relative links + processedReadmeContent = processMcpReadmeLinks(htmlContent); + } catch (error) { + console.warn('Error processing README content:', error); + processedReadmeContent = server.readme_content; + } + } + + // Calculate stats + const stats = { + githubStars: server.stars, + weeklyDownloads: server.npm_downloads || 0, + tools: 1, + lastUpdated: new Date(server.updated_at).toLocaleDateString(), + }; + + // SEO data + const title = `${formattedName} – ${categoryName} MCP Server by ${server.owner.charAt(0).toUpperCase() + server.owner.slice(1)} Model Context Protocol Tool | Free DevTools by Hexmos`; + const description = + server.description || + `${server.owner.charAt(0).toUpperCase() + server.owner.slice(1)}'s ${formattedName} MCP server helps your AI generate more accurate and context-aware responses. Supported in Copilot Agent, Cursor, Claude Code, Windsurf, and Cline – free, open source, and ready to integrate.`; + + let keywords = ['MCP', 'Model Context Protocol', formattedName, categoryName]; + if (server.keywords && server.keywords.length > 0) { + keywords = [...keywords, ...server.keywords]; + } + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, + { label: categoryName, href: `/freedevtools/mcp/${category}/1/` }, + { label: formattedName }, + ]; + + repositoryData = { + category, + categoryName, + repositoryId, + server, + formattedName, + processedReadmeContent, + stats, + breadcrumbItems, + title, + description, + keywords, + }; +} else { + return new Response(null, { status: 404 }); +} +--- + +{pageData ? ( + + +
+ +
+ + + +
+
+ Showing {pageData.repositories.length} of {pageData.totalRepositories}{' '} + repositories (Page {pageData.currentPage} of {pageData.totalPages}) +
+
+ + +
+ {pageData.repositories.map((server) => { + const formattedName = formatRepositoryName(server.name); + const repositoryId = server.repositoryId; + return ( + + ); + })} +
+ + + + + +
+ + +) : repositoryData ? ( + + +
+ +
+ + +
+ +
+ +
+

Author

+
+
+ {repositoryData.server.imageUrl ? ( + {`${repositoryData.formattedName} + ) : ( +
+ MCP Server +
+ )} +
+
+

+ {repositoryData.server.owner || 'Unknown Author'} +

+
+ + + + {repositoryData.server.license} +
+
+
+
+ + +
+

Quick Info

+
+
+ + GitHub + GitHub Stars + + + + + + {repositoryData.stats.githubStars} + +
+
+ + NPM + Weekly Downloads + + + + + + {repositoryData.stats.weeklyDownloads} + +
+
+ + + + + + Tools + + {repositoryData.stats.tools} +
+
+ + + + + Last Updated + + {repositoryData.stats.lastUpdated} +
+
+
+ + +
+

Actions

+
+ + GitHub + View on GitHub + + {repositoryData.server.npm_url && ( + + NPM + View on NPM + + )} +
+
+ + + {repositoryData.server.keywords && + repositoryData.server.keywords.length > 0 && ( +
+

Tags

+
+ {repositoryData.server.keywords.map((keyword) => ( + + {keyword} + + ))} +
+
+ )} +
+ + +
+ {repositoryData.processedReadmeContent ? ( +
+
+
+
+
+ ) : ( +
+
📝
+

No documentation available

+

+ This repository doesn't have README content available yet. +

+
+ )} +
+
+ + +
+ +
+ + + + + +) : null} + diff --git a/frontend/src/pages/mcp/[category]/index.astro b/frontend/src/pages/mcp/[category]/index.astro index b0654e9396..60030b7b4e 100644 --- a/frontend/src/pages/mcp/[category]/index.astro +++ b/frontend/src/pages/mcp/[category]/index.astro @@ -1,21 +1,140 @@ --- -import { getCollection } from 'astro:content'; - -export async function getStaticPaths() { - const categoryEntries = await getCollection('mcpCategoryData'); - - return categoryEntries.map(entry => ({ - params: { category: entry.data.category } - })); -} +import { getAllMcpCategoryIds } from '@/lib/mcp-utils'; +import { getAllMcpCategories, getMcpMetadata } from '@/lib/mcp-utils'; +import BaseLayout from '@/layouts/BaseLayout.astro'; +import Mcp from '../_Mcp.astro'; + +export const prerender = false; -// Redirect to page 1 of the category const { category } = Astro.params; +const urlPath = Astro.url.pathname; if (!category) { - throw new Error('Category parameter is required'); + return new Response(null, { status: 404 }); } -// Redirect to the first page -return Astro.redirect(`/freedevtools/mcp/${category}/1/`); +let paginationData: any = null; +let shouldRedirect = false; +let redirectUrl = ''; + +// If category is numeric, this is actually a pagination route +// Render pagination content directly (workaround for route priority) +if (/^\d+$/.test(category)) { + const currentPage = parseInt(category, 10); + + // Fetch categories data directly (SSR mode) + const allCategories = await getAllMcpCategories(); + const itemsPerPage = 30; + const totalPages = Math.ceil(allCategories.length / itemsPerPage); + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return new Response(null, { status: 404 }); + } + + // Get categories for current page + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentPageCategories = allCategories.slice(startIndex, endIndex); + + // Load MCP metadata + const metadata = await getMcpMetadata(); + + if (!metadata) { + throw new Error('MCP metadata not found'); + } + + // Calculate totals + const totalServers = metadata.totalRepositories; + const totalTools = Object.values(metadata.categories).reduce( + (sum, cat) => sum + cat.npmPackages, + 0 + ); + const totalClients = 0; + + // SEO data + const title = `Awesome MCP Servers Directory – Discover ${allCategories.length} Model Context Protocol Tools & Categories (Page ${currentPage} of ${totalPages}) | Free DevTools by Hexmos`; + const description = `Explore ${totalServers}+ verified MCP servers, tools, and clients used by Claude, Cursor, and Windsurf. Find Model Context Protocol integrations for your AI apps — free, open source, and easy to use.`; + const keywords = [ + 'MCP', + 'Model Context Protocol', + 'MCP servers', + 'MCP tools', + 'MCP clients', + 'AI tools', + 'developer tools', + 'open source', + 'repositories', + 'directory', + 'pagination', + ]; + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, + ]; + + paginationData = { + currentPage, + totalPages, + totalCategories: allCategories.length, + categories: currentPageCategories, + totalServers, + totalTools, + totalClients, + breadcrumbItems, + title, + description, + keywords, + }; +} else { + // Validate category exists + const allCategoryIds = await getAllMcpCategoryIds(); + if (!allCategoryIds.includes(category)) { + return new Response(null, { status: 404 }); + } + + // Redirect to the first page + shouldRedirect = true; + redirectUrl = `/freedevtools/mcp/${category}/1/`; +} + +if (shouldRedirect) { + return Astro.redirect(redirectUrl, 301); +} --- + +{paginationData ? ( + + + +) : null} diff --git a/frontend/src/pages/mcp/[page].astro b/frontend/src/pages/mcp/[page].astro index 6883520fb3..2eb28ca209 100644 --- a/frontend/src/pages/mcp/[page].astro +++ b/frontend/src/pages/mcp/[page].astro @@ -1,85 +1,146 @@ --- import BaseLayout from '@/layouts/BaseLayout.astro'; -import { generateMcpDirectoryPaginatedPaths } from '@/lib/mcp-utils'; +import { getAllMcpCategories, getAllMcpCategoryIds, getMcpMetadata } from '@/lib/mcp-utils'; import { getCollection } from 'astro:content'; import Mcp from './_Mcp.astro'; -// Generate static paths for MCP directory with pagination -export async function getStaticPaths({ paginate }) { - return await generateMcpDirectoryPaginatedPaths({ paginate }); +export const prerender = false; + +const { page } = Astro.params; +const urlPath = Astro.url.pathname; + +// Early return if no page param +if (!page) { + return new Response(null, { status: 404 }); } -// Get page from props -const { page } = Astro.props; +let categoryData: any = null; +let paginationData: any = null; -// Load MCP metadata -const metadataEntries = await getCollection('mcpMetadata'); -const metadata = metadataEntries[0]?.data; +// Check if page param is numeric +if (!/^\d+$/.test(page)) { + // If not numeric, it might be a category name + // Redirect to add trailing slash if missing (BEFORE checking category) + if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); + } -if (!metadata) { - throw new Error('MCP metadata not found'); -} + const allCategoryIds = await getAllMcpCategoryIds(); + const isCategory = allCategoryIds.includes(page); + + if (isCategory) { + // This is a category index route - redirect to page 1 + return Astro.redirect(`/freedevtools/mcp/${page}/1/`, 301); + } else { + // Not a valid category or page number - 404 + return new Response(null, { status: 404 }); + } +} else { + // Handle pagination route + const currentPage = parseInt(page, 10); + + // Redirect /mcp/1 to /mcp (if you have an index.astro) + // For now, keep /mcp/1/ as valid + + // Fetch categories data directly (SSR mode) + const allCategories = await getAllMcpCategories(); + const itemsPerPage = 30; + const totalPages = Math.ceil(allCategories.length / itemsPerPage); + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return new Response(null, { status: 404 }); + } -// Calculate totals -const totalServers = metadata.totalRepositories; -const totalTools = Object.values(metadata.categories).reduce( - (sum, cat) => sum + cat.npmPackages, - 0 -); -const totalClients = 0; // This would need to be calculated from actual data - -// SEO data -const title = `Awesome MCP Servers Directory – Discover ${page.total} Model Context Protocol Tools & Categories (Page ${page.currentPage} of ${page.lastPage}) | Free DevTools by Hexmos`; -const description = `Explore ${page.total}+ verified MCP servers, tools, and clients used by Claude, Cursor, and Windsurf. Find Model Context Protocol integrations for your AI apps — free, open source, and easy to use.`; -const keywords = [ - 'MCP', - 'Model Context Protocol', - 'MCP servers', - 'MCP tools', - 'MCP clients', - 'AI tools', - 'developer tools', - 'open source', - 'repositories', - 'directory', - 'pagination', -]; - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, -]; + // Get categories for current page + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentPageCategories = allCategories.slice(startIndex, endIndex); + + // Load MCP metadata + const metadata = await getMcpMetadata(); + + if (!metadata) { + throw new Error('MCP metadata not found'); + } + + // Calculate totals + const totalServers = metadata.totalRepositories; + const totalTools = Object.values(metadata.categories).reduce( + (sum, cat) => sum + cat.npmPackages, + 0 + ); + const totalClients = 0; // This would need to be calculated from actual data + + // SEO data + const title = `Awesome MCP Servers Directory – Discover ${allCategories.length} Model Context Protocol Tools & Categories (Page ${currentPage} of ${totalPages}) | Free DevTools by Hexmos`; + const description = `Explore ${totalServers}+ verified MCP servers, tools, and clients used by Claude, Cursor, and Windsurf. Find Model Context Protocol integrations for your AI apps — free, open source, and easy to use.`; + const keywords = [ + 'MCP', + 'Model Context Protocol', + 'MCP servers', + 'MCP tools', + 'MCP clients', + 'AI tools', + 'developer tools', + 'open source', + 'repositories', + 'directory', + 'pagination', + ]; + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'MCP Directory', href: '/freedevtools/mcp/1/' }, + ]; + + paginationData = { + currentPage, + totalPages, + totalCategories: allCategories.length, + categories: currentPageCategories, + totalServers, + totalTools, + totalClients, + breadcrumbItems, + title, + description, + keywords, + }; +} --- - - - +{paginationData ? ( + + + +) : null} From b21799c88a32e0a4fd4a3393cd6060bc611cf247 Mon Sep 17 00:00:00 2001 From: lovestaco Date: Sun, 16 Nov 2025 19:07:22 +0530 Subject: [PATCH 04/79] fix: svg to mcp --- frontend/src/pages/svg_icons/[category].astro | 592 +++++++++++------- frontend/src/pages/svg_icons/[page].astro | 229 ++++--- frontend/src/pages/svg_icons/_SvgIcons.astro | 1 + 3 files changed, 491 insertions(+), 331 deletions(-) diff --git a/frontend/src/pages/svg_icons/[category].astro b/frontend/src/pages/svg_icons/[category].astro index a17738b536..8989d04842 100644 --- a/frontend/src/pages/svg_icons/[category].astro +++ b/frontend/src/pages/svg_icons/[category].astro @@ -3,284 +3,408 @@ import { getClusterByName, getClusters, getIconsByCluster, + getTotalIcons, } from 'db/svg_icons/svg-icons-utils'; import AdBanner from '../../components/banner/AdBanner.astro'; import CreditsButton from '../../components/buttons/CreditsButton'; import ToolContainer from '../../components/tool/ToolContainer'; import ToolHead from '../../components/tool/ToolHead'; import BaseLayout from '../../layouts/BaseLayout.astro'; +import SvgIcons from './_SvgIcons.astro'; -export async function getStaticPaths() { - const clusters = getClusters(); - - // Use cluster names for URLs (these match cluster_svg.json cluster.name) - const categories = clusters.map((cluster) => cluster.name); - - return categories.map((category) => ({ - params: { category }, - props: { category }, - })); -} +export const prerender = false; const { category } = Astro.params; +const urlPath = Astro.url.pathname; -// Get cluster data from database -const clusterData = getClusterByName(category); - -if (!clusterData) { - return Astro.redirect('/freedevtools/svg_icons/'); +if (!category) { + return new Response(null, { status: 404 }); } -async function getCategoryIcons() { - // Get icons from SQLite database - // The category parameter is the cluster display name (from cluster.name in cluster_svg.json) - // Look up cluster by display name to get the source_folder (actual folder name used in icon table) - // The icon table uses cluster key (folder name = source_folder), not display name - // Use source_folder to query icons - // clusterData is guaranteed to exist due to check above - const dbIcons = getIconsByCluster(clusterData!.source_folder || category!); +let paginationData: any = null; +let categoryData: any = null; - const icons = dbIcons.map((icon) => { - const iconName = icon.name.replace('.svg', ''); +// If category is numeric, this is actually a pagination route +// Handle it directly (workaround for route priority) +if (/^\d+$/.test(category)) { + const currentPage = parseInt(category, 10); + // Redirect /svg_icons/1 to /svg_icons + if (currentPage === 1) { + return Astro.redirect('/freedevtools/svg_icons/'); + } + + // Fetch categories data directly (SSR mode) + const clusters = getClusters(); + const allCategories = clusters.map((cluster) => { + const icons = getIconsByCluster(cluster.source_folder || cluster.name); return { - name: iconName, - description: icon.description || `Free ${iconName} icon`, - category: category, - tags: icon.tags || [], - author: 'Free DevTools', - license: 'MIT', - url: `/freedevtools/svg_icons/${category}/${iconName}/`, - base64: icon.base64, - img_alt: icon.img_alt, + id: cluster.source_folder || cluster.name, + name: cluster.name, + description: cluster.description, + icon: `/freedevtools/svg_icons/${cluster.name}/`, + iconCount: icons.length, + url: `/freedevtools/svg_icons/${cluster.name}/`, + keywords: cluster.keywords, + features: cluster.tags, + fileNames: icons.map((icon) => icon.name), }; }); - return icons; -} + const itemsPerPage = 30; + const totalPages = Math.ceil(allCategories.length / itemsPerPage); + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return new Response(null, { status: 404 }); + } + + const totalCategories = allCategories.length; + const totalSvgIcons = getTotalIcons(); + + // Get categories for current page + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentPageCategories = allCategories.slice(startIndex, endIndex); + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'SVG Icons', href: '/freedevtools/svg_icons/' }, + { label: `Page ${currentPage}` }, + ]; + + // SEO data + const seoTitle = `Free SVG Icons - Page ${currentPage} | Online Free DevTools by Hexmos`; + const seoDescription = `Browse page ${currentPage} of ${totalPages} pages in our free SVG icons collection. Download thousands of vector graphics instantly. No registration required.`; + const canonical = `https://hexmos.com/freedevtools/svg_icons/${currentPage}/`; + + const paginatedKeywords = [ + 'svg icons', + 'vector graphics', + 'free icons', + 'download icons', + 'edit icons', + `page ${currentPage}`, + 'pagination', + 'icon collection', + 'vector graphics library', + ]; + + paginationData = { + currentPage, + totalPages, + totalCategories, + totalSvgIcons, + categories: currentPageCategories, + breadcrumbItems, + seoTitle, + seoDescription, + canonical, + paginatedKeywords, + itemsPerPage, + }; +} else { + // Redirect to add trailing slash if missing + if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); + } + + // Get cluster data from database + const clusterData = getClusterByName(category); + + if (!clusterData) { + return new Response(null, { status: 404 }); + } -const categoryIcons = await getCategoryIcons(); -const totalIcons = categoryIcons.length; - -// Use database content for about and why_choose_us -// Fallback to default content if not available in database -const aboutContent = - clusterData.about || - `Our ${category} SVG icon library offers vector-based graphics that scale beautifully across all devices and resolutions. Each icon is crafted with precision and can be customized through CSS or JavaScript. Whether you need icons for web, mobile, or print, these ${category} SVG icons deliver crisp results at any size.`; - -const whyChooseUsContent = - clusterData.why_choose_us && clusterData.why_choose_us.length > 0 - ? clusterData.why_choose_us - : [ - 'Infinitely scalable without pixelation or quality loss', - 'Editable vector format - customize colors, shapes, and sizes', - 'Small file sizes for faster website loading', - 'Perfect for responsive design and retina displays', - ]; - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'SVG Icons', href: '/freedevtools/svg_icons/' }, - { label: category }, -]; + async function getCategoryIcons() { + const dbIcons = getIconsByCluster(clusterData!.source_folder || category!); + + const icons = dbIcons.map((icon) => { + const iconName = icon.name.replace('.svg', ''); + + return { + name: iconName, + description: icon.description || `Free ${iconName} icon`, + category: category, + tags: icon.tags || [], + author: 'Free DevTools', + license: 'MIT', + url: `/freedevtools/svg_icons/${category}/${iconName}/`, + base64: icon.base64, + img_alt: icon.img_alt, + }; + }); + + return icons; + } + + const categoryIcons = await getCategoryIcons(); + const totalIcons = categoryIcons.length; + + // Use database content for about and why_choose_us + const aboutContent = + clusterData.about || + `Our ${category} SVG icon library offers vector-based graphics that scale beautifully across all devices and resolutions. Each icon is crafted with precision and can be customized through CSS or JavaScript. Whether you need icons for web, mobile, or print, these ${category} SVG icons deliver crisp results at any size.`; + + const whyChooseUsContent = + clusterData.why_choose_us && clusterData.why_choose_us.length > 0 + ? clusterData.why_choose_us + : [ + 'Infinitely scalable without pixelation or quality loss', + 'Editable vector format - customize colors, shapes, and sizes', + 'Small file sizes for faster website loading', + 'Perfect for responsive design and retina displays', + ]; + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'SVG Icons', href: '/freedevtools/svg_icons/' }, + { label: category }, + ]; + + categoryData = { + category, + clusterData, + categoryIcons, + totalIcons, + aboutContent, + whyChooseUsContent, + breadcrumbItems, + }; +} --- - 0 - ? clusterData.keywords - : [ - category, - 'svg icons', - 'vector graphics', - 'scalable icons', - 'editable svg', - ]} -> - -
- -
- + - -
-
- { - categoryIcons.map((icon) => { - const iconName = icon.name - .replace(/_/g, ' ') - .replace(/\b\w/g, (l: string) => l.toUpperCase()); - - return ( - -
-
-
- {icon.img_alt -
-
-
-
- ); - }) - } + +) : categoryData ? ( + 0 + ? categoryData.clusterData.keywords + : [ + categoryData.category, + 'svg icons', + 'vector graphics', + 'scalable icons', + 'editable svg', + ]} + > + +
+
-
- - { - clusterData.practical_application && ( -
-

- Practical Application -

-

{clusterData.practical_application}

-
- ) - } + - - { - aboutContent && ( +
-

- About {category.charAt(0).toUpperCase() + category.slice(1)} SVG - Icons -

-

{aboutContent}

-
- ) - } + { + categoryData.categoryIcons.map((icon) => { + const iconName = icon.name + .replace(/_/g, ' ') + .replace(/\b\w/g, (l: string) => l.toUpperCase()); - - { - whyChooseUsContent && whyChooseUsContent.length > 0 && ( -
-

- Why Choose our SVG Icons? -

-
    - {whyChooseUsContent.map((item) => ( -
  • {item}
  • - ))} -
+ return ( + +
+
+
+ {icon.img_alt +
+
+
+
+ ); + }) + }
- ) - } - - - { - clusterData.alternative_terms && - clusterData.alternative_terms.length > 0 && ( +
+ + { + categoryData.clusterData.practical_application && (

- Alternative Terms + Practical Application

-
- {clusterData.alternative_terms.map((term) => ( - - {term} - +

{categoryData.clusterData.practical_application}

+
+ ) + } + + + { + categoryData.aboutContent && ( +
+

+ About {categoryData.category.charAt(0).toUpperCase() + categoryData.category.slice(1)} SVG + Icons +

+

{categoryData.aboutContent}

+
+ ) + } + + + { + categoryData.whyChooseUsContent && categoryData.whyChooseUsContent.length > 0 && ( +
+

+ Why Choose our SVG Icons? +

+
    + {categoryData.whyChooseUsContent.map((item) => ( +
  • {item}
  • ))} -
+
) - } + } - - { - clusterData.tags && clusterData.tags.length > 0 && ( -
-

- Tags -

-
- {clusterData.tags.map((tag) => ( -

- {tag} -

- ))} + + { + categoryData.clusterData.alternative_terms && + categoryData.clusterData.alternative_terms.length > 0 && ( +
+

+ Alternative Terms +

+
+ {categoryData.clusterData.alternative_terms.map((term) => ( + + {term} + + ))} +
+
+ ) + } + + + { + categoryData.clusterData.tags && categoryData.clusterData.tags.length > 0 && ( +
+

+ Tags +

+
+ {categoryData.clusterData.tags.map((tag) => ( +

+ {tag} +

+ ))} +
-
- ) - } - - -
- - + + +) : null} diff --git a/frontend/src/pages/man-pages/[category]/[subcategory]/[slug].astro b/frontend/src/pages/man-pages/[category]/[subcategory]/[slug].astro index 74be6e4f16..8f0f1a63d5 100644 --- a/frontend/src/pages/man-pages/[category]/[subcategory]/[slug].astro +++ b/frontend/src/pages/man-pages/[category]/[subcategory]/[slug].astro @@ -4,32 +4,44 @@ import CreditsButton from '@/components/buttons/CreditsButton'; import ToolContainer from '@/components/tool/ToolContainer'; import ToolHead from '@/components/tool/ToolHead'; import BaseLayout from '@/layouts/BaseLayout.astro'; -import { getManPageBySlug, generateCommandStaticPaths } from '@/lib/man-pages-utils'; +import { getManPageBySlug } from '@/lib/man-pages-utils'; import SeeAlsoIndex from '@/components/seealso/SeeAlsoIndex.astro'; -export async function getStaticPaths() { - try { - const paths = generateCommandStaticPaths(); - return paths; - } catch (error) { - console.error('Error generating static paths:', error); - throw error; - } -} +export const prerender = false; const { category, subcategory, slug } = Astro.params; +const urlPath = Astro.url.pathname; // Ensure all required params are present if (!category || !subcategory || !slug) { - return Astro.redirect('/freedevtools/man-pages/'); + return new Response(null, { status: 404 }); +} + +// Redirect to add trailing slash if missing +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Check if slug is numeric (pagination route) +if (/^\d+$/.test(slug)) { + // This is actually a pagination route + const currentPage = parseInt(slug, 10); + + // Redirect /man-pages/category/subcategory/1 to /man-pages/category/subcategory + if (currentPage === 1) { + return Astro.redirect(`/freedevtools/man-pages/${category}/${subcategory}/`); + } + + // Redirect to subcategory pagination route + return Astro.redirect(`/freedevtools/man-pages/${category}/${subcategory}/${currentPage}/`, 301); } // Get man page from database by slug const manPage = getManPageBySlug(category, subcategory, slug); if (!manPage) { - // Redirect to man pages index if not found - return Astro.redirect('/freedevtools/man-pages/'); + // Return 404 if not found + return new Response(null, { status: 404 }); } // Generate table of contents from sections @@ -137,4 +149,4 @@ const breadcrumbItems = [ .lg\\:order-2 { order: -1 !important; } .lg\\:col-span-3 { grid-column: span 4 !important; } } - \ No newline at end of file + diff --git a/frontend/src/pages/man-pages/[category]/[subcategory]/index.astro b/frontend/src/pages/man-pages/[category]/[subcategory]/index.astro index abc9086366..8388a338d4 100644 --- a/frontend/src/pages/man-pages/[category]/[subcategory]/index.astro +++ b/frontend/src/pages/man-pages/[category]/[subcategory]/index.astro @@ -5,16 +5,41 @@ import Pagination from '@/components/PaginationComponent.astro'; import ToolContainer from '@/components/tool/ToolContainer'; import ToolHead from '@/components/tool/ToolHead'; import BaseLayout from '@/layouts/BaseLayout.astro'; -import { getManPagesBySubcategory, getManPagesCountBySubcategory, getManPagesBySubcategoryPaginated, generateSubcategoryStaticPaths } from '@/lib/man-pages-utils'; +import { getManPagesBySubcategory, getManPagesCountBySubcategory, getManPagesBySubcategoryPaginated, getSubCategoriesByMainCategory } from '@/lib/man-pages-utils'; -export async function getStaticPaths() { - return generateSubcategoryStaticPaths(); -} +export const prerender = false; const { category, subcategory } = Astro.params; +const urlPath = Astro.url.pathname; if (!category || !subcategory) { - throw new Error('Category and subcategory parameters are required'); + return new Response(null, { status: 404 }); +} + +// Redirect to add trailing slash if missing +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Check if subcategory is numeric (pagination route) +if (/^\d+$/.test(subcategory)) { + // This is actually a pagination route for the category + const currentPage = parseInt(subcategory, 10); + + // Redirect /man-pages/category/1 to /man-pages/category + if (currentPage === 1) { + return Astro.redirect(`/freedevtools/man-pages/${category}/`); + } + + // Redirect to category pagination route + return Astro.redirect(`/freedevtools/man-pages/${category}/${currentPage}/`, 301); +} + +// Validate subcategory exists for this category +const subcategories = getSubCategoriesByMainCategory(category); +const subcategoryNames = subcategories.map((sc) => sc.name); +if (!subcategoryNames.includes(subcategory)) { + return new Response(null, { status: 404 }); } // Efficient pagination for page 1 @@ -27,8 +52,6 @@ const currentPageManPages = getManPagesBySubcategoryPaginated(category, subcateg const totalManPagesCount = getManPagesCountBySubcategory(category, subcategory); const totalPages = Math.ceil(totalManPagesCount / itemsPerPage); -console.log("totalManPagesCount:", totalManPagesCount); - const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, { label: 'Man Pages', href: '/freedevtools/man-pages/' }, @@ -110,6 +133,7 @@ const subcategoryTitle = subcategory.replace('-', ' ').charAt(0).toUpperCase() + currentPage={currentPage} totalPages={totalPages} baseUrl={`/freedevtools/man-pages/${category}/${subcategory}/`} + alwaysIncludePageNumber={true} /> @@ -131,4 +155,4 @@ const subcategoryTitle = subcategory.replace('-', ' ').charAt(0).toUpperCase() +
-
\ No newline at end of file + diff --git a/frontend/src/pages/man-pages/[category]/index.astro b/frontend/src/pages/man-pages/[category]/index.astro index 1585d3e349..46da207578 100644 --- a/frontend/src/pages/man-pages/[category]/index.astro +++ b/frontend/src/pages/man-pages/[category]/index.astro @@ -5,16 +5,33 @@ import Pagination from '@/components/PaginationComponent.astro'; import ToolContainer from '@/components/tool/ToolContainer'; import ToolHead from '@/components/tool/ToolHead'; import BaseLayout from '@/layouts/BaseLayout.astro'; -import { getSubCategoriesByMainCategory, getSubCategoriesCountByMainCategory, getSubCategoriesByMainCategoryPaginated, getTotalManPagesCountByMainCategory, generateCategoryStaticPaths } from '@/lib/man-pages-utils'; +import { getSubCategoriesByMainCategory, getSubCategoriesCountByMainCategory, getSubCategoriesByMainCategoryPaginated, getTotalManPagesCountByMainCategory, getCategories } from '@/lib/man-pages-utils'; -export async function getStaticPaths() { - return generateCategoryStaticPaths(); -} +export const prerender = false; const { category } = Astro.params; +const urlPath = Astro.url.pathname; if (!category) { - throw new Error('Category parameter is required'); + return new Response(null, { status: 404 }); +} + +// Redirect to add trailing slash if missing +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Check if category is numeric (pagination route) +if (/^\d+$/.test(category)) { + // This is actually a pagination route - redirect to main index + return Astro.redirect('/freedevtools/man-pages/'); +} + +// Validate category exists +const allCategories = getCategories(); +const categoryNames = allCategories.map((c) => c.name); +if (!categoryNames.includes(category)) { + return new Response(null, { status: 404 }); } // Efficient pagination for page 1 @@ -114,6 +131,7 @@ const categoryTitle = category.replace('-', ' ').charAt(0).toUpperCase() + categ currentPage={currentPage} totalPages={totalPages} baseUrl={`/freedevtools/man-pages/${category}/`} + alwaysIncludePageNumber={true} /> @@ -129,4 +147,4 @@ const categoryTitle = category.replace('-', ' ').charAt(0).toUpperCase() + categ
- \ No newline at end of file + From 51949bcf352785cf27b3e0be20bfa11656ac0a2e Mon Sep 17 00:00:00 2001 From: lovestaco Date: Sun, 16 Nov 2025 19:54:13 +0530 Subject: [PATCH 07/79] fix: build man-pages command --- frontend/package-lock.json | 235 +++++++++++++++++++++++++++ frontend/package.json | 4 +- frontend/scripts/builds/man-pages.js | 211 ++++++++++++++++++++++++ 3 files changed, 449 insertions(+), 1 deletion(-) create mode 100755 frontend/scripts/builds/man-pages.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0e7fd3e6d6..6967346d03 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "freedevtools-frontend", "version": "0.0.1", "dependencies": { + "@astrojs/node": "^9.5.0", "@astrojs/react": "^4.3.0", "@astrojs/sitemap": "^3.5.1", "@astrojs/starlight": "^0.35.2", @@ -212,6 +213,20 @@ "astro": "^5.0.0" } }, + "node_modules/@astrojs/node": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-9.5.0.tgz", + "integrity": "sha512-x1whLIatmCefaqJA8FjfI+P6FStF+bqmmrib0OUGM1M3cZhAXKLgPx6UF2AzQ3JgpXgCWYM24MHtraPvZhhyLQ==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.7.4", + "send": "^1.2.0", + "server-destroy": "^1.0.1" + }, + "peerDependencies": { + "astro": "^5.14.3" + } + }, "node_modules/@astrojs/prism": { "version": "3.3.0", "license": "MIT", @@ -4687,6 +4702,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-progress/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-progress/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cli-truncate": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", @@ -5243,6 +5311,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "license": "MIT", @@ -5428,6 +5505,12 @@ "version": "0.2.0", "license": "MIT" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.243", "license": "ISC" @@ -5436,6 +5519,15 @@ "version": "9.2.2", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.1", "license": "MIT", @@ -5724,6 +5816,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "license": "MIT", @@ -6281,6 +6379,15 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "license": "MIT" @@ -6594,6 +6701,15 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "license": "MIT" @@ -7401,6 +7517,31 @@ "version": "4.2.0", "license": "BSD-2-Clause" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "dev": true, @@ -9993,6 +10134,18 @@ "version": "2.0.11", "license": "MIT" }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "license": "ISC", @@ -10841,6 +10994,15 @@ "version": "1.1.2", "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/rc": { "version": "1.2.8", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", @@ -11753,6 +11915,49 @@ "version": "1.0.0", "license": "MIT" }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serialize-error": { "version": "7.0.1", "license": "MIT", @@ -11776,6 +11981,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, "node_modules/set-blocking": { "version": "2.0.0", "license": "ISC" @@ -11829,6 +12040,12 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/sharp": { "version": "0.34.4", "hasInstallScript": true, @@ -12149,6 +12366,15 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "dev": true, @@ -12864,6 +13090,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "5.1.2", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index d7a09edc75..8331c24ef0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,9 +10,10 @@ "dev": "node --max-old-space-size=16384 ./node_modules/astro/astro.js dev", "start": "astro dev", "build": "node --max-old-space-size=16384 ./node_modules/astro/astro.js build", - "build:mcp": "node scripts/build-mcp.js", + "build:mcp": "node scripts/builds/mcp.js", "build:tldr": "node scripts/builds/tldr.js", "build:icons": "node scripts/builds/icons.js", + "build:man-pages": "node scripts/builds/man-pages.js", "preview": "astro preview", "astro": "astro", "format": "prettier --write .", @@ -23,6 +24,7 @@ "pagespeed:all:minimal": "node scripts/pageSpeed.cjs --all --minimal" }, "dependencies": { + "@astrojs/node": "^9.5.0", "@astrojs/react": "^4.3.0", "@astrojs/sitemap": "^3.5.1", "@astrojs/starlight": "^0.35.2", diff --git a/frontend/scripts/builds/man-pages.js b/frontend/scripts/builds/man-pages.js new file mode 100755 index 0000000000..7ee0d728fe --- /dev/null +++ b/frontend/scripts/builds/man-pages.js @@ -0,0 +1,211 @@ +#!/usr/bin/env node + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Colors for console output +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m' +}; + +function log(message, color = 'white') { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +function logStep(step, message) { + log(`\n🔧 ${step}`, 'cyan'); + log(message, 'white'); +} + +function logSuccess(message) { + log(`✅ ${message}`, 'green'); +} + +function logError(message) { + log(`❌ ${message}`, 'red'); +} + +function logInfo(message) { + log(`ℹ️ ${message}`, 'blue'); +} + +function logWarning(message) { + log(`⚠️ ${message}`, 'yellow'); +} + +// Get the project root directory (frontend folder) +const projectRoot = path.resolve(__dirname, '../..'); +const pagesDir = path.join(projectRoot, 'src', 'pages'); + +function excludeUnchangedSections() { + logStep('Excluding unchanged sections from build', 'Building only man-pages section...'); + + const changedSections = ['man-pages']; // Only build man-pages + logInfo(`Building strategy: ${changedSections.join(' ')}`); + + logInfo('🎯 Selective build mode - man-pages only'); + + // Get all directories in pages folder + const dirs = fs.readdirSync(pagesDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); + + for (const dir of dirs) { + const dirPath = path.join(pagesDir, dir); + const dirName = dir; + + // Only include man-pages, exclude everything else (including already excluded directories) + if (changedSections.includes(dirName)) { + logSuccess(`Including: ${dirName}`); + } else { + const excludedDirName = `_${dirName}`; + const excludedDirPath = path.join(pagesDir, excludedDirName); + + try { + fs.renameSync(dirPath, excludedDirPath); + logWarning(`Excluding: ${dirName} -> ${excludedDirName}`); + } catch (error) { + logError(`Failed to exclude ${dirName}: ${error.message}`); + } + } + } +} + +function restoreOriginalStructure() { + logStep('Restoring original folder structure', 'Moving excluded directories back...'); + + const dirs = fs.readdirSync(pagesDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory() && dirent.name.startsWith('_')) + .map(dirent => dirent.name); + + for (const dir of dirs) { + const originalName = dir.substring(1); // Remove the underscore + const currentPath = path.join(pagesDir, dir); + const originalPath = path.join(pagesDir, originalName); + + try { + fs.renameSync(currentPath, originalPath); + logSuccess(`Restored ${dir} to ${originalName}`); + } catch (error) { + logError(`Failed to restore ${dir}: ${error.message}`); + } + } +} + +function installDependencies() { + logStep('Installing dependencies', 'Running npm install...'); + + try { + execSync('npm install', { + cwd: projectRoot, + stdio: 'inherit', + env: { + ...process.env, + NODE_OPTIONS: '--max-old-space-size=16384', // 14GB for 16GB system + UV_THREADPOOL_SIZE: '64' // 4x cores for I/O operations + } + }); + logSuccess('Dependencies installed successfully'); + } catch (error) { + logError(`Failed to install dependencies: ${error.message}`); + throw error; + } +} + +function buildProject() { + logStep('Building project', 'Running Astro build for man-pages section only...'); + + try { + execSync('npx astro build', { + cwd: projectRoot, + stdio: 'inherit', + env: { + ...process.env, + NODE_OPTIONS: '--max-old-space-size=16384', // 14GB for 16GB system + UV_THREADPOOL_SIZE: '64', // 4x cores for I/O operations + NODE_OPTIONS_EXTRA: '--experimental-loader' // Enable experimental features for better performance + } + }); + logSuccess('Build completed successfully'); + } catch (error) { + logError(`Build failed: ${error.message}`); + throw error; + } +} + +function showBuildInfo() { + log('\n📦 man-pages Build Script', 'magenta'); + log('==================', 'magenta'); + log('This script will build only the man-pages section by excluding all other page directories.', 'white'); + log('The excluded directories will be temporarily renamed with an underscore prefix.', 'white'); + log('After the build, the original structure will be restored.\n', 'white'); +} + +async function main() { + try { + showBuildInfo(); + + // Check if we're in the right directory + if (!fs.existsSync(pagesDir)) { + logError(`Pages directory not found: ${pagesDir}`); + process.exit(1); + } + + // Step 1: Exclude unchanged sections + excludeUnchangedSections(); + + + // Step 3: Build project + buildProject(); + + logSuccess('\n🎉 man-pages build completed successfully!'); + logInfo('The build output is in the dist/ directory'); + + } catch (error) { + logError(`\n💥 Build failed: ${error.message}`); + process.exit(1); + } finally { + // Always restore the original structure, even if build fails + try { + restoreOriginalStructure(); + } catch (restoreError) { + logError(`Failed to restore original structure: ${restoreError.message}`); + } + } +} + +// Handle cleanup on process termination +process.on('SIGINT', () => { + logWarning('\n⚠️ Build interrupted. Restoring original structure...'); + try { + restoreOriginalStructure(); + } catch (error) { + logError(`Failed to restore: ${error.message}`); + } + process.exit(1); +}); + +process.on('SIGTERM', () => { + logWarning('\n⚠️ Build terminated. Restoring original structure...'); + try { + restoreOriginalStructure(); + } catch (error) { + logError(`Failed to restore: ${error.message}`); + } + process.exit(1); +}); + +main(); + From 8e93450b90114209fd599090f39df444420c899f Mon Sep 17 00:00:00 2001 From: lovestaco Date: Sun, 16 Nov 2025 20:25:00 +0530 Subject: [PATCH 08/79] fix: cheatsheet to ssr --- frontend/src/pages/c/[category]/[name].astro | 191 ----------- frontend/src/pages/c/[category]/[page].astro | 102 ------ frontend/src/pages/c/[category]/[slug].astro | 309 ++++++++++++++++++ .../[category]/_CategoryCheatsheetsPage.astro | 1 + frontend/src/pages/c/[category]/index.astro | 277 +++++++++++----- frontend/src/pages/c/[page].astro | 211 +++++++----- frontend/src/pages/c/_CheatsheetPage.astro | 1 + 7 files changed, 644 insertions(+), 448 deletions(-) delete mode 100644 frontend/src/pages/c/[category]/[name].astro delete mode 100644 frontend/src/pages/c/[category]/[page].astro create mode 100644 frontend/src/pages/c/[category]/[slug].astro diff --git a/frontend/src/pages/c/[category]/[name].astro b/frontend/src/pages/c/[category]/[name].astro deleted file mode 100644 index dfeb1cd24f..0000000000 --- a/frontend/src/pages/c/[category]/[name].astro +++ /dev/null @@ -1,191 +0,0 @@ ---- -import AdBanner from '../../../components/banner/AdBanner.astro'; -import Banner from '../../../components/banner/BannerIndex.astro'; -import CreditsButton from '../../../components/buttons/CreditsButton'; -import SeeAlsoIndex from '../../../components/seealso/SeeAlsoIndex.astro'; -import ToolContainer from '../../../components/tool/ToolContainer'; -import ToolHead from '../../../components/tool/ToolHead'; -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import { getCheatsheet } from '../../../lib/cheatsheets-utils'; - -function processCheatsheetLinks(html: string): string { - // First, add IDs to all headings (h1-h6) so anchor links work - html = html.replace( - /<(h[1-6])([^>]*)>([^<]+)<\/h[1-6]>/gi, - (match, tag, attributes, content) => { - // Check if heading already has an ID - const existingIdMatch = attributes.match(/id="([^"]*)"/); - let anchorId = existingIdMatch ? existingIdMatch[1] : ''; - - // If no existing ID, generate one from content - if (!anchorId) { - anchorId = content - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') // Remove special chars - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/-+/g, '-') // Replace multiple hyphens with single - .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens - } - - // Add scroll margin to offset the header - use both class and inline style - const existingClass = attributes.includes('class=') ? attributes : ''; - const scrollMarginClass = existingClass - ? existingClass.replace(/class="([^"]*)"/, 'class="$1 scroll-mt-32"') - : 'class="scroll-mt-32"'; - - // Add inline style as backup to ensure scroll margin works - const existingStyle = attributes.includes('style=') ? attributes : ''; - const scrollMarginStyle = existingStyle - ? existingStyle.replace( - /style="([^"]*)"/, - 'style="$1; scroll-margin-top: 8rem;"' - ) - : 'style="scroll-margin-top: 8rem;"'; - - return `<${tag} ${scrollMarginClass} ${scrollMarginStyle} id="${anchorId}">${content}`; - } - ); - - // Then process the links - return html.replace( - /]*?)href="([^"]*?)"([^>]*?)>/g, - ( - match: string, - beforeHref: string, - href: string, - afterHref: string - ): string => { - // Keep absolute URLs (https/http) as clickable links - if (/^https?:\/\//i.test(href)) { - // Add target="_blank" for external links - if (!match.includes('target=')) { - return ``; - } - return match; - } - - // Handle anchor links (starting with #) - keep them as clickable for scrolling - if (href.startsWith('#')) { - return match; - } - - // Disable all relative links (/, ../, ./, etc.) by removing href and adding disabled styling - return ``; - } - ); -} - -export async function getStaticPaths() { - const sheetFiles = import.meta.glob('/data/cheatsheets/**/*.html', { - eager: true, - }); - const paths: Array<{ params: { category: string; name: string } }> = []; - - for (const path of Object.keys(sheetFiles)) { - const pathParts = path.split('/'); - const category = pathParts[pathParts.length - 2]; - const fileName = pathParts[pathParts.length - 1]; - const name = fileName.replace('.html', ''); - - paths.push({ - params: { category, name }, - }); - } - - return paths; -} - -const { category, name } = Astro.params; -const cheatsheetResult = await getCheatsheet(category!, name!); - -if (!cheatsheetResult) { - return Astro.redirect('/404'); -} - -const { content, metatags } = cheatsheetResult; - -// Process the content to handle links and add anchor IDs -const processedContent = processCheatsheetLinks(content); - -// Use metatags for title and description, with fallbacks -const title = metatags.title || name; -const description = metatags.description || `Cheatsheet for ${name}`; -const keywords = metatags.keywords || [ - 'free devtools', - 'cheatsheets', - category!, - name!, -]; - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Cheatsheets', href: '/freedevtools/c/' }, - { label: category, href: `/freedevtools/c/${category}/` }, - { label: name }, -]; ---- - - - - - -
- -
- - -
-
-
-
-
- - -
- -
- -
- - diff --git a/frontend/src/pages/c/[category]/[page].astro b/frontend/src/pages/c/[category]/[page].astro deleted file mode 100644 index 5c0c730128..0000000000 --- a/frontend/src/pages/c/[category]/[page].astro +++ /dev/null @@ -1,102 +0,0 @@ ---- -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import { - getAllCheatsheetCategories, - getCheatsheetsByCategory, -} from '../../../lib/cheatsheets-utils'; -import CategoryCheatsheetsPage from './_CategoryCheatsheetsPage.astro'; - -export async function getStaticPaths() { - const categories = await getAllCheatsheetCategories(); - const paths = []; - - for (const category of categories) { - const itemsPerPage = 30; - const totalPages = Math.ceil(category.cheatsheetCount / itemsPerPage); - - for (let page = 1; page <= totalPages; page++) { - paths.push({ - params: { - category: category.id, - page: page.toString(), - }, - props: { category: category.name }, - }); - } - } - - return paths; -} - -// Get category and page from params -const { category: categorySlug, page } = Astro.params; -const categoryName = - (Astro.props?.category as string) || - categorySlug! - .replace(/-/g, ' ') - .replace(/_/g, ' ') - .replace(/\b\w/g, (l: string) => l.toUpperCase()); -const currentPage = parseInt(page || '1'); - -// Get cheatsheets for this category -const cheatsheets = await getCheatsheetsByCategory(categoryName); -const totalCheatsheets = cheatsheets.length; - -// Pagination logic -const itemsPerPage = 30; -const totalPages = Math.ceil(totalCheatsheets / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const paginatedCheatsheets = cheatsheets.slice(startIndex, endIndex); - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Cheatsheets', href: '/freedevtools/c/' }, - { label: categoryName, href: `/freedevtools/c/${categorySlug}/` }, -]; - -// SEO data -const seoTitle = `${categoryName} Cheatsheets - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = `Browse page ${currentPage} of ${categoryName.toLowerCase()} cheatsheets. Comprehensive reference covering commands, syntax, and key concepts.`; -const keywords = [ - categoryName.toLowerCase(), - 'cheatsheets', - 'reference', - 'commands', - 'syntax', - 'programming', - 'documentation', - 'page ' + currentPage, -]; ---- - - - - diff --git a/frontend/src/pages/c/[category]/[slug].astro b/frontend/src/pages/c/[category]/[slug].astro new file mode 100644 index 0000000000..b201651393 --- /dev/null +++ b/frontend/src/pages/c/[category]/[slug].astro @@ -0,0 +1,309 @@ +--- +import AdBanner from '../../../components/banner/AdBanner.astro'; +import Banner from '../../../components/banner/BannerIndex.astro'; +import CreditsButton from '../../../components/buttons/CreditsButton'; +import SeeAlsoIndex from '../../../components/seealso/SeeAlsoIndex.astro'; +import ToolContainer from '../../../components/tool/ToolContainer'; +import ToolHead from '../../../components/tool/ToolHead'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import { getCheatsheet, getCheatsheetsByCategory, getAllCheatsheetCategories } from '../../../lib/cheatsheets-utils'; +import CategoryCheatsheetsPage from './_CategoryCheatsheetsPage.astro'; + +export const prerender = false; + +const { category: categorySlug, slug } = Astro.params; +const urlPath = Astro.url.pathname; + +if (!categorySlug || !slug) { + return new Response(null, { status: 404 }); +} + +// Redirect to add trailing slash if missing (BEFORE other checks) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +let cheatsheetData: any = null; +let paginationData: any = null; + +// Check if slug is numeric (pagination route) +if (/^\d+$/.test(slug)) { + // This is actually a pagination route for the category + const currentPage = parseInt(slug, 10); + + // Redirect /c/category/1 to /c/category + if (currentPage === 1) { + return Astro.redirect(`/freedevtools/c/${categorySlug}/`); + } + + // Validate category exists + const allCategories = await getAllCheatsheetCategories(); + const categoryIds = allCategories.map((c) => c.id); + if (!categoryIds.includes(categorySlug)) { + return new Response(null, { status: 404 }); + } + + // Find the category + const category = allCategories.find((c) => c.id === categorySlug); + if (!category) { + return new Response(null, { status: 404 }); + } + + const categoryName = category.name; + + // Get cheatsheets for this category + const cheatsheets = await getCheatsheetsByCategory(categoryName); + const totalCheatsheets = cheatsheets.length; + + // Pagination logic + const itemsPerPage = 30; + const totalPages = Math.ceil(totalCheatsheets / itemsPerPage); + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return new Response(null, { status: 404 }); + } + + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedCheatsheets = cheatsheets.slice(startIndex, endIndex); + + // Breadcrumb items + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Cheatsheets', href: '/freedevtools/c/' }, + { label: categoryName, href: `/freedevtools/c/${categorySlug}/` }, + ]; + + // SEO data + const seoTitle = `${categoryName} Cheatsheets - Page ${currentPage} | Online Free DevTools by Hexmos`; + const seoDescription = `Browse page ${currentPage} of ${categoryName.toLowerCase()} cheatsheets. Comprehensive reference covering commands, syntax, and key concepts.`; + const keywords = [ + categoryName.toLowerCase(), + 'cheatsheets', + 'reference', + 'commands', + 'syntax', + 'programming', + 'documentation', + 'page ' + currentPage, + ]; + + paginationData = { + categoryName, + categorySlug, + paginatedCheatsheets, + totalCheatsheets, + currentPage, + totalPages, + seoTitle, + seoDescription, + keywords, + breadcrumbItems, + }; +} else { + // This is a cheatsheet name (slug) + const cheatsheetResult = await getCheatsheet(categorySlug, slug); + + if (!cheatsheetResult) { + return new Response(null, { status: 404 }); + } + + const { content, metatags } = cheatsheetResult; + + // Process the content to handle links and add anchor IDs + function processCheatsheetLinks(html: string): string { + // First, add IDs to all headings (h1-h6) so anchor links work + html = html.replace( + /<(h[1-6])([^>]*)>([^<]+)<\/h[1-6]>/gi, + (match, tag, attributes, content) => { + // Check if heading already has an ID + const existingIdMatch = attributes.match(/id="([^"]*)"/); + let anchorId = existingIdMatch ? existingIdMatch[1] : ''; + + // If no existing ID, generate one from content + if (!anchorId) { + anchorId = content + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') // Remove special chars + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/-+/g, '-') // Replace multiple hyphens with single + .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens + } + + // Add scroll margin to offset the header - use both class and inline style + const existingClass = attributes.includes('class=') ? attributes : ''; + const scrollMarginClass = existingClass + ? existingClass.replace(/class="([^"]*)"/, 'class="$1 scroll-mt-32"') + : 'class="scroll-mt-32"'; + + // Add inline style as backup to ensure scroll margin works + const existingStyle = attributes.includes('style=') ? attributes : ''; + const scrollMarginStyle = existingStyle + ? existingStyle.replace( + /style="([^"]*)"/, + 'style="$1; scroll-margin-top: 8rem;"' + ) + : 'style="scroll-margin-top: 8rem;"'; + + return `<${tag} ${scrollMarginClass} ${scrollMarginStyle} id="${anchorId}">${content}`; + } + ); + + // Then process the links + return html.replace( + /]*?)href="([^"]*?)"([^>]*?)>/g, + ( + match: string, + beforeHref: string, + href: string, + afterHref: string + ): string => { + // Keep absolute URLs (https/http) as clickable links + if (/^https?:\/\//i.test(href)) { + // Add target="_blank" for external links + if (!match.includes('target=')) { + return ``; + } + return match; + } + + // Handle anchor links (starting with #) - keep them as clickable for scrolling + if (href.startsWith('#')) { + return match; + } + + // Disable all relative links (/, ../, ./, etc.) by removing href and adding disabled styling + return ``; + } + ); + } + + const processedContent = processCheatsheetLinks(content); + + // Use metatags for title and description, with fallbacks + const title = metatags.title || slug; + const description = metatags.description || `Cheatsheet for ${slug}`; + const keywords = metatags.keywords || [ + 'free devtools', + 'cheatsheets', + categorySlug, + slug, + ]; + + // Breadcrumb items + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Cheatsheets', href: '/freedevtools/c/' }, + { label: categorySlug, href: `/freedevtools/c/${categorySlug}/` }, + { label: slug }, + ]; + + cheatsheetData = { + title, + description, + keywords, + processedContent, + categorySlug, + slug, + breadcrumbItems, + }; +} +--- + +{paginationData ? ( + + + +) : cheatsheetData ? ( + + + + +
+ +
+ + +
+
+
+
+
+ + +
+ +
+ +
+ + +) : null} + diff --git a/frontend/src/pages/c/[category]/_CategoryCheatsheetsPage.astro b/frontend/src/pages/c/[category]/_CategoryCheatsheetsPage.astro index f9cb306bcd..01c0dfede6 100644 --- a/frontend/src/pages/c/[category]/_CategoryCheatsheetsPage.astro +++ b/frontend/src/pages/c/[category]/_CategoryCheatsheetsPage.astro @@ -80,6 +80,7 @@ const { currentPage={currentPage} totalPages={totalPages} baseUrl={`/freedevtools/c/${categorySlug}/`} + alwaysIncludePageNumber={true} />
diff --git a/frontend/src/pages/c/[category]/index.astro b/frontend/src/pages/c/[category]/index.astro index 65aa215126..77b2c94692 100644 --- a/frontend/src/pages/c/[category]/index.astro +++ b/frontend/src/pages/c/[category]/index.astro @@ -5,84 +5,211 @@ import { getCheatsheetsByCategory, } from '../../../lib/cheatsheets-utils'; import CategoryCheatsheetsPage from './_CategoryCheatsheetsPage.astro'; +import CheatsheetPage from '../_CheatsheetPage.astro'; -export async function getStaticPaths() { - const categories = await getAllCheatsheetCategories(); +export const prerender = false; - return categories.map((category) => ({ - params: { category: category.id }, - props: { category: category.name }, - })); +const { category: categorySlug } = Astro.params; +const urlPath = Astro.url.pathname; + +if (!categorySlug) { + return new Response(null, { status: 404 }); } -// Get category from params -const { category: categorySlug } = Astro.params; -const categoryName = - (Astro.props?.category as string) || - categorySlug! - .replace(/-/g, ' ') - .replace(/_/g, ' ') - .replace(/\b\w/g, (l: string) => l.toUpperCase()); - -// Get cheatsheets for this category -const cheatsheets = await getCheatsheetsByCategory(categoryName); -const totalCheatsheets = cheatsheets.length; - -// Pagination logic for page 1 -const itemsPerPage = 30; -const currentPage = 1; -const totalPages = Math.ceil(totalCheatsheets / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const paginatedCheatsheets = cheatsheets.slice(startIndex, endIndex); - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Cheatsheets', href: '/freedevtools/c/' }, - { label: categoryName, href: `/freedevtools/c/${categorySlug}/` }, -]; - -// SEO data -const seoTitle = `${categoryName} Cheatsheets | Online Free DevTools by Hexmos`; -const seoDescription = `Comprehensive ${categoryName.toLowerCase()} cheatsheets covering commands, syntax, and key concepts for faster learning and recall.`; -const keywords = [ - categoryName.toLowerCase(), - 'cheatsheets', - 'reference', - 'commands', - 'syntax', - 'programming', - 'documentation', -]; +// Redirect to add trailing slash if missing (BEFORE other checks) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +let categoryData: any = null; +let paginationData: any = null; + +// If category is numeric, this is actually a pagination route +// Handle it directly (workaround for route priority) +if (/^\d+$/.test(categorySlug)) { + const currentPage = parseInt(categorySlug, 10); + + // Redirect /c/1 to /c + if (currentPage === 1) { + return Astro.redirect('/freedevtools/c/'); + } + + // Fetch categories data directly (SSR mode) + const allCategories = await getAllCheatsheetCategories(); + + const itemsPerPage = 30; + const totalPages = Math.ceil(allCategories.length / itemsPerPage); + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return new Response(null, { status: 404 }); + } + + const totalCategories = allCategories.length; + + // Calculate total cheatsheets + const totalCheatsheets = allCategories.reduce((total, category) => total + category.cheatsheetCount, 0); + + // Get categories for current page + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const categories = allCategories.slice(startIndex, endIndex); + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Cheatsheets', href: '/freedevtools/c/' }, + ]; + + // SEO data + const seoTitle = `Cheatsheets - Page ${currentPage} | Online Free DevTools by Hexmos`; + const seoDescription = `Browse page ${currentPage} of our cheatsheets collection. Quick reference for commands, syntax, and programming concepts.`; + const canonical = `https://hexmos.com/freedevtools/c/${currentPage}/`; + + const mainKeywords = [ + 'cheatsheets', + 'reference', + 'commands', + 'syntax', + 'programming', + 'documentation', + 'quick reference', + 'command line', + 'cli', + 'terminal', + ]; + + paginationData = { + currentPage, + totalPages, + totalCategories, + totalCheatsheets, + categories, + breadcrumbItems, + seoTitle, + seoDescription, + canonical, + mainKeywords, + itemsPerPage, + }; +} else { + // Validate category exists + const allCategories = await getAllCheatsheetCategories(); + const categoryIds = allCategories.map((c) => c.id); + if (!categoryIds.includes(categorySlug)) { + return new Response(null, { status: 404 }); + } + + // Find the category + const category = allCategories.find((c) => c.id === categorySlug); + if (!category) { + return new Response(null, { status: 404 }); + } + + const categoryName = category.name; + + // Get cheatsheets for this category + const cheatsheets = await getCheatsheetsByCategory(categoryName); + const totalCheatsheets = cheatsheets.length; + + // Pagination logic for page 1 + const itemsPerPage = 30; + const currentPage = 1; + const totalPages = Math.ceil(totalCheatsheets / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedCheatsheets = cheatsheets.slice(startIndex, endIndex); + + // Breadcrumb items + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Cheatsheets', href: '/freedevtools/c/' }, + { label: categoryName, href: `/freedevtools/c/${categorySlug}/` }, + ]; + + // SEO data + const seoTitle = `${categoryName} Cheatsheets | Online Free DevTools by Hexmos`; + const seoDescription = `Comprehensive ${categoryName.toLowerCase()} cheatsheets covering commands, syntax, and key concepts for faster learning and recall.`; + const keywords = [ + categoryName.toLowerCase(), + 'cheatsheets', + 'reference', + 'commands', + 'syntax', + 'programming', + 'documentation', + ]; + + categoryData = { + categoryName, + categorySlug, + paginatedCheatsheets, + totalCheatsheets, + currentPage, + totalPages, + seoTitle, + seoDescription, + keywords, + breadcrumbItems, + }; +} --- - - - +{paginationData ? ( + + + +) : categoryData ? ( + + + +) : null} diff --git a/frontend/src/pages/c/[page].astro b/frontend/src/pages/c/[page].astro index e281a036ff..66ddc9dd7c 100644 --- a/frontend/src/pages/c/[page].astro +++ b/frontend/src/pages/c/[page].astro @@ -1,88 +1,139 @@ --- import BaseLayout from '../../layouts/BaseLayout.astro'; -import { generateCheatsheetStaticPaths, getAllCheatsheetCategories } from '../../lib/cheatsheets-utils'; +import { getAllCheatsheetCategories } from '../../lib/cheatsheets-utils'; import CheatsheetPage from './_CheatsheetPage.astro'; -export async function getStaticPaths() { - return await generateCheatsheetStaticPaths(); -} +export const prerender = false; -// Get page from params const { page } = Astro.params; -const currentPage = parseInt(page || '1'); - -// Get all cheatsheet categories -const allCategories = await getAllCheatsheetCategories(); - -// Pagination logic -const itemsPerPage = 30; -const totalPages = Math.ceil(allCategories.length / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const categories = allCategories.slice(startIndex, endIndex); -const totalCategories = allCategories.length; - -// Calculate total cheatsheets -const totalCheatsheets = allCategories.reduce((total, category) => total + category.cheatsheetCount, 0); - -// Breadcrumb data -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Cheatsheets', href: '/freedevtools/c/' } -]; - -// SEO data -const seoTitle = currentPage === 1 - ? "Cheatsheets - Quick Reference Commands & Syntax | Online Free DevTools by Hexmos" - : `Cheatsheets - Page ${currentPage} | Online Free DevTools by Hexmos`; - -const seoDescription = currentPage === 1 - ? "Concise, easy-to-scan reference pages that summarize commands, syntax, and key concepts for faster learning and recall." - : `Browse page ${currentPage} of our cheatsheets collection. Quick reference for commands, syntax, and programming concepts.`; - -const canonical = currentPage === 1 - ? "https://hexmos.com/freedevtools/c/" - : `https://hexmos.com/freedevtools/c/${currentPage}/`; - -// Enhanced keywords for main page -const mainKeywords = [ - 'cheatsheets', - 'reference', - 'commands', - 'syntax', - 'programming', - 'documentation', - 'quick reference', - 'command line', - 'cli', - 'terminal' -]; +const urlPath = Astro.url.pathname; + +// Early return if no page param +if (!page) { + return new Response(null, { status: 404 }); +} + +let categoryData: any = null; +let paginationData: any = null; + +// Check if page param is numeric +if (!/^\d+$/.test(page)) { + // If not numeric, it might be a category name + // Redirect to add trailing slash if missing (BEFORE checking category) + if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); + } + + // Check if it's a valid category + const allCategories = await getAllCheatsheetCategories(); + const categoryIds = allCategories.map((c) => c.id); + const isCategory = categoryIds.includes(page); + + if (isCategory) { + // This is a category route - redirect to category page + return Astro.redirect(`/freedevtools/c/${page}/`, 301); + } else { + // Not a valid category or page number - 404 + return new Response(null, { status: 404 }); + } +} else { + // Handle pagination route + const currentPage = parseInt(page, 10); + + // Redirect /c/1 to /c + if (currentPage === 1) { + return Astro.redirect('/freedevtools/c/'); + } + + // Fetch categories data directly (SSR mode) + const allCategories = await getAllCheatsheetCategories(); + + const itemsPerPage = 30; + const totalPages = Math.ceil(allCategories.length / itemsPerPage); + + // Validate page number + if (currentPage > totalPages || currentPage < 1) { + return new Response(null, { status: 404 }); + } + + const totalCategories = allCategories.length; + + // Calculate total cheatsheets + const totalCheatsheets = allCategories.reduce((total, category) => total + category.cheatsheetCount, 0); + + // Get categories for current page + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const categories = allCategories.slice(startIndex, endIndex); + + // Breadcrumb data + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Cheatsheets', href: '/freedevtools/c/' }, + ]; + + // SEO data + const seoTitle = `Cheatsheets - Page ${currentPage} | Online Free DevTools by Hexmos`; + const seoDescription = `Browse page ${currentPage} of our cheatsheets collection. Quick reference for commands, syntax, and programming concepts.`; + const canonical = `https://hexmos.com/freedevtools/c/${currentPage}/`; + + // Enhanced keywords for main page + const mainKeywords = [ + 'cheatsheets', + 'reference', + 'commands', + 'syntax', + 'programming', + 'documentation', + 'quick reference', + 'command line', + 'cli', + 'terminal', + ]; + + paginationData = { + currentPage, + totalPages, + totalCategories, + totalCheatsheets, + categories, + breadcrumbItems, + seoTitle, + seoDescription, + canonical, + mainKeywords, + itemsPerPage, + }; +} --- - - - +{ + paginationData ? ( + + + + ) : null +} diff --git a/frontend/src/pages/c/_CheatsheetPage.astro b/frontend/src/pages/c/_CheatsheetPage.astro index 32dd51ec4a..505de460c3 100644 --- a/frontend/src/pages/c/_CheatsheetPage.astro +++ b/frontend/src/pages/c/_CheatsheetPage.astro @@ -104,6 +104,7 @@ const { currentPage={currentPage} totalPages={totalPages} baseUrl="/freedevtools/c/" + alwaysIncludePageNumber={true} />
From 34dfd907c865a37af9ee693b749eed033cb84a20 Mon Sep 17 00:00:00 2001 From: lovestaco Date: Sun, 16 Nov 2025 21:30:02 +0530 Subject: [PATCH 09/79] fix: sitemaps --- .../pages/man-pages/sitemap-[index].xml.ts | 157 +++++++++--------- .../pages/png_icons/sitemap-[index].xml.ts | 73 ++++---- .../pages/svg_icons/sitemap-[index].xml.ts | 71 ++++---- frontend/src/pages/tldr/sitemap.xml.ts | 9 +- 4 files changed, 162 insertions(+), 148 deletions(-) diff --git a/frontend/src/pages/man-pages/sitemap-[index].xml.ts b/frontend/src/pages/man-pages/sitemap-[index].xml.ts index 6500ac74dd..a08ffa4c5a 100644 --- a/frontend/src/pages/man-pages/sitemap-[index].xml.ts +++ b/frontend/src/pages/man-pages/sitemap-[index].xml.ts @@ -2,92 +2,91 @@ import type { APIRoute } from 'astro'; const MAX_URLS = 5000; -export async function getStaticPaths() { +// Loader function for sitemap URLs - extracted to work in both SSG and SSR +async function loadUrls() { const Database = (await import('better-sqlite3')).default; const path = (await import('path')).default; + const dbPath = path.join(process.cwd(), 'db/all_dbs/man-pages-db.db'); + const db = new Database(dbPath, { readonly: true }); + const now = new Date().toISOString(); + + // Build URLs with placeholder for site + const stmt = db.prepare(` + SELECT main_category, sub_category, slug + FROM man_pages + WHERE slug IS NOT NULL AND slug != '' + ORDER BY main_category, sub_category, slug + `); + + const manPages = stmt.all() as Array<{ + main_category: string; + sub_category: string; + slug: string; + }>; + + const urls = manPages.map((manPage) => { + return ` + + __SITE__/man-pages/${manPage.main_category}/${manPage.sub_category}/${manPage.slug}/ + ${now} + daily + 0.8 + `; + }); - // Loader function for sitemap URLs - async function loadUrls() { - const dbPath = path.join(process.cwd(), 'db/all_dbs/man-pages-db.db'); - const db = new Database(dbPath, { readonly: true }); - const now = new Date().toISOString(); - - // Build URLs with placeholder for site - const stmt = db.prepare(` - SELECT main_category, sub_category, slug - FROM man_pages - WHERE slug IS NOT NULL AND slug != '' - ORDER BY main_category, sub_category, slug - `); - - const manPages = stmt.all() as Array<{ - main_category: string; - sub_category: string; - slug: string; - }>; - - const urls = manPages.map((manPage) => { - return ` - - __SITE__/man-pages/${manPage.main_category}/${manPage.sub_category}/${manPage.slug}/ - ${now} - daily - 0.8 - `; - }); - - // Include landing page - urls.unshift(` + // Include landing page + urls.unshift(` + + __SITE__/man-pages/ + ${now} + daily + 0.9 + `); + + // Add category index pages + const categoryStmt = db.prepare(` + SELECT DISTINCT main_category + FROM man_pages + ORDER BY main_category + `); + const categories = categoryStmt.all() as Array<{ main_category: string }>; + + categories.forEach(({ main_category }) => { + urls.push(` - __SITE__/man-pages/ + __SITE__/man-pages/${main_category}/ ${now} daily - 0.9 + 0.7 `); + }); - // Add category index pages - const categoryStmt = db.prepare(` - SELECT DISTINCT main_category - FROM man_pages - ORDER BY main_category - `); - const categories = categoryStmt.all() as Array<{ main_category: string }>; - - categories.forEach(({ main_category }) => { - urls.push(` - - __SITE__/man-pages/${main_category}/ - ${now} - daily - 0.7 - `); - }); - - // Add subcategory index pages - const subcategoryStmt = db.prepare(` - SELECT DISTINCT main_category, sub_category - FROM man_pages - ORDER BY main_category, sub_category - `); - const subcategories = subcategoryStmt.all() as Array<{ - main_category: string; - sub_category: string; - }>; - - subcategories.forEach(({ main_category, sub_category }) => { - urls.push(` - - __SITE__/man-pages/${main_category}/${sub_category}/ - ${now} - daily - 0.6 - `); - }); + // Add subcategory index pages + const subcategoryStmt = db.prepare(` + SELECT DISTINCT main_category, sub_category + FROM man_pages + ORDER BY main_category, sub_category + `); + const subcategories = subcategoryStmt.all() as Array<{ + main_category: string; + sub_category: string; + }>; + + subcategories.forEach(({ main_category, sub_category }) => { + urls.push(` + + __SITE__/man-pages/${main_category}/${sub_category}/ + ${now} + daily + 0.6 + `); + }); - db.close(); - return urls; - } + db.close(); + return urls; +} +export async function getStaticPaths() { // Pre-count total pages try { const Database = (await import('better-sqlite3')).default; @@ -120,8 +119,10 @@ export async function getStaticPaths() { } export const GET: APIRoute = async ({ site, params, props }) => { - const loadUrls: () => Promise = props.loadUrls; - let urls = await loadUrls(); + // In SSR mode, props.loadUrls won't exist, so call loadUrls directly + // In SSG mode, props.loadUrls will be available + const loadUrlsFn: (() => Promise) | undefined = props?.loadUrls; + let urls = loadUrlsFn ? await loadUrlsFn() : await loadUrls(); // Replace placeholder with actual site urls = urls.map((u) => u.replace(/__SITE__/g, site?.toString() || '')); diff --git a/frontend/src/pages/png_icons/sitemap-[index].xml.ts b/frontend/src/pages/png_icons/sitemap-[index].xml.ts index e3c48ba5f1..5abacd6808 100644 --- a/frontend/src/pages/png_icons/sitemap-[index].xml.ts +++ b/frontend/src/pages/png_icons/sitemap-[index].xml.ts @@ -1,47 +1,48 @@ -// src/pages/svg_icons/sitemap-[index].xml.ts +// src/pages/png_icons/sitemap-[index].xml.ts import type { APIRoute } from 'astro'; import path from 'path'; const MAX_URLS = 5000; -export async function getStaticPaths() { +// Loader function for sitemap URLs - extracted to work in both SSG and SSR +async function loadUrls() { const { glob } = await import('glob'); + const svgFiles = await glob('**/*.svg', { cwd: './public/svg_icons' }); + const now = new Date().toISOString(); + + // Build URLs with placeholder for site + const urls = svgFiles.map((file) => { + const parts = file.split(path.sep); + const name = parts.pop()!.replace('.svg', ''); + const category = parts.pop() || 'general'; - // Loader function for sitemap URLs - async function loadUrls() { - const svgFiles = await glob('**/*.svg', { cwd: './public/svg_icons' }); - const now = new Date().toISOString(); - - // Build URLs with placeholder for site - const urls = svgFiles.map((file) => { - const parts = file.split(path.sep); - const name = parts.pop()!.replace('.svg', ''); - const category = parts.pop() || 'general'; - - return ` - - __SITE__/png_icons/${category}/${name}/ - ${now} - daily - 0.8 - - __SITE__/svg_icons/${category}/${name}.svg - Free ${name} PNG Icon Download - - `; - }); - - // Include landing page - urls.unshift(` + return ` - __SITE__/png_icons/ + __SITE__/png_icons/${category}/${name}/ ${now} daily - 0.9 - `); + 0.8 + + __SITE__/svg_icons/${category}/${name}.svg + Free ${name} PNG Icon Download + + `; + }); - return urls; - } + // Include landing page + urls.unshift(` + + __SITE__/png_icons/ + ${now} + daily + 0.9 + `); + + return urls; +} + +export async function getStaticPaths() { + const { glob } = await import('glob'); // Pre-count total pages const svgFiles = await glob('**/*.svg', { cwd: './public/svg_icons' }); @@ -55,8 +56,10 @@ export async function getStaticPaths() { } export const GET: APIRoute = async ({ site, params, props }) => { - const loadUrls: () => Promise = props.loadUrls; - let urls = await loadUrls(); + // In SSR mode, props.loadUrls won't exist, so call loadUrls directly + // In SSG mode, props.loadUrls will be available + const loadUrlsFn: (() => Promise) | undefined = props?.loadUrls; + let urls = loadUrlsFn ? await loadUrlsFn() : await loadUrls(); // Replace placeholder with actual site urls = urls.map((u) => u.replace(/__SITE__/g, site)); diff --git a/frontend/src/pages/svg_icons/sitemap-[index].xml.ts b/frontend/src/pages/svg_icons/sitemap-[index].xml.ts index e51da037bd..d8246d2bb2 100644 --- a/frontend/src/pages/svg_icons/sitemap-[index].xml.ts +++ b/frontend/src/pages/svg_icons/sitemap-[index].xml.ts @@ -4,44 +4,45 @@ import path from 'path'; const MAX_URLS = 5000; -export async function getStaticPaths() { +// Loader function for sitemap URLs - extracted to work in both SSG and SSR +async function loadUrls() { const { glob } = await import('glob'); + const svgFiles = await glob('**/*.svg', { cwd: './public/svg_icons' }); + const now = new Date().toISOString(); + + // Build URLs with placeholder for site + const urls = svgFiles.map((file) => { + const parts = file.split(path.sep); + const name = parts.pop()!.replace('.svg', ''); + const category = parts.pop() || 'general'; - // Loader function for sitemap URLs - async function loadUrls() { - const svgFiles = await glob('**/*.svg', { cwd: './public/svg_icons' }); - const now = new Date().toISOString(); - - // Build URLs with placeholder for site - const urls = svgFiles.map((file) => { - const parts = file.split(path.sep); - const name = parts.pop()!.replace('.svg', ''); - const category = parts.pop() || 'general'; - - return ` - - __SITE__/svg_icons/${category}/${name}/ - ${now} - daily - 0.8 - - __SITE__/svg_icons/${category}/${name}.svg - Free ${name} SVG Icon Download - - `; - }); - - // Include landing page - urls.unshift(` + return ` - __SITE__/svg_icons/ + __SITE__/svg_icons/${category}/${name}/ ${now} daily - 0.9 - `); + 0.8 + + __SITE__/svg_icons/${category}/${name}.svg + Free ${name} SVG Icon Download + + `; + }); - return urls; - } + // Include landing page + urls.unshift(` + + __SITE__/svg_icons/ + ${now} + daily + 0.9 + `); + + return urls; +} + +export async function getStaticPaths() { + const { glob } = await import('glob'); // Pre-count total pages const svgFiles = await glob('**/*.svg', { cwd: './public/svg_icons' }); @@ -55,8 +56,10 @@ export async function getStaticPaths() { } export const GET: APIRoute = async ({ site, params, props }) => { - const loadUrls: () => Promise = props.loadUrls; - let urls = await loadUrls(); + // In SSR mode, props.loadUrls won't exist, so call loadUrls directly + // In SSG mode, props.loadUrls will be available + const loadUrlsFn: (() => Promise) | undefined = props?.loadUrls; + let urls = loadUrlsFn ? await loadUrlsFn() : await loadUrls(); // Replace placeholder with actual site urls = urls.map((u) => u.replace(/__SITE__/g, site)); diff --git a/frontend/src/pages/tldr/sitemap.xml.ts b/frontend/src/pages/tldr/sitemap.xml.ts index 2e0e90b9a1..f0260c7423 100644 --- a/frontend/src/pages/tldr/sitemap.xml.ts +++ b/frontend/src/pages/tldr/sitemap.xml.ts @@ -60,7 +60,7 @@ export const GET: APIRoute = async ({ site }) => { ); } // Individual command pages - for (const [platform, commands] of Object.entries(byPlatform)) { + for (const [_platform, commands] of Object.entries(byPlatform)) { for (const cmd of commands) { // Remove /freedevtools prefix and ensure proper URL construction const cleanUrl = cmd.url.replace('/freedevtools', ''); @@ -75,6 +75,13 @@ export const GET: APIRoute = async ({ site }) => { } } + // Sort URLs in ascending order by extracting the value + urls.sort((a, b) => { + const urlA = a.match(/(.*?)<\/loc>/)?.[1] || ''; + const urlB = b.match(/(.*?)<\/loc>/)?.[1] || ''; + return urlA.localeCompare(urlB); + }); + const xml = `\n\n\n${urls.join('\n')}\n`; return new Response(xml, { From 7da9429628e412722ae49fd3e972c645c70337cc Mon Sep 17 00:00:00 2001 From: lovestaco Date: Sun, 16 Nov 2025 21:30:35 +0530 Subject: [PATCH 10/79] fix: serve commands --- frontend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/package.json b/frontend/package.json index 8331c24ef0..c39f9f44d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "build:tldr": "node scripts/builds/tldr.js", "build:icons": "node scripts/builds/icons.js", "build:man-pages": "node scripts/builds/man-pages.js", + "serve-ssr": " node ./dist/server/entry.mjs", "preview": "astro preview", "astro": "astro", "format": "prettier --write .", From 666c2b7d09b319cc398615f20fe889991e9483e5 Mon Sep 17 00:00:00 2001 From: lovestaco Date: Mon, 17 Nov 2025 17:41:17 +0530 Subject: [PATCH 11/79] fix: install onnxruntime node --- frontend/package-lock.json | 59 ++++++++++++++++++++++++++++++++++---- frontend/package.json | 1 + 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6967346d03..82f54de7be 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -51,6 +51,7 @@ "jsonrepair": "^3.13.0", "konva": "^10.0.2", "marked": "^16.3.0", + "onnxruntime-node": "^1.23.2", "purgecss": "^7.0.2", "qrcode": "^1.5.4", "react": "^19.1.1", @@ -1102,6 +1103,29 @@ "sharp": "^0.34.1" } }, + "node_modules/@huggingface/transformers/node_modules/onnxruntime-common": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", + "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", + "license": "MIT" + }, + "node_modules/@huggingface/transformers/node_modules/onnxruntime-node": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", + "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "global-agent": "^3.0.0", + "onnxruntime-common": "1.21.0", + "tar": "^7.0.1" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1488,6 +1512,8 @@ }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -3420,6 +3446,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "dev": true, @@ -4648,6 +4683,8 @@ }, "node_modules/chownr": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -9842,6 +9879,8 @@ }, "node_modules/minizlib": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -10183,11 +10222,15 @@ } }, "node_modules/onnxruntime-common": { - "version": "1.21.0", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.23.2.tgz", + "integrity": "sha512-5LFsC9Dukzp2WV6kNHYLNzp8sT6V02IubLCbzw2Xd6X5GOlr65gAX6xiJwyi2URJol/s71gaQLC5F2C25AAR2w==", "license": "MIT" }, "node_modules/onnxruntime-node": { - "version": "1.21.0", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.23.2.tgz", + "integrity": "sha512-OBTsG0W8ddBVOeVVVychpVBS87A9YV5sa2hJ6lc025T97Le+J4v++PwSC4XFs1C62SWyNdof0Mh4KvnZgtt4aw==", "hasInstallScript": true, "license": "MIT", "os": [ @@ -10196,9 +10239,9 @@ "linux" ], "dependencies": { + "adm-zip": "^0.5.16", "global-agent": "^3.0.0", - "onnxruntime-common": "1.21.0", - "tar": "^7.0.1" + "onnxruntime-common": "1.23.2" } }, "node_modules/onnxruntime-web": { @@ -12937,8 +12980,10 @@ } }, "node_modules/tar": { - "version": "7.5.1", - "license": "ISC", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -12980,6 +13025,8 @@ }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" diff --git a/frontend/package.json b/frontend/package.json index c39f9f44d5..7cbb5f3007 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -68,6 +68,7 @@ "jsonrepair": "^3.13.0", "konva": "^10.0.2", "marked": "^16.3.0", + "onnxruntime-node": "^1.23.2", "purgecss": "^7.0.2", "qrcode": "^1.5.4", "react": "^19.1.1", From a13a95fd1c1e7e5692c80921fa389ac8f75c63c8 Mon Sep 17 00:00:00 2001 From: lovestaco Date: Mon, 17 Nov 2025 19:32:08 +0530 Subject: [PATCH 12/79] fix: emoji to ssr --- frontend/src/pages/emojis/[category].astro | 513 +++++++++++------- .../src/pages/emojis/[category]/[page].astro | 54 +- frontend/src/pages/emojis/[page].astro | 229 -------- frontend/src/pages/emojis/[slug].astro | 453 ++++++++++++---- .../emojis/apple-emojis/[category].astro | 51 +- .../apple-emojis/[category]/[page].astro | 51 +- .../pages/emojis/apple-emojis/[slug].astro | 30 +- .../emojis/discord-emojis/[category].astro | 51 +- .../discord-emojis/[category]/[page].astro | 57 +- .../pages/emojis/discord-emojis/[slug].astro | 30 +- 10 files changed, 857 insertions(+), 662 deletions(-) delete mode 100644 frontend/src/pages/emojis/[page].astro diff --git a/frontend/src/pages/emojis/[category].astro b/frontend/src/pages/emojis/[category].astro index e9012c98c4..490adddb32 100644 --- a/frontend/src/pages/emojis/[category].astro +++ b/frontend/src/pages/emojis/[category].astro @@ -4,210 +4,353 @@ import Pagination from '../../components/PaginationComponent.astro'; import CreditsButton from '../../components/buttons/CreditsButton'; import ToolContainer from '../../components/tool/ToolContainer'; import ToolHead from '../../components/tool/ToolHead'; -import { getEmojisByCategory, getEmojiCategories } from '../../lib/emojis'; import AdBanner from '../../components/banner/AdBanner.astro'; +import EachEmojiPage from './EachEmojiPage.astro'; +import { getEmojisByCategory, getEmojiCategories, getEmojiBySlug, getEmojiImages } from '../../lib/emojis'; +import { apple_vendor_excluded_emojis, discord_vendor_excluded_emojis } from '@/lib/emojis-consts'; -export async function getStaticPaths() { - const categories = getEmojiCategories(); - - return categories - .filter((category) => category && category.trim() !== '') - .map((category) => ({ - params: { category: category.toLowerCase().replace(/[^a-z0-9]+/g, '-') }, - props: { category } - })); -} +export const prerender = false; -// Prefer original category from build-time props to preserve symbols like '&' const { category: categorySlug } = Astro.params; -const categoryName = (Astro.props?.category as string) || categorySlug!.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); - -// SEO metadata and descriptions for categories -const categorySeo: Record = { - 'Activities': { - title: 'Activities Emojis - Sports, Events, and Hobbies | Online Free DevTools by Hexmos', - description: 'Explore activities emojis covering sports, games, celebrations, and hobbies. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['activities emojis', 'sports emojis', 'games emojis', 'celebration emojis', 'hobby emojis', 'copy emoji', 'emoji meanings'] - }, - 'Animals & Nature': { - title: 'Animals & Nature Emojis - Wildlife, Plants, and Weather | Online Free DevTools by Hexmos', - description: 'Discover animals and nature emojis including wildlife, pets, plants, and weather symbols. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['animals emojis', 'nature emojis', 'wildlife emojis', 'plant emojis', 'weather emojis', 'copy emoji', 'emoji meanings'] - }, - 'Food & Drink': { - title: 'Food & Drink Emojis - Meals, Beverages, and Snacks | Online Free DevTools by Hexmos', - description: 'Browse food and drink emojis including meals, beverages, fruits, vegetables, and snacks. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['food emojis', 'drink emojis', 'meal emojis', 'beverage emojis', 'fruit emojis', 'copy emoji', 'emoji meanings'] - }, - 'Objects': { - title: 'Objects Emojis - Technology, Tools, and Items | Online Free DevTools by Hexmos', - description: 'Explore object emojis including technology, tools, clothing, and everyday items. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['object emojis', 'technology emojis', 'tool emojis', 'clothing emojis', 'item emojis', 'copy emoji', 'emoji meanings'] - }, - 'People & Body': { - title: 'People & Body Emojis - Faces, Gestures, and Body Parts | Online Free DevTools by Hexmos', - description: 'Discover people and body emojis including faces, gestures, body parts, and family members. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['people emojis', 'body emojis', 'face emojis', 'gesture emojis', 'family emojis', 'copy emoji', 'emoji meanings'] - }, - 'Smileys & Emotion': { - title: 'Smileys & Emotion Emojis - Faces, Feelings, and Expressions | Online Free DevTools by Hexmos', - description: 'Browse smileys and emotion emojis including faces, feelings, and expressions. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['smiley emojis', 'emotion emojis', 'face emojis', 'feeling emojis', 'expression emojis', 'copy emoji', 'emoji meanings'] - }, - 'Symbols': { - title: 'Symbols Emojis - Signs, Shapes, and Icons | Online Free DevTools by Hexmos', - description: 'Explore symbol emojis including signs, shapes, icons, and special characters. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['symbol emojis', 'sign emojis', 'shape emojis', 'icon emojis', 'character emojis', 'copy emoji', 'emoji meanings'] - }, - 'Travel & Places': { - title: 'Travel & Places Emojis - Destinations, Transportation, and Locations | Online Free DevTools by Hexmos', - description: 'Discover travel and places emojis including destinations, transportation, and location symbols. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['travel emojis', 'places emojis', 'destination emojis', 'transportation emojis', 'location emojis', 'copy emoji', 'emoji meanings'] - }, - 'Flags': { - title: 'Flags Emojis - Country and Regional Flags | Online Free DevTools by Hexmos', - description: 'Browse flag emojis including country flags, regional flags, and special flags. Copy emoji, view meanings, and find shortcodes instantly.', - keywords: ['flag emojis', 'country emojis', 'regional emojis', 'national emojis', 'copy emoji', 'emoji meanings'] + +// Early return if no category param +if (!categorySlug) { + return new Response(null, { status: 404 }); +} + +// Check if category is numeric (pagination route) +if (/^\d+$/.test(categorySlug)) { + // This is actually a pagination route - redirect to [slug].astro + return Astro.redirect(`/freedevtools/emojis/${categorySlug}/`, 301); +} + +// Get all categories to validate +const allCategories = getEmojiCategories(); +const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); + +let emojiData: any = null; +let categoryData: any = null; + +// Check if this is actually an emoji slug (not a category) +// Since [category].astro matches first, we need to handle emoji slugs here +// This is a workaround for route priority not working as expected +const emoji = getEmojiBySlug(categorySlug); +if (emoji && !categorySlugs.includes(categorySlug)) { + // This is an emoji slug, not a category - handle it here + const images = getEmojiImages(categorySlug); + + const cleanDescription = (text?: string) => { + if (!text) return ''; + return text + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/[?]{2,}/g, '') + .trim(); + }; + + const categoryName = (emoji.category || 'Other') as string; + const categorySlugForBreadcrumb = categoryName.toLowerCase().replace(/[^a-z0-9]+/g, '-'); + const cleanedDescription = cleanDescription(emoji.description); + const seoDescription = cleanedDescription || + `Learn about the ${emoji.title || emoji.slug} emoji ${emoji.code || ''}. Find meanings, shortcodes, and usage information.`; + + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Emojis', href: '/freedevtools/emojis/' }, + { label: categoryName, href: `/freedevtools/emojis/${categorySlugForBreadcrumb}/` }, + { label: emoji.title || emoji.slug }, + ]; + + emojiData = { + emoji, + images, + categoryName, + categorySlugForBreadcrumb, + breadcrumbItems, + seoDescription, + }; +} else { + // Validate category exists + if (!categorySlugs.includes(categorySlug)) { + return new Response(null, { status: 404 }); } -}; - -const seoData = categorySeo[categoryName] || { - title: `${categoryName} Emojis | Online Free DevTools by Hexmos`, - description: `Explore ${categoryName.toLowerCase()} emojis. Copy emoji, view meanings, and find shortcodes instantly.`, - keywords: [`${categoryName.toLowerCase()} emojis`, 'copy emoji', 'emoji meanings', 'emoji shortcodes'] -}; - -// Get emojis for this category -const emojis = await getEmojisByCategory(categoryName); -const totalEmojis = emojis.length; - -// Pagination logic for page 1 -const itemsPerPage = 36; -const currentPage = 1; -const totalPages = Math.ceil(totalEmojis / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const paginatedEmojis = emojis.slice(startIndex, endIndex); - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: categoryName, href: `/freedevtools/emojis/${categorySlug}/` } -]; + + // Find the actual category name from the slug + const categoryName = allCategories.find( + cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug + ) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); + + // SEO metadata and descriptions for categories + const categorySeo: Record = { + 'Activities': { + title: 'Activities Emojis - Sports, Events, and Hobbies | Online Free DevTools by Hexmos', + description: 'Explore activities emojis covering sports, games, celebrations, and hobbies. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: ['activities emojis', 'sports emojis', 'games emojis', 'celebration emojis', 'hobby emojis', 'copy emoji', 'emoji meanings'] + }, + 'Animals & Nature': { + title: 'Animals & Nature Emojis - Wildlife, Plants, and Weather | Online Free DevTools by Hexmos', + description: 'Discover animals and nature emojis including wildlife, pets, plants, and weather symbols. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: ['animals emojis', 'nature emojis', 'wildlife emojis', 'plant emojis', 'weather emojis', 'copy emoji', 'emoji meanings'] + }, + 'Food & Drink': { + title: 'Food & Drink Emojis - Meals, Beverages, and Snacks | Online Free DevTools by Hexmos', + description: 'Browse food and drink emojis including meals, beverages, fruits, vegetables, and snacks. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: ['food emojis', 'drink emojis', 'meal emojis', 'beverage emojis', 'fruit emojis', 'copy emoji', 'emoji meanings'] + }, + 'Objects': { + title: 'Objects Emojis - Technology, Tools, and Items | Online Free DevTools by Hexmos', + description: 'Explore object emojis including technology, tools, clothing, and everyday items. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: ['object emojis', 'technology emojis', 'tool emojis', 'clothing emojis', 'item emojis', 'copy emoji', 'emoji meanings'] + }, + 'People & Body': { + title: 'People & Body Emojis - Faces, Gestures, and Body Parts | Online Free DevTools by Hexmos', + description: 'Discover people and body emojis including faces, gestures, body parts, and family members. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: ['people emojis', 'body emojis', 'face emojis', 'gesture emojis', 'family emojis', 'copy emoji', 'emoji meanings'] + }, + 'Smileys & Emotion': { + title: 'Smileys & Emotion Emojis - Faces, Feelings, and Expressions | Online Free DevTools by Hexmos', + description: 'Browse smileys and emotion emojis including faces, feelings, and expressions. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: ['smiley emojis', 'emotion emojis', 'face emojis', 'feeling emojis', 'expression emojis', 'copy emoji', 'emoji meanings'] + }, + 'Symbols': { + title: 'Symbols Emojis - Signs, Shapes, and Icons | Online Free DevTools by Hexmos', + description: 'Explore symbol emojis including signs, shapes, icons, and special characters. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: ['symbol emojis', 'sign emojis', 'shape emojis', 'icon emojis', 'character emojis', 'copy emoji', 'emoji meanings'] + }, + 'Travel & Places': { + title: 'Travel & Places Emojis - Destinations, Transportation, and Locations | Online Free DevTools by Hexmos', + description: 'Discover travel and places emojis including destinations, transportation, and location symbols. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: ['travel emojis', 'places emojis', 'destination emojis', 'transportation emojis', 'location emojis', 'copy emoji', 'emoji meanings'] + }, + 'Flags': { + title: 'Flags Emojis - Country and Regional Flags | Online Free DevTools by Hexmos', + description: 'Browse flag emojis including country flags, regional flags, and special flags. Copy emoji, view meanings, and find shortcodes instantly.', + keywords: ['flag emojis', 'country emojis', 'regional emojis', 'national emojis', 'copy emoji', 'emoji meanings'] + } + }; + + const seoData = categorySeo[categoryName] || { + title: `${categoryName} Emojis | Online Free DevTools by Hexmos`, + description: `Explore ${categoryName.toLowerCase()} emojis. Copy emoji, view meanings, and find shortcodes instantly.`, + keywords: [`${categoryName.toLowerCase()} emojis`, 'copy emoji', 'emoji meanings', 'emoji shortcodes'] + }; + + // Get emojis for this category + const emojis = await getEmojisByCategory(categoryName); + const totalEmojis = emojis.length; + + // Pagination logic for page 1 + const itemsPerPage = 36; + const currentPage = 1; + const totalPages = Math.ceil(totalEmojis / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedEmojis = emojis.slice(startIndex, endIndex); + + // Breadcrumb items + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Emojis', href: '/freedevtools/emojis/' }, + { label: categoryName, href: `/freedevtools/emojis/${categorySlug}/` } + ]; + + categoryData = { + categoryName, + categorySlug, + seoData, + emojis, + totalEmojis, + itemsPerPage, + currentPage, + totalPages, + paginatedEmojis, + breadcrumbItems, + }; +} --- - - -
- -
- - - -
-
-
-
{totalEmojis.toLocaleString()}
-
Emojis
-
-
-
{totalPages}
-
Pages
+{emojiData ? ( + + +
+ +
+ + + + +
+
+ + ← Back to Emojis + + {emojiData.emoji.category ? ( + + View{' '} + {emojiData.emoji.category + .replace(/-/g, ' ') + .replace(/\b\w/g, (l) => l.toUpperCase())}{' '} + Category + + ) : null} + {emojiData.emoji.apple_vendor_description && + !apple_vendor_excluded_emojis.includes(emojiData.emoji.slug) ? ( + + 🍎 View Apple Version + + ) : null} + {emojiData.emoji.discord_vendor_description && + !discord_vendor_excluded_emojis.includes(emojiData.emoji.slug) ? ( + + 🎮 View Discord Version + + ) : null} +
+
+
+
+) : categoryData ? ( + + +
+ +
+ + + +
+
+
+
{categoryData.totalEmojis.toLocaleString()}
+
Emojis
+
+
+
{categoryData.totalPages}
+
Pages
+
- -
-
- Showing {paginatedEmojis.length} of {totalEmojis} emojis (Page {currentPage} of {totalPages}) + +
+
+ Showing {categoryData.paginatedEmojis.length} of {categoryData.totalEmojis} emojis (Page {categoryData.currentPage} of {categoryData.totalPages}) +
-
- -
- {paginatedEmojis.map((emoji) => { - const emojiChar = emoji.code || ''; - const emojiTitle = emoji.title || emoji.slug; + +
+ {categoryData.paginatedEmojis.map((emoji) => { + const emojiChar = emoji.code || ''; + const emojiTitle = emoji.title || emoji.slug; - // Count visible components (graphemes) + joining characters (ZWJ) - const graphemeCount = [...emojiChar].length; - const zwjCount = (emojiChar.match(/\u200d/g) || []).length; + // Count visible components (graphemes) + joining characters (ZWJ) + const graphemeCount = [...emojiChar].length; + const zwjCount = (emojiChar.match(/\u200d/g) || []).length; - // Complexity heuristic - const complexity = graphemeCount + zwjCount * 2; + // Complexity heuristic + const complexity = graphemeCount + zwjCount * 2; - // Adjust font size dynamically - let emojiSizeClass = 'text-5xl'; - if (complexity > 10) emojiSizeClass = 'text-xl'; // very complex (family, kiss variants) - else if (complexity > 6) emojiSizeClass = 'text-3xl'; // multi-skin or gendered sequences - else if (complexity > 3) emojiSizeClass = 'text-4xl'; // moderately complex (2–3 parts) + // Adjust font size dynamically + let emojiSizeClass = 'text-5xl'; + if (complexity > 10) emojiSizeClass = 'text-xl'; // very complex (family, kiss variants) + else if (complexity > 6) emojiSizeClass = 'text-3xl'; // multi-skin or gendered sequences + else if (complexity > 3) emojiSizeClass = 'text-4xl'; // moderately complex (2–3 parts) - return ( - -
- {emojiChar} -
-
- {emojiTitle} -
+
+ {emojiChar} +
+
+ {emojiTitle} +
+
+ ); + })} +
+ + + + +
+
+ + ← Back to Emojis - ); - })} -
- - - - - -
- - + + +) : null} \ No newline at end of file + diff --git a/frontend/src/pages/emojis/[category]/[page].astro b/frontend/src/pages/emojis/[category]/[page].astro index fc0d15feac..1af9860658 100644 --- a/frontend/src/pages/emojis/[category]/[page].astro +++ b/frontend/src/pages/emojis/[category]/[page].astro @@ -7,39 +7,35 @@ import ToolHead from '../../../components/tool/ToolHead'; import { getEmojisByCategory, getEmojiCategories } from '../../../lib/emojis'; import AdBanner from '../../../components/banner/AdBanner.astro'; +export const prerender = false; + const { category: categorySlug, page } = Astro.params; -const currentPage = parseInt(page || '1'); +const urlPath = Astro.url.pathname; -export async function getStaticPaths() { - const categories = getEmojiCategories(); - - const paths: any[] = []; - - for (const category of categories) { - if (category && category.trim() !== '') { - const categorySlug = category.toLowerCase().replace(/[^a-z0-9]+/g, '-'); - const emojis = await getEmojisByCategory(category); - const itemsPerPage = 30; - const totalPages = Math.ceil(emojis.length / itemsPerPage); - - // Generate pages for this category - for (let pageNum = 1; pageNum <= totalPages; pageNum++) { - paths.push({ - params: { - category: categorySlug, - page: pageNum.toString() - }, - props: { category } - }); - } - } - } - - return paths; +// Handle trailing slash redirect FIRST (before any logic) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Validate page is numeric +if (!page || !/^\d+$/.test(page)) { + return new Response(null, { status: 404 }); +} + +const currentPage = parseInt(page, 10); + +// Validate category exists +const allCategories = getEmojiCategories(); +const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); + +if (!categorySlug || !categorySlugs.includes(categorySlug)) { + return new Response(null, { status: 404 }); } -// Prefer original category from build-time props to preserve symbols like '&' -const categoryName = (Astro.props?.category as string) || categorySlug!.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); +// Find the actual category name from the slug +const categoryName = allCategories.find( + cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug +) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); // SEO metadata and descriptions for categories const categorySeo: Record = { diff --git a/frontend/src/pages/emojis/[page].astro b/frontend/src/pages/emojis/[page].astro deleted file mode 100644 index 33386ff80c..0000000000 --- a/frontend/src/pages/emojis/[page].astro +++ /dev/null @@ -1,229 +0,0 @@ ---- -import CreditsButton from '../../components/buttons/CreditsButton'; -import Pagination from '../../components/PaginationComponent.astro'; -import BaseLayout from '../../layouts/BaseLayout.astro'; -import ToolContainer from '../../components/tool/ToolContainer'; -import ToolHead from '../../components/tool/ToolHead'; -import { getAllEmojis, getEmojiCategories } from '../../lib/emojis'; -import AdBanner from '../../components/banner/AdBanner.astro'; - -export async function getStaticPaths() { - const emojis = await getAllEmojis(); - const categories = getEmojiCategories(); - - const itemsPerPage = 30; - const totalPages = Math.ceil(categories.length / itemsPerPage); - - const paths = []; - for (let page = 1; page <= totalPages; page++) { - paths.push({ - params: { page: page.toString() }, - }); - } - - return paths; -} - -const { page } = Astro.params; -const currentPage = parseInt(page || '1'); - -const emojis = await getAllEmojis(); - -// Metadata-based categories -const categories = getEmojiCategories(); -const emojisByCategory: Record = {}; -for (const cat of categories) { - emojisByCategory[cat] = emojis.filter( - (e) => - (e.fluentui_metadata?.group || - e.emoji_net_data?.category || - (e as any).given_category || - 'Other') === cat - ); -} - -// Sort categories and emojis within categories -const sortedCategories = Object.keys(emojisByCategory).sort(); -for (const category of sortedCategories) { - emojisByCategory[category].sort((a, b) => { - const titleA = a.title || a.fluentui_metadata?.cldr || a.slug || ''; - const titleB = b.title || b.fluentui_metadata?.cldr || b.slug || ''; - return titleA.localeCompare(titleB); - }); -} - -// Pagination logic -const itemsPerPage = 36; -const totalPages = Math.ceil(sortedCategories.length / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const paginatedCategories = sortedCategories.slice(startIndex, endIndex); - -// Category icons mapping -const categoryIconMap: Record = { - 'Smileys & Emotion': '😀', - 'People & Body': '👤', - 'Animals & Nature': '🐶', - 'Food & Drink': '🍎', - 'Travel & Places': '✈️', - Activities: '⚽', - Objects: '📱', - Symbols: '❤️', - Flags: '🏁', - Other: '❓', -}; - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis', href: '/freedevtools/emojis/' }, -]; ---- - - - -
- -
- - - -
-
-
-
- {sortedCategories.length} -
-
Categories
-
-
-
- {emojis.length.toLocaleString()} -
-
Emojis
-
-
-
- -
- { - paginatedCategories.map((category) => ( -
-
-
- - {categoryIconMap[category] || '❓'} - -
- - {category.replace('-', ' ')} - -
- -

- {emojisByCategory[category].length} emojis available -

- -
- {emojisByCategory[category].slice(0, 5).map((emoji) => { - const emojiName = - emoji.title || emoji.fluentui_metadata?.cldr || emoji.slug; - const truncatedName = - emojiName.length > 27 - ? emojiName.substring(0, 27) + '...' - : emojiName; - return ( - - {emoji.code || emoji.emoji} {truncatedName} - - ); - })} - {emojisByCategory[category].length > 5 && ( -

- +{emojisByCategory[category].length - 5} more items -

- )} -
- - -
- )) - } -
- - - - - - -
-
- - diff --git a/frontend/src/pages/emojis/[slug].astro b/frontend/src/pages/emojis/[slug].astro index 26dbe27ba5..caf5cc7964 100644 --- a/frontend/src/pages/emojis/[slug].astro +++ b/frontend/src/pages/emojis/[slug].astro @@ -4,134 +4,357 @@ import CreditsButton from '../../components/buttons/CreditsButton'; import ToolContainer from '../../components/tool/ToolContainer'; import ToolHead from '../../components/tool/ToolHead'; import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getEmojiBySlug, getEmojiImages } from '../../lib/emojis'; +import Pagination from '../../components/PaginationComponent.astro'; +import { getEmojiBySlug, getEmojiImages, getAllEmojis, getEmojiCategories } from '../../lib/emojis'; import { apple_vendor_excluded_emojis, discord_vendor_excluded_emojis } from '@/lib/emojis-consts'; import EachEmojiPage from './EachEmojiPage.astro'; -export async function getStaticPaths() { - const { getAllEmojis } = await import('../../lib/emojis'); - const emojis = getAllEmojis(); - - return emojis - .filter((emoji) => emoji.slug && emoji.slug.trim() !== '') - .map((emoji) => ({ - params: { slug: emoji.slug }, - props: { emoji }, - })); -} +export const prerender = false; const { slug } = Astro.params; -const emoji = getEmojiBySlug(slug!); -const images = getEmojiImages(slug!); -if (!emoji) { - return Astro.redirect('/freedevtools/emojis/'); +// Early return if no slug param +if (!slug) { + return new Response(null, { status: 404 }); } -const cleanDescription = (text?: string) => { - if (!text) return ''; - return text - .replace(/<[^>]*>/g, '') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/[?]{2,}/g, '') - .trim(); -}; - -// Get category for breadcrumb -const categoryName = (emoji.category || 'Other') as string; - -const categorySlug = categoryName.toLowerCase().replace(/[^a-z0-9]+/g, '-'); - -// Breadcrumb items -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: categoryName, href: `/freedevtools/emojis/${categorySlug}/` }, - { label: emoji.title || emoji.slug }, -]; +// Check if slug is numeric (pagination) +const isNumericPage = slug && /^\d+$/.test(slug); +const pageNumber = isNumericPage ? parseInt(slug, 10) : null; + +let paginationData: any = null; +let emojiData: any = null; + +if (pageNumber !== null) { + // Handle pagination route + const currentPage = pageNumber; + const emojis = await getAllEmojis(); + const categories = getEmojiCategories(); + const emojisByCategory: Record = {}; + + for (const cat of categories) { + emojisByCategory[cat] = emojis.filter( + (e) => + (e.category || 'Other') === cat + ); + } + + const sortedCategories = Object.keys(emojisByCategory).sort(); + for (const category of sortedCategories) { + emojisByCategory[category].sort((a, b) => { + const titleA = a.title || a.slug || ''; + const titleB = b.title || b.slug || ''; + return titleA.localeCompare(titleB); + }); + } + + const itemsPerPage = 36; + const totalPages = Math.ceil(sortedCategories.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedCategories = sortedCategories.slice(startIndex, endIndex); + + const categoryIconMap: Record = { + 'Smileys & Emotion': '😀', + 'People & Body': '👤', + 'Animals & Nature': '🐶', + 'Food & Drink': '🍎', + 'Travel & Places': '✈️', + Activities: '⚽', + Objects: '📱', + Symbols: '❤️', + Flags: '🏁', + Other: '❓', + }; + + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Emojis', href: '/freedevtools/emojis/' }, + ]; + + paginationData = { + currentPage, + totalPages, + sortedCategories, + paginatedCategories, + emojisByCategory, + emojis, + categoryIconMap, + breadcrumbItems, + }; +} else { + // Handle emoji page route + const emoji = getEmojiBySlug(slug!); + const images = getEmojiImages(slug!); + + if (!emoji) { + return new Response(null, { status: 404 }); + } + + const cleanDescription = (text?: string) => { + if (!text) return ''; + return text + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/[?]{2,}/g, '') + .trim(); + }; + + // Get category for breadcrumb + const categoryName = (emoji.category || 'Other') as string; + const categorySlug = categoryName.toLowerCase().replace(/[^a-z0-9]+/g, '-'); + + // Clean description now (don't store function) + const cleanedDescription = cleanDescription(emoji.description); + const seoDescription = cleanedDescription || + `Learn about the ${emoji.title || emoji.slug} emoji ${emoji.code || ''}. Find meanings, shortcodes, and usage information.`; + + // Breadcrumb items + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Emojis', href: '/freedevtools/emojis/' }, + { label: categoryName, href: `/freedevtools/emojis/${categorySlug}/` }, + { label: emoji.title || emoji.slug }, + ]; + + emojiData = { + emoji, + images, + categoryName, + categorySlug, + breadcrumbItems, + cleanedDescription, + seoDescription, + }; +} --- - - -
- -
- - - - - -
-
- - ← Back to Emojis - - { - emoji.category ? ( - - View{' '} - {emoji.category - .replace(/-/g, ' ') - .replace(/\b\w/g, (l) => l.toUpperCase())}{' '} - Category - - ) : null - } +{paginationData ? ( + + +
+ +
+ - { - emoji.apple_vendor_description && - !apple_vendor_excluded_emojis.includes(emoji.slug) ? ( - - 🍎 View Apple Version - - ) : null - } +
+
+
+
+ {paginationData.sortedCategories.length} +
+
Categories
+
+
+
+ {paginationData.emojis.length.toLocaleString()} +
+
Emojis
+
+
+
+
{ - emoji.discord_vendor_description && - !discord_vendor_excluded_emojis.includes(emoji.slug) ? ( - - 🎮 View Discord Version - - ) : null + paginationData.paginatedCategories.map((category) => ( +
+
+
+ + {paginationData.categoryIconMap[category] || '❓'} + +
+ + {category.replace('-', ' ')} + +
+ +

+ {paginationData.emojisByCategory[category].length} emojis available +

+ +
+ {paginationData.emojisByCategory[category].slice(0, 5).map((emoji) => { + const emojiName = + emoji.title || emoji.slug; + const truncatedName = + emojiName.length > 27 + ? emojiName.substring(0, 27) + '...' + : emojiName; + return ( + + {emoji.code || emoji.emoji} {truncatedName} + + ); + })} + {paginationData.emojisByCategory[category].length > 5 && ( +

+ +{paginationData.emojisByCategory[category].length - 5} more items +

+ )} +
+ + +
+ )) } +
- - + + + +
+
+) : emojiData ? ( + + +
+
-
- - + + + + + +
+
+ + ← Back to Emojis + + { + emojiData.emoji.category ? ( + + View{' '} + {emojiData.emoji.category + .replace(/-/g, ' ') + .replace(/\b\w/g, (l) => l.toUpperCase())}{' '} + Category + + ) : null + } + + { + emojiData.emoji.apple_vendor_description && + !apple_vendor_excluded_emojis.includes(emojiData.emoji.slug) ? ( + + 🍎 View Apple Version + + ) : null + } + + { + emojiData.emoji.discord_vendor_description && + !discord_vendor_excluded_emojis.includes(emojiData.emoji.slug) ? ( + + 🎮 View Discord Version + + ) : null + } + + + +
+
+ + +) : null} + + diff --git a/frontend/src/pages/emojis/apple-emojis/[category].astro b/frontend/src/pages/emojis/apple-emojis/[category].astro index 28b6aff92f..938f603189 100644 --- a/frontend/src/pages/emojis/apple-emojis/[category].astro +++ b/frontend/src/pages/emojis/apple-emojis/[category].astro @@ -5,23 +5,44 @@ import Pagination from '../../../components/PaginationComponent.astro'; import CreditsButton from '../../../components/buttons/CreditsButton'; import ToolContainer from '../../../components/tool/ToolContainer'; import ToolHead from '../../../components/tool/ToolHead'; -import { getEmojisByCategory, getEmojiCategories, fetchLatestAppleImage } from '../../../lib/emojis'; - -// Generate static paths -export async function getStaticPaths() { - const categories = getEmojiCategories(); - return categories - .filter((cat) => cat && cat.trim() !== '') - .map((cat) => ({ - params: { category: cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') }, - props: { category: cat }, - })); -} +import { getEmojisByCategory, getEmojiCategories, fetchLatestAppleImage, getEmojiBySlug } from '../../../lib/emojis'; + +export const prerender = false; const { category: categorySlug } = Astro.params; -const categoryName = - (Astro.props?.category as string) || - categorySlug!.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); +const urlPath = Astro.url.pathname; + +// Handle trailing slash redirect FIRST (before any logic) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Check if category is numeric (pagination route) +if (categorySlug && /^\d+$/.test(categorySlug)) { + // This is actually a pagination route - redirect to [slug].astro + return Astro.redirect(`/freedevtools/emojis/apple-emojis/${categorySlug}/`, 301); +} + +// Get all categories to validate +const allCategories = getEmojiCategories(); +const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); + +// Check if this is actually an emoji slug (not a category) +const emoji = getEmojiBySlug(categorySlug!); +if (emoji && !categorySlugs.includes(categorySlug!)) { + // This is an emoji slug, not a category - redirect to emoji page + return Astro.redirect(`/freedevtools/emojis/apple-emojis/${categorySlug}/`, 301); +} + +// Validate category exists +if (!categorySlug || !categorySlugs.includes(categorySlug)) { + return new Response(null, { status: 404 }); +} + +// Find the actual category name from the slug +const categoryName = allCategories.find( + cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug +) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); // const emojis = await getEmojisByCategory(categoryName, "apple"); const emojis = (await getEmojisByCategory(categoryName, "apple")) diff --git a/frontend/src/pages/emojis/apple-emojis/[category]/[page].astro b/frontend/src/pages/emojis/apple-emojis/[category]/[page].astro index e59f55a556..6ea2be8e7e 100644 --- a/frontend/src/pages/emojis/apple-emojis/[category]/[page].astro +++ b/frontend/src/pages/emojis/apple-emojis/[category]/[page].astro @@ -7,36 +7,35 @@ import ToolHead from '../../../../components/tool/ToolHead'; import { getEmojisByCategory, getEmojiCategories, fetchLatestAppleImage } from '../../../../lib/emojis'; import AdBanner from '../../../../components/banner/AdBanner.astro'; +export const prerender = false; + const { category: categorySlug, page } = Astro.params; -const currentPage = parseInt(page || '1'); - -export async function getStaticPaths() { - const categories = getEmojiCategories(); - const paths: any[] = []; - for (const category of categories) { - if (category && category.trim() !== '') { - const categorySlug = category.toLowerCase().replace(/[^a-z0-9]+/g, '-'); - const emojis = await getEmojisByCategory(category, "apple"); - const itemsPerPage = 30; - const totalPages = Math.ceil(emojis.length / itemsPerPage); - - for (let pageNum = 1; pageNum <= totalPages; pageNum++) { - paths.push({ - params: { - category: categorySlug, - page: pageNum.toString() - }, - props: { category } - }); - } - } - } +const urlPath = Astro.url.pathname; + +// Handle trailing slash redirect FIRST (before any logic) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Validate page is numeric +if (!page || !/^\d+$/.test(page)) { + return new Response(null, { status: 404 }); +} + +const currentPage = parseInt(page, 10); + +// Validate category exists +const allCategories = getEmojiCategories(); +const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); - return paths; +if (!categorySlug || !categorySlugs.includes(categorySlug)) { + return new Response(null, { status: 404 }); } -const categoryName = (Astro.props?.category as string) || - categorySlug!.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); +// Find the actual category name from the slug +const categoryName = allCategories.find( + cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug +) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); const categorySeo: Record = { 'Activities': { diff --git a/frontend/src/pages/emojis/apple-emojis/[slug].astro b/frontend/src/pages/emojis/apple-emojis/[slug].astro index fceae61804..0a0ecfbe7f 100644 --- a/frontend/src/pages/emojis/apple-emojis/[slug].astro +++ b/frontend/src/pages/emojis/apple-emojis/[slug].astro @@ -1,19 +1,33 @@ --- import BaseLayout from '../../../layouts/BaseLayout.astro'; import AdBanner from '../../../components/banner/AdBanner'; -import { getAllAppleEmojis,getAppleEmojiBySlug } from '@/lib/emojis'; +import { getAllAppleEmojis,getAppleEmojiBySlug, getEmojiCategories } from '@/lib/emojis'; import VendorEmojiPage from '@/components/VendorEmojiPage'; import { discord_vendor_excluded_emojis } from '@/lib/emojis-consts'; -export async function getStaticPaths() { - const emojis = getAllAppleEmojis(); - return emojis.map((emoji) => ({ - params: { slug: emoji.slug }, - props: { emoji } - })); -} +export const prerender = false; const { slug } = Astro.params; +const urlPath = Astro.url.pathname; + +// Handle trailing slash redirect FIRST (before any logic) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Check if slug is numeric (pagination) - 404 since there's no pagination on apple-emojis index +if (slug && /^\d+$/.test(slug)) { + return new Response(null, { status: 404 }); +} + +// Check if slug is a category - redirect to category route +const allCategories = getEmojiCategories(); +const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); +if (categorySlugs.includes(slug!)) { + // This is a category - redirect to category route + return Astro.redirect(`/freedevtools/emojis/apple-emojis/${slug}/`, 301); +} + const emoji = getAppleEmojiBySlug(slug!); if (!emoji) { diff --git a/frontend/src/pages/emojis/discord-emojis/[category].astro b/frontend/src/pages/emojis/discord-emojis/[category].astro index 82a4ca0b6b..60e0543bca 100644 --- a/frontend/src/pages/emojis/discord-emojis/[category].astro +++ b/frontend/src/pages/emojis/discord-emojis/[category].astro @@ -5,23 +5,44 @@ import Pagination from '../../../components/PaginationComponent.astro'; import CreditsButton from '../../../components/buttons/CreditsButton'; import ToolContainer from '../../../components/tool/ToolContainer'; import ToolHead from '../../../components/tool/ToolHead'; -import { getEmojisByCategory, getEmojiCategories, fetchLatestDiscordImage } from '../../../lib/emojis'; - -// Generate static paths -export async function getStaticPaths() { - const categories = getEmojiCategories(); - return categories - .filter((cat) => cat && cat.trim() !== '') - .map((cat) => ({ - params: { category: cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') }, - props: { category: cat }, - })); -} +import { getEmojisByCategory, getEmojiCategories, fetchLatestDiscordImage, getEmojiBySlug } from '../../../lib/emojis'; + +export const prerender = false; const { category: categorySlug } = Astro.params; -const categoryName = - (Astro.props?.category as string) || - categorySlug!.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); +const urlPath = Astro.url.pathname; + +// Handle trailing slash redirect FIRST (before any logic) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Check if category is numeric (pagination route) +if (categorySlug && /^\d+$/.test(categorySlug)) { + // This is actually a pagination route - redirect to [slug].astro + return Astro.redirect(`/freedevtools/emojis/discord-emojis/${categorySlug}/`, 301); +} + +// Get all categories to validate +const allCategories = getEmojiCategories(); +const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); + +// Check if this is actually an emoji slug (not a category) +const emoji = getEmojiBySlug(categorySlug!); +if (emoji && !categorySlugs.includes(categorySlug!)) { + // This is an emoji slug, not a category - redirect to emoji page + return Astro.redirect(`/freedevtools/emojis/discord-emojis/${categorySlug}/`, 301); +} + +// Validate category exists +if (!categorySlug || !categorySlugs.includes(categorySlug)) { + return new Response(null, { status: 404 }); +} + +// Find the actual category name from the slug +const categoryName = allCategories.find( + cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug +) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); // Load Discord emojis const emojis = (await getEmojisByCategory(categoryName, "discord")) diff --git a/frontend/src/pages/emojis/discord-emojis/[category]/[page].astro b/frontend/src/pages/emojis/discord-emojis/[category]/[page].astro index 4fa8c9c739..4a5b4b1f2d 100644 --- a/frontend/src/pages/emojis/discord-emojis/[category]/[page].astro +++ b/frontend/src/pages/emojis/discord-emojis/[category]/[page].astro @@ -7,42 +7,35 @@ import ToolHead from '../../../../components/tool/ToolHead'; import { getEmojisByCategory, getEmojiCategories, fetchLatestDiscordImage } from '../../../../lib/emojis'; import AdBanner from '../../../../components/banner/AdBanner.astro'; +export const prerender = false; + const { category: categorySlug, page } = Astro.params; -const currentPage = parseInt(page || '1'); - -export async function getStaticPaths() { - const categories = getEmojiCategories(); - const paths: any[] = []; - for (const category of categories) { - if (category && category.trim() !== '') { - const categorySlug = category.toLowerCase().replace(/[^a-z0-9]+/g, '-'); - const emojis = (await getEmojisByCategory(category, "discord")) - .map(e => ({ - ...e, - latestDiscordImage: fetchLatestDiscordImage(e.slug) - })) - .filter(e => e.latestDiscordImage); - - const itemsPerPage = 30; - const totalPages = Math.ceil(emojis.length / itemsPerPage); - - for (let pageNum = 1; pageNum <= totalPages; pageNum++) { - paths.push({ - params: { - category: categorySlug, - page: pageNum.toString() - }, - props: { category } - }); - } - } - } +const urlPath = Astro.url.pathname; + +// Handle trailing slash redirect FIRST (before any logic) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Validate page is numeric +if (!page || !/^\d+$/.test(page)) { + return new Response(null, { status: 404 }); +} + +const currentPage = parseInt(page, 10); + +// Validate category exists +const allCategories = getEmojiCategories(); +const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); - return paths; +if (!categorySlug || !categorySlugs.includes(categorySlug)) { + return new Response(null, { status: 404 }); } -const categoryName = (Astro.props?.category as string) || - categorySlug!.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); +// Find the actual category name from the slug +const categoryName = allCategories.find( + cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug +) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); const categorySeo: Record = { 'Activities': { diff --git a/frontend/src/pages/emojis/discord-emojis/[slug].astro b/frontend/src/pages/emojis/discord-emojis/[slug].astro index a602c851d6..8aa7e02b1b 100644 --- a/frontend/src/pages/emojis/discord-emojis/[slug].astro +++ b/frontend/src/pages/emojis/discord-emojis/[slug].astro @@ -1,20 +1,34 @@ --- import BaseLayout from '../../../layouts/BaseLayout.astro'; import AdBanner from '../../../components/banner/AdBanner'; -import { getAllDiscordEmojis, getDiscordEmojiBySlug } from '@/lib/emojis'; +import { getAllDiscordEmojis, getDiscordEmojiBySlug, getEmojiCategories } from '@/lib/emojis'; import VendorEmojiPage from '@/components/VendorEmojiPage'; import { apple_vendor_excluded_emojis } from '@/lib/emojis-consts'; -export async function getStaticPaths() { - const emojis = getAllDiscordEmojis(); - return emojis.map((emoji) => ({ - params: { slug: emoji.slug }, - props: { emoji } - })); -} +export const prerender = false; const { slug } = Astro.params; +const urlPath = Astro.url.pathname; + +// Handle trailing slash redirect FIRST (before any logic) +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} + +// Check if slug is numeric (pagination) - 404 since there's no pagination on discord-emojis index +if (slug && /^\d+$/.test(slug)) { + return new Response(null, { status: 404 }); +} + +// Check if slug is a category - redirect to category route +const allCategories = getEmojiCategories(); +const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); +if (categorySlugs.includes(slug!)) { + // This is a category - redirect to category route + return Astro.redirect(`/freedevtools/emojis/discord-emojis/${slug}/`, 301); +} + const emoji = getDiscordEmojiBySlug(slug!); if (!emoji) { From 4d882eec2adc5bb74b1b7d9c283d15ad10e50e63 Mon Sep 17 00:00:00 2001 From: lovestaco Date: Mon, 17 Nov 2025 19:32:49 +0530 Subject: [PATCH 13/79] feat: ssr options, rm critical inline css, use node adaptor --- frontend/astro.config.mjs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/frontend/astro.config.mjs b/frontend/astro.config.mjs index ea4c78c5bc..7ae8b7f902 100644 --- a/frontend/astro.config.mjs +++ b/frontend/astro.config.mjs @@ -1,17 +1,23 @@ // @ts-check +import node from '@astrojs/node'; import react from "@astrojs/react"; import tailwind from "@astrojs/tailwind"; import compressor from "astro-compressor"; import { defineConfig } from "astro/config"; import path from "path"; +// These integrations are only needed for static/SSG builds, not SSR mode +// import { performCriticalCssInline } from './integrations/critical-css-inlining.mjs'; +// import { unwrapFDT, wrapFDT } from './integrations/wrap-astro.mjs'; -import { performCriticalCssInline } from './integrations/critical-css-inlining.mjs'; -import { unwrapFDT, wrapFDT } from './integrations/wrap-astro.mjs'; // https://astro.build/config export default defineConfig({ + adapter: node({ + mode: 'standalone', + experimentalDisableStreaming: false + }), site: 'https://hexmos.com/freedevtools', - output: 'static', + output: 'server', base: "/freedevtools", trailingSlash: 'ignore', prefetch: { @@ -26,10 +32,10 @@ export default defineConfig({ // }) //compressor({ gzip: { level: 9 }, brotli: true }), compressor({ gzip: { level: 9 }, brotli: false }), - wrapFDT(), // Wraps freedevtools folder around _astro for doing the critical-css inline - performCriticalCssInline(), - // playformInline(), // Adds inline critical css to avoid render blocking - unwrapFDT() // Unwraps freedevtools folder around _astro + // These integrations are only needed for static/SSG builds, not SSR mode + // wrapFDT(), // Wraps freedevtools folder around _astro for doing the critical-css inline + // performCriticalCssInline(), + // unwrapFDT() // Unwraps freedevtools folder around _astro ], cacheDir: ".astro/cache", build: { @@ -73,3 +79,6 @@ export default defineConfig({ logLevel: 'warn', }, }); + + + From 90a2aaa4b7731e5d888684d42e4dfd5d90eab4c5 Mon Sep 17 00:00:00 2001 From: lovestaco Date: Mon, 17 Nov 2025 19:32:59 +0530 Subject: [PATCH 14/79] fix: ssr scripts --- frontend/scripts/ssr/SSR_CONVERSION_GUIDE.md | 909 ++++++++++++++++++ .../ssr/convert_static_emojis_to_ssr.txt | 65 ++ 2 files changed, 974 insertions(+) create mode 100644 frontend/scripts/ssr/SSR_CONVERSION_GUIDE.md create mode 100644 frontend/scripts/ssr/convert_static_emojis_to_ssr.txt diff --git a/frontend/scripts/ssr/SSR_CONVERSION_GUIDE.md b/frontend/scripts/ssr/SSR_CONVERSION_GUIDE.md new file mode 100644 index 0000000000..27e090443a --- /dev/null +++ b/frontend/scripts/ssr/SSR_CONVERSION_GUIDE.md @@ -0,0 +1,909 @@ +# SSR Mode Conversion Guide + +This document details the process of converting Astro static pages to SSR (Server-Side Rendering) mode, specifically for the TLDR section. Use this guide when converting other sections (MCP, SVG icons, etc.) to SSR mode. + +## Table of Contents + +1. [Overview](#overview) +2. [Key Changes Required](#key-changes-required) +3. [Route Structure Changes](#route-structure-changes) +4. [Removing Static Generation Code](#removing-static-generation-code) +5. [Content Collection Usage](#content-collection-usage) +6. [Middleware for Route Priority](#middleware-for-route-priority) +7. [Step-by-Step Conversion Process](#step-by-step-conversion-process) +8. [Common Issues and Solutions](#common-issues-and-solutions) + +--- + +## Overview + +### What Changed + +- **Before**: Pages were pre-rendered at build time using `getStaticPaths()` +- **After**: Pages are rendered on-demand at request time (SSR mode) + +### Why SSR? + +- Dynamic content that changes frequently +- Large number of pages that would be slow to pre-render +- Need for real-time data fetching + +--- + +## Key Changes Required + +### 1. Remove `getStaticPaths()` + +**Static Mode (Before):** + +```typescript +export async function getStaticPaths() { + const items = await getCollection('collection'); + return items.map((item) => ({ + params: { id: item.id }, + props: { data: item.data }, + })); +} + +const { data } = Astro.props; // Data from getStaticPaths +``` + +**SSR Mode (After):** + +```typescript +export const prerender = false; // Explicitly disable prerendering + +const { id } = Astro.params; // Get params directly +const entry = await getCollection('collection'); +const item = entry.find((e) => e.id === id); // Fetch data directly +``` + +### 2. Fetch Data Directly in Component + +In SSR mode, you cannot use `Astro.props` from `getStaticPaths()`. Instead: + +- Fetch data directly using `getCollection()` or other data sources +- Use `Astro.params` to get route parameters +- Handle data fetching asynchronously in the frontmatter + +--- + +## Route Structure Changes + +### Problem: Route Collisions in SSR + +In SSR mode, Astro cannot have ambiguous dynamic routes. For example: + +- `/tldr/[platform]/[command].astro` +- `/tldr/[platform]/[page].astro` + +Both match the pattern `/tldr/[platform]/[something]`, causing collisions. + +### Solution: Consolidate Routes + +**Before (2 separate files):** + +``` +src/pages/tldr/[platform]/ + ├── [command].astro (handles /tldr/platform/command) + └── [page].astro (handles /tldr/platform/2) +``` + +**After (1 consolidated file):** + +``` +src/pages/tldr/[platform]/ + └── [slug].astro (handles both /tldr/platform/command AND /tldr/platform/2) +``` + +### Implementation Pattern + +```typescript +// src/pages/tldr/[platform]/[slug].astro +--- +const { platform, slug } = Astro.params; + +// Check if slug is numeric (pagination) or a string (command) +const isNumericPage = slug && /^\d+$/.test(slug); +const pageNumber = isNumericPage ? parseInt(slug, 10) : null; +const command = !isNumericPage ? slug : null; + +if (pageNumber !== null) { + // Handle pagination route + // ... pagination logic +} else if (command) { + // Handle command route + // ... command logic +} +--- +``` + +--- + +## Removing Static Generation Code + +### Files to Remove/Modify + +1. **Remove `getStaticPaths()` functions** from all SSR pages +2. **Remove `export const prerender = true`** (or set to `false`) +3. **Remove utility functions** that generate static paths (if only used for static generation) + +### Example: Before and After + +**Before (Static):** + +```typescript +// src/pages/tldr/[platform]/[command].astro +export async function getStaticPaths() { + const entries = await getCollection('tldr'); + return entries.map((entry) => ({ + params: { platform: '...', command: '...' }, + })); +} + +const { platform, command } = Astro.params; +const { data } = Astro.props; // ❌ Won't work in SSR +``` + +**After (SSR):** + +```typescript +// src/pages/tldr/[platform]/[slug].astro +export const prerender = false; // ✅ Explicitly SSR + +const { platform, slug } = Astro.params; +const entries = await getCollection('tldr'); // ✅ Fetch directly +const entry = entries.find(/* ... */); +``` + +--- + +## Content Collection Usage + +### How Content Collections Work in SSR + +Content collections are defined in `src/content.config.ts` and work the same in both static and SSR modes: + +```typescript +// src/content.config.ts +const tldr = defineCollection({ + loader: glob({ + pattern: '**/*.md', + base: 'data/tldr', + }), + schema: z.object({ + title: z.string(), + description: z.string(), + // ... other fields + }), +}); +``` + +### Accessing Collections in SSR + +```typescript +import { getCollection } from 'astro:content'; + +// Get all entries +const allEntries = await getCollection('tldr'); + +// Filter entries +const platformEntries = allEntries.filter((entry) => { + const pathParts = entry.id.split('/'); + return pathParts[pathParts.length - 2] === platform; +}); + +// Access entry data (validated by schema) +const title = entry.data.title; // ✅ Type-safe +const description = entry.data.description; // ✅ Type-safe +const keywords = entry.data.keywords; // ✅ Optional, type-safe +``` + +### Rendering Content + +```typescript +import { render } from 'astro:content'; + +const { Content } = await render(entry); +// Use in template +``` + +--- + +## Middleware for Route Priority + +### Problem: Route Priority in SSR + +In SSR mode, route priority doesn't always work as expected. For example: + +- `/tldr/[page].astro` might match `/tldr/adb/` before `/tldr/[platform]/index.astro` + +### Solution: Handle in Route File (Recommended) + +**Avoid middleware rewrites** - they can cause redirect loops. Instead, handle route priority directly in the route file that matches first: + +```typescript +// src/pages/tldr/[page].astro +--- +const { page } = Astro.params; +const urlPath = Astro.url.pathname; + +// Early return if no page param +if (!page) { + return new Response(null, { status: 404 }); +} + +// Check if page param is numeric +if (!/^\d+$/.test(page)) { + // If not numeric, it might be a platform name + // Redirect to add trailing slash if missing (BEFORE checking platform) + if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); + } + + const allPlatforms = await getAllTldrPlatforms(); + const isPlatform = allPlatforms.some((p) => p.name === page); + + if (isPlatform) { + // This is a platform index route - render it here + // This is a workaround for route priority not working as expected + const platform = page!; + const allCommands = await getTldrPlatformCommands(platform); + // ... render platform index content + } else { + // Not a valid platform - 404 + return new Response(null, { status: 404 }); + } +} else { + // Handle pagination route + // ... +} +--- +``` + +### Alternative: Minimal Middleware (If Needed) + +If you must use middleware, keep it simple and avoid rewrites that conflict with route handling: + +```typescript +// src/middleware.ts +import type { MiddlewareHandler } from 'astro'; + +// Minimal middleware - just pass through +// Route files handle their own logic to avoid conflicts +export const onRequest: MiddlewareHandler = async (context, next) => { + return next(); +}; +``` + +### Why Avoid Middleware Rewrites? + +1. **Redirect Loops**: Middleware rewrites can conflict with route file redirects, causing infinite loops +2. **Route Priority**: Astro's route matching happens after middleware, so rewrites may not work as expected +3. **Complexity**: Handling logic in the route file is simpler and more maintainable +4. **Performance**: Avoiding middleware lookups reduces overhead on every request + +--- + +## Step-by-Step Conversion Process + +### Step 1: Identify Route Collisions + +Check for routes that could match the same URL pattern: + +```bash +# Look for conflicting dynamic routes +find src/pages/section -name "*.astro" | grep -E "\[.*\]" +``` + +Common collisions: + +- `[page].astro` vs `[category]/index.astro` +- `[id].astro` vs `[slug].astro` +- `[name].astro` vs `[category]/[name].astro` + +### Step 2: Consolidate Conflicting Routes + +**Option A: Merge into single route with logic** + +```typescript +// [slug].astro - handles both cases +const isNumeric = /^\d+$/.test(slug); +if (isNumeric) { + // Handle pagination +} else { + // Handle content +} +``` + +**Option B: Use different path structures** + +``` +Before: /section/[page].astro and /section/[category]/index.astro +After: /section/page/[page].astro and /section/[category]/index.astro +``` + +### Step 3: Remove Static Generation Code + +For each page file: + +1. **Add SSR flag:** + + ```typescript + export const prerender = false; + ``` + +2. **Remove `getStaticPaths()`:** + + ```typescript + // ❌ Remove this + export async function getStaticPaths() { ... } + ``` + +3. **Replace `Astro.props` with direct fetching:** + + ```typescript + // ❌ Before + const { data } = Astro.props; + + // ✅ After + const { id } = Astro.params; + const entries = await getCollection('collection'); + const item = entries.find((e) => e.id === id); + ``` + +### Step 4: Update Data Fetching + +**Before:** + +```typescript +// Data passed via props from getStaticPaths +const { platforms, items } = Astro.props; +``` + +**After:** + +```typescript +// Fetch data directly +const allPlatforms = await getAllPlatforms(); +const items = await getItems(); +``` + +### Step 5: Handle Route Priority Issues + +If route priority doesn't work correctly: + +**Option A: Handle in Route File (Recommended)** + +- Check if route should be handled by another route in the file that matches first +- If so, render that route's content directly +- Handle trailing slash redirects **before** checking route validity +- Example: `[page].astro` detecting platform routes and rendering platform index + +```typescript +// In [page].astro - handles both pagination and platform routes +if (!/^\d+$/.test(page)) { + // Redirect trailing slash FIRST + if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); + } + + // Then check if it's a platform + const isPlatform = await checkIfPlatform(page); + if (isPlatform) { + // Render platform index content directly + return renderPlatformIndex(page); + } +} +``` + +**Option B: Minimal Middleware (Only if absolutely necessary)** + +- Keep middleware simple - just pass through +- Avoid `context.rewrite()` as it can cause redirect loops +- Let route files handle their own logic + +### Step 6: Update Utility Functions + +Remove or modify utility functions that only generate static paths: + +**Before:** + +```typescript +// Only used for getStaticPaths +export async function generateStaticPaths() { + return paths.map(p => ({ params: p, props: {...} })); +} +``` + +**After:** + +```typescript +// Used for direct data fetching +export async function getAllItems() { + const entries = await getCollection('collection'); + return entries.map((entry) => ({ + id: entry.id, + data: entry.data, + // ... transform as needed + })); +} +``` + +### Step 7: Test All Routes + +Test every route type: + +```bash +# Test main index +curl http://localhost:4321/freedevtools/section/ + +# Test pagination +curl http://localhost:4321/freedevtools/section/2/ + +# Test category index +curl http://localhost:4321/freedevtools/section/category/ + +# Test category pagination +curl http://localhost:4321/freedevtools/section/category/2/ + +# Test item pages +curl http://localhost:4321/freedevtools/section/category/item/ +``` + +--- + +## Common Issues and Solutions + +### Issue 1: Route Collision Warnings + +**Error:** + +``` +[WARN] [router] The route "/section/[id]" is defined in both +"src/pages/section/[id].astro" and "src/pages/section/[slug].astro" +using SSR mode. A dynamic SSR route cannot be defined more than once. +``` + +**Solution:** + +- Consolidate into single route file +- Use logic to distinguish between different types +- Example: Check if param is numeric vs string + +### Issue 2: `getStaticPaths()` Ignored Warning + +**Error:** + +``` +[WARN] [router] getStaticPaths() ignored in dynamic page +/src/pages/section/[page].astro. Add `export const prerender = true;` +to prerender the page as static HTML during the build process. +``` + +**Solution:** + +- Remove `getStaticPaths()` function +- Add `export const prerender = false;` +- Fetch data directly in component + +### Issue 3: `Astro.props` is Undefined + +**Error:** + +``` +TypeError: Cannot read properties of undefined (reading 'length') +``` + +**Solution:** + +- `Astro.props` only works with `getStaticPaths()` in static mode +- In SSR, fetch data directly: + + ```typescript + // ❌ Won't work in SSR + const { items } = Astro.props; + + // ✅ Works in SSR + const items = await getItems(); + ``` + +### Issue 4: Redirect Loops + +**Error:** + +``` +ERR_TOO_MANY_REDIRECTS +HTTP 508: Astro detected a loop where you tried to call the rewriting logic more than four times +``` + +**Solution:** + +The most common cause is middleware trying to rewrite routes while the route file also handles redirects. **Fix: Remove middleware rewrites and handle redirects directly in the route file.** + +1. **Simplify or remove middleware:** + + ```typescript + // src/middleware.ts - Keep it simple + export const onRequest: MiddlewareHandler = async (context, next) => { + return next(); // Just pass through + }; + ``` + +2. **Handle trailing slashes in route file BEFORE other logic:** + + ```typescript + // In [page].astro or similar route file + const { page } = Astro.params; + const urlPath = Astro.url.pathname; + + // Check if page param is numeric + if (!/^\d+$/.test(page)) { + // Redirect to add trailing slash FIRST (before platform detection) + if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); + } + + // Then check if it's a platform + const allPlatforms = await getAllPlatforms(); + const isPlatform = allPlatforms.some((p) => p.name === page); + // ... rest of logic + } + ``` + +3. **Key principles:** + - Handle trailing slash redirects **before** checking if route is valid + - Don't use `context.rewrite()` in middleware if route files also handle redirects + - One redirect per request - avoid multiple redirects in the same flow + +### Issue 5: Route Priority Not Working + +**Symptom:** + +- Wrong route handles a URL +- Expected: `/section/[category]/index.astro` +- Actual: `/section/[page].astro` matches first +- OR: Expected: `/section/[page].astro` +- Actual: `/section/[category]/index.astro` matches first (more specific route) + +**Solution:** + +**Option A: Handle in the route file that matches first (Recommended)** + +When a more specific (nested) route matches first, handle the conflicting case directly: + +```typescript +// In [category]/index.astro (matches first for /section/category/) +if (/^\d+$/.test(category)) { + // This is actually a pagination route - render it here + const currentPage = parseInt(category, 10); + // ... fetch and render pagination content directly + return ...; +} +// Otherwise handle as category index +``` + +**Option B: Handle in the less specific route file** + +When a less specific route matches first, detect and handle the more specific case: + +```typescript +// In [page].astro (matches first for /section/page/) +if (!/^\d+$/.test(page)) { + // This might be a category - check and handle + const allCategories = await getAllCategories(); + if (allCategories.includes(page)) { + // Render category index content directly + return ; + } +} +// Otherwise handle as pagination +``` + +**Key Principle:** Handle the conflicting case in whichever route file matches first, rather than trying to redirect or rewrite. + +--- + +## TLDR-Specific Implementation Details + +### Route Structure + +``` +src/pages/tldr/ +├── index.astro # Main index (/tldr/) +├── [page].astro # Main pagination (/tldr/2/) +│ └── Handles platform routes too (workaround for route priority) +├── [platform]/ +│ ├── index.astro # Platform index (/tldr/adb/) +│ └── [slug].astro # Platform pagination + commands +│ └── Handles both /tldr/adb/2/ and /tldr/adb/command/ +└── credits.astro # Static page +``` + +### Key Files Modified + +1. **`src/pages/tldr/[platform]/[slug].astro`** + - Consolidated `[command].astro` and `[page].astro` + - Checks if slug is numeric (pagination) or string (command) + - Fetches data directly using `getCollection('tldr')` + +2. **`src/pages/tldr/[page].astro`** + - Removed `getStaticPaths()` + - Fetches platforms directly using `getAllTldrPlatforms()` + - Handles platform index routes as workaround for route priority + - **Handles trailing slash redirects BEFORE platform detection** (prevents redirect loops) + - Early return for missing page param + +3. **`src/pages/tldr/[platform]/index.astro`** + - Removed `getStaticPaths()` + - Fetches commands directly using `getTldrPlatformCommands()` + +4. **`src/lib/tldr-utils.ts`** + - Kept utility functions but they now return data directly + - Removed static path generation functions (or made them SSR-compatible) + +5. **`src/middleware.ts`** + - Simplified to just pass through (no rewrites) + - Route files handle their own logic to avoid redirect loops + - Middleware rewrites were removed to prevent conflicts + +### Content Collection Usage + +```typescript +// Get all tldr entries +const tldrEntries = await getCollection('tldr'); + +// Access validated data (from content.config.ts schema) +entry.data.title; // string (required) +entry.data.description; // string (required) +entry.data.keywords; // string[] (optional) +entry.data.relatedTools; // array (optional) + +// Render markdown content +const { Content } = await render(entry); +``` + +--- + +## Checklist for Converting Other Sections + +When converting MCP, SVG icons, or other sections: + +- [ ] Identify all dynamic route files +- [ ] Check for route collisions (same URL pattern) +- [ ] Consolidate conflicting routes into single files +- [ ] Remove all `getStaticPaths()` functions +- [ ] Add `export const prerender = false;` to all SSR pages +- [ ] Replace `Astro.props` with direct data fetching +- [ ] Update utility functions to return data instead of static paths +- [ ] Test all route types (index, pagination, category, items) +- [ ] Handle route priority in route files (avoid middleware rewrites) +- [ ] Handle trailing slash redirects BEFORE checking route validity +- [ ] Verify content collection access works correctly +- [ ] Check for any build warnings about ignored `getStaticPaths()` +- [ ] Test redirects don't cause loops +- [ ] Verify all pages render correctly in SSR mode + +--- + +## MCP-Specific Implementation Details + +### Route Structure + +``` +src/pages/mcp/ +├── index.astro # Main index (/mcp/) +├── [page].astro # Main pagination (/mcp/2/) +│ └── Handles category detection (redirects to /category/1/) +├── [category]/ +│ ├── index.astro # Category index (/mcp/category/) +│ │ └── Handles numeric categories (pagination) directly +│ └── [slug].astro # Category pagination + repositories +│ └── Handles both /mcp/category/1/ and /mcp/category/repo-name/ +└── credits.astro # Static page +``` + +### Key Files Modified + +1. **`src/pages/mcp/[category]/[slug].astro`** + - Consolidated `[page].astro` and `[repositoryId].astro` + - Checks if slug is numeric (pagination) or string (repository) + - Fetches data directly using `getEntry('mcpCategoryData', category)` + - Uses `Object.entries()` to map repositories with IDs + +2. **`src/pages/mcp/[page].astro`** + - Removed `getStaticPaths()` + - Fetches categories directly using `getAllMcpCategories()` + - Handles category detection (redirects to `/category/1/` if it's a category name) + - Handles trailing slash redirects BEFORE checking category validity + +3. **`src/pages/mcp/[category]/index.astro`** + - Removed `getStaticPaths()` + - **Handles numeric categories directly** - renders pagination content when category is numeric + - This is a workaround for route priority: `[category]/index.astro` matches `/mcp/1/` before `[page].astro` + - Validates category exists before redirecting to page 1 + +4. **`src/lib/mcp-utils.ts`** + - Added SSR utility functions: + - `getAllMcpCategories()` - Get all categories for directory pagination + - `getAllMcpCategoryIds()` - Get category IDs for validation + - `getMcpCategoryById()` - Get category by ID + - `getMcpCategoryRepositories()` - Get repositories for a category + - `getMcpMetadata()` - Get MCP metadata + +### Route Priority Solution for MCP + +**Problem:** `/mcp/[category]` route collision between `[category]/index.astro` and `[page].astro` + +**Solution:** Handle numeric categories directly in `[category]/index.astro`: + +```typescript +// src/pages/mcp/[category]/index.astro +--- +const { category } = Astro.params; + +// If category is numeric, this is actually a pagination route +// Render pagination content directly (workaround for route priority) +if (/^\d+$/.test(category)) { + const currentPage = parseInt(category, 10); + // ... fetch and render pagination content + return ...; +} + +// Otherwise, validate category and redirect to page 1 +const allCategoryIds = await getAllMcpCategoryIds(); +if (!allCategoryIds.includes(category)) { + return new Response(null, { status: 404 }); +} +return Astro.redirect(`/freedevtools/mcp/${category}/1/`, 301); +--- +``` + +**Key Learning:** When a more specific (nested) route matches first, handle the conflicting case directly in that route file rather than trying to redirect or rewrite. + +### Content Collection Usage + +```typescript +// Get category entry directly (more efficient than getCollection + find) +const categoryEntry = await getEntry('mcpCategoryData', category); + +// Access category data +const categoryData = categoryEntry.data; +const repositories = categoryData.repositories; + +// Map repositories with IDs +const allRepositories = Object.entries(repositories).map( + ([repositoryId, server]) => ({ + ...server, + repositoryId: repositoryId, + }) +); +``` + +--- + +## Example: Converting MCP Pages (Actual Implementation) + +### Step 1: Identify Collisions + +- `/mcp/[category]` collision: `[category]/index.astro` vs `[page].astro` +- `/mcp/[category]/[page]` collision: `[page].astro` vs `[repositoryId].astro` + +### Step 2: Consolidate Routes + +**Consolidate `[page].astro` and `[repositoryId].astro` into `[slug].astro`:** + +```typescript +// [slug].astro - handles both pagination and repositories +const isNumericPage = /^\d+$/.test(slug); +const pageNumber = isNumericPage ? parseInt(slug, 10) : null; +const repositoryId = !isNumericPage ? slug : null; + +if (pageNumber !== null) { + // Handle pagination route +} else if (repositoryId) { + // Handle repository route +} +``` + +**Handle route priority in `[category]/index.astro`:** + +```typescript +// [category]/index.astro - handles numeric categories directly +if (/^\d+$/.test(category)) { + // Render pagination content directly + // This prevents [page].astro from needing to handle it +} +``` + +### Step 3: Remove Static Code + +- Remove all `getStaticPaths()` functions +- Add `export const prerender = false;` to all files +- Replace `Astro.props` with direct data fetching + +### Step 4: Test + +```bash +# Test main pagination +curl http://localhost:4321/freedevtools/mcp/1/ +curl http://localhost:4321/freedevtools/mcp/2/ + +# Test category index +curl http://localhost:4321/freedevtools/mcp/apis-and-http-requests/ + +# Test category pagination +curl http://localhost:4321/freedevtools/mcp/apis-and-http-requests/1/ +curl http://localhost:4321/freedevtools/mcp/apis-and-http-requests/2/ + +# Test repository pages +curl http://localhost:4321/freedevtools/mcp/scheduling-and-calendars/mumunha--cal_dot_com_mcpserver/ +``` + +--- + +## Performance Considerations + +### Caching in Middleware + +Middleware runs on every request, so cache expensive operations: + +```typescript +let cache: string[] | null = null; + +async function getExpensiveData() { + if (cache) return cache; + // Expensive operation + cache = await expensiveOperation(); + return cache; +} +``` + +### Content Collection Caching + +Astro automatically caches content collections, but be mindful of: + +- Large collections (consider pagination) +- Complex filtering operations +- Multiple `getCollection()` calls (cache results when possible) + +--- + +## Summary + +**Key Takeaways:** + +1. SSR mode requires direct data fetching, not `getStaticPaths()` +2. Route collisions must be resolved by consolidating routes +3. **Handle route priority in route files, not middleware** (avoids redirect loops) +4. **Handle conflicting cases in whichever route matches first** (more specific routes match before less specific ones) +5. **Handle trailing slash redirects BEFORE checking route validity** +6. **Use `getEntry()` for direct lookups** instead of `getCollection()` + `find()` when you know the ID +7. Content collections work the same in both modes +8. Always test all route types after conversion +9. Keep middleware simple - avoid rewrites that conflict with route handling + +**Files Typically Modified:** + +- Route files: Remove `getStaticPaths()`, add `prerender = false`, handle route priority directly +- Utility files: Change from path generation to data fetching +- Middleware: Keep simple (pass through) or remove entirely +- No changes needed to `content.config.ts` (collections work in both modes) + +**Critical Pattern for Redirect Loops:** + +Always handle trailing slash redirects **before** checking if a route is valid: + +```typescript +// ✅ CORRECT: Redirect first, then check +if (!urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} +const isValid = await checkRoute(page); + +// ❌ WRONG: Check first, then redirect (can cause loops) +const isValid = await checkRoute(page); +if (isValid && !urlPath.endsWith('/')) { + return Astro.redirect(`${urlPath}/`, 301); +} +``` diff --git a/frontend/scripts/ssr/convert_static_emojis_to_ssr.txt b/frontend/scripts/ssr/convert_static_emojis_to_ssr.txt new file mode 100644 index 0000000000..01253180ab --- /dev/null +++ b/frontend/scripts/ssr/convert_static_emojis_to_ssr.txt @@ -0,0 +1,65 @@ + +I want the ssr mode to work for my whole site, there is a lot of problems occuring one after another First lets focus on /emojis first + +When doing npm run build getting this error +19:00:03 [WARN] [router] The route "/emojis/apple-emojis/[category]" is defined in both "src/pages/emojis/apple-emojis/[category].astro" and "src/pages/emojis/apple-emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:00:03 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:00:03 [WARN] [router] The route "/emojis/discord-emojis/[category]" is defined in both "src/pages/emojis/discord-emojis/[category].astro" and "src/pages/emojis/discord-emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:00:03 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:00:03 [WARN] [router] The route "/emojis/[category]" is defined in both "src/pages/emojis/[category].astro" and "src/pages/emojis/[page].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:00:03 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:00:03 [WARN] [router] The route "/emojis/[category]" is defined in both "src/pages/emojis/[category].astro" and "src/pages/emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:00:03 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:00:03 [WARN] [router] The route "/emojis/[page]" is defined in both "src/pages/emojis/[page].astro" and "src/pages/emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:00:03 [WARN] [router] A collision will result in an hard error in following versions of Astro. + +19:00:33 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/[page].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:39 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/[slug].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:39 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/[category].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:40 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/apple-emojis/[category].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:40 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/apple-emojis/[slug].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:40 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/discord-emojis/[category].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:40 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/discord-emojis/[slug].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. +19:00:40 [WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/emojis/[category]/[page].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process. + +18:44:18 [@astrojs/node] Server listening on http://localhost:4321 + +URL is slightly differnt in this emoji. +https://hexmos.com/freedevtools/emojis/activities/ is a category +Inside activiteis https://hexmos.com/freedevtools/emojis/jack-o-lantern/ is there +but my colleague has made mistkae of not including the cateogy in the URL + +All 12k pages of emoji are already insdexd by google, so we need to retain the urls as is and still get it working + +Many of the emoji pages are not working + +Working +http://localhost:4321/freedevtools/emojis/ +http://localhost:4321/freedevtools/emojis/activities/ +http://localhost:4321/freedevtools/emojis/activities/2/#pagination-info + + +Broken pages +http://localhost:4321/freedevtools/emojis/jack-o-lantern/ this is supposed to be the end page but it is shwoing pagination component in this which is wrong + +Not sure why + + +DO NOT MAKE THIS TRUE, I need SSR only +export const prerender = false; + +I dont want static + +Read the astro documentation i have given astro mcp aswell @mcp.json + +NPM run preview +19:07:59 [WARN] [router] The route "/emojis/apple-emojis/[category]" is defined in both "src/pages/emojis/apple-emojis/[category].astro" and "src/pages/emojis/apple-emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:07:59 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:07:59 [WARN] [router] The route "/emojis/discord-emojis/[category]" is defined in both "src/pages/emojis/discord-emojis/[category].astro" and "src/pages/emojis/discord-emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:07:59 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:07:59 [WARN] [router] The route "/emojis/[category]" is defined in both "src/pages/emojis/[category].astro" and "src/pages/emojis/[page].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:07:59 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:07:59 [WARN] [router] The route "/emojis/[category]" is defined in both "src/pages/emojis/[category].astro" and "src/pages/emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:07:59 [WARN] [router] A collision will result in an hard error in following versions of Astro. +19:07:59 [WARN] [router] The route "/emojis/[page]" is defined in both "src/pages/emojis/[page].astro" and "src/pages/emojis/[slug].astro" using SSR mode. A dynamic SSR route cannot be defined more than once. +19:07:59 [WARN] [router] A collision will result in an hard error in following versions of Astro. From 04e9e9c01a133221186c5a74d86a3d231a2e56d0 Mon Sep 17 00:00:00 2001 From: lovestaco Date: Mon, 17 Nov 2025 20:22:45 +0530 Subject: [PATCH 15/79] fix: apple and discord emoji ssr --- frontend/src/pages/emojis/[category].astro | 304 ++++++++++++--- frontend/src/pages/emojis/[slug].astro | 360 ------------------ .../emojis/apple-emojis/[category].astro | 243 ++++++++---- .../pages/emojis/apple-emojis/[slug].astro | 116 ------ .../emojis/discord-emojis/[category].astro | 245 ++++++++---- .../pages/emojis/discord-emojis/[slug].astro | 116 ------ frontend/src/pages/tldr/sitemap.xml.ts | 2 +- 7 files changed, 599 insertions(+), 787 deletions(-) delete mode 100644 frontend/src/pages/emojis/[slug].astro delete mode 100644 frontend/src/pages/emojis/apple-emojis/[slug].astro delete mode 100644 frontend/src/pages/emojis/discord-emojis/[slug].astro diff --git a/frontend/src/pages/emojis/[category].astro b/frontend/src/pages/emojis/[category].astro index 490adddb32..ff314c07b7 100644 --- a/frontend/src/pages/emojis/[category].astro +++ b/frontend/src/pages/emojis/[category].astro @@ -6,7 +6,7 @@ import ToolContainer from '../../components/tool/ToolContainer'; import ToolHead from '../../components/tool/ToolHead'; import AdBanner from '../../components/banner/AdBanner.astro'; import EachEmojiPage from './EachEmojiPage.astro'; -import { getEmojisByCategory, getEmojiCategories, getEmojiBySlug, getEmojiImages } from '../../lib/emojis'; +import { getEmojisByCategory, getEmojiCategories, getEmojiBySlug, getEmojiImages, getAllEmojis } from '../../lib/emojis'; import { apple_vendor_excluded_emojis, discord_vendor_excluded_emojis } from '@/lib/emojis-consts'; export const prerender = false; @@ -18,24 +18,82 @@ if (!categorySlug) { return new Response(null, { status: 404 }); } -// Check if category is numeric (pagination route) -if (/^\d+$/.test(categorySlug)) { - // This is actually a pagination route - redirect to [slug].astro - return Astro.redirect(`/freedevtools/emojis/${categorySlug}/`, 301); -} - // Get all categories to validate const allCategories = getEmojiCategories(); const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); let emojiData: any = null; let categoryData: any = null; +let paginationData: any = null; + +// Check if category is numeric (pagination route) +// Handle pagination directly here to avoid route collision +if (/^\d+$/.test(categorySlug)) { + // This is actually a pagination route - handle it directly + const currentPage = parseInt(categorySlug, 10); + const emojis = await getAllEmojis(); + const categories = getEmojiCategories(); + const emojisByCategory: Record = {}; + + for (const cat of categories) { + emojisByCategory[cat] = emojis.filter( + (e) => + (e.category || 'Other') === cat + ); + } + + const sortedCategories = Object.keys(emojisByCategory).sort(); + for (const category of sortedCategories) { + emojisByCategory[category].sort((a, b) => { + const titleA = a.title || a.slug || ''; + const titleB = b.title || b.slug || ''; + return titleA.localeCompare(titleB); + }); + } + + const itemsPerPage = 36; + const totalPages = Math.ceil(sortedCategories.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedCategories = sortedCategories.slice(startIndex, endIndex); + + const categoryIconMap: Record = { + 'Smileys & Emotion': '😀', + 'People & Body': '👤', + 'Animals & Nature': '🐶', + 'Food & Drink': '🍎', + 'Travel & Places': '✈️', + Activities: '⚽', + Objects: '📱', + Symbols: '❤️', + Flags: '🏁', + Other: '❓', + }; + + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Emojis', href: '/freedevtools/emojis/' }, + ]; + + paginationData = { + currentPage, + totalPages, + sortedCategories, + paginatedCategories, + emojisByCategory, + emojis, + categoryIconMap, + breadcrumbItems, + }; +} // Check if this is actually an emoji slug (not a category) // Since [category].astro matches first, we need to handle emoji slugs here // This is a workaround for route priority not working as expected -const emoji = getEmojiBySlug(categorySlug); -if (emoji && !categorySlugs.includes(categorySlug)) { +// Only check if not already handled as pagination +if (!paginationData) { + const emoji = getEmojiBySlug(categorySlug); + if (emoji && !categorySlugs.includes(categorySlug)) { // This is an emoji slug, not a category - handle it here const images = getEmojiImages(categorySlug); @@ -74,19 +132,19 @@ if (emoji && !categorySlugs.includes(categorySlug)) { breadcrumbItems, seoDescription, }; -} else { - // Validate category exists - if (!categorySlugs.includes(categorySlug)) { - return new Response(null, { status: 404 }); - } + } else { + // Validate category exists + if (!categorySlugs.includes(categorySlug)) { + return new Response(null, { status: 404 }); + } - // Find the actual category name from the slug - const categoryName = allCategories.find( - cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug - ) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); + // Find the actual category name from the slug + const categoryName = allCategories.find( + cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug + ) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); - // SEO metadata and descriptions for categories - const categorySeo: Record = { + // SEO metadata and descriptions for categories + const categorySeo: Record = { 'Activities': { title: 'Activities Emojis - Sports, Events, and Hobbies | Online Free DevTools by Hexmos', description: 'Explore activities emojis covering sports, games, celebrations, and hobbies. Copy emoji, view meanings, and find shortcodes instantly.', @@ -132,49 +190,185 @@ if (emoji && !categorySlugs.includes(categorySlug)) { description: 'Browse flag emojis including country flags, regional flags, and special flags. Copy emoji, view meanings, and find shortcodes instantly.', keywords: ['flag emojis', 'country emojis', 'regional emojis', 'national emojis', 'copy emoji', 'emoji meanings'] } - }; + }; - const seoData = categorySeo[categoryName] || { - title: `${categoryName} Emojis | Online Free DevTools by Hexmos`, - description: `Explore ${categoryName.toLowerCase()} emojis. Copy emoji, view meanings, and find shortcodes instantly.`, - keywords: [`${categoryName.toLowerCase()} emojis`, 'copy emoji', 'emoji meanings', 'emoji shortcodes'] - }; + const seoData = categorySeo[categoryName] || { + title: `${categoryName} Emojis | Online Free DevTools by Hexmos`, + description: `Explore ${categoryName.toLowerCase()} emojis. Copy emoji, view meanings, and find shortcodes instantly.`, + keywords: [`${categoryName.toLowerCase()} emojis`, 'copy emoji', 'emoji meanings', 'emoji shortcodes'] + }; - // Get emojis for this category - const emojis = await getEmojisByCategory(categoryName); - const totalEmojis = emojis.length; + // Get emojis for this category + const emojis = await getEmojisByCategory(categoryName); + const totalEmojis = emojis.length; - // Pagination logic for page 1 - const itemsPerPage = 36; - const currentPage = 1; - const totalPages = Math.ceil(totalEmojis / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const paginatedEmojis = emojis.slice(startIndex, endIndex); + // Pagination logic for page 1 + const itemsPerPage = 36; + const currentPage = 1; + const totalPages = Math.ceil(totalEmojis / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedEmojis = emojis.slice(startIndex, endIndex); - // Breadcrumb items - const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: categoryName, href: `/freedevtools/emojis/${categorySlug}/` } - ]; + // Breadcrumb items + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Emojis', href: '/freedevtools/emojis/' }, + { label: categoryName, href: `/freedevtools/emojis/${categorySlug}/` } + ]; - categoryData = { - categoryName, - categorySlug, - seoData, - emojis, - totalEmojis, - itemsPerPage, - currentPage, - totalPages, - paginatedEmojis, - breadcrumbItems, - }; + categoryData = { + categoryName, + categorySlug, + seoData, + emojis, + totalEmojis, + itemsPerPage, + currentPage, + totalPages, + paginatedEmojis, + breadcrumbItems, + }; + } } --- -{emojiData ? ( +{paginationData ? ( + + +
+ +
+ + +
+
+
+
+ {paginationData.sortedCategories.length} +
+
Categories
+
+
+
+ {paginationData.emojis.length.toLocaleString()} +
+
Emojis
+
+
+
+ +
+ { + paginationData.paginatedCategories.map((category) => ( +
+
+
+ + {paginationData.categoryIconMap[category] || '❓'} + +
+ + {category.replace('-', ' ')} + +
+ +

+ {paginationData.emojisByCategory[category].length} emojis available +

+ +
+ {paginationData.emojisByCategory[category].slice(0, 5).map((emoji) => { + const emojiName = + emoji.title || emoji.slug; + const truncatedName = + emojiName.length > 27 + ? emojiName.substring(0, 27) + '...' + : emojiName; + return ( + + {emoji.code || emoji.emoji} {truncatedName} + + ); + })} + {paginationData.emojisByCategory[category].length > 5 && ( +

+ +{paginationData.emojisByCategory[category].length - 5} more items +

+ )} +
+ + +
+ )) + } +
+ + + + +
+
+) : emojiData ? ( = {}; - - for (const cat of categories) { - emojisByCategory[cat] = emojis.filter( - (e) => - (e.category || 'Other') === cat - ); - } - - const sortedCategories = Object.keys(emojisByCategory).sort(); - for (const category of sortedCategories) { - emojisByCategory[category].sort((a, b) => { - const titleA = a.title || a.slug || ''; - const titleB = b.title || b.slug || ''; - return titleA.localeCompare(titleB); - }); - } - - const itemsPerPage = 36; - const totalPages = Math.ceil(sortedCategories.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const paginatedCategories = sortedCategories.slice(startIndex, endIndex); - - const categoryIconMap: Record = { - 'Smileys & Emotion': '😀', - 'People & Body': '👤', - 'Animals & Nature': '🐶', - 'Food & Drink': '🍎', - 'Travel & Places': '✈️', - Activities: '⚽', - Objects: '📱', - Symbols: '❤️', - Flags: '🏁', - Other: '❓', - }; - - const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis', href: '/freedevtools/emojis/' }, - ]; - - paginationData = { - currentPage, - totalPages, - sortedCategories, - paginatedCategories, - emojisByCategory, - emojis, - categoryIconMap, - breadcrumbItems, - }; -} else { - // Handle emoji page route - const emoji = getEmojiBySlug(slug!); - const images = getEmojiImages(slug!); - - if (!emoji) { - return new Response(null, { status: 404 }); - } - - const cleanDescription = (text?: string) => { - if (!text) return ''; - return text - .replace(/<[^>]*>/g, '') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/[?]{2,}/g, '') - .trim(); - }; - - // Get category for breadcrumb - const categoryName = (emoji.category || 'Other') as string; - const categorySlug = categoryName.toLowerCase().replace(/[^a-z0-9]+/g, '-'); - - // Clean description now (don't store function) - const cleanedDescription = cleanDescription(emoji.description); - const seoDescription = cleanedDescription || - `Learn about the ${emoji.title || emoji.slug} emoji ${emoji.code || ''}. Find meanings, shortcodes, and usage information.`; - - // Breadcrumb items - const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: categoryName, href: `/freedevtools/emojis/${categorySlug}/` }, - { label: emoji.title || emoji.slug }, - ]; - - emojiData = { - emoji, - images, - categoryName, - categorySlug, - breadcrumbItems, - cleanedDescription, - seoDescription, - }; -} ---- - -{paginationData ? ( - - -
- -
- - -
-
-
-
- {paginationData.sortedCategories.length} -
-
Categories
-
-
-
- {paginationData.emojis.length.toLocaleString()} -
-
Emojis
-
-
-
- -
- { - paginationData.paginatedCategories.map((category) => ( -
-
-
- - {paginationData.categoryIconMap[category] || '❓'} - -
- - {category.replace('-', ' ')} - -
- -

- {paginationData.emojisByCategory[category].length} emojis available -

- -
- {paginationData.emojisByCategory[category].slice(0, 5).map((emoji) => { - const emojiName = - emoji.title || emoji.slug; - const truncatedName = - emojiName.length > 27 - ? emojiName.substring(0, 27) + '...' - : emojiName; - return ( - - {emoji.code || emoji.emoji} {truncatedName} - - ); - })} - {paginationData.emojisByCategory[category].length > 5 && ( -

- +{paginationData.emojisByCategory[category].length - 5} more items -

- )} -
- - -
- )) - } -
- - - - -
-
-) : emojiData ? ( - - -
- -
- - - - - -
-
- - ← Back to Emojis - - { - emojiData.emoji.category ? ( - - View{' '} - {emojiData.emoji.category - .replace(/-/g, ' ') - .replace(/\b\w/g, (l) => l.toUpperCase())}{' '} - Category - - ) : null - } - - { - emojiData.emoji.apple_vendor_description && - !apple_vendor_excluded_emojis.includes(emojiData.emoji.slug) ? ( - - 🍎 View Apple Version - - ) : null - } - - { - emojiData.emoji.discord_vendor_description && - !discord_vendor_excluded_emojis.includes(emojiData.emoji.slug) ? ( - - 🎮 View Discord Version - - ) : null - } - - - -
-
-
-
-) : null} - - diff --git a/frontend/src/pages/emojis/apple-emojis/[category].astro b/frontend/src/pages/emojis/apple-emojis/[category].astro index 938f603189..27614f7a1a 100644 --- a/frontend/src/pages/emojis/apple-emojis/[category].astro +++ b/frontend/src/pages/emojis/apple-emojis/[category].astro @@ -5,7 +5,9 @@ import Pagination from '../../../components/PaginationComponent.astro'; import CreditsButton from '../../../components/buttons/CreditsButton'; import ToolContainer from '../../../components/tool/ToolContainer'; import ToolHead from '../../../components/tool/ToolHead'; -import { getEmojisByCategory, getEmojiCategories, fetchLatestAppleImage, getEmojiBySlug } from '../../../lib/emojis'; +import { getEmojisByCategory, getEmojiCategories, fetchLatestAppleImage, getEmojiBySlug, getAppleEmojiBySlug } from '../../../lib/emojis'; +import VendorEmojiPage from '@/components/VendorEmojiPage'; +import { discord_vendor_excluded_emojis } from '@/lib/emojis-consts'; export const prerender = false; @@ -17,46 +19,73 @@ if (!urlPath.endsWith('/')) { return Astro.redirect(`${urlPath}/`, 301); } -// Check if category is numeric (pagination route) -if (categorySlug && /^\d+$/.test(categorySlug)) { - // This is actually a pagination route - redirect to [slug].astro - return Astro.redirect(`/freedevtools/emojis/apple-emojis/${categorySlug}/`, 301); -} - // Get all categories to validate const allCategories = getEmojiCategories(); const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); +// Check if category is numeric (pagination route) - 404 since there's no pagination on apple-emojis index +if (categorySlug && /^\d+$/.test(categorySlug)) { + return new Response(null, { status: 404 }); +} + +let emojiData: any = null; +let categoryData: any = null; + // Check if this is actually an emoji slug (not a category) +// Since [category].astro matches first, we need to handle emoji slugs here const emoji = getEmojiBySlug(categorySlug!); if (emoji && !categorySlugs.includes(categorySlug!)) { - // This is an emoji slug, not a category - redirect to emoji page - return Astro.redirect(`/freedevtools/emojis/apple-emojis/${categorySlug}/`, 301); -} - -// Validate category exists -if (!categorySlug || !categorySlugs.includes(categorySlug)) { - return new Response(null, { status: 404 }); -} + // This is an emoji slug, not a category - handle it here + const appleEmoji = getAppleEmojiBySlug(categorySlug!); + + if (!appleEmoji) { + return new Response(null, { status: 404 }); + } + + const cleanDescription = (text?: string) => { + if (!text) return ''; + return text + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/[?]{2,}/g, '') + .trim(); + }; + + const cleanedDescription = cleanDescription(appleEmoji.apple_vendor_description || appleEmoji.description); + + emojiData = { + emoji: appleEmoji, + cleanedDescription, + }; +} else { + // Validate category exists + if (!categorySlug || !categorySlugs.includes(categorySlug)) { + return new Response(null, { status: 404 }); + } -// Find the actual category name from the slug -const categoryName = allCategories.find( - cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug -) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); + // Find the actual category name from the slug + const categoryName = allCategories.find( + cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug + ) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); -// const emojis = await getEmojisByCategory(categoryName, "apple"); -const emojis = (await getEmojisByCategory(categoryName, "apple")) - .map(e => ({ - ...e, - latestAppleImage: fetchLatestAppleImage(e.slug) - })); + // const emojis = await getEmojisByCategory(categoryName, "apple"); + const emojis = (await getEmojisByCategory(categoryName, "apple")) + .map(e => ({ + ...e, + latestAppleImage: fetchLatestAppleImage(e.slug) + })); -if (emojis.length === 0) { - return Astro.redirect('/freedevtools/emojis/apple-emojis/'); -} + if (emojis.length === 0) { + return Astro.redirect('/freedevtools/emojis/apple-emojis/'); + } -// --- Apple-specific category descriptions --- -export const appleCategoryDescriptions = { + // --- Apple-specific category descriptions --- + const appleCategoryDescriptions: Record = { "Smileys & Emotion": "Apple's Smileys feature glossy shading and expressive faces that set the tone for iMessage and social platforms. The distinctive rounded eyes and vibrant gradients are signature Apple touches.", "People & Body": "Apple leads on inclusivity—supporting diverse skin tones, gender options, and custom Memoji. People and gestures have soft edges and subtle shadows, making them feel inviting and lively on iOS.", "Animals & Nature": "Animals in Apple's emoji set are playful and detailed, often with friendly eyes and vivid colors. Nature motifs leverage semi-realistic illustrations that feel right at home in iOS dark and light mode.", @@ -66,57 +95,132 @@ export const appleCategoryDescriptions = { "Objects": "Apple renders everyday objects with photorealistic vibes—from high-res tech gadgets to lifelike accessories—ensuring each emoji looks detailed and familiar across Apple devices.", "Symbols": "Apple's symbols blend clarity with style. Glassy gradients, subtle depth, and clean shapes set apart icons like hearts, arrows, and warning signs from standard flat glyphs.", "Flags": "Apple flags maintain accurate proportions and vibrant colors for easy recognition, with an extra emphasis on clarity and accessibility for global users.", - "Other": "Apple's unique emojis in the 'Other' category often represent novelty, tech, and recent trends, all interpreted with its signature visual polish and device-optimized detail." -}; - -// --- Pagination setup --- -const totalEmojis = emojis.length; -const itemsPerPage = 36; -const currentPage = 1; -const totalPages = Math.ceil(totalEmojis / itemsPerPage); -const startIndex = (currentPage - 1) * itemsPerPage; -const endIndex = startIndex + itemsPerPage; -const paginatedEmojis = emojis.slice(startIndex, endIndex); - -// --- Breadcrumbs --- -const breadcrumbItems = [ - { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: 'Apple Emojis', href: '/freedevtools/emojis/apple-emojis/' }, - { label: categoryName, href: `/freedevtools/emojis/apple-emojis/${categorySlug}/` } -]; + "Other": "Apple's unique emojis in the 'Other' category often represent novelty, tech, and recent trends, all interpreted with its signature visual polish and device-optimized detail." + }; + + // --- Pagination setup --- + const totalEmojis = emojis.length; + const itemsPerPage = 36; + const currentPage = 1; + const totalPages = Math.ceil(totalEmojis / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedEmojis = emojis.slice(startIndex, endIndex); + + // --- Breadcrumbs --- + const breadcrumbItems = [ + { label: 'Free DevTools', href: '/freedevtools/' }, + { label: 'Emojis', href: '/freedevtools/emojis/' }, + { label: 'Apple Emojis', href: '/freedevtools/emojis/apple-emojis/' }, + { label: categoryName, href: `/freedevtools/emojis/apple-emojis/${categorySlug}/` } + ]; + + categoryData = { + categoryName, + categorySlug, + emojis, + totalEmojis, + itemsPerPage, + currentPage, + totalPages, + paginatedEmojis, + breadcrumbItems, + appleCategoryDescriptions, + }; +} --- - +{emojiData ? ( + +
+
+ +
+
+ +
+ + + + +
+
+ + ← Back to Apple Emojis + + + {emojiData.emoji.category ? ( + + View {emojiData.emoji.category.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())} Category + + ) : null} + + {emojiData.emoji.discord_vendor_description && + !discord_vendor_excluded_emojis.includes(emojiData.emoji.slug) ? ( + + 🎮 View Discord Version + + ) : null} +
+
+
+
+) : categoryData ? ( +
-
{totalEmojis.toLocaleString()}
+
{categoryData.totalEmojis.toLocaleString()}
Emojis
-
{totalPages}
+
{categoryData.totalPages}
Pages
@@ -125,13 +229,13 @@ const breadcrumbItems = [
- Showing {paginatedEmojis.length} of {totalEmojis} emojis (Page {currentPage} of {totalPages}) + Showing {categoryData.paginatedEmojis.length} of {categoryData.totalEmojis} emojis (Page {categoryData.currentPage} of {categoryData.totalPages})
- {paginatedEmojis.map((emoji) => { + {categoryData.paginatedEmojis.map((emoji) => { const emojiChar = emoji.code || ''; const graphemeCount = [...emojiChar].length; const zwjCount = (emojiChar.match(/\u200d/g) || []).length; @@ -173,9 +277,9 @@ const breadcrumbItems = [ @@ -192,6 +296,7 @@ const breadcrumbItems = [
+) : null} diff --git a/frontend/src/pages/emojis/_EmojiPage.astro b/frontend/src/pages/emojis/_EmojiPage.astro index 97bae6cf9d..b52e9b3909 100644 --- a/frontend/src/pages/emojis/_EmojiPage.astro +++ b/frontend/src/pages/emojis/_EmojiPage.astro @@ -4,15 +4,15 @@ import CreditsButton from '../../components/buttons/CreditsButton'; import Pagination from '../../components/PaginationComponent.astro'; import ToolContainer from '../../components/tool/ToolContainer'; import ToolHead from '../../components/tool/ToolHead'; -import { categoryIconMap } from "../../lib/emojis" -const { - categories, - emojisByCategory, - currentPage, - totalPages, - totalCategories, - totalEmojis, - breadcrumbItems +import { categoryIconMap } from 'db/emojis/emojis-utils'; +const { + categories, + emojisByCategory, + currentPage, + totalPages, + totalCategories, + totalEmojis, + breadcrumbItems, } = Astro.props; --- @@ -34,7 +34,9 @@ const {
Categories
-
{totalEmojis.toLocaleString()}
+
+ {totalEmojis.toLocaleString()} +
Emojis
@@ -43,84 +45,99 @@ const {
- Showing {categories.length} of {totalCategories} categories (Page {currentPage} of {totalPages}) + Showing {categories.length} of {totalCategories} categories (Page { + currentPage + } of {totalPages})
-
- {categories.map((category: any) => { - const emojis = emojisByCategory[category]; +
+ { + categories.map((category: any) => { + const emojis = emojisByCategory[category]; - // Hide "Other" category if it has no emojis - if (category === "Other" && (!emojis || emojis.length === 0)) return null; - return ( -
-
-
- - {categoryIconMap[category] || "❓"} - -
- - {category.replace('-', ' ')} - -
- -

- {emojisByCategory[category].length} emojis available -

- -
- {emojisByCategory[category].slice(0, 5).map((emoji: any) => { - const emojiName = emoji.title || emoji.fluentui_metadata?.cldr || emoji.slug; - const truncatedName = emojiName.length > 27 ? emojiName.substring(0, 27) + '...' : emojiName; - return ( - +
+
+ + {categoryIconMap[category] || '❓'} + +
+
- {emoji.code || emoji.emoji} {truncatedName} + {category.replace('-', ' ')} - ); - })} - {emojisByCategory[category].length > 5 && ( -

- +{emojisByCategory[category].length - 5} more items +

+ +

+ {emojisByCategory[category].length} emojis available

- )} -
- - -
- )})} + +
+ {emojisByCategory[category].slice(0, 5).map((emoji: any) => { + const emojiName = + emoji.title || emoji.fluentui_metadata?.cldr || emoji.slug; + const truncatedName = + emojiName.length > 27 + ? emojiName.substring(0, 27) + '...' + : emojiName; + return ( + + {emoji.code || emoji.emoji} {truncatedName} + + ); + })} + {emojisByCategory[category].length > 5 && ( +

+ +{emojisByCategory[category].length - 5} more items +

+ )} +
+ + +
+ ); + }) + }
- +
-

Vendors

+

+ Vendors +

Explore emoji designs by platform vendors

-
diff --git a/frontend/src/pages/emojis/apple-emojis/[category].astro b/frontend/src/pages/emojis/apple-emojis/[category].astro index 27614f7a1a..46ccda5b2a 100644 --- a/frontend/src/pages/emojis/apple-emojis/[category].astro +++ b/frontend/src/pages/emojis/apple-emojis/[category].astro @@ -5,7 +5,7 @@ import Pagination from '../../../components/PaginationComponent.astro'; import CreditsButton from '../../../components/buttons/CreditsButton'; import ToolContainer from '../../../components/tool/ToolContainer'; import ToolHead from '../../../components/tool/ToolHead'; -import { getEmojisByCategory, getEmojiCategories, fetchLatestAppleImage, getEmojiBySlug, getAppleEmojiBySlug } from '../../../lib/emojis'; +import { getEmojisByCategory, getEmojiCategories, fetchLatestAppleImage, getEmojiBySlug, getAppleEmojiBySlug } from 'db/emojis/emojis-utils'; import VendorEmojiPage from '@/components/VendorEmojiPage'; import { discord_vendor_excluded_emojis } from '@/lib/emojis-consts'; diff --git a/frontend/src/pages/emojis/apple-emojis/[category]/[page].astro b/frontend/src/pages/emojis/apple-emojis/[category]/[page].astro index 6ea2be8e7e..bb8b0ccce0 100644 --- a/frontend/src/pages/emojis/apple-emojis/[category]/[page].astro +++ b/frontend/src/pages/emojis/apple-emojis/[category]/[page].astro @@ -4,7 +4,11 @@ import Pagination from '../../../../components/PaginationComponent.astro'; import CreditsButton from '../../../../components/buttons/CreditsButton'; import ToolContainer from '../../../../components/tool/ToolContainer'; import ToolHead from '../../../../components/tool/ToolHead'; -import { getEmojisByCategory, getEmojiCategories, fetchLatestAppleImage } from '../../../../lib/emojis'; +import { + getEmojisByCategory, + getEmojiCategories, + fetchLatestAppleImage, +} from 'db/emojis/emojis-utils'; import AdBanner from '../../../../components/banner/AdBanner.astro'; export const prerender = false; @@ -26,62 +30,130 @@ const currentPage = parseInt(page, 10); // Validate category exists const allCategories = getEmojiCategories(); -const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); +const categorySlugs = allCategories.map((cat) => + cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') +); if (!categorySlug || !categorySlugs.includes(categorySlug)) { return new Response(null, { status: 404 }); } // Find the actual category name from the slug -const categoryName = allCategories.find( - cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug -) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); - -const categorySeo: Record = { - 'Activities': { - title: 'Apple Vendor Activities Emojis - Sports, Events & Hobbies | Free DevTools by Hexmos', - description: 'Browse Apple-style activities emojis including sports, games, celebrations, and hobbies. Copy emoji and view meanings instantly.', - keywords: ['apple emojis', 'activities emojis', 'sports', 'games', 'celebration', 'hobby', 'copy emoji'] +const categoryName = + allCategories.find( + (cat) => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug + ) || + categorySlug + .replace(/-/g, ' ') + .replace(/\b\w/g, (l: string) => l.toUpperCase()); + +const categorySeo: Record< + string, + { title: string; description: string; keywords: string[] } +> = { + Activities: { + title: + 'Apple Vendor Activities Emojis - Sports, Events & Hobbies | Free DevTools by Hexmos', + description: + 'Browse Apple-style activities emojis including sports, games, celebrations, and hobbies. Copy emoji and view meanings instantly.', + keywords: [ + 'apple emojis', + 'activities emojis', + 'sports', + 'games', + 'celebration', + 'hobby', + 'copy emoji', + ], }, 'Animals & Nature': { - title: 'Apple Vendor Animals & Nature Emojis - Wildlife, Plants & Weather | Free DevTools by Hexmos', - description: 'Explore Apple-style animals and nature emojis featuring wildlife, plants, and weather icons. Copy emoji and view meanings instantly.', - keywords: ['apple emojis', 'animals', 'nature', 'wildlife', 'plants', 'weather', 'copy emoji'] + title: + 'Apple Vendor Animals & Nature Emojis - Wildlife, Plants & Weather | Free DevTools by Hexmos', + description: + 'Explore Apple-style animals and nature emojis featuring wildlife, plants, and weather icons. Copy emoji and view meanings instantly.', + keywords: [ + 'apple emojis', + 'animals', + 'nature', + 'wildlife', + 'plants', + 'weather', + 'copy emoji', + ], }, 'Food & Drink': { - title: 'Apple Vendor Food & Drink Emojis - Meals & Beverages | Free DevTools by Hexmos', - description: 'Discover Apple-style food and drink emojis for meals, beverages, and snacks. Copy emoji and find meanings instantly.', - keywords: ['apple emojis', 'food', 'drink', 'meals', 'beverages', 'snacks', 'copy emoji'] + title: + 'Apple Vendor Food & Drink Emojis - Meals & Beverages | Free DevTools by Hexmos', + description: + 'Discover Apple-style food and drink emojis for meals, beverages, and snacks. Copy emoji and find meanings instantly.', + keywords: [ + 'apple emojis', + 'food', + 'drink', + 'meals', + 'beverages', + 'snacks', + 'copy emoji', + ], }, 'People & Body': { - title: 'Apple Vendor People & Body Emojis - Faces & Gestures | Free DevTools by Hexmos', - description: 'Explore Apple-style people and body emojis including faces, gestures, and family members. Copy emoji and view meanings instantly.', - keywords: ['apple emojis', 'people', 'faces', 'gestures', 'body', 'copy emoji'] + title: + 'Apple Vendor People & Body Emojis - Faces & Gestures | Free DevTools by Hexmos', + description: + 'Explore Apple-style people and body emojis including faces, gestures, and family members. Copy emoji and view meanings instantly.', + keywords: [ + 'apple emojis', + 'people', + 'faces', + 'gestures', + 'body', + 'copy emoji', + ], }, 'Smileys & Emotion': { - title: 'Apple Vendor Smileys & Emotion Emojis - Faces & Feelings | Free DevTools by Hexmos', - description: 'Find Apple-style smileys and emotion emojis that express feelings and moods. Copy emoji and view meanings instantly.', - keywords: ['apple emojis', 'smileys', 'emotions', 'faces', 'feelings', 'copy emoji'] + title: + 'Apple Vendor Smileys & Emotion Emojis - Faces & Feelings | Free DevTools by Hexmos', + description: + 'Find Apple-style smileys and emotion emojis that express feelings and moods. Copy emoji and view meanings instantly.', + keywords: [ + 'apple emojis', + 'smileys', + 'emotions', + 'faces', + 'feelings', + 'copy emoji', + ], + }, + Flags: { + title: + 'Apple Vendor Flag Emojis - Country & Regional Flags | Free DevTools by Hexmos', + description: + 'Browse Apple-style flag emojis featuring country, regional, and pride flags. Copy emoji and view meanings instantly.', + keywords: [ + 'apple emojis', + 'flags', + 'country flags', + 'regional flags', + 'copy emoji', + ], }, - 'Flags': { - title: 'Apple Vendor Flag Emojis - Country & Regional Flags | Free DevTools by Hexmos', - description: 'Browse Apple-style flag emojis featuring country, regional, and pride flags. Copy emoji and view meanings instantly.', - keywords: ['apple emojis', 'flags', 'country flags', 'regional flags', 'copy emoji'] - } }; const seoData = categorySeo[categoryName] || { title: `Apple Vendor ${categoryName} Emojis | Free DevTools by Hexmos`, description: `Explore Apple-style ${categoryName.toLowerCase()} emojis. Copy emoji and view meanings instantly.`, - keywords: [`apple ${categoryName.toLowerCase()} emojis`, 'copy emoji', 'emoji meanings'] + keywords: [ + `apple ${categoryName.toLowerCase()} emojis`, + 'copy emoji', + 'emoji meanings', + ], }; // const emojis = await getEmojisByCategory(categoryName, "apple"); -const emojis = (await getEmojisByCategory(categoryName, "apple")) - .map(e => ({ - ...e, - latestAppleImage: fetchLatestAppleImage(e.slug) - })); +const emojis = (await getEmojisByCategory(categoryName, 'apple')).map((e) => ({ + ...e, + latestAppleImage: fetchLatestAppleImage(e.slug), +})); const totalEmojis = emojis.length; @@ -95,11 +167,14 @@ const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, { label: 'Emojis', href: '/freedevtools/emojis/' }, { label: 'Apple Emojis', href: '/freedevtools/emojis/apple-emojis/' }, - { label: categoryName, href: `/freedevtools/emojis/apple-emojis/${categorySlug}/` } + { + label: categoryName, + href: `/freedevtools/emojis/apple-emojis/${categorySlug}/`, + }, ]; --- - @@ -126,7 +206,9 @@ const breadcrumbItems = [
-
{totalEmojis.toLocaleString()}
+
+ {totalEmojis.toLocaleString()} +
Emojis
@@ -138,22 +220,27 @@ const breadcrumbItems = [
- Showing {paginatedEmojis.length} of {totalEmojis} emojis (Page {currentPage} of {totalPages}) + Showing {paginatedEmojis.length} of {totalEmojis} emojis (Page { + currentPage + } of {totalPages})
-
- {paginatedEmojis.map((emoji) => { +
+ { + paginatedEmojis.map((emoji) => { const emojiChar = emoji.code || ''; const graphemeCount = [...emojiChar].length; const zwjCount = (emojiChar.match(/\u200d/g) || []).length; const complexity = graphemeCount + zwjCount * 2; let emojiSizeClass = 'h-10 w-10'; - + if (complexity > 10) emojiSizeClass = 'h-6 w-6'; else if (complexity > 6) emojiSizeClass = 'h-8 w-8'; - + return ( {emojiChar}
)}
- - + }) + } +
- -
+
@@ -208,6 +300,7 @@ const breadcrumbItems = [ diff --git a/frontend/src/pages/emojis/apple-emojis/index.astro b/frontend/src/pages/emojis/apple-emojis/index.astro index a77fd607f5..76d1ba94b8 100644 --- a/frontend/src/pages/emojis/apple-emojis/index.astro +++ b/frontend/src/pages/emojis/apple-emojis/index.astro @@ -1,7 +1,12 @@ --- -import BaseLayout from '../../../layouts/BaseLayout.astro'; +import { + fetchImageFromDB, + getAllEmojis, + getEmojiCategories, + type EmojiData, +} from 'db/emojis/emojis-utils'; import AdBanner from '../../../components/banner/AdBanner'; -import { getAllEmojis, getEmojiCategories, fetchImageFromDB } from '../../../lib/emojis'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; // Load all emojis const emojis = await getAllEmojis(); @@ -42,21 +47,24 @@ const totalCategories = sortedCategories.length; const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: 'Apple Emojis' } + { label: 'Apple Emojis' }, ]; // SEO data -const seoTitle = currentPage === 1 - ? "Apple Emojis Reference - Browse & Copy Apple Emojis | Online Free DevTools by Hexmos" - : `Apple Emojis Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; +const seoTitle = + currentPage === 1 + ? 'Apple Emojis Reference - Browse & Copy Apple Emojis | Online Free DevTools by Hexmos' + : `Apple Emojis Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = currentPage === 1 - ? "Browse Apple's version of emojis by category. Copy instantly, explore meanings, and discover platform-specific emoji designs." - : `Browse page ${currentPage} of Apple's emoji reference. Copy instantly, explore meanings, and discover iOS-style emoji artwork.`; +const seoDescription = + currentPage === 1 + ? "Browse Apple's version of emojis by category. Copy instantly, explore meanings, and discover platform-specific emoji designs." + : `Browse page ${currentPage} of Apple's emoji reference. Copy instantly, explore meanings, and discover iOS-style emoji artwork.`; -const canonical = currentPage === 1 - ? "https://hexmos.com/freedevtools/emojis/apple-emojis/" - : `https://hexmos.com/freedevtools/emojis/apple-emojis/${currentPage}/`; +const canonical = + currentPage === 1 + ? 'https://hexmos.com/freedevtools/emojis/apple-emojis/' + : `https://hexmos.com/freedevtools/emojis/apple-emojis/${currentPage}/`; const mainKeywords = [ 'apple emojis', @@ -68,28 +76,44 @@ const mainKeywords = [ 'emoji meanings', 'emoji shortcodes', 'emoji library', - 'emoji copy' + 'emoji copy', ]; // Category icons export const categoryIconMap: Record = { - "Smileys & Emotion": fetchImageFromDB("slightly-smiling-face", "slightly-smiling-face_iOS_18.4.png")!, - "People & Body": fetchImageFromDB("bust-in-silhouette", "bust-in-silhouette_1f464_iOS_18.4.png")!, - "Animals & Nature": fetchImageFromDB("dog-face", "dog-face_1f436_iOS_18.4.png")!, - "Food & Drink": fetchImageFromDB("red-apple", "red-apple_1f34e_iOS_18.4.png")!, - "Travel & Places": fetchImageFromDB("airplane", "airplane_iOS_18.4.png")!, - "Activities": fetchImageFromDB("soccer-ball", "soccer-ball_26bd_iOS_18.4.png")!, - "Objects": fetchImageFromDB("mobile-phone", "mobile-phone_iOS_18.4.png")!, - "Symbols": fetchImageFromDB("check-mark-button", "check-mark-button_2705_iOS_18.4.png")!, - "Flags": fetchImageFromDB("chequered-flag", "chequered-flag_iOS_18.4.png")!, - "Other": fetchImageFromDB("question-mark", "question-mark_2753_iOS_18.4.png")!, + 'Smileys & Emotion': fetchImageFromDB( + 'slightly-smiling-face', + 'slightly-smiling-face_iOS_18.4.png' + )!, + 'People & Body': fetchImageFromDB( + 'bust-in-silhouette', + 'bust-in-silhouette_1f464_iOS_18.4.png' + )!, + 'Animals & Nature': fetchImageFromDB( + 'dog-face', + 'dog-face_1f436_iOS_18.4.png' + )!, + 'Food & Drink': fetchImageFromDB( + 'red-apple', + 'red-apple_1f34e_iOS_18.4.png' + )!, + 'Travel & Places': fetchImageFromDB('airplane', 'airplane_iOS_18.4.png')!, + Activities: fetchImageFromDB('soccer-ball', 'soccer-ball_26bd_iOS_18.4.png')!, + Objects: fetchImageFromDB('mobile-phone', 'mobile-phone_iOS_18.4.png')!, + Symbols: fetchImageFromDB( + 'check-mark-button', + 'check-mark-button_2705_iOS_18.4.png' + )!, + Flags: fetchImageFromDB('chequered-flag', 'chequered-flag_iOS_18.4.png')!, + Other: fetchImageFromDB('question-mark', 'question-mark_2753_iOS_18.4.png')!, }; - --- - = { partOf="Free DevTools" partOfUrl="https://hexmos.com/freedevtools/" keywords={mainKeywords} - features={["Apple-style designs", "Browse by category", "Copy instantly", "Compare with Unicode", "Free access"]} + features={[ + 'Apple-style designs', + 'Browse by category', + 'Copy instantly', + 'Compare with Unicode', + 'Free access', + ]} emojiCategory="Apple Emojis" >
@@ -113,16 +143,23 @@ export const categoryIconMap: Record = {

@@ -130,56 +167,70 @@ export const categoryIconMap: Record = {

- Apple's emojis are known for their vibrant, playful designs and the frequent updates that come with each iOS release. - Instead of relying on standard Unicode glyphs, Apple creates custom artwork for every emoji, which makes their style instantly recognizable to iPhone, iPad, and Mac users. - Discover Apple's full emoji collection here, complete with previews and instant copy options for every category. - Whether you're exploring the newest emojis from the latest iOS update or comparing how platforms differ, this page makes it easy to dive into Apple's creative emoji style. + Apple's emojis are known for their vibrant, playful designs and the + frequent updates that come with each iOS release. Instead of relying on + standard Unicode glyphs, Apple creates custom artwork for every emoji, + which makes their style instantly recognizable to iPhone, iPad, and Mac + users. Discover Apple's full emoji collection here, complete with + previews and instant copy options for every category. Whether you're + exploring the newest emojis from the latest iOS update or comparing how + platforms differ, this page makes it easy to dive into Apple's creative + emoji style.

-
- {paginatedCategories.map((category) => ( -
-
-
- {category +
+ { + paginatedCategories.map((category) => ( +
+ - - {category} - +

+ {emojisByCategory[category].length} emojis available +

-

- {emojisByCategory[category].length} emojis available -

-
- ))} + )) + }
- diff --git a/frontend/src/pages/emojis/discord-emojis/[category].astro b/frontend/src/pages/emojis/discord-emojis/[category].astro index 592d2113ca..d8fe8c04ed 100644 --- a/frontend/src/pages/emojis/discord-emojis/[category].astro +++ b/frontend/src/pages/emojis/discord-emojis/[category].astro @@ -5,7 +5,7 @@ import Pagination from '../../../components/PaginationComponent.astro'; import CreditsButton from '../../../components/buttons/CreditsButton'; import ToolContainer from '../../../components/tool/ToolContainer'; import ToolHead from '../../../components/tool/ToolHead'; -import { getEmojisByCategory, getEmojiCategories, fetchLatestDiscordImage, getEmojiBySlug, getDiscordEmojiBySlug } from '../../../lib/emojis'; +import { getEmojisByCategory, getEmojiCategories, fetchLatestDiscordImage, getEmojiBySlug, getDiscordEmojiBySlug } from 'db/emojis/emojis-utils'; import VendorEmojiPage from '@/components/VendorEmojiPage'; import { apple_vendor_excluded_emojis } from '@/lib/emojis-consts'; diff --git a/frontend/src/pages/emojis/discord-emojis/[category]/[page].astro b/frontend/src/pages/emojis/discord-emojis/[category]/[page].astro index 4a5b4b1f2d..413df47bdf 100644 --- a/frontend/src/pages/emojis/discord-emojis/[category]/[page].astro +++ b/frontend/src/pages/emojis/discord-emojis/[category]/[page].astro @@ -4,7 +4,11 @@ import Pagination from '../../../../components/PaginationComponent.astro'; import CreditsButton from '../../../../components/buttons/CreditsButton'; import ToolContainer from '../../../../components/tool/ToolContainer'; import ToolHead from '../../../../components/tool/ToolHead'; -import { getEmojisByCategory, getEmojiCategories, fetchLatestDiscordImage } from '../../../../lib/emojis'; +import { + getEmojisByCategory, + getEmojiCategories, + fetchLatestDiscordImage, +} from 'db/emojis/emojis-utils'; import AdBanner from '../../../../components/banner/AdBanner.astro'; export const prerender = false; @@ -26,63 +30,132 @@ const currentPage = parseInt(page, 10); // Validate category exists const allCategories = getEmojiCategories(); -const categorySlugs = allCategories.map(cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-')); +const categorySlugs = allCategories.map((cat) => + cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') +); if (!categorySlug || !categorySlugs.includes(categorySlug)) { return new Response(null, { status: 404 }); } // Find the actual category name from the slug -const categoryName = allCategories.find( - cat => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug -) || categorySlug.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); - -const categorySeo: Record = { - 'Activities': { - title: 'Discord Vendor Activities Emojis - Sports, Events & Hobbies | Free DevTools by Hexmos', - description: 'Browse Discord-style activities emojis including sports, games, celebrations, and hobbies. Copy emoji and view meanings instantly.', - keywords: ['discord emojis', 'activities emojis', 'sports', 'games', 'celebration', 'hobby', 'copy emoji'] +const categoryName = + allCategories.find( + (cat) => cat.toLowerCase().replace(/[^a-z0-9]+/g, '-') === categorySlug + ) || + categorySlug + .replace(/-/g, ' ') + .replace(/\b\w/g, (l: string) => l.toUpperCase()); + +const categorySeo: Record< + string, + { title: string; description: string; keywords: string[] } +> = { + Activities: { + title: + 'Discord Vendor Activities Emojis - Sports, Events & Hobbies | Free DevTools by Hexmos', + description: + 'Browse Discord-style activities emojis including sports, games, celebrations, and hobbies. Copy emoji and view meanings instantly.', + keywords: [ + 'discord emojis', + 'activities emojis', + 'sports', + 'games', + 'celebration', + 'hobby', + 'copy emoji', + ], }, 'Animals & Nature': { - title: 'Discord Vendor Animals & Nature Emojis - Wildlife, Plants & Weather | Free DevTools by Hexmos', - description: 'Explore Discord-style animals and nature emojis featuring wildlife, plants, and weather icons. Copy emoji and view meanings instantly.', - keywords: ['discord emojis', 'animals', 'nature', 'wildlife', 'plants', 'weather', 'copy emoji'] + title: + 'Discord Vendor Animals & Nature Emojis - Wildlife, Plants & Weather | Free DevTools by Hexmos', + description: + 'Explore Discord-style animals and nature emojis featuring wildlife, plants, and weather icons. Copy emoji and view meanings instantly.', + keywords: [ + 'discord emojis', + 'animals', + 'nature', + 'wildlife', + 'plants', + 'weather', + 'copy emoji', + ], }, 'Food & Drink': { - title: 'Discord Vendor Food & Drink Emojis - Meals & Beverages | Free DevTools by Hexmos', - description: 'Discover Discord-style food and drink emojis for meals, beverages, and snacks. Copy emoji and find meanings instantly.', - keywords: ['discord emojis', 'food', 'drink', 'meals', 'beverages', 'snacks', 'copy emoji'] + title: + 'Discord Vendor Food & Drink Emojis - Meals & Beverages | Free DevTools by Hexmos', + description: + 'Discover Discord-style food and drink emojis for meals, beverages, and snacks. Copy emoji and find meanings instantly.', + keywords: [ + 'discord emojis', + 'food', + 'drink', + 'meals', + 'beverages', + 'snacks', + 'copy emoji', + ], }, 'People & Body': { - title: 'Discord Vendor People & Body Emojis - Faces & Gestures | Free DevTools by Hexmos', - description: 'Explore Discord-style people and body emojis including faces, gestures, and family members. Copy emoji and view meanings instantly.', - keywords: ['discord emojis', 'people', 'faces', 'gestures', 'body', 'copy emoji'] + title: + 'Discord Vendor People & Body Emojis - Faces & Gestures | Free DevTools by Hexmos', + description: + 'Explore Discord-style people and body emojis including faces, gestures, and family members. Copy emoji and view meanings instantly.', + keywords: [ + 'discord emojis', + 'people', + 'faces', + 'gestures', + 'body', + 'copy emoji', + ], }, 'Smileys & Emotion': { - title: 'Discord Vendor Smileys & Emotion Emojis - Faces & Feelings | Free DevTools by Hexmos', - description: 'Find Discord-style smileys and emotion emojis that express feelings and moods. Copy emoji and view meanings instantly.', - keywords: ['discord emojis', 'smileys', 'emotions', 'faces', 'feelings', 'copy emoji'] + title: + 'Discord Vendor Smileys & Emotion Emojis - Faces & Feelings | Free DevTools by Hexmos', + description: + 'Find Discord-style smileys and emotion emojis that express feelings and moods. Copy emoji and view meanings instantly.', + keywords: [ + 'discord emojis', + 'smileys', + 'emotions', + 'faces', + 'feelings', + 'copy emoji', + ], + }, + Flags: { + title: + 'Discord Vendor Flag Emojis - Country & Regional Flags | Free DevTools by Hexmos', + description: + 'Browse Discord-style flag emojis featuring country, regional, and pride flags. Copy emoji and view meanings instantly.', + keywords: [ + 'discord emojis', + 'flags', + 'country flags', + 'regional flags', + 'copy emoji', + ], }, - 'Flags': { - title: 'Discord Vendor Flag Emojis - Country & Regional Flags | Free DevTools by Hexmos', - description: 'Browse Discord-style flag emojis featuring country, regional, and pride flags. Copy emoji and view meanings instantly.', - keywords: ['discord emojis', 'flags', 'country flags', 'regional flags', 'copy emoji'] - } }; const seoData = categorySeo[categoryName] || { title: `Discord Vendor ${categoryName} Emojis | Free DevTools by Hexmos`, description: `Explore Discord-style ${categoryName.toLowerCase()} emojis. Copy emoji and view meanings instantly.`, - keywords: [`discord ${categoryName.toLowerCase()} emojis`, 'copy emoji', 'emoji meanings'] + keywords: [ + `discord ${categoryName.toLowerCase()} emojis`, + 'copy emoji', + 'emoji meanings', + ], }; // Load Discord vendor emojis -const emojis = (await getEmojisByCategory(categoryName, "discord")) - .map(e => ({ +const emojis = (await getEmojisByCategory(categoryName, 'discord')) + .map((e) => ({ ...e, - latestDiscordImage: fetchLatestDiscordImage(e.slug) + latestDiscordImage: fetchLatestDiscordImage(e.slug), })) - .filter(e => e.latestDiscordImage); // ❌ Exclude if image missing + .filter((e) => e.latestDiscordImage); // ❌ Exclude if image missing const totalEmojis = emojis.length; @@ -96,11 +169,14 @@ const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, { label: 'Emojis', href: '/freedevtools/emojis/' }, { label: 'Discord Emojis', href: '/freedevtools/emojis/discord-emojis/' }, - { label: categoryName, href: `/freedevtools/emojis/discord-emojis/${categorySlug}/` } + { + label: categoryName, + href: `/freedevtools/emojis/discord-emojis/${categorySlug}/`, + }, ]; --- - @@ -127,7 +208,9 @@ const breadcrumbItems = [
-
{totalEmojis.toLocaleString()}
+
+ {totalEmojis.toLocaleString()} +
Emojis
@@ -139,62 +222,73 @@ const breadcrumbItems = [
- Showing {paginatedEmojis.length} of {totalEmojis} emojis (Page {currentPage} of {totalPages}) + Showing {paginatedEmojis.length} of {totalEmojis} emojis (Page { + currentPage + } of {totalPages})
-
- {paginatedEmojis.map((emoji) => { - const emojiChar = emoji.code || ''; - const graphemeCount = [...emojiChar].length; - const zwjCount = (emojiChar.match(/\u200d/g) || []).length; - const complexity = graphemeCount + zwjCount * 2; - let emojiSizeClass = 'h-10 w-10'; - - if (complexity > 10) emojiSizeClass = 'h-6 w-6'; - else if (complexity > 6) emojiSizeClass = 'h-8 w-8'; - - return ( - -
- {emoji.latestDiscordImage ? ( - {emoji.title (e.currentTarget.style.display = 'none')} - /> - ) : ( -
{emojiChar}
- )} -
-
- {emoji.title} -
-
- ); - })} +
+ { + paginatedEmojis.map((emoji) => { + const emojiChar = emoji.code || ''; + const graphemeCount = [...emojiChar].length; + const zwjCount = (emojiChar.match(/\u200d/g) || []).length; + const complexity = graphemeCount + zwjCount * 2; + let emojiSizeClass = 'h-10 w-10'; + + if (complexity > 10) emojiSizeClass = 'h-6 w-6'; + else if (complexity > 6) emojiSizeClass = 'h-8 w-8'; + + return ( + +
+ {emoji.latestDiscordImage ? ( + {emoji.title (e.currentTarget.style.display = 'none')} + /> + ) : ( +
{emojiChar}
+ )} +
+
+ {emoji.title} +
+
+ ); + }) + }
- -
+
- ← Back to Discord Emojis @@ -208,6 +302,7 @@ const breadcrumbItems = [ diff --git a/frontend/src/pages/emojis/discord-emojis/index.astro b/frontend/src/pages/emojis/discord-emojis/index.astro index 6f4ce86151..ecb1408fdf 100644 --- a/frontend/src/pages/emojis/discord-emojis/index.astro +++ b/frontend/src/pages/emojis/discord-emojis/index.astro @@ -1,7 +1,12 @@ --- -import BaseLayout from '../../../layouts/BaseLayout.astro'; +import { + fetchImageFromDB, + getAllEmojis, + getEmojiCategories, + type EmojiData, +} from 'db/emojis/emojis-utils'; import AdBanner from '../../../components/banner/AdBanner'; -import { getAllEmojis, getEmojiCategories, fetchImageFromDB } from '../../../lib/emojis'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; // Load all emojis const emojis = await getAllEmojis(); @@ -42,21 +47,24 @@ const totalCategories = sortedCategories.length; const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, { label: 'Emojis', href: '/freedevtools/emojis/' }, - { label: 'Discord Emojis' } + { label: 'Discord Emojis' }, ]; // SEO data -const seoTitle = currentPage === 1 - ? "Discord Emojis Reference - Browse & Copy Discord Emojis | Online Free DevTools by Hexmos" - : `Discord Emojis Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; +const seoTitle = + currentPage === 1 + ? 'Discord Emojis Reference - Browse & Copy Discord Emojis | Online Free DevTools by Hexmos' + : `Discord Emojis Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = currentPage === 1 - ? "Browse Discord-styled emojis by category. Copy instantly, view Discord-style artwork, and compare them with other platform vendors." - : `Browse page ${currentPage} of the Discord emoji reference with instant copy and full category browsing.`; +const seoDescription = + currentPage === 1 + ? 'Browse Discord-styled emojis by category. Copy instantly, view Discord-style artwork, and compare them with other platform vendors.' + : `Browse page ${currentPage} of the Discord emoji reference with instant copy and full category browsing.`; -const canonical = currentPage === 1 - ? "https://hexmos.com/freedevtools/emojis/discord-emojis/" - : `https://hexmos.com/freedevtools/emojis/discord-emojis/${currentPage}/`; +const canonical = + currentPage === 1 + ? 'https://hexmos.com/freedevtools/emojis/discord-emojis/' + : `https://hexmos.com/freedevtools/emojis/discord-emojis/${currentPage}/`; const mainKeywords = [ 'discord emojis', @@ -67,27 +75,59 @@ const mainKeywords = [ 'emoji meanings', 'emoji shortcodes', 'emoji library', - 'emoji copy' + 'emoji copy', ]; // Category icons — using Discord vendor images export const categoryIconMap: Record = { - "Smileys & Emotion": fetchImageFromDB("slightly-smiling-face", "slightly-smiling-face_1f642_twitter_15.0.3.png")!, - "People & Body": fetchImageFromDB("bust-in-silhouette", "bust-in-silhouette_1f464_twitter_15.0.3.png")!, - "Animals & Nature": fetchImageFromDB("dog-face", "dog-face_1f436_twitter_15.0.3.png")!, - "Food & Drink": fetchImageFromDB("red-apple", "red-apple_1f34e_twitter_15.0.3.png")!, - "Travel & Places": fetchImageFromDB("airplane", "airplane_2708-fe0f_twitter_15.0.3.png")!, - "Activities": fetchImageFromDB("soccer-ball", "soccer-ball_26bd_twitter_15.0.3.png")!, - "Objects": fetchImageFromDB("mobile-phone", "mobile-phone_1f4f1_twitter_15.0.3.png")!, - "Symbols": fetchImageFromDB("check-mark-button", "check-mark-button_2705_twitter_15.0.3.png")!, - "Flags": fetchImageFromDB("chequered-flag", "chequered-flag_1f3c1_twitter_15.0.3.png")!, - "Other": fetchImageFromDB("question-mark", "question-mark_2753_twitter_15.0.3.png")!, + 'Smileys & Emotion': fetchImageFromDB( + 'slightly-smiling-face', + 'slightly-smiling-face_1f642_twitter_15.0.3.png' + )!, + 'People & Body': fetchImageFromDB( + 'bust-in-silhouette', + 'bust-in-silhouette_1f464_twitter_15.0.3.png' + )!, + 'Animals & Nature': fetchImageFromDB( + 'dog-face', + 'dog-face_1f436_twitter_15.0.3.png' + )!, + 'Food & Drink': fetchImageFromDB( + 'red-apple', + 'red-apple_1f34e_twitter_15.0.3.png' + )!, + 'Travel & Places': fetchImageFromDB( + 'airplane', + 'airplane_2708-fe0f_twitter_15.0.3.png' + )!, + Activities: fetchImageFromDB( + 'soccer-ball', + 'soccer-ball_26bd_twitter_15.0.3.png' + )!, + Objects: fetchImageFromDB( + 'mobile-phone', + 'mobile-phone_1f4f1_twitter_15.0.3.png' + )!, + Symbols: fetchImageFromDB( + 'check-mark-button', + 'check-mark-button_2705_twitter_15.0.3.png' + )!, + Flags: fetchImageFromDB( + 'chequered-flag', + 'chequered-flag_1f3c1_twitter_15.0.3.png' + )!, + Other: fetchImageFromDB( + 'question-mark', + 'question-mark_2753_twitter_15.0.3.png' + )!, }; --- - = { partOf="Free DevTools" partOfUrl="https://hexmos.com/freedevtools/" keywords={mainKeywords} - features={["Discord-style designs", "Browse by category", "Copy instantly", "Compare with Unicode", "Free access"]} + features={[ + 'Discord-style designs', + 'Browse by category', + 'Copy instantly', + 'Compare with Unicode', + 'Free access', + ]} emojiCategory="Discord Emojis" >
@@ -111,16 +157,23 @@ export const categoryIconMap: Record = {

@@ -128,51 +181,62 @@ export const categoryIconMap: Record = {

- Discord's emoji style is clean, bold, and designed for chat. - These emojis look sharp and expressive, which makes them perfect for fast-paced conversations. - Explore the full Discord emoji collection here, sorted by category with quick copy options. + Discord's emoji style is clean, bold, and designed for chat. These + emojis look sharp and expressive, which makes them perfect for + fast-paced conversations. Explore the full Discord emoji collection + here, sorted by category with quick copy options.

-
- {paginatedCategories.map((category) => ( -
-
-
- {category +
+ { + paginatedCategories.map((category) => ( +
+ - - {category} - +

+ {emojisByCategory[category].length} emojis available +

-

- {emojisByCategory[category].length} emojis available -

-
- ))} + )) + }
-
+
diff --git a/frontend/src/pages/emojis/discord-emojis/sitemap.xml.ts b/frontend/src/pages/emojis/discord-emojis/sitemap.xml.ts index 189077faa5..d1536bd381 100644 --- a/frontend/src/pages/emojis/discord-emojis/sitemap.xml.ts +++ b/frontend/src/pages/emojis/discord-emojis/sitemap.xml.ts @@ -1,5 +1,5 @@ -import { getAllDiscordEmojis } from "@/lib/emojis"; import type { APIRoute } from "astro"; +import { getAllDiscordEmojis } from "db/emojis/emojis-utils"; export const GET: APIRoute = async ({ site }) => { const now = new Date().toISOString(); diff --git a/frontend/src/pages/emojis/index.astro b/frontend/src/pages/emojis/index.astro index 3727383cc5..4d988db4b9 100644 --- a/frontend/src/pages/emojis/index.astro +++ b/frontend/src/pages/emojis/index.astro @@ -1,6 +1,10 @@ --- +import { + getAllEmojis, + getEmojiCategories, + type EmojiData, +} from 'db/emojis/emojis-utils'; import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getAllEmojis, getEmojiCategories } from '../../lib/emojis'; import EmojiPage from './_EmojiPage.astro'; const emojis = await getAllEmojis(); @@ -35,27 +39,29 @@ const totalPages = Math.ceil(sortedCategories.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const paginatedCategories = sortedCategories.slice(startIndex, endIndex); -const totalCategories = sortedCategories.length - +const totalCategories = sortedCategories.length; // Breadcrumb items const breadcrumbItems = [ { label: 'Free DevTools', href: '/freedevtools/' }, - { label: 'Emojis' } + { label: 'Emojis' }, ]; // SEO data -const seoTitle = currentPage === 1 - ? "Emoji Reference - Browse & Copy Emojis | Online Free DevTools by Hexmos" - : `Emoji Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; +const seoTitle = + currentPage === 1 + ? 'Emoji Reference - Browse & Copy Emojis | Online Free DevTools by Hexmos' + : `Emoji Reference - Page ${currentPage} | Online Free DevTools by Hexmos`; -const seoDescription = currentPage === 1 - ? "Explore the emoji reference by category. Find meanings, names, and shortcodes. Browse thousands of emojis and copy instantly. Free, fast, no signup." - : `Browse page ${currentPage} of our emoji reference. Find meanings, names, and shortcodes. Copy emojis instantly.`; +const seoDescription = + currentPage === 1 + ? 'Explore the emoji reference by category. Find meanings, names, and shortcodes. Browse thousands of emojis and copy instantly. Free, fast, no signup.' + : `Browse page ${currentPage} of our emoji reference. Find meanings, names, and shortcodes. Copy emojis instantly.`; -const canonical = currentPage === 1 - ? "https://hexmos.com/freedevtools/emojis/" - : `https://hexmos.com/freedevtools/emojis/${currentPage}/`; +const canonical = + currentPage === 1 + ? 'https://hexmos.com/freedevtools/emojis/' + : `https://hexmos.com/freedevtools/emojis/${currentPage}/`; // Enhanced keywords for main page const mainKeywords = [ @@ -68,18 +74,18 @@ const mainKeywords = [ 'free emojis', 'emoji search', 'emoji copy', - 'unicode emojis' + 'unicode emojis', ]; --- - - Date: Sat, 22 Nov 2025 18:56:14 +0530 Subject: [PATCH 42/79] fix: emoji discord and apple --- frontend/db/emojis/emojis-utils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/db/emojis/emojis-utils.ts b/frontend/db/emojis/emojis-utils.ts index 8dfda4b155..5620c7a4fb 100644 --- a/frontend/db/emojis/emojis-utils.ts +++ b/frontend/db/emojis/emojis-utils.ts @@ -1,4 +1,4 @@ -import { default as apple_vendor_excluded_emojis, default as discord_vendor_excluded_emojis } from '@/lib/emojis-consts'; +import { apple_vendor_excluded_emojis, discord_vendor_excluded_emojis } from '@/lib/emojis-consts'; import Database from 'better-sqlite3'; import path from 'path'; @@ -176,11 +176,12 @@ export function getEmojisByCategory(category: string, vendor?: string): EmojiDat return rows .filter(r => { + if (!r.slug) return false; if (vendor === "discord") { - return !discord_vendor_excluded_emojis.includes(r.slug); + return !discord_vendor_excluded_emojis?.includes(r.slug); } if (vendor === "apple") { - return !apple_vendor_excluded_emojis.includes(r.slug); + return !apple_vendor_excluded_emojis?.includes(r.slug); } return true; }) From cc99952abc5abba876a98d638ab83341b7d56579 Mon Sep 17 00:00:00 2001 From: lovestaco Date: Sat, 22 Nov 2025 19:05:10 +0530 Subject: [PATCH 43/79] fix: emoji sitemap --- frontend/db/emojis/emojis-utils.ts | 1 + frontend/src/pages/emojis/apple-emojis/sitemap.xml.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/db/emojis/emojis-utils.ts b/frontend/db/emojis/emojis-utils.ts index 5620c7a4fb..51b4f96b56 100644 --- a/frontend/db/emojis/emojis-utils.ts +++ b/frontend/db/emojis/emojis-utils.ts @@ -63,6 +63,7 @@ export function getDb(): Database.Database { dbInstance = new Database(dbPath, { readonly: true }); dbInstance.pragma('journal_mode = OFF'); dbInstance.pragma('synchronous = OFF'); + dbInstance.pragma('mmap_size = 1073741824'); return dbInstance; } diff --git a/frontend/src/pages/emojis/apple-emojis/sitemap.xml.ts b/frontend/src/pages/emojis/apple-emojis/sitemap.xml.ts index 192c703b89..4732bd3dde 100644 --- a/frontend/src/pages/emojis/apple-emojis/sitemap.xml.ts +++ b/frontend/src/pages/emojis/apple-emojis/sitemap.xml.ts @@ -1,4 +1,4 @@ -import { getAllAppleEmojis } from "@/lib/emojis"; +import { getAllAppleEmojis } from "db/emojis/emojis-utils"; import type { APIRoute } from "astro"; export const GET: APIRoute = async ({ site }) => { From 80264cccc70a1ae4b56c4e692823a508c9a1428a Mon Sep 17 00:00:00 2001 From: lovestaco Date: Sat, 22 Nov 2025 19:15:23 +0530 Subject: [PATCH 44/79] fix: emoji sitemap --- frontend/src/pages/emojis/sitemap.xml.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/emojis/sitemap.xml.ts b/frontend/src/pages/emojis/sitemap.xml.ts index 609c44284b..fa52312790 100644 --- a/frontend/src/pages/emojis/sitemap.xml.ts +++ b/frontend/src/pages/emojis/sitemap.xml.ts @@ -2,7 +2,7 @@ import type { APIRoute } from "astro"; export const GET: APIRoute = async ({ site }) => { const now = new Date().toISOString(); - const { getAllEmojis } = await import("@/lib/emojis"); + const { getAllEmojis } = await import("db/emojis/emojis-utils"); const emojis = getAllEmojis(); // Predefined allowed categories From e4842d08247dbb9b25534d89e5342beafd2c0ab1 Mon Sep 17 00:00:00 2001 From: lovestaco Date: Sat, 22 Nov 2025 20:04:27 +0530 Subject: [PATCH 45/79] fix: optimize emoji query --- frontend/db/emojis/emojis-utils.ts | 548 ++++++++++++++++++ frontend/src/components/banner/Banner.astro | 1 - frontend/src/pages/emojis/_EmojiPage.astro | 16 +- .../emojis/apple-emojis/[category].astro | 10 +- .../src/pages/emojis/apple-emojis/index.astro | 84 ++- .../emojis/discord-emojis/[category].astro | 12 +- .../pages/emojis/discord-emojis/index.astro | 84 ++- frontend/src/pages/emojis/index.astro | 42 +- 8 files changed, 713 insertions(+), 84 deletions(-) diff --git a/frontend/db/emojis/emojis-utils.ts b/frontend/db/emojis/emojis-utils.ts index 51b4f96b56..7c480dbae5 100644 --- a/frontend/db/emojis/emojis-utils.ts +++ b/frontend/db/emojis/emojis-utils.ts @@ -152,6 +152,13 @@ export function getEmojiBySlug(slug: string): EmojiData | null { }; } +// === Get total emoji count === +export function getTotalEmojis(): number { + const db = getDb(); + const row = db.prepare('SELECT COUNT(*) as count FROM emojis').get() as { count: number } | undefined; + return row?.count ?? 0; +} + // === Fetch categories === export function getEmojiCategories(): string[] { const db = getDb(); @@ -167,6 +174,297 @@ export function getEmojiCategories(): string[] { return Array.from(new Set(normalized)).sort() as string[]; } +// === Optimized: Get categories with preview emojis in a single query === +export interface CategoryWithPreviewEmojis { + category: string; + count: number; + previewEmojis: Array<{ + code: string; + slug: string; + title: string; + }>; +} + +export function getCategoriesWithPreviewEmojis( + previewEmojisPerCategory: number = 5 +): CategoryWithPreviewEmojis[] { + const db = getDb(); + const validCategories = Object.keys(categoryIconMap); + const validCategoriesPlaceholders = validCategories.map(() => '?').join(','); + + // Build the query with proper parameter binding + const query = ` + WITH normalized_emojis AS ( + SELECT + CASE + WHEN category IN (${validCategoriesPlaceholders}) THEN category + ELSE 'Other' + END as normalized_category, + code, + slug, + title + FROM emojis + WHERE category IS NOT NULL + ), + normalized_categories AS ( + SELECT DISTINCT normalized_category + FROM normalized_emojis + ), + category_counts AS ( + SELECT + normalized_category as category, + COUNT(*) as count + FROM normalized_emojis + GROUP BY normalized_category + ), + category_emojis AS ( + SELECT + nc.normalized_category as category, + cc.count, + ( + SELECT json_group_array( + json_object( + 'code', e.code, + 'slug', e.slug, + 'title', e.title + ) + ) + FROM ( + SELECT code, slug, title + FROM normalized_emojis + WHERE normalized_category = nc.normalized_category + ORDER BY + CASE WHEN slug LIKE '%-skin-tone%' OR slug LIKE '%skin-tone%' THEN 1 ELSE 0 END, + COALESCE(title, slug) COLLATE NOCASE + LIMIT ? + ) e + ) as preview_emojis + FROM normalized_categories nc + JOIN category_counts cc ON nc.normalized_category = cc.category + ORDER BY nc.normalized_category + ) + SELECT category, count, preview_emojis + FROM category_emojis + WHERE category != 'Other' + ORDER BY category + `; + + const stmt = db.prepare(query); + const results = stmt.all(...validCategories, previewEmojisPerCategory) as Array<{ + category: string; + count: number; + preview_emojis: string; + }>; + + return results.map((row) => { + let previewEmojis: Array<{ code: string; slug: string; title: string }> = []; + try { + const parsed = JSON.parse(row.preview_emojis || '[]'); + previewEmojis = Array.isArray(parsed) ? parsed.filter((emoji: any) => emoji !== null) : []; + } catch (e) { + previewEmojis = []; + } + + return { + category: row.category, + count: row.count, + previewEmojis, + }; + }); +} + +// === Optimized: Get categories with preview emojis for Apple vendor === +export function getAppleCategoriesWithPreviewEmojis( + previewEmojisPerCategory: number = 5 +): CategoryWithPreviewEmojis[] { + const db = getDb(); + const validCategories = Object.keys(categoryIconMap); + const validCategoriesPlaceholders = validCategories.map(() => '?').join(','); + const excludedSlugs = apple_vendor_excluded_emojis || []; + const excludedPlaceholders = excludedSlugs.map(() => '?').join(','); + + // Build the query with vendor exclusion filtering + const query = ` + WITH normalized_emojis AS ( + SELECT + CASE + WHEN category IN (${validCategoriesPlaceholders}) THEN category + ELSE 'Other' + END as normalized_category, + code, + slug, + title + FROM emojis + WHERE category IS NOT NULL + ${excludedSlugs.length > 0 ? `AND slug NOT IN (${excludedPlaceholders})` : ''} + ), + normalized_categories AS ( + SELECT DISTINCT normalized_category + FROM normalized_emojis + ), + category_counts AS ( + SELECT + normalized_category as category, + COUNT(*) as count + FROM normalized_emojis + GROUP BY normalized_category + ), + category_emojis AS ( + SELECT + nc.normalized_category as category, + cc.count, + ( + SELECT json_group_array( + json_object( + 'code', e.code, + 'slug', e.slug, + 'title', e.title + ) + ) + FROM ( + SELECT code, slug, title + FROM normalized_emojis + WHERE normalized_category = nc.normalized_category + ORDER BY + CASE WHEN slug LIKE '%-skin-tone%' OR slug LIKE '%skin-tone%' THEN 1 ELSE 0 END, + COALESCE(title, slug) COLLATE NOCASE + LIMIT ? + ) e + ) as preview_emojis + FROM normalized_categories nc + JOIN category_counts cc ON nc.normalized_category = cc.category + ORDER BY nc.normalized_category + ) + SELECT category, count, preview_emojis + FROM category_emojis + WHERE category != 'Other' + ORDER BY category + `; + + const params = excludedSlugs.length > 0 + ? [...validCategories, ...excludedSlugs, previewEmojisPerCategory] + : [...validCategories, previewEmojisPerCategory]; + + const stmt = db.prepare(query); + const results = stmt.all(...params) as Array<{ + category: string; + count: number; + preview_emojis: string; + }>; + + return results.map((row) => { + let previewEmojis: Array<{ code: string; slug: string; title: string }> = []; + try { + const parsed = JSON.parse(row.preview_emojis || '[]'); + previewEmojis = Array.isArray(parsed) ? parsed.filter((emoji: any) => emoji !== null) : []; + } catch (e) { + previewEmojis = []; + } + + return { + category: row.category, + count: row.count, + previewEmojis, + }; + }); +} + +// === Optimized: Get categories with preview emojis for Discord vendor === +export function getDiscordCategoriesWithPreviewEmojis( + previewEmojisPerCategory: number = 5 +): CategoryWithPreviewEmojis[] { + const db = getDb(); + const validCategories = Object.keys(categoryIconMap); + const validCategoriesPlaceholders = validCategories.map(() => '?').join(','); + const excludedSlugs = discord_vendor_excluded_emojis || []; + const excludedPlaceholders = excludedSlugs.map(() => '?').join(','); + + // Build the query with vendor exclusion filtering + const query = ` + WITH normalized_emojis AS ( + SELECT + CASE + WHEN category IN (${validCategoriesPlaceholders}) THEN category + ELSE 'Other' + END as normalized_category, + code, + slug, + title + FROM emojis + WHERE category IS NOT NULL + ${excludedSlugs.length > 0 ? `AND slug NOT IN (${excludedPlaceholders})` : ''} + ), + normalized_categories AS ( + SELECT DISTINCT normalized_category + FROM normalized_emojis + ), + category_counts AS ( + SELECT + normalized_category as category, + COUNT(*) as count + FROM normalized_emojis + GROUP BY normalized_category + ), + category_emojis AS ( + SELECT + nc.normalized_category as category, + cc.count, + ( + SELECT json_group_array( + json_object( + 'code', e.code, + 'slug', e.slug, + 'title', e.title + ) + ) + FROM ( + SELECT code, slug, title + FROM normalized_emojis + WHERE normalized_category = nc.normalized_category + ORDER BY + CASE WHEN slug LIKE '%-skin-tone%' OR slug LIKE '%skin-tone%' THEN 1 ELSE 0 END, + COALESCE(title, slug) COLLATE NOCASE + LIMIT ? + ) e + ) as preview_emojis + FROM normalized_categories nc + JOIN category_counts cc ON nc.normalized_category = cc.category + ORDER BY nc.normalized_category + ) + SELECT category, count, preview_emojis + FROM category_emojis + WHERE category != 'Other' + ORDER BY category + `; + + const params = excludedSlugs.length > 0 + ? [...validCategories, ...excludedSlugs, previewEmojisPerCategory] + : [...validCategories, previewEmojisPerCategory]; + + const stmt = db.prepare(query); + const results = stmt.all(...params) as Array<{ + category: string; + count: number; + preview_emojis: string; + }>; + + return results.map((row) => { + let previewEmojis: Array<{ code: string; slug: string; title: string }> = []; + try { + const parsed = JSON.parse(row.preview_emojis || '[]'); + previewEmojis = Array.isArray(parsed) ? parsed.filter((emoji: any) => emoji !== null) : []; + } catch (e) { + previewEmojis = []; + } + + return { + category: row.category, + count: row.count, + previewEmojis, + }; + }); +} + // === Fetch by category === export function getEmojisByCategory(category: string, vendor?: string): EmojiData[] { const db = getDb(); @@ -202,6 +500,256 @@ export function getEmojisByCategory(category: string, vendor?: string): EmojiDat })); } +// === Optimized: Get emojis by category with latest Discord images in a single query === +export interface EmojiWithDiscordImage extends EmojiData { + latestDiscordImage: string | null; +} + +export function getEmojisByCategoryWithDiscordImages(category: string): EmojiWithDiscordImage[] { + const db = getDb(); + const excludedSlugs = discord_vendor_excluded_emojis || []; + const excludedPlaceholders = excludedSlugs.length > 0 ? excludedSlugs.map(() => '?').join(',') : ''; + + // Get all emojis for the category + const emojisQuery = ` + SELECT + code, + slug, + title, + description, + category, + apple_vendor_description, + unicode, + keywords, + also_known_as, + version, + senses, + shortcodes + FROM emojis + WHERE lower(category) = lower(?) + AND slug IS NOT NULL + ${excludedSlugs.length > 0 ? `AND slug NOT IN (${excludedPlaceholders})` : ''} + `; + + const emojisParams = excludedSlugs.length > 0 ? [category, ...excludedSlugs] : [category]; + const emojiRows = db.prepare(emojisQuery).all(...emojisParams) as Array<{ + code: string; + slug: string; + title: string; + description: string; + category: string; + apple_vendor_description: string; + unicode: string; + keywords: string; + also_known_as: string; + version: string; + senses: string; + shortcodes: string; + }>; + + if (emojiRows.length === 0) return []; + + // Get all images for these emojis in a single query + const slugs = emojiRows.map(e => e.slug); + const imagePlaceholders = slugs.map(() => '?').join(','); + const imagesQuery = ` + SELECT emoji_slug, filename, image_data + FROM images + WHERE emoji_slug IN (${imagePlaceholders}) + AND image_type = 'twemoji-vendor' + ORDER BY emoji_slug, filename COLLATE NOCASE + `; + + const imageRows = db.prepare(imagesQuery).all(...slugs) as Array<{ + emoji_slug: string; + filename: string; + image_data: Buffer; + }>; + + // Group images by emoji slug and find latest for each + const imagesBySlug = new Map>(); + for (const img of imageRows) { + if (!imagesBySlug.has(img.emoji_slug)) { + imagesBySlug.set(img.emoji_slug, []); + } + imagesBySlug.get(img.emoji_slug)!.push({ + filename: img.filename, + image_data: img.image_data, + }); + } + + const parseVersion = (name: string): number => { + const match = name.match(/[_-]([\d.]+)\.(png|jpg|jpeg|webp|svg)$/i); + return match ? parseFloat(match[1]) : 0; + }; + + // Process emojis and attach latest images + return emojiRows.map((row) => { + const images = imagesBySlug.get(row.slug) || []; + let latestDiscordImage: string | null = null; + + if (images.length > 0) { + // Find image with highest version + const latest = images.reduce((best, img) => { + return parseVersion(img.filename) > parseVersion(best.filename) ? img : best; + }, images[0]); + + // Convert to base64 + const buffer = Buffer.from(latest.image_data); + const head = buffer.toString('ascii', 0, 20); + + let mime = 'application/octet-stream'; + if (head.includes('(row.unicode) || [], + keywords: parseJSON(row.keywords) || [], + alsoKnownAs: parseJSON(row.also_known_as) || [], + version: parseJSON(row.version) as EmojiData['version'], + senses: parseJSON(row.senses) as EmojiData['senses'], + shortcodes: parseJSON(row.shortcodes) as EmojiData['shortcodes'], + latestDiscordImage, + }; + }).filter(e => e.latestDiscordImage !== null); // Only return emojis with images +} + +// === Optimized: Get emojis by category with latest Apple images in a single query === +export interface EmojiWithAppleImage extends EmojiData { + latestAppleImage: string | null; +} + +export function getEmojisByCategoryWithAppleImages(category: string): EmojiWithAppleImage[] { + const db = getDb(); + const excludedSlugs = apple_vendor_excluded_emojis || []; + const excludedPlaceholders = excludedSlugs.length > 0 ? excludedSlugs.map(() => '?').join(',') : ''; + + // Get all emojis for the category + const emojisQuery = ` + SELECT + code, + slug, + title, + description, + category, + apple_vendor_description, + unicode, + keywords, + also_known_as, + version, + senses, + shortcodes + FROM emojis + WHERE lower(category) = lower(?) + AND slug IS NOT NULL + ${excludedSlugs.length > 0 ? `AND slug NOT IN (${excludedPlaceholders})` : ''} + `; + + const emojisParams = excludedSlugs.length > 0 ? [category, ...excludedSlugs] : [category]; + const emojiRows = db.prepare(emojisQuery).all(...emojisParams) as Array<{ + code: string; + slug: string; + title: string; + description: string; + category: string; + apple_vendor_description: string; + unicode: string; + keywords: string; + also_known_as: string; + version: string; + senses: string; + shortcodes: string; + }>; + + if (emojiRows.length === 0) return []; + + // Get all images for these emojis in a single query (Apple images have iOS in filename) + const slugs = emojiRows.map(e => e.slug); + const imagePlaceholders = slugs.map(() => '?').join(','); + const imagesQuery = ` + SELECT emoji_slug, filename, image_data + FROM images + WHERE emoji_slug IN (${imagePlaceholders}) + AND filename LIKE '%iOS%' + ORDER BY emoji_slug, filename COLLATE NOCASE + `; + + const imageRows = db.prepare(imagesQuery).all(...slugs) as Array<{ + emoji_slug: string; + filename: string; + image_data: Buffer; + }>; + + // Group images by emoji slug and find latest for each + const imagesBySlug = new Map>(); + for (const img of imageRows) { + if (!imagesBySlug.has(img.emoji_slug)) { + imagesBySlug.set(img.emoji_slug, []); + } + imagesBySlug.get(img.emoji_slug)!.push({ + filename: img.filename, + image_data: img.image_data, + }); + } + + const parseIOSVersion = (name: string): number => { + const match = name.match(/iOS[_\s]?([\d.]+)/i); + return match ? parseFloat(match[1]) : 0; + }; + + // Process emojis and attach latest images + return emojiRows.map((row) => { + const images = imagesBySlug.get(row.slug) || []; + let latestAppleImage: string | null = null; + + if (images.length > 0) { + // Find image with highest iOS version + const latest = images.reduce((best, img) => { + return parseIOSVersion(img.filename) > parseIOSVersion(best.filename) ? img : best; + }, images[0]); + + // Convert to base64 + const buffer = Buffer.from(latest.image_data); + const head = buffer.toString('ascii', 0, 20); + + let mime = 'application/octet-stream'; + if (head.includes('(row.unicode) || [], + keywords: parseJSON(row.keywords) || [], + alsoKnownAs: parseJSON(row.also_known_as) || [], + version: parseJSON(row.version) as EmojiData['version'], + senses: parseJSON(row.senses) as EmojiData['senses'], + shortcodes: parseJSON(row.shortcodes) as EmojiData['shortcodes'], + latestAppleImage, + }; + }); +} + export function getEmojiImages(slug) { const db = getDb(); diff --git a/frontend/src/components/banner/Banner.astro b/frontend/src/components/banner/Banner.astro index 7bad7016b7..a64ffebe13 100644 --- a/frontend/src/components/banner/Banner.astro +++ b/frontend/src/components/banner/Banner.astro @@ -50,7 +50,6 @@ const adsenseHtmlMobile = `