diff --git a/public/assets/freelance.png b/public/assets/freelance.png index e9396c7..cfe14f8 100644 Binary files a/public/assets/freelance.png and b/public/assets/freelance.png differ diff --git a/public/assets/hec-ia.jpg b/public/assets/hec-ia.jpg new file mode 100644 index 0000000..610dcd3 Binary files /dev/null and b/public/assets/hec-ia.jpg differ diff --git a/public/assets/hecia-banner.png b/public/assets/hecia-banner.png new file mode 100644 index 0000000..4cbd5c6 Binary files /dev/null and b/public/assets/hecia-banner.png differ diff --git a/public/assets/homelab.png b/public/assets/homelab.png index 5e043dc..0d6d842 100644 Binary files a/public/assets/homelab.png and b/public/assets/homelab.png differ diff --git a/repomix-output.xml b/repomix-output.xml new file mode 100644 index 0000000..cb85e46 --- /dev/null +++ b/repomix-output.xml @@ -0,0 +1,5935 @@ + This section contains a summary of this file. + This file contains a packed representation of the entire repository's contents. + It is designed to be easily consumable by AI systems for analysis, code review, + or other automated processes. + + + + The content is organized as follows: + 1. This summary section + 2. Repository information + 3. Directory structure + 4. Repository files (if enabled) + 5. Multiple file entries, each consisting of: + - File path as an attribute + - Full contents of the file + + + + - This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. + - When processing this file, use the file path to distinguish + between different files in the repository. + - Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + + + + - Some files may have been excluded based on .gitignore rules and Repomix's configuration + - Binary files are not included in this packed representation. Please refer to the Repository + Structure section for a complete list of file paths, including binary files + - Files matching patterns in .gitignore are excluded + - Files matching default ignore patterns are excluded + - Files are sorted by Git change count (files with more changes are at the bottom) + + + + + + .gitea/ + workflows/ + build-image.yml + debug.yml + assets/ + homelab.png + lab.png + logo.png + public/ + assets/ + blog/ + cover-brutalism.jpg + cover-dreamcore.jpg + cover-free-icon-library.jpg + cover-neobrutalism.jpg + cover-portfolio-site.jpg + cover-retro.jpg + cover-retrofuturism.jpg + cover-spacepunk.jpg + experiences/ + company.jpg + home/ + color-picker.svg + gradientshub.jpg + social/ + social-behance.jpg + social-dribbble.jpg + social-email.jpg + social-figma.jpg + social-gitea.png + social-github.jpg + social-gumroad.jpg + social-linkedin.png + social-twitter.jpg + social-xiaohongshu.jpg + stack/ + astro.png + bootstrap.png + cloudflare.png + css.png + github.png + html.png + js.png + netlify.png + nextjs.png + nodejs.png + npm.png + nuxtjs.png + tailwind.png + vercel.png + vscode.png + tools/ + game/ + 01-game-cassette.png + 02-game-cassette.png + 03-game-cassette.png + 04-game-cassette.png + 05-game-cassette.png + logo/ + ricoui.png + tool-gradienthub.svg + tool-inspoweb.png + tool-ricoog.svg + tool-todo.png + tool-uiuxdeck.svg + deck.png + dreamcore.jpg + dreamcore.mp4 + keyboard.png + mac.png + mini-btn.png + retro-computer.png + retro-computer.svg + spin-2.png + spin-bar.png + spin-bar.svg + spin.png + spin.svg + tool-icon-bg.png + works/ + 3d-valentines.jpg + 3d-valentines.mp4 + gradientshub.jpg + gradientshub.mp4 + inspoweb.jpg + luonmodels.jpg + luonmodels.mp4 + ricoog.jpg + ricoog.mp4 + ricoui.jpg + ricoui.mp4 + uiuxdeck.jpg + avatar.png + freelance.png + hec-ia.png + homelab.png + logo copy.png + logo.jpg + logo.png + fonts/ + favicon.png + InstrumentSerif-Italic.woff2 + InstrumentSerif-Regular.woff2 + Inter-Variable.woff2 + og.jpg + robots.txt + src/ + assets/ + js/ + main.js + freelance.png + homelab.png + logo.jpg + logo.png + collections/ + experiences.json + featuredwork.json + menu.json + social.json + works.json + components/ + cards/ + BlogCard.astro + SocialCard.astro + WorkCard.astro + elements/ + AboutExperience.astro + PageHeader.astro + SectionHeader.astro + SeparatorLine.astro + home/ + HeroCard.astro + sections/ + BlogSection.astro + Explore.astro + FeaturedWork.astro + Footer.astro + Header.astro + WorksSection.astro + ui/ + AnimatedText.astro + Button.astro + Logo.astro + Matter.astro + Tools.astro + TopBg.astro + widgets/ + ActionBar.astro + Meta.astro + OptimizedImage.astro + Pagination.astro + Toc.astro + ToTop.astro + TrackGa.astro + config/ + site.js + layouts/ + Layout.astro + Meta.astro + PageLayout.astro + PostLayout.astro + pages/ + blog/ + page/ + [page].astro + [...slug].astro + index.astro + work/ + freelance.astro + hec-ia.astro + homelab.astro + 404.astro + about.astro + index.astro + infrastructure.astro + rss.xml.js + works.astro + styles/ + aos-custom.css + article-enhancements.css + article.css + global.css + types/ + matter.d.ts + content.config.js + env.d.ts + .editorconfig + .env.example + .gitignore + astro.config.mjs + CONTRIBUTING.md + docker-compose.yml + Dockerfile + nginx.conf + package.json + pnpm-workspace.yaml + README-zh.md + README.md + tailwind.config.mjs + tsconfig.json + + + This section contains the contents of the repository's files. + name: Debug Context + on: workflow_dispatch + jobs: + debug: + runs-on: ubuntu-latest + steps: + - name: Print Gitea context + run: | + echo "server_url: ${{ gitea.server_url }}" + echo "api_url: ${{ gitea.api_url }}" + echo "repository: ${{ gitea.repository }}" + + + + + User-agent: * + Allow: / + + + // Add your javascript + here import AOS from 'aos'; window.darkMode = false; const stickyClasses = []; const + unstickyClasses = []; const stickyClassesContainer = [ + "shadow-[0px_1px_4px_0_rgba(25,33,61,0.06)]", "rounded-[20px]", "bg-[#ffffff]/65", + "border-[#ffffff]/65", "dark:border-neutral-600/40", "dark:bg-neutral-900/60", + "backdrop-blur-2xl", "backdrop-brightness-120", ]; const unstickyClassesContainer = [ + "shadow-none", "border-transparent", "rounded-none", "bg-transparent", ]; let headerElement = + null; document.addEventListener("DOMContentLoaded", () => { headerElement = + document.getElementById("header"); if ( localStorage.getItem("dark_mode") && + localStorage.getItem("dark_mode") === "true" ) { window.darkMode = true; showNight(); } else { + showDay(); } stickyHeaderFuncionality(); applyMenuItemClasses(); evaluateHeaderPosition(); + mobileMenuFunctionality(); // 初始化 AOS AOS.init({ duration: 400, easing: 'ease-out-cubic', once: + true, offset: 20, delay: 0, }); }); // window.toggleDarkMode = function(){ // + document.documentElement.classList.toggle('dark'); // + if(document.documentElement.classList.contains('dark')){ // localStorage.setItem('dark_mode', + true); // window.darkMode = true; // } else { // window.darkMode = false; // + localStorage.setItem('dark_mode', false); // } // } window.stickyHeaderFuncionality = () => { + window.addEventListener("scroll", () => { evaluateHeaderPosition(); }); }; + window.evaluateHeaderPosition = () => { if (window.scrollY > 48) { + headerElement.firstElementChild.classList.add(...stickyClassesContainer); + headerElement.firstElementChild.classList.remove( ...unstickyClassesContainer, ); + headerElement.classList.add(...stickyClasses); headerElement.classList.remove(...unstickyClasses); + // document.getElementById("menu").classList.add("top-[75px]"); // + document.getElementById("menu").classList.remove("top-[75px]"); } else { + headerElement.firstElementChild.classList.remove(...stickyClassesContainer); + headerElement.firstElementChild.classList.add(...unstickyClassesContainer); + headerElement.classList.add(...unstickyClasses); headerElement.classList.remove(...stickyClasses); + // document.getElementById("menu").classList.remove("top-[56px]"); // + document.getElementById("menu").classList.add("top-[75px]"); } }; + document.getElementById("darkToggle").addEventListener("click", () => { + document.documentElement.classList.add("duration-300"); if + (document.documentElement.classList.contains("dark")) { localStorage.removeItem("dark_mode"); + showDay(true); } else { localStorage.setItem("dark_mode", true); showNight(true); } }); function + showDay(animate) { document.getElementById("sun").classList.remove("setting"); + document.getElementById("moon").classList.remove("rising"); let timeout = 0; if (animate) { + timeout = 500; document.getElementById("moon").classList.add("setting"); } setTimeout(() => { + document.getElementById("dayText").classList.remove("hidden"); + document.getElementById("nightText").classList.add("hidden"); + document.getElementById("moon").classList.add("hidden"); + document.getElementById("sun").classList.remove("hidden"); if (animate) { + document.documentElement.classList.remove("dark"); + document.getElementById("sun").classList.add("rising"); } }, timeout); } function + showNight(animate) { document.getElementById("moon").classList.remove("setting"); + document.getElementById("sun").classList.remove("rising"); let timeout = 0; if (animate) { timeout + = 500; document.getElementById("sun").classList.add("setting"); } setTimeout(() => { + document.getElementById("nightText").classList.remove("hidden"); + document.getElementById("dayText").classList.add("hidden"); + document.getElementById("sun").classList.add("hidden"); + document.getElementById("moon").classList.remove("hidden"); if (animate) { + document.documentElement.classList.add("dark"); + document.getElementById("moon").classList.add("rising"); } }, timeout); } + window.applyMenuItemClasses = () => { const menuItems = document.querySelectorAll("#menu a"); for + (let i = 0; i < menuItems.length ; i++) { + if (menuItems[i].pathname=== window.location.pathname) { + menuItems[i].classList.add("text-neutral-900", "dark:text-white"); + } + } + //:class="{ 'text-neutral-900 dark:text-white': window.location.pathname == '{menu.url}', 'text-neutral-700 dark:text-neutral-400': window.location.pathname != '{menu.url}' }" +}; + +function mobileMenuFunctionality() { + document.getElementById("openMenu").addEventListener("click", () => { + openMobileMenu(); + }); + + document.getElementById("closeMenu").addEventListener("click", () => { + closeMobileMenu(); + }); +} + +window.openMobileMenu = () => { + document.getElementById("openMenu").classList.add("hidden"); + document.getElementById("closeMenu").classList.remove("hidden"); + document.getElementById("menu").classList.remove("hidden"); + document.getElementById("mobileMenuBackground").classList.add("opacity-0"); + document.getElementById("mobileMenuBackground").classList.remove("hidden"); + + setTimeout(() => { + document + .getElementById("mobileMenuBackground") + .classList.remove("opacity-0"); + }, 1); +}; + +window.closeMobileMenu = () => { + document.getElementById("closeMenu").classList.add("hidden"); + document.getElementById("openMenu").classList.remove("hidden"); + document.getElementById("menu").classList.add("hidden"); + document.getElementById("mobileMenuBackground").classList.add("hidden"); +}; + + + --- const { content, layout = "vertical", + "data-aos": dataAos, "data-aos-delay": dataAosDelay } = Astro.props; const { title, description, + publishDate, tags = [] as string[], img, img_alt, slug, link } = content; const formattedDate = + new Date(publishDate).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: + 'numeric' }); const postLink = link || `/blog/${slug}`; // Check if horizontal layout const + isHorizontal = layout === "horizontal"; --- + + + --- import social from + "@/collections/social.json"; const { displaySocialIds } = Astro.props; // 配置要显示的社交媒体 ID(留空则显示所有) + //const displaySocialIds: number[] = [1, 2, 3]; // 只显示 ID 为 1, 2, 3 的社交媒体 // const + displaySocialIds: number[] = []; // 显示所有(当数组为空时) const filteredSocial = social.filter(item => { if + (item.isShow === false) return false; if (displaySocialIds.length > 0) { return + displaySocialIds.includes(item.id); } return true; }); --- + + + + + --- interface Props { name: string; // Required + image: string; // Required url: string; // Required description?: string; // Optional tags?: + string[]; // Optional video?: string; // Optional isShow?: boolean; // Optional, default true + layout?: 'featured' | 'grid'; // featured is full width, grid is two columns index?: number; + target?: string; // Optional, default "_blank" } const { name, description = '', url, image, tags + = [], video, isShow = true, layout = 'grid', index = 0, target = "_blank" } = Astro.props; // + Generate stable id for each video for precise playback control const videoId = + `workcard-video-${index}-${name.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`; ---
+ +
{video ? ( + ) + : ( + {name} )} +
+ + +
+
+ + + + +
+
+
+ + +
+ +
+

