diff --git a/.github/workflows/seo.yml b/.github/workflows/seo.yml index 6cd40c4..e9f7227 100644 --- a/.github/workflows/seo.yml +++ b/.github/workflows/seo.yml @@ -60,35 +60,39 @@ jobs: npx --yes linkinator http://localhost:3000 --recurse --skip "^(?!http://localhost:3000)" # 4. DEEP AUDIT: Unlighthouse - # Scans all pages and fails if SEO score is below 100 (configured in unlighthouse.config.ts) + # Scans all pages for both desktop and mobile devices + # Fails if SEO score is below 100 (configured in unlighthouse.config.ts) # The '--build-static' flag generates the HTML report files. # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD tells Puppeteer to use the system Chromium from setup-chromium # CHROMIUM_PATH is automatically set by the setup-chromium action - - name: Run Unlighthouse Site-Wide Audit + - name: Run Unlighthouse Mobile Audit working-directory: website env: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true" PUPPETEER_EXECUTABLE_PATH: ${{ env.CHROMIUM_PATH }} run: | - npx --yes @unlighthouse/cli@latest --build-static + npx --yes @unlighthouse/cli@latest --build-static --mobile --output-path ./.unlighthouse/mobile + + - name: Run Unlighthouse Desktop Audit + working-directory: website + env: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true" + PUPPETEER_EXECUTABLE_PATH: ${{ env.CHROMIUM_PATH }} + run: | + npx --yes @unlighthouse/cli@latest --build-static --desktop --output-path ./.unlighthouse/desktop # 5. UPLOAD ARTIFACT # This takes the generated report and saves it as a zip file. # Note: Unlighthouse runs in website/ directory, so output is at website/.unlighthouse - - name: Debug - List Unlighthouse output - if: always() - run: | - echo "Checking for Unlighthouse output..." - ls -la website/.unlighthouse 2>/dev/null || echo "website/.unlighthouse not found" - ls -la website/.unlighthouse/client 2>/dev/null || echo "website/.unlighthouse/client not found" - find website -name ".unlighthouse" -type d 2>/dev/null || echo "No .unlighthouse directories found" - + # Includes both mobile and desktop reports - name: Create SEO Report Zip if: always() run: | cd website zip -r seo-report.zip .unlighthouse/ || echo "Failed to create zip, but continuing..." ls -lh seo-report.zip || echo "Zip file not created" + echo "Report includes:" + ls -la .unlighthouse/ 2>/dev/null || echo "No .unlighthouse directory" - name: Upload SEO Report uses: actions/upload-artifact@v4 diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 9dbb0e5..01bafe7 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -49,7 +49,8 @@ export default defineConfig({ serialize: (item) => { // Read environment variable at runtime (during build) // The serialize function runs during sitemap generation, so process.env should be available - const envBaseUrl = typeof process !== 'undefined' && process.env?.SITE_BASE_URL; + const envBaseUrl = + typeof process !== "undefined" && process.env?.SITE_BASE_URL; const baseUrl = envBaseUrl || SITE_BASE_URL; // Replace production URLs with the correct base URL if env var is set @@ -69,6 +70,6 @@ export default defineConfig({ return item; }, }), - react() + react(), ], -}); \ No newline at end of file +}); diff --git a/website/src/components/features/blog/BlogList.tsx b/website/src/components/features/blog/BlogList.tsx index 4d28b8a..13a22dd 100644 --- a/website/src/components/features/blog/BlogList.tsx +++ b/website/src/components/features/blog/BlogList.tsx @@ -159,7 +159,7 @@ export const BlogList: React.FC = ({

