--- import type { CollectionEntry } from 'astro:content'; import type { Heading } from '@/types'; import { siteConfig } from '@/config'; import { generateProjectSEO } from '@/utils/seo'; import { optimizePostImagePath, optimizeContentImagePath } from '@/utils/images'; import { processWikilinksInHTML } from '@/utils/internallinks'; import { getCollection } from 'astro:content'; import { shouldShowPost, processPost, generateTOC, getAdjacentPosts, formatDate, formatDateMobile, isValidDate, getPostSlug } from '@/utils/markdown'; import { hasProjectCategories } from '@/utils/categories'; import { normalizeStatus, getStatusDisplayText, hasStatusStyling, getStatusClasses } from '@/utils/status'; import BaseLayout from '@/layouts/BaseLayout.astro'; import TableOfContents from '@/components/TableOfContents.astro'; import LinkedMentions from '@/components/LinkedMentions.astro'; import GiscusComments from '@/components/GiscusComments.astro'; import Lightbox from '@/components/Lightbox.astro'; import Icon from '@/components/Icon.astro'; import { Image } from 'astro:assets'; import { devConfig } from '@/config/dev'; import { getOptimizedFormat } from '@/utils/images'; import ImageWrapper from '@/components/ImageWrapper.astro'; export interface Props { project: CollectionEntry<'projects'>; } const { project } = Astro.props; // Normalize status for consistent handling const status = normalizeStatus(project.data.status); // Smart repository icon detection const getRepositoryIcon = (repositoryUrl: string) => { if (!repositoryUrl) return 'folder-git-2'; const url = repositoryUrl.toLowerCase(); if (url.includes('github.com') || url.includes('github.io')) { return 'github'; } else if (url.includes('gitlab.com')) { return 'gitlab'; } else { return 'folder-git-2'; } }; // Check if we're in development mode const isDev = import.meta.env.DEV; // Generate SEO data const siteUrl = import.meta.env.SITE || siteConfig.site; const projectUrl = `${siteUrl.replace(/\/+$/, '')}/projects/${project.id}`; const seoData = generateProjectSEO(project, projectUrl); // Image handling is now done by ImageWrapper component // Generate structured data for the project const structuredData = { "@context": "https://schema.schema.org", "@type": "CreativeWork", "name": project.data.title, "description": project.data.description, "author": { "@type": "Person", "name": siteConfig.author }, "publisher": { "@type": "Organization", "name": siteConfig.title, "logo": { "@type": "ImageObject", "url": siteConfig.site + "/favicon.ico" } }, "dateCreated": project.data.date.toISOString(), "dateModified": project.data.date.toISOString(), "mainEntityOfPage": { "@type": "WebPage", "@id": Astro.url.href }, "url": (project.data.projectUrl || project.data.demoUrl || project.data.demoURL) || Astro.url.href, "codeRepository": project.data.repositoryUrl }; // Get all posts for wikilink resolution const allPosts = await getCollection('posts'); const visiblePosts = allPosts.filter(p => shouldShowPost(p as any, isDev)).map(p => ({ ...p, slug: getPostSlug(p as any) })); // Get all projects to check if any have categories const allProjectsForCategories = await getCollection('projects'); const visibleProjectsForCategories = allProjectsForCategories.filter(p => shouldShowPost(p as any, isDev)); const projectsHaveCategories = hasProjectCategories(allProjectsForCategories); // Process the project content and get processed data including word count const { Content, headings } = await processPost(project as any); // Generate table of contents if enabled - global setting, then hideTOC frontmatter (default true for projects) const hideTOC = project.data.hideTOC === true; const shouldShowTOC = siteConfig.tableOfContents.enabled && !hideTOC && headings.length > 0; const toc = shouldShowTOC ? await generateTOC(headings) : []; // Get adjacent projects for navigation const { prev: prevProject, next: nextProject } = getAdjacentPosts(visibleProjectsForCategories as any, project.id); ---