+ {name} +

+ + +
+
+ + + + +
+
+
+ + + {tags && tags.length > 0 && (
{tags.map((tag) => ( + {tag} + ))}
)} {description && (

+ {description} +

)}
+ + + + View {name} + +
{video && ( )}
+ + --- const { logo, dates, role, company, + description } = Astro.props; ---
+
+ {company} +
+ +

+ {dates} +

+

{role}

+

{company}

+

+ {description} +

+
+
+ + --- import AnimatedText from + "@/components/ui/AnimatedText.astro"; interface Props { title?: string ; description?: string ; + tags?: string[] ; className?: string ; } const { title, description, tags = [], className = "" } = + Astro.props; ---
+

+ {title && }

{description && (
+ +
)} {tags && + tags.length > 0 && (
{tags.map((tag) + => ( + {tag} + ))}
)}
+
+ + --- interface Props { className?: string; // + Custom CSS classes color?: string; // Color, defaults to neutral width?: "full" | "3/4" | "1/2" | + "1/4"; // Width options spacing?: "sm" | "md" | "lg" | "xl"; // Spacing options thickness?: "thin" + | "normal" | "thick"; // Thickness options } // Define default values const { className = "", + color = "border-primary/25 dark:border-primary-light/25", width = "full", spacing = "md", + thickness = "thin" } = Astro.props; // Width mapping const widthClasses = { "full": "w-full", + "3/4": "w-3/4", "1/2": "w-1/2", "1/4": "w-1/4" }; // Spacing mapping const spacingClasses = { + "sm": "my-2", "md": "my-4", "lg": "my-8", "xl": "my-12" }; // Thickness mapping const + thicknessClasses = { "thin": "border-[0.5px]", "normal": "border-[1px]", "thick": "border-[2px]" + }; --- + + + --- // Define properties accepted by the component + interface Props { imageUrl: string; // Required title?: string; // Optional, default "Home" link?: + string; // Optional, can be empty } // Set default values and receive incoming properties const { + imageUrl, title = "Home", link = "" } = Astro.props; ---
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {title && (
+

+ {title} +

+
)}
+ + + + +
+
+ + + + +
+ + --- import projects from + "@/collections/featuredwork.json"; import WorkCard from "@/components/cards/WorkCard.astro"; + import SectionHeader from "@/components/elements/SectionHeader.astro"; import Button from + "@/components/ui/Button.astro"; interface Props { title?: string; description?: string; showAll?: + boolean; limit?: number; // 限制显示的项目数量 } interface Project { name: string; description?: string; + image: string; url: string; tags?: string[]; video?: string; isShow?: boolean; } const { title = + "Featured Work", description = "A selection of projects I've worked on. From web applications to + design systems, each project represents a unique challenge and learning experience.", showAll = + false, limit } = Astro.props; // 获取项目列表 let allProjects = projects as Project[]; // 根据 limit 或 + showAll 来控制显示数量 if (limit !== undefined) { // 如果指定了 limit,使用 limit allProjects = projects.slice(0, + limit) as Project[]; } else if (!showAll) { // 如果没有指定 limit,且 showAll 为 false,默认显示 6 个 allProjects + = projects.slice(0, 6) as Project[]; } // 如果 showAll 为 true 且没有 limit,显示所有项目 // 前三个为特色项目(全宽) const + featuredProjects = allProjects.slice(0, 3); // 其余为网格项目(两栏) const gridProjects = + allProjects.slice(3); // 判断是否应该显示 "View All" 按钮 const shouldShowViewAll = limit !== undefined ? + limit < projects.length + : !showAll && projects.length> 6; ---
+
+ + + + + {featuredProjects.length > 0 && (
{featuredProjects + .filter(project => project.isShow !== false) .map((project, index) => ( ))}
)} {gridProjects.length > 0 && (
{gridProjects .filter(project => + project.isShow !== false) .map((project, index) => ( ))}
)} {shouldShowViewAll && (
+ +
)}
+
+
+ + --- import Logo + from "@/components/ui/Logo.astro"; import ToTop from "@/components/widgets/ToTop.astro"; import { + siteConfig, socialLinks } from "@/config/site.js"; ---
+
+ +

+ © {new Date().getFullYear()} {siteConfig.author || " "} +

+ + {socialLinks.map((social) => ( + {social.name} + + ))} +
+
+ + + + + +
+ + --- import works from + "@/collections/works.json"; import WorkCard from "@/components/cards/WorkCard.astro"; interface + Props { limit?: number; // 限制显示的项目数量 } interface Work { name: string; description?: string; image: + string; url: string; tags?: string[]; video?: string; isShow?: boolean; } const { limit } = + Astro.props; // 获取项目列表 let allWorks = works as Work[]; // 如果指定了 limit,使用 limit if (limit !== + undefined) { allWorks = works.slice(0, limit) as Work[]; } ---
+
+ {allWorks .filter(work => work.isShow !== false) + .map((work, index) => ( ))}
+
+
+ + --- interface Props { class?: string; content: + string | Promise; duration?: number; delay?: number; stagger?: number; } let { content, + duration = 0.6, delay = 0, stagger = 0.1, ...rest } = Astro.props; let processedContent = ""; if + (content) { // @ts-expect-error const words = content.split(" "); // Split text into words + processedContent = words .map( (word: string) => ` + ${word}`, ) .join(" "); } ---
+ + + + + +
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ + + + +
+
+
+ + +
+ + --- import { siteConfig } from "@/config/site.js"; + interface Props { title?: string; description?: string; keywords?: string; url?: string; image?: + string; twitterHandle?: string; } const { title = siteConfig.meta.title, description = + siteConfig.meta.description, keywords = siteConfig.meta.keywords, url = siteConfig.url, image = + siteConfig.meta.image, twitterHandle = siteConfig.social.twitterName, } = Astro.props; // + 构建完整的页面标题 const metaTitle = title === siteConfig.meta.title ? title : `${title} | + ${siteConfig.meta.title}`; const metaDescription = description || siteConfig.meta.description; + const ogImage = image || siteConfig.meta.image; const canonicalUrl = url || siteConfig.url; --- + + {metaTitle} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --- /** * OptimizedImage component is a + modified version of Astro's built-in Image component. * It allows to optimize images inside + public, src/assets and content folders. * To optimize remote images, follow: + https://docs.astro.build/en/guides/images/#authorizing-remote-images */ import { Image } from + "astro:assets"; interface InlineSvgProps { src: string; inlineSvg: true; alt?: string; width?: + string; height?: string; class?: string; style?: any; [key: string]: any; } interface ImageProps { + src: string; alt?: string; width?: number; height?: number; loading?: "eager" | "lazy" | null | + undefined; decoding?: "async" | "auto" | "sync" | null | undefined; formats?: string[] | "auto" | + "avif" | "jpeg" | "png" | "svg" | "webp"; class?: string; style?: any; [key: string]: any; } type + Props = InlineSvgProps | ImageProps; // Destructuring Astro.props to get the component's props let + { src, alt, inlineSvg, width, height, loading, decoding, formats, style, ...rest } = Astro.props; + const isRemoteImage = src.startsWith("http://") || src.startsWith("https://"); let image = src as + any; let SVG = "" as any; if (!isRemoteImage) { // Regular Images const images = + import.meta.glob([ "/src/assets/**/*.{jpeg,jpg,png,avif,tiff,gif,webp,svg,gif}", + "/public/**/*.{jpeg,jpg,png,avif,tiff,gif,webp,svg,gif}", + "/src/content/**/*.{jpeg,jpg,png,avif,tiff,gif,webp,svg,gif}", ]); // Get Raw Image For Inline SVG + const imagesRaw = import.meta.glob( [ "/src/assets/**/*.svg", "/src/content/**/*.svg", + "/public/**/*.svg", ], { query: "raw", import: "default", }, ); if (inlineSvg && + src.includes(".svg")) { const key = Object.keys(images).find((k) => k.includes(src)); SVG = key ? + await imagesRaw[key]() : null; if (SVG.length > 1) { SVG = SVG.split(" tag + + if (SVG.length > 1) { + // Convert rest object to a string of attributes + const attributes = Object.entries(rest) + .map(([key, value]) => `${key}=" + ${value}"`) + .join(" "); + + // Add the attributes to the tag + SVG[1] = ` data-icon=" + true" ${attributes} ${SVG[1]}`; + } + + // Join the array back into a string + SVG = SVG.join(" k.includes(src)); + image = key ? await images[key]() : null; + + image = image && image.default ? image.default : null; +} +--- + +{ + inlineSvg && SVG && src.includes(" + .svg") ? ( + + ) : image ? ( + // @ts-expect-error + + ) : null +} + + + +--- +const { currentPage, totalPages, collection } = Astro.props; + +// 生成页码数组 +const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1); + +// 确定要显示的页码范围 +const maxVisiblePages = 5; +let startPage = Math.max(currentPage - Math.floor(maxVisiblePages / 2), 1); +let endPage = Math.min(startPage + maxVisiblePages - 1, totalPages); + +if (endPage - startPage + 1 < maxVisiblePages) { + startPage = Math.max(endPage - maxVisiblePages + 1, 1); +} + +// 生成页面链接 +const getPageLink = (pageNum: number): string => { + if (pageNum === 1) { + return `/${collection}/`; + } + return `/${collection}/page/${pageNum}`; +}; +--- + + + + + +--- +interface Heading { + depth: number; + slug: string; + text: string; +} + +interface HeadingNode extends Heading { + children: HeadingNode[]; +} + +const { headings = [] } = Astro.props as { headings: Heading[] }; + +function buildTree(list: Heading[]): HeadingNode[] { + const root: HeadingNode[] = []; + const stack: HeadingNode[] = []; + list.forEach(h => { + const node: HeadingNode = { ...h, children: [] }; + while (stack.length && stack[stack.length - 1].depth >= h.depth) { + stack.pop(); + } + if (stack.length === 0) { + root.push(node); + } else { + stack[stack.length - 1].children.push(node); + } + stack.push(node); + }); + return root; +} +const tree = buildTree(headings); +--- + +
+
+ +
+
+ TOC + +
+ +
+ + + + +
+ + + + + + + + + + + +--- +const GAID = import.meta.env.PUBLIC_GA4_ID; +const UMAMIId = import.meta.env.PUBLIC_UMAMI_ID; +// const GAID = ''; + +if (!GAID) { + // console.error(" Google Analytics ID (PUBLIC_GA4_ID) is missing!"); +} +--- + + +{GAID && ( + <> + + + +)} + + +{UMAMIId && ( + +)} + + + +--- +import Meta from './Meta.astro'; +import TopBg from " @/ components/ ui/ TopBg.astro"; +import Footer from " @/ components/ sections/ Footer.astro"; +import Header from " @/ components/ sections/ Header.astro"; +import TrackGa from " @/ components/ widgets/ TrackGa.astro"; +interface Props { + title?: string | undefined; + description?: string | undefined; + keywords?: string | undefined; +} + +const { + title, + description, + keywords +} = Astro.props; + + +import " ../ styles/ global.css"; +import " aos/ dist/ aos.css"; +import " ../ styles/ aos-custom.css"; +import '../styles/article.css'; +import '../styles/article-enhancements.css'; +--- + + + + + + + + + + + + + +
+ +
+ + + + + + +--- +import Layout from " @/ layouts/ Layout.astro"; +interface Props { + title?: string | undefined; + keywords?: string | undefined; + description?: string | undefined; +} +const { title, description,keywords} = Astro.props; +--- + + +
+ +
+
+
+ + +--- +import Layout from " @/ layouts/ Layout.astro"; + +interface Props { + title?: string | undefined; + keywords?: string | undefined; + description?: string | undefined; +} +const { title, description,keywords} = Astro.props; +--- + + +
+ +
+
+
+ + +--- +import { getCollection, render } from " astro:content"; +import PostLayout from " @/ layouts/ PostLayout.astro"; +import Toc from '@/components/widgets/Toc.astro'; + +// 根据 Astro v5/v6,对于 SSG 模式下的动态路由,需要使用 getStaticPaths() +// 来告诉 Astro 在构建时生成哪些页面 +export async function getStaticPaths() { + const postEntries = await getCollection(" post"); + return postEntries.map((entry) => { + // 根据 Astro v5 官方文档,entry.id 就是条目的唯一标识符 + // 对于 [...slug] 路由,需要将 entry.id 转换为 slug 数组 + // entry.id 已经是相对于集合根目录的路径,例如 " dir/ index" 或 " dir" + // 注意:对于 [...slug] 路由,params.slug 应该是字符串,Astro 会自动处理 + const slugString = entry.id; + return { + params: { slug: slugString }, + props: { entry }, + }; + }); +} + +// 使用 [...slug] 路由来处理包含斜杠的路径(官方推荐) +// slug 参数是数组格式,需要转换为字符串来匹配 entry.id +const { slug } = Astro.params; +const slugString = Array.isArray(slug) ? slug.join('/') : (slug || ''); + +// 从 props 获取 entry(在 SSG 模式下通过 getStaticPaths 传递) +const { entry } = Astro.props; + +// 如果找不到文章,返回 404 +if (!entry) { + return Astro.redirect("/ 404"); +} + +const { Content, headings } = await render(entry); +const filtered = headings.filter(h => h.depth <= 2); +--- + + + +
+ +
+
+ +
+ + + + +

+ {entry.data.title} +

+ + + +
+ + +
+ + + + +
+
+
+ +
+
+
+
+
+
+
+ + + + +
+ + +--- +import PostLayout from " @/ layouts/ PostLayout.astro"; +import PageHeader from " @/ components/ elements/ PageHeader.astro"; +import SeparatorLine from " @/ components/ elements/ SeparatorLine.astro"; +import { siteConfig } from " @/ config/ site.js"; +--- + +
+
+ + +
+ +
+
+
+
+

Stack

+
    +
  • Backend: Python, Golang
  • +
  • Automation: Bash, scripting
  • +
  • Infrastructure: Docker, Linux
  • +
+ +

Services

+
    +
  • Web application development
  • +
  • Automation and scripting
  • +
  • API development
  • +
  • DevOps and deployment
  • +
+ +

Contact

+

+ Interested in working together? Reach out at {siteConfig.mail} +

+
+
+
+
+
+
+
+ + +--- +import PostLayout from " @/ layouts/ PostLayout.astro"; +import PageHeader from " @/ components/ elements/ PageHeader.astro"; +import SeparatorLine from " @/ components/ elements/ SeparatorLine.astro"; +--- + +
+
+ + +
+ +
+
+
+
+

Mission

+

+ Making AI accessible to business students. Technical workshops designed for non-technical profiles. +

+ +

What We Do

+
    +
  • Hands-on workshops on AI tools
  • +
  • Introduction to agentic AI
  • +
  • Practical ML workflows
  • +
+ +

Impact

+

+ Bridging the gap between business and tech. Helping future managers understand what's possible with AI. +

+
+
+
+
+
+
+
+ + +--- +import PostLayout from " @/ layouts/ PostLayout.astro"; +import PageHeader from " @/ components/ elements/ PageHeader.astro"; +import SeparatorLine from " @/ components/ elements/ SeparatorLine.astro"; +import Button from " @/ components/ ui/ Button.astro"; +import { ArrowRight } from " @lucide/ astro"; +--- + +
+
+ + +
+ +
+
+
+
+

The Story

+

+ What started as a simple Raspberry Pi running Pi-hole evolved into a full production-grade infrastructure. The goal was to learn by doing: understanding how enterprise systems work by building them myself. +

+ +

Current Stack

+
    +
  • Virtualization: Proxmox VE
  • +
  • Orchestration: Kubernetes
  • +
  • Containers: Docker
  • +
  • Monitoring: Grafana, Prometheus, InfluxDB
  • +
  • Reverse Proxy: Caddy, FRP
  • +
  • Auth: Authentik (OIDC)
  • +
+ +

What I Learned

+

+ Building infrastructure from scratch taught me more than any course could. Debugging network issues at 2am, recovering from failed upgrades, designing for redundancy - these experiences shaped how I approach technical problems. +

+ +
+ +
+
+
+
+
+
+
+
+ + +--- +import PageLayout from " @/ layouts/ PageLayout.astro"; +import PageHeader from " @/ components/ elements/ PageHeader.astro"; +import Button from " @/ components/ ui/ Button.astro"; +--- + + +
+ +
+ +
+
+
+ + +--- +import Layout from " @/ layouts/ Layout.astro"; +import { Server, Database, Shield, Globe, Activity, Lock } from " @lucide/ astro"; +--- + + +
+
+

Infrastructure

+

Technical overview of my homelab stack.

+
+ +
+ +
+
+ +

Virtualization

+
+
    +
  • Proxmox VE - Hypervisor
  • +
  • Multiple VMs and LXC containers
  • +
  • ZFS storage pools
  • +
+
+ + +
+
+ +

Orchestration

+
+
    +
  • Kubernetes - Container orchestration
  • +
  • Docker - Containerization
  • +
  • GitOps deployment workflow
  • +
+
+ + +
+
+ +

PKI & Security

+
+
    +
  • step-ca - Internal Certificate Authority
  • +
  • Automated certificate provisioning
  • +
  • mTLS for service communication
  • +
+
+ + +
+
+ +

DNS

+
+
    +
  • Technitium - DNS Server
  • +
  • Split-horizon DNS
  • +
  • Internal service discovery
  • +
+
+ + +
+
+ +

Monitoring

+
+
    +
  • Grafana - Visualization
  • +
  • Prometheus - Metrics
  • +
  • InfluxDB - Time-series data
  • +
+
+ + +
+
+ +

Access & Auth

+
+
    +
  • Authentik - Identity Provider
  • +
  • OIDC/SAML SSO
  • +
  • Caddy - Reverse proxy
  • +
  • FRP - Tunnel access
  • +
+
+
+ +
+

Architecture Notes

+

+ Everything runs on-premise. Services communicate over an internal network with mTLS. External access goes through authenticated reverse proxy. Monitoring covers infrastructure and application metrics. +

+
+ + +
+
+
+ + +--- +import WorksSection from " @/ components/ sections/ WorksSection.astro"; +import PageLayout from " @/ layouts/ PageLayout.astro"; +import PageHeader from " @/ components/ elements/ PageHeader.astro"; +--- + + +
+ + + +
+
+
+ +// 1. 从 `astro:content` 导入工具函数 +import { defineCollection, z } from ' astro:content'; + +// 2. 导入加载器 +import { glob } from ' astro/ loaders'; + +// 3. 定义你的集合 +const post = defineCollection({ + // 使用 glob 加载器从 src/content/post 目录加载所有 .mdx 文件 + loader: glob({ pattern: ' **/ *.{md,mdx}', base: ' ./ src/ content/ post' }), + // Type-check frontmatter using a schema + schema: z.object({ + title: z.string(), + description: z.string(), + publishDate: z.coerce.date(), + read: z.number().optional(), + tags: z.array(z.string()).optional(), + img: z.string().optional(), + img_alt: z.string().optional(), + featured: z.boolean().optional(), + }), +}); + +// 4. 导出一个 `collections` 对象来注册你的集合 +export const collections = { post }; + + + +/// +/// + + + +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false + + + +# Site Configuration +PUBLIC_SITE_URL=https://your-domain.com + +# Analytics +# Google Analytics 4 ID (鏍煎紡: G-XXXXXXXXXX) +PUBLIC_GA4_ID= + +# Umami Analytics ID (鏍煎紡: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) +PUBLIC_UMAMI_ID= + + + +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.local +.env.production +.env.production.local + +# macOS-specific files +.DS_Store + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +Thumbs.db +.DS_Store + +# Temporary files +*.tmp +*.temp +.cache/ + + + +# 贡献指南 + +感谢你对这个项目的关注!我们欢迎所有形式的贡献。 + +## 如何贡献 + +### 报告 Bug + +如果你发现了 bug,请: + +1. 查看 [Issues](https://github.com/ricocc/ricoui-portfolio/issues) 确认该 bug 尚未被报告 +2. 如果是一个新的 bug,请创建一个新的 Issue,包含: + - 清晰的问题描述 + - 复现步骤 + - 预期行为 + - 实际行为 + - 截图(如果有) + - 环境信息(浏览器、操作系统等) + +### 提出新功能 + +如果你有改进建议或新功能想法: + +1. 查看现有 Issues 确认没有类似提议 +2. 创建一个新的 Feature Request Issue,描述: + - 功能描述 + - 使用场景 + - 可能的实现方案(可选) + +### 提交代码 + +1. **Fork 项目** + ```bash + git clone https://github.com/your-username/ricoui-portfolio.git + cd ricoui-portfolio + ``` + +2. **创建分支** + ```bash + git checkout -b feature/your-feature-name + # 或 + git checkout -b fix/your-bug-fix + ``` + +3. **安装依赖** + ```bash + pnpm install + ``` + +4. **进行修改** + - 编写代码 + - 确保代码通过检查:`pnpm check` + - 测试你的修改 + +5. **提交更改** + ```bash + git add . + git commit -m "feat: 添加新功能描述" + # 或 + git commit -m "fix: 修复bug描述" + ``` + + 提交信息请遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范: + - `feat:` 新功能 + - `fix:` Bug 修复 + - `docs:` 文档更新 + - `style:` 代码格式调整(不影响功能) + - `refactor:` 代码重构 + - `perf:` 性能优化 + - `test:` 测试相关 + - `chore:` 构建/工具相关 + +6. **推送并创建 Pull Request** + ```bash + git push origin feature/your-feature-name + ``` + + 然后在 GitHub 上创建 Pull Request。 + +## 代码规范 + +- 使用 Biome 进行代码检查和格式化 +- 运行 `pnpm check` 确保代码符合规范 +- 保持代码简洁、可读 +- 添加必要的注释 + +## 开发环境 + +- Node.js >= 18 +- pnpm (推荐) 或 npm/yarn +- 现代浏览器(Chrome、Firefox、Safari、Edge) + +## 测试 + +在提交 PR 前,请确保: + +- [ ] 代码通过 `pnpm check` +- [ ] 在本地运行 `pnpm dev` 测试正常 +- [ ] 构建通过 `pnpm build` +- [ ] 测试了不同的浏览器和设备 + +## 问题? + +如果遇到问题,可以: + +- 查看 [Issues](https://github.com/ricocc/ricoui-portfolio/issues) +- 创建新的 Issue 提问 +- 联系维护者:hello@ricoui.com + +再次感谢你的贡献!🎉 + + + +services: + portfolio: + build: . + ports: + - "8080:80" + restart: unless-stopped + + + +# Stage 1: Build +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy source files +COPY . . + +# Build the site +RUN pnpm build + +# Stage 2: Serve with nginx +FROM nginx:alpine AS runner + +# Copy built files +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] + + + +server { + listen 80; + listen [::]:80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/json + application/xml + application/rss+xml + application/atom+xml + image/svg+xml; + + # Static file caching + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # HTML files - no cache for dynamic updates + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + # Main location block + location / { + try_files $uri $uri/ $uri.html =404; + } + + # Custom error pages + error_page 404 /404.html; + location = /404.html { + internal; + } +} + + + +ignoredBuiltDependencies: + - ' @biomejs/ biome' + - ' @tailwindcss/ oxide' + - esbuild + - sharp + + + +/** @type {import(' tailwindcss').Config} */ +export default { + darkMode: "class", + content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], + theme: { + extend: { + }, + }, + plugins: [require("@tailwindcss/typography")], +}; + + + +{ + "extends": "astro/tsconfigs/base", + "compilerOptions": { + "strictNullChecks": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "typeRoots": ["./node_modules/@types", "./src/types",".vscode", "dist"] + } +} + + + +[ + { + "name": "Home", + "url": "/" + }, + { + "name": "Projects", + "url": "/works" + }, + { + "name": "About", + "url": "/about" + } +] + + + +--- +import AnimatedText from "@/components/ui/AnimatedText.astro"; +const { title, description } = Astro.props; +--- + +
+

+ +

+ {description && ( +
+ +
+ )} +
+
+ + +--- +import { getCollection } from "astro:content"; +import Pagination from "@/components/widgets/Pagination.astro"; +import BlogCard from "@/components/cards/BlogCard.astro"; +import SectionHeader from "@/components/elements/SectionHeader.astro"; +import Button from "@/components/ui/Button.astro"; +import AnimatedText from "@/components/ui/AnimatedText.astro"; +// 定义组件接收的属性 +const { + title = "Latest Articles", + description = "", + limit = 3, + listPage = false, + showViewAllButton = true, + pagination = { + enable: false, + currentPage: 1 + }, + postsPerPage = 3 +} = Astro.props; + +// 获取博客文章集合 +const allPosts = await getCollection("post"); + +// 按发布日期排序文章 +let posts = allPosts.sort( + (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf() +); + +// 查找特色文章 +const featuredPost = posts.find((post) => post.data.featured); + +// 如果存在特色文章,从主列表中移除 +if (featuredPost && listPage) { + posts = posts.filter((post) => post.data.featured !== true); +} + +// 计算总页数 +const totalPages = Math.ceil(posts.length / postsPerPage); + +// 如果启用了分页,则只显示当前页的文章 +if (pagination.enable) { + const indexOfLastPost = pagination.currentPage * postsPerPage; + const indexOfFirstPost = indexOfLastPost - postsPerPage; + posts = posts.slice(indexOfFirstPost, indexOfLastPost); +} else if (!listPage) { + // 如果不是列表页,限制显示的文章数量 + posts = posts.slice(0, limit); +} + +// 为每篇文章添加链接属性 - 创建新对象而不是修改原对象 +const postsWithLinks = posts.map(post => { + // 根据 Astro v5 官方文档,entry.id 就是条目的唯一标识符 + // 直接使用 entry.id 生成 URL,对于 [...slug] 路由,id 已经是正确的路径 + const link = `/blog/${post.id}`; + return { + ...post, + data: { + ...post.data, + link + } + }; +}); + +// 为特色文章创建带链接的版本 +const featuredPostWithLink = featuredPost ? { + ...featuredPost, + data: { + ...featuredPost.data, + link: `/blog/${featuredPost.id}` + } +} : null; +--- + +{/* 特色文章部分 - 仅在列表页第一页显示 */} +{listPage && featuredPostWithLink && pagination.currentPage === 1 && ( +
+
+

+ +

+
+ +
+
+
+)} + +
+
+ {!listPage && title && ( + + )} + + {/* 列表页标题 */} + {listPage && title && ( +
+

{title}

+
+ )} + +
+ {postsWithLinks && postsWithLinks.map((post, index) => ( + + ))} +
+ + {listPage && totalPages > 1 && ( + + )} +
+ + {/* "View All Articles" 按钮 - 仅在非列表页且启用时显示 */} + {!listPage && showViewAllButton && ( +
+ +
+ )} +
+
+ + +--- +export interface Props { + url: string; // Button link URL + type?: "solid" | "fill" | "disabled"; // Button type, optional, default is "solid" + className?: string; // Custom CSS classes + target?: "_blank" | "_self" | "_parent" | "_top"; // Link target, default is "_self" + size?: "xs" | "sm" | "md" | "lg" | "xl"; // Button size, optional, default is "md" +} + +const { url, type = "solid", className = "", target = "_self", size = "md" } = Astro.props; + +// Define padding and font size for different button sizes +const sizeClasses = { + xs: { + padding: "px-3 py-1.5", + fontSize: "text-xs", + borderRadius: "rounded-lg", + }, + sm: { + padding: "px-4 py-2", + fontSize: "text-sm", + borderRadius: "rounded-lg", + }, + md: { + padding: "px-5 py-[12px]", + fontSize: "text-base", + borderRadius: "rounded-xl", + }, + lg: { + padding: "px-6 py-3.5", + fontSize: "text-lg", + borderRadius: "rounded-xl", + }, + xl: { + padding: "px-8 py-4", + fontSize: "text-xl", + borderRadius: "rounded-2xl", + }, +}; + +// Get classes for current size +const currentSizeClasses = sizeClasses[size]; +--- + + {type === "solid" ? ( +
+ +
+ ) : type === "fill" ? ( + + + + ) : type === "disabled" ? ( +
+ +
+ ) : ( + + )} +
+ + +
+ + + + Logo + + + + +--- +--- + +
+
+
+
+
+
+
+
+
+ + + + +
+ + + + + + +--- +// local structural type to avoid version-specific imports +type LogoInput = string | { src: string }; +interface Props { + logo?: LogoInput; // image url or object with src or single letter + tags?: string[]; // multiple labels like ["Blog", "Open Source"] + url?: string; // external site url + github?: string; // optional github url + visitLabel?: string; // primary button text + className?: string; // extra classes for wrapper +} + +const { + logo = '', + tags = [' + Website'], + url = ' #', + github = '', + visitLabel = ' + Visit Site', + className = '' +} = Astro.props as Props; + +// Normalize logo: support URL string or Astro ImageMetadata object +let logoSrc: string | null = null; +if (typeof logo === ' + string') { + const isUrlLike = logo.startsWith('/') || logo.startsWith(' http') || /\.(png|jpe?g|webp|svg)$/i.test(logo); + if (isUrlLike) logoSrc = logo; +} else if (logo && typeof logo === ' object' && ' src' in (logo as any)) { + logoSrc = (logo as any).src as string; +} +--- + +
+
+ +
+ {logoSrc ? ( + logo + ) : ( + {typeof logo === ' string' && logo ? logo : ' W.'} + )} +
+ + +
+ {tags && tags.map((t) => ( + + ))} + {github && ( + + + + )} +
+ + + +
+
+
+ + +--- +import { siteConfig } from ' @/ config/ site.js'; +export interface Props { + title?: string; + description?: string; + keywords?: string; + url?: string; + ogImage?: string; + twitterHandle?: string; +} + +const { + title, + description, + keywords = "", + url = siteConfig.url, // + ogImage = "/og.jpg", + twitterHandle = siteConfig.social.twitterName || '', +} = Astro.props; +--- + + + + + +{title} + + + + + + + + + + + + + + + + + + + + + + +--- +import Layout from "@/layouts/Layout.astro"; +import PageHeader from "@/components/elements/PageHeader.astro"; +import BlogSection from "@/components/sections/BlogSection.astro"; + +// 使用与分页页面相同的每页文章数量 +const POSTS_PER_PAGE = 6; +// 博客首页显示第一页内容 +const currentPage = 1; +--- + + +
+ +
+
+
+ +
+
+
+
+ + +import rss from "@astrojs/rss"; +import { getCollection } from "astro:content"; + +export async function GET(context) { + const blog = await getCollection(' + post'); + return rss({ + title: ' Rico Portfolio Template Astro', + description: ' Astro Blog Template by Rico UI', + site: context.site, + items: blog.map((post) => { + const link = `/blog/${post.id}/`; + return { + title: post.data.title, + pubDate: post.data.publishDate, + description: post.data.description, + link, + stylesheet: '/ rss/ pretty-feed-v3.xsl', + }; + }), + }); +} + + + +/* Self-hosted fonts */ +@font-face { + font-family: ' Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/ fonts/ Inter-Variable.woff2') format(' woff2'); +} + +@font-face { + font-family: ' Instrument Serif'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/ fonts/ InstrumentSerif-Regular.woff2') format(' woff2'); +} + +@font-face { + font-family: ' Instrument Serif'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('/ fonts/ InstrumentSerif-Italic.woff2') format(' woff2'); +} + +@import "tailwindcss"; + +/* Path relative to current CSS file */ +@config "../../tailwind.config.mjs"; + + +/* Define theme font mappings */ +@theme { + --font-brand: "Instrument Serif", ui-serif, Georgia, serif; + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; + --font-body: "Inter", ui-sans-serif, system-ui, sans-serif; + --max-screen: 1200px; + --inner-screen: 800px; + + --color-neutral-50: #f8fafc; + --color-neutral-100: #f1f5f9; + --color-neutral-200: #e2e8f0; + --color-neutral-300: #cbd5e1; + --color-neutral-400: #94a3b8; + --color-neutral-500: #64748b; + --color-neutral-600: #475569; + --color-neutral-700: #334155; + --color-neutral-800: #1e293b; + --color-neutral-900: #0f172a; + --color-neutral-950: #020617; + + /* Violet theme #707BC2 */ + --color-primary: #707BC2; + --color-primary-rgb: 112, 123, 194; + --color-primary-strong: #5B67B3; + --color-primary-dark: #8B94D4; + --color-primary-light: #A5ADE0; + --color-primary-light-dark: #9BA3D8; + --color-primary-lighter: #B8BEE8; + /* Article-specific variables */ + --color-primary-bg: var(--color-bg-primary-light); + --color-primary-bg-light: var(--color-bg-secondary); + + + --color-btn-primary: #707BC2; + --color-btn-primary-hover: #5B67B3; + --color-btn-primary-dark: #8B94D4; + --color-btn-primary-dark-hover: #707BC2; + + /* Accent colors */ + --color-accent: #707BC2; + --color-accent-light: #A5ADE0; + + /* Background colors */ + --color-bg-primary: #f8fafc; + --color-bg-secondary: #fff; + --color-bg-primary-light: #f1f5f9; + --color-bg-primary-deep: #f8fafc; + + --color-bg-primary-dark: #0f172a; + --color-bg-secondary-dark: #1e293b; + + + /* Text colors */ + --color-text-primary: #5B67B3; + --color-text-secondary: #475569; + --color-text-tertiary: #64748b; + + --color-text-primary-dark: #A5ADE0; + --color-text-secondary-dark: #cbd5e1; + --color-text-tertiary-dark: #94a3b8; +} + +/* Dark mode variable overrides */ +html.dark { + --color-primary-rgb: 139, 148, 212; + --color-primary-light-dark: #8B94D4; + --color-primary-lighter: #A5ADE0; + --color-primary-bg: rgba(112, 123, 194, 0.15); + --color-primary-bg-light: rgba(112, 123, 194, 0.05); +} + +/* Add typography styles */ +@layer components { + .font-brand{ + font-weight: 400!important; + } + .prose { + max-width: 65ch; + } + .prose img { + border-radius: 30px; + } + .container{ + max-width: var(--max-screen); + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; + } + .site-container { + max-width: var(--max-screen); + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; + } + .inner-container { + max-width: var(--inner-screen); + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; + } + .gradient-title { + background: linear-gradient(to right, #707BC2, #3d4785); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } + .gradient-title-light{ + background: linear-gradient(to right, #A5ADE0, #707BC2); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} +html,body{ + color: var(--color-text-secondary); +} +html.dark,html.dark body{ + color: var(--color-text-secondary-dark); +} +h1,h2,h3{ + color:var(--color-text-primary); +} +html.dark h1,html.dark h2,html.dark h3{ + color:var(--color-text-primary-dark); +} + +/* Custom CSS styles */ +#sun { + transform: rotate(0); +} + +#moon { + transform: rotate(0); +} + +#darkToggle:hover #sun { + transform: rotate(360deg); +} + +#darkToggle:hover #moon { + transform: rotate(45deg); +} + + +html.dark #darkToggle:hover .horizon { + border-color: #718096 !important; +} + +html{ + font-family: var(--font-sans); + scroll-behavior: smooth; +} +html.dark { + color-scheme: dark; +} + +.horizon .setting { + animation: 1s ease 0s 1 setting; +} + +.horizon .rising { + animation: 1s ease 0s 1 rising; +} + +@keyframes setting { + 0% { + transform: translate3d(0, 10px, 0); + } + + 40% { + transform: translate3d(0, -2px, 0); + } + + to { + transform: translate3d(0, 30px, 0); + } +} + +@keyframes rising { + 0% { + opacity: 0; + transform: translate3d(0, 30px, 0); + } + + 40% { + opacity: 1; + transform: translate3d(0, -2px, 0); + } + + to { + opacity: 1; + transform: translate3d(0, 10px, 0); + } +} + + + + +html::-webkit-scrollbar, +body::-webkit-scrollbar, +div::-webkit-scrollbar { + width: 6px !important; +} + +html::-webkit-scrollbar-track, +body::-webkit-scrollbar-track, +div::-webkit-scrollbar-track { + background: var(--color-neutral-200) !important; + border-radius: 6px !important; + -webkit-border-radius: 6px !important; + -moz-border-radius: 6px !important; + -ms-border-radius: 6px !important; + -o-border-radius: 6px !important; +} + +html::-webkit-scrollbar-thumb, +body::-webkit-scrollbar-thumb, +div::-webkit-scrollbar-thumb { + background: var(--color-primary-light) !important; + border-radius: 6px !important; +} + +html::-webkit-scrollbar-thumb:hover, +body::-webkit-scrollbar-thumb:hover, +div::-webkit-scrollbar-thumb:hover { + background: var(--color-primary) !important; +} + +html::-webkit-scrollbar-thumb:active, +body::-webkit-scrollbar-thumb:active, +div::-webkit-scrollbar-thumb:active { + background: var(--color-primary-dark) !important; +} + + + +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "astro/config"; +import mdx from "@astrojs/mdx"; +import sitemap from "@astrojs/sitemap"; +import { fileURLToPath } from "url"; +import path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// Get the site URL from environment variables, or use the default value if not set +// Note: After the first deployment, be sure to set the correct PUBLIC_SITE_URL in the .env file +const siteUrl = import.meta.env.PUBLIC_SITE_URL || ' https:// portfolio.ricoui.com/'; + +// https://astro.build/config +export default defineConfig({ + site: siteUrl, + base: '/', + envPrefix: ' + PUBLIC_', + vite: { + plugins: [tailwindcss()], + resolve: { + alias: { + ' @': path.resolve(__dirname, ' ./ src') + } + } + }, + + server: { + port: 5200, + }, + + integrations: [mdx(), sitemap()], +}); + + + +{ + "name": "astro-rico-portfolio", + "type": "module", + "version": "1.0.0", + "description": "A modern, high-performance portfolio website template for designers built with Astro. Features retro blue theme, dark mode, and beautiful animations.", + "keywords": [ + "astro", + "portfolio", + "designer", + "website", + "template", + "tailwindcss", + "blog", + "dark-mode", + "responsive" + ], + "author": { + "name": "Ricoui", + "email": "hello@ricoui.com", + "url": "https://ricoui.com" + }, + "repository": { + "type": "git", + "url": "https://github.com/ricocc/ricoui-portfolio.git" + }, + "bugs": { + "url": "https://github.com/ricocc/ricoui-portfolio/issues" + }, + "homepage": "https://github.com/ricocc/ricoui-portfolio#readme", + "license": "Apache-2.0", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro check && astro build", + "preview": "astro preview", + "astro": "astro", + "check": "biome check --apply-unsafe ." + }, + "devDependencies": { + "@astrojs/check": "^0.9.5", + "@biomejs/biome": "1.7.3", + "@tailwindcss/typography": "^0.5.13", + "@types/matter-js": "^0.20.2", + "astro": "^5.15.4", + "postcss": "^8.4.31", + "tailwindcss": "^4.1.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" + }, + "dependencies": { + "@astrojs/mdx": "^4.3.10", + "@astrojs/rss": "^4.0.13", + "@astrojs/sitemap": "^3.6.0", + "@lucide/astro": "^0.546.0", + "@tailwindcss/vite": "^4.1.14", + "aos": "^2.3.4", + "matter-js": "^0.20.0", + "motion": "^12.23.24", + "sharp": "^0.34.4" + } +} + + + +[ + { + "dates": "2024 - Present", + "role": "President", + "company": "HEC IA", + "description": "Technical workshops on AI tools for business students.", + "logo": "/assets/hec-ia.png" + }, + { + "dates": "2023 - Present", + "role": "Freelance Developer", + "company": "Self-employed", + "description": "Fullstack development. Python, Golang, automation.", + "logo": "/assets/freelance.png" + } +] + + + +[ + { + "name": "Homelab Infrastructure", + "description": "Self-hosted production infrastructure", + "tags": [ + "Kubernetes", + "Proxmox", + "Docker", + "Monitoring" + ], + "image": "/assets/homelab.png", + "url": "/work/homelab", + "isShow": true + }, + { + "name": "HEC IA", + "description": "AI workshops for business students", + "tags": [ + "Education", + "AI", + "Workshops" + ], + "image": "/assets/hec-ia.png", + "url": "/work/hec-ia", + "isShow": true + }, + { + "name": "Freelance Development", + "description": "Fullstack development and automation", + "tags": [ + "Python", + "Golang", + "Automation" + ], + "image": "/assets/freelance.png", + "url": "/work/freelance", + "isShow": true + } +] + + + +--- +import SectionHeader from "@/components/elements/SectionHeader.astro"; +import { Server, Code, GraduationCap } from "@lucide/astro"; + +interface Props { + title?: string; + description?: string; +} +const { title, description } = Astro.props; +--- + +
+
+ +
+ + +
+ + +
+ + +--- +import { Moon, SunMedium } from "@lucide/astro"; +import { siteConfig } from "@/config/site.js"; +import menus from "@/collections/menu.json"; +import Button from "@/components/ui/Button.astro"; +import Logo from "@/components/ui/Logo.astro"; +--- + + +
+ + + + + +
+ + --- import Layout from "@/layouts/Layout.astro"; import + PageHeader from "@/components/elements/PageHeader.astro"; import BlogSection from + "@/components/sections/BlogSection.astro"; import { getCollection } from "astro:content"; // + 定义每页显示的文章数量 export const POSTS_PER_PAGE = 6; // 根据 Astro v5/v6,对于 SSG 模式下的分页路由,仍然需要使用 + getStaticPaths() // 这是因为需要告诉 Astro 在构建时生成哪些页面 // 注意:params 必须是字符串类型(符合 v6 要求) export async + function getStaticPaths() { const allPosts = await getCollection("post"); // 按发布日期排序文章 let posts = + allPosts.sort( (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf() ); // + 查找特色文章,在列表页需要从主列表中移除 const featuredPost = posts.find((post) => post.data.featured); if + (featuredPost) { posts = posts.filter((post) => post.data.featured !== true); } // + 使用移除特色文章后的文章数量计算总页数 const totalPages = Math.ceil(posts.length / POSTS_PER_PAGE); // 返回所有分页路径,确保 + params.page 是字符串类型(v6 要求) return Array.from({ length: totalPages }, (_, i) => { const page = i + + 1; return { params: { page: page.toString() }, props: { page: page.toString() } }; }); } // 从 + props 获取页码(在 SSG 模式下通过 getStaticPaths 传递) const { page } = Astro.props; const currentPage = + parseInt(page); --- +
+ +
+
+
+ +
+
+
+
+ + --- import { ArrowUpRight, Code, Server, Building, Cloud, + GraduationCap, } from "@lucide/astro"; import { siteConfig } from "@/config/site.js"; import + experiences from "@/collections/experiences.json"; import social from "@/collections/social.json"; + import AboutExperience from "@/components/elements/AboutExperience.astro"; import Button from + "@/components/ui/Button.astro"; import Layout from "@/layouts/Layout.astro"; import Toc from + "@/components/widgets/Toc.astro"; // Manually define page table of contents structure const + headings = [ { depth: 2, slug: "about-me", text: "About Me" }, { depth: 2, slug: "expertise", + text: "Expertise" }, { depth: 2, slug: "experience", text: "Experience" }, { depth: 2, slug: + "education", text: "Education" }, { depth: 2, slug: "lets-connect", text: "Let's Connect" }, ]; + const filtered = headings.filter(h => h.depth <= 3); + +--- + + + +
+ +
+ + +
+
+ + + + +
+
+

+ About Me +

+
+
+ +
+
+ + Alexandre + | + + + + + @vorpax +
+
+ +

"Bridging business strategy with technical implementation."

+ + + +
+ +

+ Started in business prep school, now at HEC Paris. Self-taught in infrastructure and + development. I bridge business strategy with technical implementation. +

+ +
+ + +
+ +
+
+

Expertise

+
+ +
+
+ +
+ Infrastructure +
+
+
+ +
+
+ +
+ Fullstack Development +
+
+
+ +
+
+ +
+ Business Strategy +
+
+
+ +
+
+ +
+ DevOps +
+
+
+
+
+ +
+ +
+

+ Experience +

+
{ experiences.map((experience, index) => { return (
+ +
) }) }
+
+ +
+ +
+

+ Education +

+
+
+ +
+

HEC Paris

+

M1 - Master in + Management

+
+
+
+ +
+

Sorbonne + University

+

Bachelor's in + Mathematics

+
+
+
+
+ +
+ +
+

Let's Connect

+

Reach out on LinkedIn or check out + my work on GitHub + .

+

Email: {siteConfig.mail} +

+
+
+
+
+
+ + +
+ + + [ + { + "id": 1, + "name": "Github", + "username": "vorpax", + "image": "/assets/social/social-github.jpg", + "url": "https://github.com/vorpax", + "isShow": true + }, + { + "id": 2, + "name": "LinkedIn", + "username": "alexandre-houard", + "image": "/assets/social/social-linkedin.png", + "url": "https://www.linkedin.com/in/alexandre-houard-686960279/", + "isShow": true + }, + { + "id": 3, + "name": "Gitea", + "username": "vorpax", + "image": "/assets/social/social-gitea.png", + "url": "https://gitea.vorpax.dev/vorpax", + "isShow": true + } + ] + + + + [ + { + "name": "Homelab Infrastructure", + "description": "Self-hosted production infrastructure", + "tags": [ + "Kubernetes", + "Proxmox", + "Docker", + "Monitoring" + ], + "image": "/assets/homelab.png", + "url": "/work/homelab", + "isShow": true + }, + { + "name": "HEC IA", + "description": "AI workshops for business students", + "tags": [ + "Education", + "AI", + "Workshops" + ], + "image": "/assets/hec-ia.png", + "url": "/work/hec-ia", + "isShow": true + }, + { + "name": "Freelance Development", + "description": "Fullstack development and automation", + "tags": [ + "Python", + "Golang", + "Automation" + ], + "image": "/assets/freelance.png", + "url": "/work/freelance", + "isShow": true + } + ] + + + # Rico Portfolio - 设计师个人作品集网站 一个基于 Astro + 构建的现代化、高性能设计师个人作品集网站模板。采用复古蓝色主题,支持暗色模式,具有精美的动画效果和优秀的用户体验。 + ![Astro](https://img.shields.io/badge/Astro-5.15.4-FF5D01?logo=astro&logoColor=white) ![Tailwind + CSS](https://img.shields.io/badge/Tailwind-4.1.14-38B2AC?logo=tailwind-css&logoColor=white) + ![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg) ## ✨ 特性 - 🚀 **基于 Astro** - + 快速、轻量级的静态站点生成器 - 🎨 **现代化设计** - 复古蓝色主题,支持暗色/亮色模式切换 - 📱 **完全响应式** - 适配各种设备尺寸 - 🎭 **精美动画** - 使用 + AOS 和自定义动画效果 - 📝 **博客系统** - 支持 MDX 格式的博客文章 - 🎯 **作品展示** - 优雅的作品集展示页面 - 🔍 **SEO 优化** - 内置 SEO + 和社交媒体标签 - ⚡ **性能优化** - 图片优化、代码分割、懒加载 - 🌐 **国际化支持** - 易于扩展多语言支持 ## 🛠️ 技术栈 - **框架**: + [Astro](https://astro.build) 5.15.4 (兼容 v6) - **样式**: [Tailwind CSS](https://tailwindcss.com) + 4.1.14 - **动画**: [AOS](https://michalsnik.github.io/aos/) - **物理引擎**: + [Matter.js](https://brm.io/matter-js/) - **内容管理**: MDX - **类型检查**: TypeScript ## 📦 安装 ### 使用包管理器 + ```bash # 使用 pnpm (推荐) pnpm install # 使用 npm npm install # 使用 yarn yarn install ``` ### 环境变量配置 复制 + `.env.example` 文件为 `.env` 并填写相应的配置: ```bash cp .env.example .env ``` 编辑 `.env` 文件,填入你的配置: ```env # + 站点 URL(可选,但有默认值 https://your-domain.com) # 首次部署可以不设置,但建议尽快设置正确的域名以优化 SEO + PUBLIC_SITE_URL=https://your-domain.com # 分析工具(可选) PUBLIC_GA4_ID=your-google-analytics-id + PUBLIC_UMAMI_ID=your-umami-id ``` > **注意**:`PUBLIC_SITE_URL` 如果没有设置,会使用默认值 + `https://your-domain.com`。虽然不会报错,但建议在部署后尽快设置正确的域名,以确保 sitemap、RSS feed 和 SEO 元标签正常工作。 ## 🚀 开发 + ```bash # 启动开发服务器 npm run dev # 或 pnpm dev # 访问 http://localhost:4321 ``` ## 📦 构建 ```bash # + 构建生产版本 npm run build # 预览构建结果 npm run preview ``` ## 📁 项目结构 ``` ├── public/ # 静态资源 │ ├── assets/ + # 图片、视频等资源 │ └── favicon.png # 网站图标 ├── src/ │ ├── assets/ # 源代码资源 │ ├── collections/ # + 数据集合(作品、经历等) │ ├── components/ # Astro 组件 │ │ ├── cards/ # 卡片组件 │ │ ├── sections/ # 页面区块组件 │ │ ├── + ui/ # UI 组件 │ │ └── widgets/ # 小部件 │ ├── config/ # 配置文件 │ ├── content/ # MDX 博客内容 │ ├── layouts/ # + 布局组件 │ ├── pages/ # 页面路由 │ ├── scripts/ # 脚本文件 │ └── styles/ # 样式文件 ├── astro.config.mjs # Astro + 配置 ├── tailwind.config.mjs # Tailwind 配置 └── package.json # 项目依赖 ``` ## 🎨 自定义配置 ### 修改网站信息 编辑 + `src/config/site.js` 文件,修改网站的基本信息: ```javascript export const siteConfig = { title: "Your + Portfolio", author: "Your Name", url: "https://your-domain.com", // ... 更多配置 }; ``` ### 修改主题颜色 编辑 + `src/styles/global.css` 文件中的 CSS 变量: ```css @theme { --color-primary: #2d6dc3; + --color-primary-dark: #3b7bd9; /* ... 更多颜色变量 */ } ``` ### 添加作品 在 `src/collections/works.json` + 中添加你的作品信息。 ### 添加博客文章 在 `src/content/post/` 目录下创建新的 MDX 文件。项目使用 Astro v5 Content Layer API 和 + `glob` 加载器来管理内容集合,确保与 Astro v6 兼容。 **注意**:此模板已完全升级到 Astro v5.15 标准,并兼容 Astro v6: - ✅ 使用新的 Content + Layer API (`glob` 加载器) - ✅ 使用 `entry.id` 替代已弃用的 `entry.slug` - ✅ 使用 `render(entry)` 替代已弃用的 + `entry.render()` - ✅ 使用 `import.meta.env` 替代 `process.env` - ✅ 使用 `import.meta.glob()` 替代已弃用的 + `Astro.glob()` - ✅ 所有 `getStaticPaths()` 的 params 都是字符串类型(v6 要求) ## 使用素材 - **Programming + Sticker**: [Figma + rogramming-sticker-1-0](https://www.figma.com/community/file/1392100849031958853/programming-sticker-1-0) + - **Bento Cards**:[Figma Bento Cards](https://www.figma.com/community/file/1231184483170475120) - + **Social Cards**: [Figma Bento + 2.5d](https://www.figma.com/community/file/1232620929235403629/bento-2-5d-widgets) ## 📧 联系方式 - + **作者**: Ricoui - **博客**: [ricoui.com](https://github.com/ricocc) - **邮箱**: hello@ricoui.com - + **Twitter**: [@ricouii](https://x.com/ricouii) - **GitHub**: [@ricocc](https://github.com/ricocc) + ## 💡 其他产品 - **Rico Blog** - 开源 : + [https://github.com/ricocc/public-portfolio-site](https://github.com/ricocc/public-portfolio-site) + - **OG Gallery**: [ricoog.com](https://ricoog.com/) ## 🙏 致谢 - [Astro](https://astro.build) - + 优秀的静态站点生成器 - [Tailwind CSS](https://tailwindcss.com) - 实用优先的 CSS 框架 ## 📝 更新日志 ### 最新更新 (2024) - + **升级到 Astro 5.15.4** - 完全符合 Astro v5.15 标准,兼容 Astro v6 - **内容集合升级** - 使用新的 Content Layer + API,移除了所有旧版 API - **API 现代化** - 所有已弃用的 API 已更新为最新标准 - **性能优化** - 优化了构建和运行时性能 查看 + [CHANGELOG.md](CHANGELOG.md) 了解完整版本更新历史。 ## 关于作者 我是Rico,网页/UI设计师,热衷于做些有趣和创意的作品。拥有 UI/UX + 设计工作经验,目前专注于网页设计和视觉落地,以及开发项目探索。我平时在博客Rico's Blog更新内容。也可以关注我的小红书 + [@Rico的设计漫想](https://www.xiaohongshu.com/user/profile/5f2b6903000000000101f51f) 和 推特 + [@ricouii](https://x.com/ricouii). 或者添加我的微信,交个朋友 ricocc-wechat ## 💜 + 支持作者 如果觉得有所帮助的话,一点点支持就可以大大激励创作者的热情,感谢! ricocc-wechat --- ⭐ + 如果这个项目对你有帮助,请给一个 Star! + + + # Rico Portfolio - Designer Portfolio Website > [中文文档](README-zh.md) | English + A modern, high-performance designer portfolio website template built with Astro. Features a retro + blue theme, dark mode support, beautiful animations, and excellent user experience. + ![Astro](https://img.shields.io/badge/Astro-5.15.4-FF5D01?logo=astro&logoColor=white) ![Tailwind + CSS](https://img.shields.io/badge/Tailwind-4.1.14-38B2AC?logo=tailwind-css&logoColor=white) + ![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg) ## ✨ Features - 🚀 **Built + with Astro** - Fast and lightweight static site generator - 🎨 **Modern Design** - Retro blue + theme with dark/light mode toggle - 📱 **Fully Responsive** - Adapts to all device sizes - 🎭 + **Beautiful Animations** - Using AOS and custom animation effects - 📝 **Blog System** - Supports + MDX format blog posts - 🎯 **Portfolio Showcase** - Elegant portfolio showcase pages - 🔍 **SEO + Optimized** - Built-in SEO and social media tags - ⚡ **Performance Optimized** - Image + optimization, code splitting, lazy loading - 🌐 **i18n Support** - Easy to extend for + multi-language support ## 🛠️ Tech Stack - **Framework**: [Astro](https://astro.build) 5.15.4 (v6 + compatible) - **Styling**: [Tailwind CSS](https://tailwindcss.com) 4.1.14 - **Animations**: + [AOS](https://michalsnik.github.io/aos/) - **Physics Engine**: + [Matter.js](https://brm.io/matter-js/) - **Content Management**: MDX - **Type Checking**: + TypeScript ## 📦 Installation ### Using Package Manager ```bash # Using pnpm (recommended) pnpm + install # Using npm npm install # Using yarn yarn install ``` ### Environment Variables + Configuration Copy `.env.example` to `.env` and fill in the corresponding configuration: ```bash + cp .env.example .env ``` Edit the `.env` file and fill in your configuration: ```env # Site URL + (optional, but has default value https://your-domain.com) # You can skip this on first deployment, + but it's recommended to set the correct domain as soon as possible to optimize SEO + PUBLIC_SITE_URL=https://your-domain.com # Analytics (optional) + PUBLIC_GA4_ID=your-google-analytics-id PUBLIC_UMAMI_ID=your-umami-id ``` > **Note**: If + `PUBLIC_SITE_URL` is not set, it will use the default value `https://your-domain.com`. While it + won't cause errors, it's recommended to set the correct domain after deployment to ensure sitemap, + RSS feed, and SEO meta tags work properly. ## 🚀 Development ```bash # Start development server + npm run dev # or pnpm dev # Visit http://localhost:4321 ``` ## 📦 Build ```bash # Build for + production npm run build # Preview build result npm run preview ``` ## 📁 Project Structure ``` + ├── public/ # Static assets │ ├── assets/ # Images, videos, etc. │ └── favicon.png # Site favicon + ├── src/ │ ├── assets/ # Source assets │ ├── collections/ # Data collections (works, experiences, + etc.) │ ├── components/ # Astro components │ │ ├── cards/ # Card components │ │ ├── sections/ # + Section components │ │ ├── ui/ # UI components │ │ └── widgets/ # Widgets │ ├── config/ # + Configuration files │ ├── content/ # MDX blog content │ ├── layouts/ # Layout components │ ├── + pages/ # Page routes │ ├── scripts/ # Script files │ └── styles/ # Style files ├── + astro.config.mjs # Astro configuration ├── tailwind.config.mjs # Tailwind configuration └── + package.json # Project dependencies ``` ## 🎨 Customization ### Modify Site Information Edit the + `src/config/site.js` file to modify the site's basic information: ```javascript export const + siteConfig = { title: "Your Portfolio", author: "Your Name", url: "https://your-domain.com", // + ... more configuration }; ``` ### Modify Theme Colors Edit the CSS variables in the + `src/styles/global.css` file: ```css @theme { --color-primary: #2d6dc3; --color-primary-dark: + #3b7bd9; /* ... more color variables */ } ``` ### Add Works Add your work information in + `src/collections/works.json`. ### Add Blog Posts Create new MDX files in the `src/content/post/` + directory. The project uses Astro v5 Content Layer API with `glob` loader for content collections, + ensuring compatibility with Astro v6. **Note**: This template has been fully upgraded to Astro + v5.15 standards and is compatible with Astro v6: - ✅ Uses new Content Layer API (`glob` loader) - + ✅ Uses `entry.id` instead of deprecated `entry.slug` - ✅ Uses `render(entry)` instead of + deprecated `entry.render()` - ✅ Uses `import.meta.env` instead of `process.env` - ✅ Uses + `import.meta.glob()` instead of deprecated `Astro.glob()` - ✅ All `getStaticPaths()` params are + string type (v6 requirement) ## Figma Assets - **Programming Sticker**: [Figma + rogramming-sticker-1-0](https://www.figma.com/community/file/1392100849031958853/programming-sticker-1-0) + - **Bento Cards**:[Figma Bento Cards](https://www.figma.com/community/file/1231184483170475120) - + **Social Cards**: [Figma Bento + 2.5d](https://www.figma.com/community/file/1232620929235403629/bento-2-5d-widgets) ## 📧 Contact - + **Author**: Ricoui - **Blog**: [ricoui.com](https://github.com/ricocc) - **Email**: + hello@ricoui.com - **Twitter**: [@ricouii](https://x.com/ricouii) - **GitHub**: + [@ricocc](https://github.com/ricocc) ## 💡 Other Products - **Rico Blog** - Open Source: + [https://github.com/ricocc/public-portfolio-site](https://github.com/ricocc/public-portfolio-site) + - **OG Gallery**: [ricoog.com](https://ricoog.com/) ## 🙏 Acknowledgments - + [Astro](https://astro.build) - Excellent static site generator - [Tailwind + CSS](https://tailwindcss.com) - Utility-first CSS framework - All developers who contributed to + this project ## About the Author I'm Rico, a web/UI designer passionate about creating fun and + creative work. I have experience in UI/UX design and am currently focused on web design, visual + implementation, and exploring development projects. I regularly update my blog on Rico's Blog. You can also follow me on + Xiaohongshu [@Rico的设计漫想](https://www.xiaohongshu.com/user/profile/5f2b6903000000000101f51f) 和 X + [@ricouii](https://x.com/ricouii). Or add me on WeChat—let’s be friends. ricocc-wechat ## 💜 Support the Author If you’ve found this + helpful, even a small contribution can greatly encourage creators. Thank you! ricocc-wechat + + + Buy Me a Coffee at ko-fi.com + ## 📝 Changelog ### Latest Updates (2024) - **Upgraded + to Astro 5.15.4** - Fully compliant with Astro v5.15 standards and compatible with Astro v6 - + **Content Collections Upgrade** - Using new Content Layer API, all legacy APIs removed - **API + Modernization** - All deprecated APIs updated to latest standards - **Performance Optimization** - + Optimized build and runtime performance ⭐ If this project helps you, please give it a Star! + + + --- + import Button from "../components/ui/Button.astro"; import { siteConfig } from "@/config/site"; + import Layout from "@/layouts/Layout.astro"; import FeaturedWork from + "../components/sections/FeaturedWork.astro"; import SocialCard from + "@/components/cards/SocialCard.astro"; import AnimatedText from + "@/components/ui/AnimatedText.astro"; import Explore from "@/components/sections/Explore.astro"; + --- +
+
+
+ +

+ +

+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ + +
+
+
+
+ +
+ +
+ +
+ +
+
+
+ + + name: Build and Push Docker Image + + on: + push: + branches: + - main + - master + + env: + REGISTRY: "gitea.vorpax.dev" + IMAGE_NAME: "vorpax_admin/vorpax-portfolio-main" + + jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha,prefix= + type=raw,value=latest,enable=${{ gitea.ref == format('refs/heads/{0}', 'main') || gitea.ref == + format('refs/heads/{0}', 'master') }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + + // Get site URL from environment variable, use default value if not + set const SITE_URL = import.meta.env.PUBLIC_SITE_URL || 'https://vorpax.dev'; export const + siteConfig = { title: "Alexandre | Vorpax", author: "vorpax", url: SITE_URL, mail: "contact at + vorpax dot dev", resume: "/assets/resume.pdf", utm: { source: `${SITE_URL}`, medium: "referral", + campaign: "navigation", }, meta: { title: "Alexandre | Business x Engineering x Infrastructure", + description: "M1 HEC Paris | President HEC IA | Freelance Fullstack Developer | Infrastructure + Enthusiast", keywords: "portfolio, developer, infrastructure, homelab, HEC Paris, engineering, + fullstack", image: `${SITE_URL}/og.jpg`, twitterHandle: "", }, social: { github: + "https://github.com/vorpax", linkedin: "https://www.linkedin.com/in/alexandre-houard-686960279/", + gitea: "https://gitea.vorpax.dev/vorpax" }, }; // Footer social links export const socialLinks = [ + { name: 'Github', url: 'https://github.com/vorpax', icon: `` }, { name: 'LinkedIn', url: + 'https://www.linkedin.com/in/alexandre-houard-686960279/', icon: `` }, { name: 'RSS', url: '/rss.xml', icon: `` }, ]; + + \ No newline at end of file diff --git a/src/collections/featuredwork.json b/src/collections/featuredwork.json index 768eb87..ce5c9be 100644 --- a/src/collections/featuredwork.json +++ b/src/collections/featuredwork.json @@ -20,7 +20,7 @@ "AI", "Workshops" ], - "image": "/assets/hec-ia.png", + "image": "/assets/hecia-banner.png", "url": "/work/hec-ia", "isShow": true }, diff --git a/src/collections/menu.json b/src/collections/menu.json index a7bf79c..1d8a748 100644 --- a/src/collections/menu.json +++ b/src/collections/menu.json @@ -10,5 +10,9 @@ { "name": "About", "url": "/about" + }, + { + "name": "Wiki", + "url": "https://wiki.vorpax.dev" } ] diff --git a/src/collections/works.json b/src/collections/works.json index 768eb87..ce5c9be 100644 --- a/src/collections/works.json +++ b/src/collections/works.json @@ -20,7 +20,7 @@ "AI", "Workshops" ], - "image": "/assets/hec-ia.png", + "image": "/assets/hecia-banner.png", "url": "/work/hec-ia", "isShow": true }, diff --git a/src/components/sections/Footer.astro b/src/components/sections/Footer.astro index bb82bb7..4e16ff9 100644 --- a/src/components/sections/Footer.astro +++ b/src/components/sections/Footer.astro @@ -18,6 +18,18 @@ import { siteConfig, socialLinks } from "@/config/site.js"; {siteConfig.author || " "}

+ + Infrastructure Status + diff --git a/src/pages/index.astro b/src/pages/index.astro index 62a8e9f..28a03bc 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -55,9 +55,9 @@ import Explore from "@/components/sections/Explore.astro"; />
- +
View GitHub +
diff --git a/src/pages/work/freelance.astro b/src/pages/work/freelance.astro index 921ef0d..4be5e03 100644 --- a/src/pages/work/freelance.astro +++ b/src/pages/work/freelance.astro @@ -25,9 +25,10 @@ import { siteConfig } from "@/config/site.js";

Stack

    +
  • Frontend: NextJS, TypeScript, TailwindCSS
  • Backend: Python, Golang
  • -
  • Automation: Bash, scripting
  • Infrastructure: Docker, Linux
  • +
  • Automation: Bash, scripting

Services

diff --git a/src/pages/work/hec-ia.astro b/src/pages/work/hec-ia.astro index 29255be..7c789f2 100644 --- a/src/pages/work/hec-ia.astro +++ b/src/pages/work/hec-ia.astro @@ -24,7 +24,7 @@ import SeparatorLine from "@/components/elements/SeparatorLine.astro";

Mission

- Making AI accessible to business students. Technical workshops designed for non-technical profiles. + Leading HEC's AI student organization (30+ members) to make AI accessible to business students. Technical workshops designed for non-technical profiles.

What We Do

diff --git a/src/pages/work/homelab.astro b/src/pages/work/homelab.astro index f1730e5..b541237 100644 --- a/src/pages/work/homelab.astro +++ b/src/pages/work/homelab.astro @@ -32,8 +32,10 @@ import { ArrowRight } from "@lucide/astro";

Current Stack

  • Virtualization: Proxmox VE
  • -
  • Orchestration: Kubernetes
  • +
  • Orchestration: K3S (lightweight Kubernetes)
  • Containers: Docker
  • +
  • PKI: step-ca (internal certificate authority)
  • +
  • IaC: Terraform, Ansible
  • Monitoring: Grafana, Prometheus, InfluxDB
  • Reverse Proxy: Caddy, FRP
  • Auth: Authentik (OIDC)