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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions .github/workflows/seo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -69,6 +70,6 @@ export default defineConfig({
return item;
},
}),
react()
react(),
],
});
});
4 changes: 2 additions & 2 deletions website/src/components/features/blog/BlogList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export const BlogList: React.FC<Props> = ({
</p>
</a>
{featuredPost.data.tags.length > 0 && (
<div className="flex flex-wrap gap-2 max-h-tags-featured overflow-hidden">
<div className="max-h-tags-featured flex flex-wrap gap-2 overflow-hidden">
{featuredPost.data.tags.slice(0, 8).map((tag) => (
<Tag key={tag}>
<a
Expand Down Expand Up @@ -216,7 +216,7 @@ export const BlogList: React.FC<Props> = ({
<p className="text-gray-300">{post.data.description}</p>
</a>
{post.data.tags.length > 0 && (
<div className="mt-auto flex flex-wrap gap-2 overflow-hidden max-h-tags">
<div className="max-h-tags mt-auto flex flex-wrap gap-2 overflow-hidden">
{post.data.tags.slice(0, 4).map((tag) => (
<Tag key={tag}>
<a
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/features/blog/BlogPost.astro
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const metadata = createMetadata({
});

const articleSchema: WithContext<Article> = {
"@context": "https://schema.org",
"@context": "https://schema.org",
"@type": "Article",
headline: title,
description: description,
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/features/blog/Card.astro
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const formattedDate = pubDate.toLocaleDateString(currentLocale, {
</a>
{
tags.length > 0 && (
<div class="mt-auto flex flex-wrap overflow-hidden max-h-tags gap-2">
<div class="max-h-tags mt-auto flex flex-wrap gap-2 overflow-hidden">
{tags.slice(0, 4).map((tag) => (
<Tag>
<a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ const t = await createTranslator(locale);
const closeIcon = document.querySelector(".close-icon");
const mobileNavLinks = document.getElementById("mobile-nav-links");
const links = mobileNavLinks?.querySelectorAll("a");
const groupToggles =
mobileNavLinks?.querySelectorAll<HTMLButtonElement>(".mobile-group-toggle");
const groupToggles = mobileNavLinks?.querySelectorAll<HTMLButtonElement>(
".mobile-group-toggle",
);

const updateLinkStatus = (link: HTMLAnchorElement, isActive: boolean) => {
if (isActive) {
Expand Down
33 changes: 18 additions & 15 deletions website/src/components/ui/react/Alert.tsx
Original file line number Diff line number Diff line change
@@ -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<
Expand All @@ -30,32 +30,35 @@ const Alert = React.forwardRef<
{...props}
/>
));
Alert.displayName = 'Alert';
Alert.displayName = "Alert";

const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, children, ...props }, ref) => (
<h5
ref={ref}
className={applyStyles('mb-1 font-medium leading-none tracking-tight', className)}
className={applyStyles(
"mb-1 leading-none font-medium tracking-tight",
className,
)}
{...props}
>
{children}
</h5>
));
AlertTitle.displayName = 'AlertTitle';
AlertTitle.displayName = "AlertTitle";

const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={applyStyles('text-sm [&_p]:leading-relaxed', className)}
className={applyStyles("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
));
AlertDescription.displayName = 'AlertDescription';
AlertDescription.displayName = "AlertDescription";

export { Alert, AlertDescription, AlertTitle };
49 changes: 25 additions & 24 deletions website/src/components/ui/react/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,58 @@
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<HTMLButtonElement>,
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
const Comp = asChild ? Slot : "button";
return (
<Comp
className={applyStyles(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
},
);
Button.displayName = 'Button';
Button.displayName = "Button";

export { Button, buttonVariants };
Loading
Loading