{featuredPost.data.tags.length > 0 && ( -
+
{featuredPost.data.tags.slice(0, 8).map((tag) => ( = ({

{post.data.description}

{post.data.tags.length > 0 && ( -
+
{post.data.tags.slice(0, 4).map((tag) => ( = { - "@context": "https://schema.org", + "@context": "https://schema.org", "@type": "Article", headline: title, description: description, diff --git a/website/src/components/features/blog/Card.astro b/website/src/components/features/blog/Card.astro index db81a10..7d7a331 100644 --- a/website/src/components/features/blog/Card.astro +++ b/website/src/components/features/blog/Card.astro @@ -74,7 +74,7 @@ const formattedDate = pubDate.toLocaleDateString(currentLocale, { { tags.length > 0 && ( -
+
{tags.slice(0, 4).map((tag) => ( (".mobile-group-toggle"); + const groupToggles = mobileNavLinks?.querySelectorAll( + ".mobile-group-toggle", + ); const updateLinkStatus = (link: HTMLAnchorElement, isActive: boolean) => { if (isActive) { diff --git a/website/src/components/ui/react/Alert.tsx b/website/src/components/ui/react/Alert.tsx index a697ec8..2b29b0f 100644 --- a/website/src/components/ui/react/Alert.tsx +++ b/website/src/components/ui/react/Alert.tsx @@ -1,22 +1,22 @@ -import { cva, type VariantProps } from 'class-variance-authority'; -import * as React from 'react'; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; -import { applyStyles } from '@/utils/apply-styles'; +import { applyStyles } from "@/utils/apply-styles"; const alertVariants = cva( - 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7', + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", { variants: { variant: { - default: 'bg-background text-foreground', + default: "bg-background text-foreground", destructive: - 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive' - } + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, }, defaultVariants: { - variant: 'default' - } - } + variant: "default", + }, + }, ); const Alert = React.forwardRef< @@ -30,7 +30,7 @@ const Alert = React.forwardRef< {...props} /> )); -Alert.displayName = 'Alert'; +Alert.displayName = "Alert"; const AlertTitle = React.forwardRef< HTMLParagraphElement, @@ -38,13 +38,16 @@ const AlertTitle = React.forwardRef< >(({ className, children, ...props }, ref) => (
{children}
)); -AlertTitle.displayName = 'AlertTitle'; +AlertTitle.displayName = "AlertTitle"; const AlertDescription = React.forwardRef< HTMLParagraphElement, @@ -52,10 +55,10 @@ const AlertDescription = React.forwardRef< >(({ className, ...props }, ref) => (
)); -AlertDescription.displayName = 'AlertDescription'; +AlertDescription.displayName = "AlertDescription"; export { Alert, AlertDescription, AlertTitle }; diff --git a/website/src/components/ui/react/Button.tsx b/website/src/components/ui/react/Button.tsx index 9d37d5e..08851bf 100644 --- a/website/src/components/ui/react/Button.tsx +++ b/website/src/components/ui/react/Button.tsx @@ -1,48 +1,49 @@ -import { Slot } from '@radix-ui/react-slot'; -import { cva, type VariantProps } from 'class-variance-authority'; -import * as React from 'react'; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; -import { applyStyles } from '@/utils/apply-styles'; +import { applyStyles } from "@/utils/apply-styles"; const buttonVariants = cva( - 'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 aria-[disabled=true]:pointer-events-none aria-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 aria-[disabled=true]:pointer-events-none aria-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { default: - 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + "bg-primary text-primary-foreground shadow hover:bg-primary/90", destructive: - 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: - 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: - 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline' + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", }, size: { - default: 'h-9 px-4 py-2', - sm: 'h-8 rounded-md px-3 text-xs', - lg: 'h-10 rounded-md px-8', - icon: 'size-9' - } + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "size-9", + }, }, defaultVariants: { - variant: 'default', - size: 'default' - } - } + variant: "default", + size: "default", + }, + }, ); export interface ButtonProps - extends React.ButtonHTMLAttributes, + extends + React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; + const Comp = asChild ? Slot : "button"; return ( ( {...props} /> ); - } + }, ); -Button.displayName = 'Button'; +Button.displayName = "Button"; export { Button, buttonVariants }; diff --git a/website/src/components/ui/react/Pagination.tsx b/website/src/components/ui/react/Pagination.tsx index d37c79b..d93a133 100644 --- a/website/src/components/ui/react/Pagination.tsx +++ b/website/src/components/ui/react/Pagination.tsx @@ -1,67 +1,67 @@ -import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'; -import * as React from 'react'; +import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"; +import * as React from "react"; -import type { ButtonProps } from '@/components/ui/react/Button'; -import { buttonVariants } from '@/components/ui/react/Button'; -import { applyStyles } from '@/utils/apply-styles'; +import type { ButtonProps } from "@/components/ui/react/Button"; +import { buttonVariants } from "@/components/ui/react/Button"; +import { applyStyles } from "@/utils/apply-styles"; -const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => ( +const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (