├── public
├── robots.txt
├── zaggonaut.png
├── michael-dam-unsplash.webp
└── favicon.svg
├── images
└── README.png
├── .vscode
├── settings.json
├── extensions.json
└── launch.json
├── src
├── components
│ ├── common
│ │ ├── Section.astro
│ │ └── Anchor.astro
│ ├── home
│ │ ├── Hero.astro
│ │ ├── FeaturedArticles.astro
│ │ └── FeaturedProjects.astro
│ ├── ArticleSnippet.astro
│ ├── Prose.astro
│ ├── ProjectSnippet.astro
│ ├── ThemeToggle.astro
│ ├── Header.astro
│ └── Footer.astro
├── lib
│ ├── types.ts
│ └── utils.ts
├── pages
│ ├── projects
│ │ ├── [id].astro
│ │ └── index.astro
│ ├── blog
│ │ ├── [id].astro
│ │ └── index.astro
│ ├── 404.astro
│ └── index.astro
├── layouts
│ ├── Layout.astro
│ ├── BlogLayout.astro
│ └── ProjectLayout.astro
├── styles
│ └── global.css
└── content.config.ts
├── astro.config.mjs
├── tsconfig.json
├── .gitignore
├── package.json
├── content
├── projects
│ └── zaggonaut.md
├── configuration.toml
└── blogs
│ └── html-intro.md
├── LICENSE.md
└── README.md
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
--------------------------------------------------------------------------------
/images/README.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RATIU5/zaggonaut/HEAD/images/README.png
--------------------------------------------------------------------------------
/public/zaggonaut.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RATIU5/zaggonaut/HEAD/public/zaggonaut.png
--------------------------------------------------------------------------------
/public/michael-dam-unsplash.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RATIU5/zaggonaut/HEAD/public/michael-dam-unsplash.webp
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "Frontmatter",
4 | "opsz",
5 | "Zaggonaut"
6 | ]
7 | }
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/common/Section.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const { class: className } = Astro.props;
3 | ---
4 |
5 |
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/astro.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { defineConfig } from "astro/config";
3 | import tailwindcss from "@tailwindcss/vite";
4 |
5 | // https://astro.build/config
6 | export default defineConfig({
7 | vite: {
8 | plugins: [tailwindcss()],
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionEntry } from "astro:content";
2 |
3 | export type ArticleFrontmatter = CollectionEntry<"blog">["data"] & {
4 | url: string;
5 | };
6 |
7 | export type ProjectFrontmatter = CollectionEntry<"project">["data"] & {
8 | url: string;
9 | };
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "include": [".astro/types.d.ts", "**/*"],
4 | "exclude": ["dist"],
5 | "compilerOptions": {
6 | "allowImportingTsExtensions": true,
7 | "allowJs": true,
8 | "strictNullChecks": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 |
4 | # generated types
5 | .astro/
6 |
7 | # dependencies
8 | node_modules/
9 |
10 | # logs
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
23 | # jetbrains setting folder
24 | .idea/
25 |
26 | pnpm-lock.yaml
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zaggonaut",
3 | "type": "module",
4 | "private": true,
5 | "version": "0.0.1",
6 | "packageManager": "pnpm@10.6.0",
7 | "scripts": {
8 | "dev": "astro dev",
9 | "build": "astro build",
10 | "preview": "astro preview",
11 | "astro": "astro",
12 | "preinstall": "npx only-allow pnpm"
13 | },
14 | "dependencies": {
15 | "@astrojs/tailwind": "^6.0.2",
16 | "@tailwindcss/typography": "^0.5.16",
17 | "@tailwindcss/vite": "^4.1.11",
18 | "astro": "^5.10.1",
19 | "tailwindcss": "^4.1.11",
20 | "toml": "^3.0.0"
21 | },
22 | "devDependencies": {
23 | "@types/node": "^22.15.33"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/pages/projects/[id].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection, render } from "astro:content";
3 | import ProjectLayout from "../../layouts/ProjectLayout.astro";
4 |
5 | export async function getStaticPaths() {
6 | const projects = await getCollection("project");
7 | return projects.map((project) => ({
8 | params: { id: project.data.slug },
9 | props: {
10 | project: {
11 | ...project,
12 | data: {
13 | ...project.data,
14 | url: `/projects/${project.data.slug}`,
15 | },
16 | },
17 | },
18 | }));
19 | }
20 |
21 | const { project } = Astro.props;
22 | const { Content } = await render(project);
23 | ---
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/components/home/Hero.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getConfigurationCollection } from "../../lib/utils";
3 |
4 | const { data: config } = await getConfigurationCollection();
5 | ---
6 |
7 |
8 |
13 |
14 |
17 | {config.hero.title}
18 |
19 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/components/ArticleSnippet.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { processArticleDate } from "../lib/utils";
3 | import Anchor from "./common/Anchor.astro";
4 |
5 | export type Props = {
6 | title: string;
7 | description: string;
8 | url: string;
9 | duration?: string;
10 | timestamp?: Date;
11 | };
12 |
13 | const { title, description, url, duration, timestamp } = Astro.props;
14 | ---
15 |
16 |
17 |
18 | {title}
19 |
20 |
21 | {description}
22 |
23 |
26 | {timestamp ?
{processArticleDate(timestamp)}
: null}
27 | {duration ?
{duration}
: null}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/pages/blog/[id].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection, render } from "astro:content";
3 | import BlogLayout from "../../layouts/BlogLayout.astro";
4 | import { getConfigurationCollection } from "../../lib/utils";
5 |
6 | export async function getStaticPaths() {
7 | const posts = await getCollection("blog");
8 | const { data: config } = await getConfigurationCollection();
9 | return posts.map((article) => ({
10 | params: { id: article.data.slug },
11 | props: {
12 | article: {
13 | ...article,
14 | data: {
15 | ...article.data,
16 | url: `${config.site.baseUrl}/blog/${article.data.slug}`,
17 | },
18 | },
19 | },
20 | }));
21 | }
22 |
23 | const { article } = Astro.props;
24 | const { Content } = await render(article);
25 | ---
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/common/Anchor.astro:
--------------------------------------------------------------------------------
1 | ---
2 | export type Props = {
3 | url: string;
4 | class?: string;
5 | external?: boolean;
6 | "aria-label"?: string;
7 | };
8 |
9 | const { url, external, class: className } = Astro.props;
10 | ---
11 |
12 |
21 |
22 | {
23 | external ? (
24 |
29 |
34 |
35 | ) : null
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/content/projects/zaggonaut.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Zaggonaut
3 | slug: zaggonaut
4 | description: A retro-inspired theme for Astro, built using TypeScript, TailwindCSS, and Astro.
5 | longDescription: Zaggonaut is a retro-inspired theme for Astro, built using TypeScript, TailwindCSS, and Astro.
6 | cardImage: "https://zaggonaut.dev/michael-dam-unsplash.webp"
7 | tags: ["astro", "portfolio", "open-source"]
8 | githubUrl: https://github.com/RATIU5/zaggonaut
9 | timestamp: 2025-02-24T02:39:03+00:00
10 | featured: true
11 | ---
12 |
13 | ## The Details
14 |
15 | Zaggonaut is a retro-inspired theme for Astro, built using TypeScript, TailwindCSS, and Astro. Use this theme to power your own personal website, blog, or portfolio with flexibility and customization.
16 |
17 | ## The Features
18 |
19 | - Content Collections
20 | - Dark & light mode
21 | - Customizable colors
22 | - 100 / 100 Lighthouse score
23 | - Fully accessible
24 | - Fully responsive
25 | - Type-safe
26 |
27 | ## The Future
28 |
29 | Check out [the theme website](https://zaggonaut.dev) to see it in action!
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 RATIU5
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/components/home/FeaturedArticles.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { ArticleFrontmatter } from "../../lib/types";
3 | import { getConfigurationCollection } from "../../lib/utils";
4 | import ArticleSnippet from "../ArticleSnippet.astro";
5 | import Anchor from "../common/Anchor.astro";
6 |
7 | type Props = {
8 | featuredArticles: Array;
9 | }
10 |
11 |
12 | const { featuredArticles } = Astro.props;
13 | const { data: config } = await getConfigurationCollection();
14 | ---
15 |
16 |
17 |
{config.texts.articlesName}
18 |
{config.texts.viewAll}
19 |
20 |
--------------------------------------------------------------------------------
/src/components/home/FeaturedProjects.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { ProjectFrontmatter } from "../../lib/types";
3 | import { getConfigurationCollection } from "../../lib/utils";
4 | import Anchor from "../common/Anchor.astro";
5 | import ProjectSnippet from "../ProjectSnippet.astro";
6 |
7 | type Props = {
8 | featuredProjects: Array;
9 | }
10 |
11 | const { featuredProjects } = Astro.props;
12 | const { data: config } = await getConfigurationCollection();
13 | ---
14 |
15 |
16 |
{config.texts.projectsName}
17 |
{config.texts.viewAll}
18 |
19 |
--------------------------------------------------------------------------------
/src/components/Prose.astro:
--------------------------------------------------------------------------------
1 | ---
2 | type Props = {
3 | class?: string;
4 | };
5 |
6 | const { class: customClass } = Astro.props;
7 | ---
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/ProjectSnippet.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Anchor from "./common/Anchor.astro";
3 |
4 | export type Props = {
5 | title: string;
6 | description: string;
7 | url: string;
8 | githubUrl?: string;
9 | liveUrl?: string;
10 | tags: string[];
11 | };
12 |
13 | const { title, description, url, githubUrl, liveUrl, tags } = Astro.props;
14 | ---
15 |
16 |
17 |
18 |
19 | {title}
20 |
21 |
22 | {
23 | githubUrl ? (
24 |
25 | GitHub
26 |
27 | ) : null
28 | }
29 | {
30 | liveUrl ? (
31 |
32 | Live
33 |
34 | ) : null
35 | }
36 |
37 |
38 |
39 | {description}
40 |
41 |
42 | {
43 | tags.map((tag) => (
44 |
45 | {tag}
46 |
47 | ))
48 | }
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/layouts/Layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Footer from "../components/Footer.astro";
3 | import Header from "../components/Header.astro";
4 | import "../styles/global.css";
5 | ---
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { getCollection, type CollectionEntry } from "astro:content";
2 |
3 | /**
4 | * Shortens a string by removing words at the end until it fits within a certain length.
5 | * @param content the content to shorten
6 | * @param maxLength the maximum length of the shortened content (default is 20)
7 | * @returns a shortened version of the content
8 | */
9 | export const getShortDescription = (content: string, maxLength = 20) => {
10 | const splitByWord = content.split(" ");
11 | const length = splitByWord.length;
12 | return length > maxLength ? splitByWord.slice(0, maxLength).join(" ") + "..." : content;
13 | };
14 |
15 | /**
16 | * Processes the date of an article and returns a string representing the processed date.
17 | * @param timestamp the timestamp to process
18 | * @returns a string representing the processed timestamp
19 | */
20 | export const processArticleDate = (date: Date) => {
21 | const monthSmall = date.toLocaleString("default", { month: "short" });
22 | const day = date.getDate();
23 | const year = date.getFullYear();
24 | return `${monthSmall} ${day}, ${year}`;
25 | };
26 |
27 | let configCache: CollectionEntry<'configuration'> | null = null;
28 |
29 | /**
30 | * Retrieves the configuration collection entry from the content directory.
31 | * It checks if the configuration is already cached to avoid multiple reads.
32 | * There can only be one configuration file, so it throws an error if there are multiple or none.
33 | * @returns the configuration collection entry
34 | */
35 | export const getConfigurationCollection = async (): Promise> => {
36 | if (configCache) return configCache;
37 |
38 | const configs = await getCollection("configuration");
39 | if (configs.length !== 1) {
40 | throw new Error("Configuration file not found or multiple configuration files present.");
41 | }
42 | configCache = configs[0];
43 | return configs[0];
44 | }
--------------------------------------------------------------------------------
/src/components/ThemeToggle.astro:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | ---
4 |
5 |
6 |
12 |
17 |
22 |
23 |
24 |
25 |
38 |
--------------------------------------------------------------------------------
/src/pages/404.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Section from "../components/common/Section.astro";
3 | import Layout from "../layouts/Layout.astro";
4 | import { getConfigurationCollection } from "../lib/utils";
5 |
6 | const { data: config } = await getConfigurationCollection();
7 | ---
8 |
9 |
10 |
11 | {config.notFoundMeta.title}
12 |
16 |
17 |
21 | {config.notFoundMeta.keywords && (
22 |
23 | )}
24 | {config.notFoundMeta.cardImage && (
25 |
26 | )}
27 |
28 |
29 |
30 |
34 | {config.notFoundMeta.cardImage && (
35 |
36 | )}
37 |
38 |
39 |
40 |
41 |
42 |
43 | Well, this is awkward
44 |
45 | 404 - Page not found
46 |
47 | It seems that this page does not exist. If you want to return to safety, click here to go home .
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/content/configuration.toml:
--------------------------------------------------------------------------------
1 | # Due to how Astro handles type inference with this structure,
2 | # we need to prefix all configuration keys with an underscore.
3 |
4 | [_.site]
5 | baseUrl = "http://zaggonaut.dev"
6 |
7 | [_.globalMeta]
8 | title = "Zaggonaut"
9 | description = "Retro-Inspired Theme & Built for Astro"
10 | longDescription = "Zaggonaut is a retro-inspired theme for Astro, built using TypeScript, TailwindCSS, and Astro."
11 | cardImage = "https://zaggonaut.dev/michael-dam-unsplash.webp"
12 | keywords = ["web development", "design", "technology"]
13 |
14 | [_.blogMeta]
15 | title = "Zaggonaut Blog"
16 | description = "Practical wisdom, unfiltered thoughts, and hot takes."
17 | longDescription = "Web development, tech trends, and the occasional programming mishap."
18 | cardImage = "https://zaggonaut.dev/michael-dam-unsplash.webp"
19 | keywords = ["web development", "design", "technology"]
20 |
21 | [_.projectMeta]
22 | title = "Zaggonaut Projects"
23 | description = "A list of my web development projects and developer tools."
24 | longDescription = "All of my projects, including both frontend and full-stack applications."
25 | cardImage = "https://zaggonaut.dev/michael-dam-unsplash.webp"
26 | keywords = ["web development", "design", "technology"]
27 |
28 | [_.notFoundMeta]
29 | title = "Zaggonaut 404"
30 | description = "The page you are looking for does not exist."
31 | longDescription = "The page you are looking for does not exist. Maybe it never existed. Spooky."
32 | cardImage = "https://zaggonaut.dev/michael-dam-unsplash.webp"
33 |
34 | [_.hero]
35 | title = "Zaggonaut"
36 | subtitle = "Retro-Inspired Theme & Built for Astro"
37 | image = "https://zaggonaut.dev/michael-dam-unsplash.webp"
38 | ctaText = "View Projects"
39 | ctaUrl = "/projects"
40 |
41 | [_.personal]
42 | name = "Zaggonaut"
43 | githubProfile = "https://github.com/RATIU5/zaggonaut"
44 | twitterProfile = "https://twitter.com"
45 | linkedinProfile = "https://linkedin.com/"
46 |
47 | [_.texts]
48 | articlesName = "Articles"
49 | projectsName = "Projects"
50 | viewAll = "View All"
51 | noArticles = "No articles found."
52 | noProjects = "No projects found."
53 |
54 | [_.menu]
55 | home = "/"
56 | projects = "/projects"
57 | blog = "/blog"
--------------------------------------------------------------------------------
/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from "../layouts/Layout.astro";
3 | import Hero from "../components/home/Hero.astro";
4 | import Section from "../components/common/Section.astro";
5 | import FeaturedProjects from "../components/home/FeaturedProjects.astro";
6 | import FeaturedArticles from "../components/home/FeaturedArticles.astro";
7 | import { getConfigurationCollection } from "../lib/utils";
8 | import { getCollection } from "astro:content";
9 |
10 |
11 | const { data: config } = await getConfigurationCollection();
12 |
13 | const featuredProjects = (await getCollection("project", (e) => e.data.featured)).map((e) => ({
14 | ...e.data,
15 | url: `${config.site.baseUrl}/projects/${e.data.slug}`,
16 | }));
17 | const featuredArticles = (await getCollection("blog", (e) => e.data.featured)).map((e) => ({
18 | ...e.data,
19 | url: `${config.site.baseUrl}/blog/${e.data.slug}`
20 | }));
21 | ---
22 |
23 |
24 |
25 | {config.globalMeta.title}
26 |
30 |
31 |
35 | {config.globalMeta.keywords && (
36 |
37 | )}
38 | {config.globalMeta.cardImage && (
39 |
40 | )}
41 |
42 |
43 |
44 |
48 | {config.globalMeta.cardImage && (
49 |
50 | )}
51 |
52 |
53 |
54 |
55 |
58 |
61 |
64 |
65 |
--------------------------------------------------------------------------------
/src/pages/blog/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from "astro:content";
3 | import { getConfigurationCollection } from "../../lib/utils";
4 | import Layout from "../../layouts/Layout.astro";
5 | import ArticleSnippet from "../../components/ArticleSnippet.astro";
6 | import Section from "../../components/common/Section.astro";
7 |
8 | const articles = (await getCollection("blog")).sort(
9 | (a, b) => b.data.timestamp.valueOf() - a.data.timestamp.valueOf(),
10 | );
11 |
12 | const { data: config } = await getConfigurationCollection();
13 | ---
14 |
15 |
16 |
17 | {config.blogMeta.title}
18 |
22 |
23 |
27 | {config.blogMeta.keywords && (
28 |
29 | )}
30 | {config.blogMeta.cardImage && (
31 |
32 | )}
33 |
34 |
35 |
36 |
40 | {config.blogMeta.cardImage && (
41 |
42 | )}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {config.texts.articlesName}
51 |
52 |
53 |
54 | {
55 | articles.map((article) => (
56 |
57 |
64 |
65 | ))
66 | }
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/layouts/BlogLayout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getConfigurationCollection, processArticleDate } from "../lib/utils";
3 | import type { ArticleFrontmatter } from "../lib/types";
4 | import Section from "../components/common/Section.astro";
5 | import Prose from "../components/Prose.astro";
6 | import Layout from "./Layout.astro";
7 |
8 | type Props = { frontmatter: ArticleFrontmatter };
9 |
10 | const { frontmatter } = Astro.props;
11 | const articleDate = processArticleDate(frontmatter.timestamp);
12 | const { data: config } = await getConfigurationCollection();
13 | ---
14 |
15 |
16 |
17 | {`${frontmatter.title} • ${config.personal.name}`}
18 |
22 |
26 |
27 | {
28 | frontmatter.tags && (
29 |
30 | )
31 | }
32 | {
33 | frontmatter.cardImage && (
34 |
35 | )
36 | }
37 |
41 |
42 |
46 |
47 | {
48 | frontmatter.cardImage && (
49 |
50 | )
51 | }
52 |
53 |
54 |
58 |
59 |
60 |
61 |
64 | {frontmatter.title}
65 |
66 |
67 | {articleDate}
68 | {frontmatter.readTime} min
69 |
70 |
71 |
72 |
73 |
74 | ~{config.personal.name}
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/pages/projects/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from "astro:content";
3 | import { getConfigurationCollection } from "../../lib/utils";
4 | import Section from "../../components/common/Section.astro";
5 | import ProjectSnippet from "../../components/ProjectSnippet.astro";
6 | import Layout from "../../layouts/Layout.astro";
7 |
8 | const projects = (await getCollection("project")).sort(
9 | (a, b) => b.data.timestamp.valueOf() - a.data.timestamp.valueOf(),
10 | );
11 |
12 | const { data: config } = await getConfigurationCollection();
13 | ---
14 |
15 |
16 |
17 | {config.projectMeta.title}
18 |
22 |
23 |
27 | {config.projectMeta.keywords && (
28 |
29 | )}
30 | {config.projectMeta.cardImage && (
31 |
32 | )}
33 |
34 |
35 |
36 |
40 | {config.projectMeta.cardImage && (
41 |
42 | )}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
{config.texts.projectsName}
50 |
51 |
52 | {
53 | projects.map((project) => (
54 |
55 |
63 |
64 | ))
65 | }
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Zaggonaut is a retro-inspired theme for Astro, built using TypeScript, TailwindCSS, and of course, Astro.
4 |
5 | > [!NOTE]
6 | > Introducing Zaggonaut 2.0!
7 | > This is a complete rewrite of the internal content layer, making use of Astro's new Content Collections feature, among other enhancements.
8 |
9 | If you are looking for the original Zaggonaut theme, you can find it [on the v1 branch](https://github.com/RATIU5/zaggonaut/tree/v1).
10 |
11 | ## Getting Started
12 |
13 | [View the demo](https://zaggonaut.dev) or [view the source code](https://github.com/RATIU5/zaggonaut).
14 |
15 | Alternatively, you can create a new Astro project with Zaggonaut like this:
16 |
17 | ```bash
18 | # pnpm
19 | pnpm create astro@latest --template RATIU5/zaggonaut
20 | ```
21 |
22 | > [!IMPORTANT]
23 | > Currently, `pnpm` is the only supported package manager due to `npm` throwing peer-dependency conflicts.
24 |
25 | ## Features
26 |
27 | - Content Collections
28 | - Dark & light mode
29 | - Customizable colors
30 | - 100 / 100 Lighthouse score
31 | - Fully accessible
32 | - Fully responsive
33 | - Type-safe
34 | - SEO-friendly
35 |
36 | ## Customization
37 |
38 | The entire theme is fully customizable. The theme is setup a specific way to make it easy to customize.
39 |
40 | ### Colors
41 |
42 | You can customize the colors of the theme by editing the `src/styles/global.css` file.
43 | This file uses Tailwind CSS and CSS variables to customize the colors of the theme.
44 | Zaggonaut uses the following CSS variables:
45 |
46 | - `--color-zag-dark`: The dark color of the theme
47 | - `--color-zag-light`: The light color of the theme
48 | - `--color-zag-dark-muted`: The dark color of the theme with a slight opacity
49 | - `--color-zag-light-muted`: The light color of the theme with a slight opacity
50 | - `--color-zag-accent-light`: The light accent color of the theme
51 | - `--color-zag-accent-light-muted`: The light accent color of the theme with a slight opacity
52 | - `--color-zag-accent-dark`: The dark accent color of the theme
53 | - `--color-zag-accent-dark-muted`: The dark accent color of the theme with a slight opacity
54 |
55 | ### Content Customization
56 |
57 | 95% of the content you'll want to customize will be located inside the `content` directory. Let's break down the specific files/directories you may want to edit:
58 |
59 | - `content/configuration.toml`: This file contains the site configuration, such as metadata, social links, and text content.
60 |
61 | - `content/blogs/`: This directory contains your blog posts. Each post is a Markdown file with metadata in the frontmatter at the top.
62 |
63 | - `content/projects/`: This directory contains your projects. Each project is a Markdown file also with metadata in the frontmatter.
64 |
65 | ## Ready To Try?
66 |
67 | Check out [the theme website](https://zaggonaut.dev) to give it a spin!
--------------------------------------------------------------------------------
/src/layouts/ProjectLayout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getConfigurationCollection, processArticleDate } from "../lib/utils";
3 | import type { ProjectFrontmatter } from "../lib/types";
4 | import Prose from "../components/Prose.astro";
5 | import Layout from "./Layout.astro";
6 | import Section from "../components/common/Section.astro";
7 | import Anchor from "../components/common/Anchor.astro";
8 |
9 | type Props = { frontmatter: ProjectFrontmatter };
10 |
11 | const { frontmatter } = Astro.props;
12 | const { data: config } = await getConfigurationCollection();
13 | ---
14 |
15 |
16 |
17 | {`${frontmatter.title} • ${config.personal.name}`}
18 |
22 |
26 |
27 | {
28 | frontmatter.tags && (
29 |
30 | )
31 | }
32 | {
33 | frontmatter.cardImage && (
34 |
35 | )
36 | }
37 |
41 |
42 |
46 |
47 | {
48 | frontmatter.cardImage && (
49 |
50 | )
51 | }
52 |
53 |
54 |
58 |
59 |
60 |
61 |
62 | {frontmatter.title}
63 |
64 |
65 | {
66 | frontmatter.tags
67 | ? frontmatter.tags.map((tag) => (
68 |
69 | {tag}
70 |
71 | ))
72 | : null
73 | }
74 |
75 |
76 | {
77 | frontmatter.githubUrl ? (
78 |
79 | GitHub
80 |
81 | ) : null
82 | }
83 | {
84 | frontmatter.liveDemoUrl ? (
85 |
86 | Demo
87 |
88 | ) : null
89 | }
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/src/components/Header.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getConfigurationCollection } from "../lib/utils";
3 | import Anchor from "./common/Anchor.astro";
4 | import ThemeToggle from "./ThemeToggle.astro";
5 |
6 | const { data: config } = await getConfigurationCollection();
7 | ---
8 |
9 |
10 |
13 |
14 |
21 |
22 |
23 |
24 |
25 |
28 |
31 | {
32 | Object.entries(config.menu).map((i) => (
33 |
{i[0]}
34 | ))
35 | }
36 |
37 |
38 | {
39 | config.personal.githubProfile && (
40 |
44 |
51 |
52 |
53 |
54 | )
55 | }
56 |
57 |
58 |
59 |
60 |
61 |
86 |
--------------------------------------------------------------------------------
/content/blogs/html-intro.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: A Basic Introduction to HTML
3 | slug: intro-to-html
4 | description: A brief guide covering the essentials of HTML.
5 | longDescription: HTML is the foundation of all websites. This guide will walk you through creating your first simple website using HTML.
6 | cardImage: "https://zaggonaut.dev/michael-dam-unsplash.webp"
7 | tags: ["code", "html"]
8 | readTime: 4
9 | featured: true
10 | timestamp: 2024-12-18T02:39:03+00:00
11 | ---
12 |
13 | HTML (HyperText Markup Language) is the foundation of all websites. Even with modern frameworks and tools, understanding HTML basics remains essential for web development. This guide will walk you through creating your first simple website using HTML.
14 |
15 | ## Understanding HTML Basics
16 |
17 | HTML uses elements enclosed in tags to structure content. Most elements have opening and closing tags that wrap around content:
18 |
19 | ```html
20 | Content goes here
21 | ```
22 |
23 | ## Essential Tools
24 |
25 | To get started, you'll need:
26 | - A text editor (like VS Code, Sublime Text, or even Notepad)
27 | - A web browser to view your website
28 |
29 | ## Creating Your First HTML Page
30 |
31 | 1. Open your text editor and create a new file
32 | 2. Save it as "index.html"
33 | 3. Add the following code:
34 |
35 | ```html
36 |
37 |
38 |
39 |
40 |
41 | My First Website
42 |
43 |
44 | Welcome to My Website
45 | This is my first website created with HTML.
46 |
47 |
48 | ```
49 |
50 | 4. Save the file and open it in your browser
51 |
52 | ## Understanding the Structure
53 |
54 | - ``: Tells browsers you're using HTML5
55 | - ``: The root element of your page
56 | - ``: Contains meta-information about your document
57 | - ``: Sets the page title shown in browser tabs
58 | - ` `: Contains all visible content
59 |
60 | ## Adding More Content
61 |
62 | ### Headings
63 |
64 | HTML offers six heading levels:
65 |
66 | ```html
67 | Main Heading
68 | Subheading
69 | Smaller Subheading
70 |
71 | ```
72 |
73 | ### Paragraphs and Text Formatting
74 |
75 | ```html
76 | This is a paragraph.
77 | This is bold text and italic text .
78 | ```
79 |
80 | ### Links
81 |
82 | ```html
83 | Visit Example.com
84 | ```
85 |
86 | ### Images
87 |
88 | ```html
89 |
90 | ```
91 |
92 | ### Lists
93 |
94 | Unordered list:
95 | ```html
96 |
97 | Item 1
98 | Item 2
99 |
100 | ```
101 |
102 | Ordered list:
103 | ```html
104 |
105 | First item
106 | Second item
107 |
108 | ```
109 |
110 | ## Next Steps
111 |
112 | After mastering basic HTML, consider learning:
113 | - CSS for styling your website
114 | - JavaScript for adding interactivity
115 | - Responsive design techniques
116 |
117 | Remember, every website you visit is built with HTML at its core. With practice, you'll be able to create increasingly complex web pages that form the foundation of your web development journey.
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @plugin "@tailwindcss/typography";
3 |
4 | @variant dark (&:where(.dark, .dark *));
5 |
6 | @font-face {
7 | font-family: "Literata Variable";
8 | font-style: normal;
9 | font-display: swap;
10 | font-weight: 200 900;
11 | src: url(https://cdn.jsdelivr.net/fontsource/fonts/literata:vf@latest/latin-opsz-normal.woff2)
12 | format("woff2-variations");
13 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
14 | U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
15 | U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
16 | }
17 |
18 | @font-face {
19 | font-family: "press-start-2p";
20 | font-style: normal;
21 | font-display: swap;
22 | font-weight: 400;
23 | src: url(https://cdn.jsdelivr.net/fontsource/fonts/press-start-2p@latest/latin-400-normal.woff2)
24 | format("woff2"),
25 | url(https://cdn.jsdelivr.net/fontsource/fonts/press-start-2p@latest/latin-400-normal.woff)
26 | format("woff");
27 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
28 | U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
29 | U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
30 | }
31 |
32 | /*
33 | Welcome! Zaggonaut colors originate from these theme variables, feel
34 | free to change them!
35 | */
36 |
37 | @theme {
38 | --font-mono: "IBM Plex Mono", ui-monospace, monospace;
39 | --font-display: "press-start-2p", ui-monospace, monospace;
40 | --font-serif: "Literata Variable", ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
41 | --color-zag-dark: var(--color-neutral-900);
42 | --color-zag-dark-opaque: oklch(from var(--color-neutral-900) l c h / 0.5);
43 | --color-zag-light: var(--color-neutral-100);
44 | --color-zag-light-opaque: oklch(from var(--color-neutral-100) l c h / 0.5);
45 | --color-zag-dark-muted: var(--color-neutral-600);
46 | --color-zag-light-muted: var(--color-neutral-400);
47 | --color-zag-accent-light: var(--color-emerald-400);
48 | --color-zag-accent-light-muted: var(--color-emerald-300);
49 | --color-zag-accent-dark: var(--color-emerald-600);
50 | --color-zag-accent-dark-muted: var(--color-emerald-800);
51 | }
52 |
53 | @layer base {
54 | :root {
55 | --zag-stroke: 2px;
56 | --zag-offset: 6px;
57 | --zag-transition-duration: 0.15s;
58 | --zag-transition-timing-function: ease-in-out;
59 | }
60 |
61 | ::selection {
62 | background-color: var(--color-zag-accent-light);
63 | color: var(--color-zag-dark-muted);
64 | }
65 |
66 | .zag-transition {
67 | @media (prefers-reduced-motion: no-preference) {
68 | transition:
69 | background-color var(--zag-transition-duration) var(--zag-transition-timing-function),
70 | color var(--zag-transition-duration) var(--zag-transition-timing-function),
71 | fill var(--zag-transition-duration) var(--zag-transition-timing-function),
72 | border-color var(--zag-transition-duration) var(--zag-transition-timing-function),
73 | transform var(--zag-transition-duration) var(--zag-transition-timing-function);
74 | }
75 | }
76 |
77 | .zag-bg {
78 | background-color: var(--color-zag-light);
79 | :where(.dark, .dark *) & {
80 | background-color: var(--color-zag-dark);
81 | }
82 | }
83 |
84 | .-zag-bg {
85 | background-color: var(--color-zag-dark);
86 | :where(.dark, .dark *) & {
87 | background-color: var(--color-zag-light);
88 | }
89 | }
90 |
91 | .zag-text {
92 | color: var(--color-zag-dark);
93 | :where(.dark, .dark *) & {
94 | color: var(--color-zag-light);
95 | }
96 | }
97 |
98 | .-zag-text {
99 | color: var(--color-zag-light);
100 | :where(.dark, .dark *) & {
101 | color: var(--color-zag-dark);
102 | }
103 | }
104 |
105 | .zag-muted {
106 | color: var(--color-zag-dark-muted);
107 | :where(.dark, .dark *) & {
108 | color: var(--color-zag-light-muted);
109 | }
110 | }
111 |
112 | .zag-fill {
113 | fill: var(--color-zag-dark);
114 | &:where(.dark, .dark *) {
115 | fill: var(--color-zag-light);
116 | }
117 | }
118 |
119 | .zag-text-muted {
120 | color: var(--color-zag-dark-muted);
121 | &:where(.dark, .dark *) {
122 | color: var(--color-zag-light-muted);
123 | }
124 | }
125 |
126 | .zag-border-b {
127 | border-bottom: var(--zag-stroke) solid;
128 | border-color: var(--color-zag-dark);
129 | &:where(.dark, .dark *) {
130 | border-color: var(--color-zag-light);
131 | }
132 | }
133 |
134 | .zag-offset {
135 | text-underline-offset: var(--zag-offset);
136 | }
137 |
138 | .hover-zag-dotted-grid:hover::before {
139 | --dot-bg: var(--color-zag-light);
140 | --dot-color: var(--color-zag-dark-muted);
141 | --dot-size: 1.5px;
142 | --dot-space: 16px;
143 | content: "";
144 | position: absolute;
145 | top: 0;
146 | left: 0;
147 | width: 100%;
148 | height: 100%;
149 | display: block;
150 | z-index: -1;
151 | background:
152 | linear-gradient(90deg, var(--dot-bg) calc(var(--dot-space) - var(--dot-size)), transparent 1%) bottom right / var(--dot-space) var(--dot-space),
153 | linear-gradient(var(--dot-bg) calc(var(--dot-space) - var(--dot-size)), transparent 1%) bottom right / var(--dot-space) var(--dot-space),
154 | var(--dot-color);
155 | pointer-events: none;
156 | }
157 |
158 | :where(.dark, .dark *) .hover-zag-dotted-grid:hover::before {
159 | --dot-bg: var(--color-zag-dark);
160 | --dot-color: var(--color-zag-light-muted);
161 | }
162 |
163 | .hover-zag-dotted-grid:hover::after {
164 | content: "";
165 | position: absolute;
166 | top: 0;
167 | left: 0;
168 | width: 100%;
169 | height: 100%;
170 | display: block;
171 | z-index: -1;
172 | background: linear-gradient(159deg, var(--color-zag-light) 0%, var(--color-zag-light) 50%, var(--color-zag-light-opaque) 100%);
173 | pointer-events: none;
174 | }
175 |
176 | :where(.dark, .dark *) .hover-zag-dotted-grid:hover::after {
177 | background: linear-gradient(159deg, var(--color-zag-dark) 0%, var(--color-zag-dark) 50%, var(--color-zag-dark-opaque) 100%);
178 | }
179 |
180 | .opsz {
181 | font-variation-settings: "opsz" 72;
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/src/components/Footer.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getConfigurationCollection } from "../lib/utils";
3 | import Anchor from "./common/Anchor.astro";
4 | import Section from "./common/Section.astro";
5 |
6 | const { data: config } = await getConfigurationCollection();
7 |
8 | const date = new Date();
9 | const year = date.getFullYear();
10 | ---
11 |
12 |
102 |
103 |
213 |
--------------------------------------------------------------------------------
/src/content.config.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection, z } from "astro:content";
2 | import { file, glob } from "astro/loaders";
3 | import { parse as parseToml } from "toml";
4 |
5 | /**
6 | * Loader and schema for the configuration collection.
7 | * It loads a TOML file from the `content/configuration.toml` path and defines the schema for the configuration data.
8 | */
9 | const configuration = defineCollection({
10 | loader: file("content/configuration.toml", {
11 | parser: (text) => JSON.parse(JSON.stringify(parseToml(text))),
12 | }),
13 | schema: z.object({
14 | /**
15 | * Core site configuration.
16 | */
17 | site: z.object({
18 | /**
19 | * This should be the base URL of your live site,
20 | * and is used to generate absolute URLs for links and metadata.
21 | */
22 | baseUrl: z.string().url(),
23 | }),
24 |
25 | /**
26 | * The global metadata for the site. If specific page metadata is not provided,
27 | * this metadata will be used as a fallback for SEO and Open Graph tags.
28 | */
29 | globalMeta: z.object({
30 | /**
31 | * The title of the page, used in the HTML `` tag and Open Graph metadata.
32 | */
33 | title: z.string(),
34 |
35 | /**
36 | * The short description of the page, used in Open Graph metadata and as a fallback for SEO.
37 | */
38 | description: z.string(),
39 |
40 | /**
41 | * The long description of the page, used in Open Graph metadata and as a fallback for SEO.
42 | */
43 | longDescription: z.string().optional(),
44 |
45 | /**
46 | * The URL of the card image for social media sharing.
47 | */
48 | cardImage: z.string().url().optional(),
49 |
50 | /**
51 | * Keywords for SEO, used in the ` ` tag.
52 | */
53 | keywords: z.array(z.string()).optional(),
54 | }),
55 |
56 | notFoundMeta: z.object({
57 | /**
58 | * The title of the page, used in the HTML `` tag and Open Graph metadata.
59 | */
60 | title: z.string(),
61 |
62 | /**
63 | * The short description of the page, used in Open Graph metadata and as a fallback for SEO.
64 | */
65 | description: z.string(),
66 |
67 | /**
68 | * The long description of the page, used in Open Graph metadata and as a fallback for SEO.
69 | */
70 | longDescription: z.string().optional(),
71 |
72 | /**
73 | * The URL of the card image for social media sharing.
74 | */
75 | cardImage: z.string().url().optional(),
76 |
77 | /**
78 | * Keywords for SEO, used in the ` ` tag.
79 | */
80 | keywords: z.array(z.string()).optional(),
81 | }),
82 |
83 | /**
84 | * The blog page's metadata.
85 | */
86 | blogMeta: z.object({
87 | /**
88 | * The title of the page, used in the HTML `` tag and Open Graph metadata.
89 | */
90 | title: z.string(),
91 |
92 | /**
93 | * The short description of the page, used in Open Graph metadata and as a fallback for SEO.
94 | */
95 | description: z.string(),
96 |
97 | /**
98 | * The long description of the page, used in Open Graph metadata and as a fallback for SEO.
99 | */
100 | longDescription: z.string().optional(),
101 |
102 | /**
103 | * The URL of the card image for social media sharing.
104 | */
105 | cardImage: z.string().url().optional(),
106 |
107 | /**
108 | * Keywords for SEO, used in the ` ` tag.
109 | */
110 | keywords: z.array(z.string()).optional(),
111 | }),
112 |
113 | /**
114 | * The project page's metadata.
115 | */
116 | projectMeta: z.object({
117 | /**
118 | * The title of the page, used in the HTML `` tag and Open Graph metadata.
119 | */
120 | title: z.string(),
121 |
122 | /**
123 | * The short description of the page, used in Open Graph metadata and as a fallback for SEO.
124 | */
125 | description: z.string(),
126 |
127 | /**
128 | * The long description of the page, used in Open Graph metadata and as a fallback for SEO.
129 | */
130 | longDescription: z.string().optional(),
131 |
132 | /**
133 | * The URL of the card image for social media sharing.
134 | */
135 | cardImage: z.string().url().optional(),
136 |
137 | /**
138 | * Keywords for SEO, used in the ` ` tag.
139 | */
140 | keywords: z.array(z.string()).optional(),
141 | }),
142 |
143 | /**
144 | * The hero section configuration.
145 | */
146 | hero: z.object({
147 | /**
148 | * The title displayed in the hero section.
149 | */
150 | title: z.string().default("Zaggonaut"),
151 |
152 | /**
153 | * The subtitle displayed in the hero section.
154 | */
155 | subtitle: z.string().default("Retro-Inspired Theme & Built for Astro"),
156 |
157 | /**
158 | * The URL of the hero image, used as a background image in the hero section.
159 | */
160 | image: z.string().url().optional(),
161 |
162 | /**
163 | * The text displayed in the call-to-action button in the hero section.
164 | */
165 | ctaText: z.string().default("View Projects"),
166 |
167 | /**
168 | * The URL of the call-to-action button in the hero section.
169 | */
170 | ctaUrl: z.string().default("/projects"),
171 | }),
172 |
173 | /**
174 | * The personal information of the site owner or author.
175 | */
176 | personal: z.object({
177 | /**
178 | * The name of the site owner or author, used in various places throughout the site.
179 | */
180 | name: z.string().default("Zaggonaut"),
181 |
182 | /**
183 | * The GitHub profile URL of the site owner or author.
184 | */
185 | githubProfile: z.string().url().optional(),
186 |
187 | /**
188 | * The Twitter profile URL of the site owner or author.
189 | */
190 | twitterProfile: z.string().url().optional(),
191 |
192 | /**
193 | * The LinkedIn profile URL of the site owner or author.
194 | */
195 | linkedinProfile: z.string().url().optional(),
196 | }),
197 |
198 | /**
199 | * Commonly used text used throughout the site.
200 | */
201 | texts: z.object({
202 | /**
203 | * The text used when displaying the articles section on the homepage.
204 | */
205 | articlesName: z.string().default("Articles"),
206 |
207 | /**
208 | * The text used when displaying the projects section on the homepage.
209 | */
210 | projectsName: z.string().default("Projects"),
211 |
212 | /**
213 | * The text used for the "View All" button in the articles and projects sections.
214 | */
215 | viewAll: z.string().default("View All"),
216 |
217 | /**
218 | * The text displayed when there are no articles found.
219 | */
220 | noArticles: z.string().default("No articles found."),
221 |
222 | /**
223 | * The text displayed when there are no projects found.
224 | */
225 | noProjects: z.string().default("No projects found."),
226 | }),
227 |
228 | /**
229 | * The menu configuration for the site.
230 | * This defines the URLs for the main navigation links.
231 | */
232 | menu: z.object({
233 | home: z.string().default("/"),
234 | projects: z.string().default("/projects"),
235 | blog: z.string().default("/blog"),
236 | /** Add other menu items here **/
237 | }),
238 | }),
239 | });
240 |
241 | /**
242 | * Loader and schema for the blog collection.
243 | * It loads markdown files from the `content/blogs` directory and defines the schema for each blog post.
244 | */
245 | const blog = defineCollection({
246 | loader: glob({ pattern: "**/*.md", base: "./content/blogs" }),
247 | schema: z
248 | .object({
249 | /**
250 | * The title of the blog post.
251 | */
252 | title: z.string(),
253 |
254 | /**
255 | * The slug for the blog post, used in the URL.
256 | */
257 | slug: z.string().optional(),
258 |
259 | /**
260 | * A short description of the blog post, used in Open Graph metadata and as a fallback for SEO.
261 | */
262 | description: z.string(),
263 |
264 | /**
265 | * The long description of the blog post, used in Open Graph metadata and as a fallback for SEO.
266 | */
267 | longDescription: z.string().optional(),
268 |
269 | /**
270 | * The URL of the card image for social media sharing.
271 | */
272 | cardImage: z.string().url().optional(),
273 |
274 | /**
275 | * The tags associated with the blog post, used for categorization and filtering.
276 | */
277 | tags: z.array(z.string()).optional(),
278 |
279 | /**
280 | * The estimated reading time of the blog post, in minutes.
281 | */
282 | readTime: z.number().optional(),
283 |
284 | /**
285 | * Whether the blog post is featured on the homepage.
286 | */
287 | featured: z.boolean().default(false),
288 |
289 | /**
290 | * The timestamp of the blog post, used for sorting and displaying the date.
291 | */
292 | timestamp: z.date().transform((val) => new Date(val)),
293 | })
294 | .transform((data) => {
295 | const slug =
296 | data.slug ??
297 | data.title
298 | .toLowerCase()
299 | .replace(/\s+/g, "-")
300 | .replace(/[^\w-]/g, "");
301 | const newData = {
302 | ...data,
303 | slug,
304 | };
305 | return newData;
306 | }),
307 | });
308 |
309 | /**
310 | * Loader and schema for the project collection.
311 | * It loads markdown files from the `content/projects` directory and defines the schema for each project.
312 | */
313 | const project = defineCollection({
314 | loader: glob({ pattern: "**/*.md", base: "./content/projects" }),
315 | schema: z.object({
316 | /**
317 | * The title of the project.
318 | */
319 | title: z.string(),
320 |
321 | /**
322 | * The slug for the project, used in the URL.
323 | */
324 | slug: z.string().optional(),
325 |
326 | /**
327 | * The short description of the project, used in Open Graph metadata and as a fallback for SEO.
328 | */
329 | description: z.string(),
330 |
331 | /**
332 | * The long description of the project, used in Open Graph metadata and as a fallback for SEO.
333 | */
334 | longDescription: z.string().optional(),
335 |
336 | /**
337 | * The URL of the card image for social media sharing.
338 | */
339 | cardImage: z.string().url().optional(),
340 |
341 | /**
342 | * The tags associated with the project, used for categorization and filtering.
343 | */
344 | tags: z.array(z.string()).optional(),
345 |
346 | /**
347 | * The github repository URL for the project.
348 | */
349 | githubUrl: z.string().url().optional(),
350 |
351 | /**
352 | * The live demo URL for the project, if applicable.
353 | */
354 | liveDemoUrl: z.string().url().optional(),
355 |
356 | /**
357 | * The timestamp of the project, used for sorting and displaying the date.
358 | */
359 | timestamp: z.date().transform((val) => new Date(val)),
360 |
361 | /**
362 | * Whether the project is featured on the homepage.
363 | */
364 | featured: z.boolean().default(false),
365 | }).transform((data) => {
366 | const slug =
367 | data.slug ??
368 | data.title
369 | .toLowerCase()
370 | .replace(/\s+/g, "-")
371 | .replace(/[^\w-]/g, "");
372 | const newData = {
373 | ...data,
374 | slug,
375 | };
376 | return newData;
377 | }),
378 | });
379 |
380 | export const collections = { blog, project, configuration };
381 |
--------------------------------------------------------------------------------