Initial commit

This commit is contained in:
Thuan Bui
2026-03-13 11:19:35 +09:00
committed by GitHub
commit 36b45142d9
89 changed files with 15695 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+101
View File
@@ -0,0 +1,101 @@
{
"count": 444,
"uniques": 280,
"clones": [
{
"timestamp": "2026-02-16T00:00:00Z",
"count": 3,
"uniques": 3
},
{
"timestamp": "2026-02-17T00:00:00Z",
"count": 0,
"uniques": 0
},
{
"timestamp": "2026-02-18T00:00:00Z",
"count": 0,
"uniques": 0
},
{
"timestamp": "2026-02-19T00:00:00Z",
"count": 0,
"uniques": 0
},
{
"timestamp": "2026-02-20T00:00:00Z",
"count": 0,
"uniques": 0
},
{
"timestamp": "2026-02-21T00:00:00Z",
"count": 18,
"uniques": 14
},
{
"timestamp": "2026-02-22T00:00:00Z",
"count": 41,
"uniques": 25
},
{
"timestamp": "2026-02-23T00:00:00Z",
"count": 15,
"uniques": 14
},
{
"timestamp": "2026-02-24T00:00:00Z",
"count": 34,
"uniques": 23
},
{
"timestamp": "2026-02-25T00:00:00Z",
"count": 17,
"uniques": 13
},
{
"timestamp": "2026-02-26T00:00:00Z",
"count": 14,
"uniques": 12
},
{
"timestamp": "2026-02-27T00:00:00Z",
"count": 30,
"uniques": 17
},
{
"timestamp": "2026-02-28T00:00:00Z",
"count": 21,
"uniques": 11
},
{
"timestamp": "2026-03-01T00:00:00Z",
"count": 82,
"uniques": 31
},
{
"timestamp": "2026-03-02T00:00:00Z",
"count": 74,
"uniques": 42
},
{
"timestamp": "2026-03-03T00:00:00Z",
"count": 37,
"uniques": 27
},
{
"timestamp": "2026-03-04T00:00:00Z",
"count": 17,
"uniques": 14
},
{
"timestamp": "2026-03-05T00:00:00Z",
"count": 25,
"uniques": 20
},
{
"timestamp": "2026-03-06T00:00:00Z",
"count": 16,
"uniques": 14
}
]
}
+73
View File
@@ -0,0 +1,73 @@
import fs from "node:fs";
async function saveTraffic() {
const token = process.env.GRAPH_TOKEN;
const repo =
process.env.GITHUB_REPOSITORY || "devgelo-labs/astro-starter-pro";
const filePath = "./.github/data/clones.json";
const response = await fetch(
`https://api.github.com/repos/${repo}/traffic/clones`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (!response.ok) throw new Error(`GitHub API error: ${response.statusText}`);
const apiData = await response.json();
let localData = { count: 0, uniques: 0, clones: [] };
// 1. Si el archivo ya existe, leerlo
if (fs.existsSync(filePath)) {
try {
localData = JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch {
console.warn(
"⚠️ Warning: Error reading existing clones.json, starting fresh.",
);
}
}
// 2. Unir (Merge) los clones nuevos con los viejos sin duplicar fechas
const combinedClones = [...localData.clones];
apiData.clones.forEach((newClone) => {
const index = combinedClones.findIndex(
(c) => c.timestamp === newClone.timestamp,
);
if (index !== -1) {
// Si el día ya existe, actualizamos los números (por si subieron durante el día)
combinedClones[index] = newClone;
} else {
// Si el día es nuevo, lo agregamos
combinedClones.push(newClone);
}
});
// 3. Ordenar por fecha y actualizar totales globales
combinedClones.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
const totalCount = combinedClones.reduce((sum, c) => sum + c.count, 0);
const totalUniques = combinedClones.reduce((sum, c) => sum + c.uniques, 0);
const finalData = {
count: totalCount,
uniques: totalUniques,
clones: combinedClones,
};
if (!fs.existsSync("./.github/data"))
fs.mkdirSync("./.github/data", { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(finalData, null, 2));
console.log(
`✅ Historial actualizado: ${combinedClones.length} días registrados.`,
);
}
saveTraffic().catch((err) => {
console.error(err);
process.exit(1);
});
+32
View File
@@ -0,0 +1,32 @@
name: Save Repo Traffic
on:
schedule:
- cron: "0 0 * * 0"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
- name: Fetch and Save Data
env:
GRAPH_TOKEN: ${{ secrets.GRAPH_TOKEN }}
run: node .github/save_traffic.js
- name: Commit and Push changes
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add .github/data/clones.json
git commit -m "data: update traffic clones" || exit 0
git push
+24
View File
@@ -0,0 +1,24 @@
# 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.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
+1
View File
@@ -0,0 +1 @@
npx lint-staged
+14
View File
@@ -0,0 +1,14 @@
{
"semi": true,
"trailingComma": "all",
"tabWidth": 2,
"plugins": ["prettier-plugin-astro"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}
+4
View File
@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Angelo Pescetto | Devgelo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+192
View File
@@ -0,0 +1,192 @@
# 🚀 Astro Starter Pro
**Astro Starter Pro** is a professional, open-source template for building fast websites using **[Astro 5](https://astro.build/) + [Tailwind CSS 4](https://tailwindcss.com/)**. Designed with industry best practices, optimized SEO, and a modern development experience.
<br>
[![GitHub stars](https://badgen.net/github/stars/devgelo-labs/astro-starter-pro?icon=github&label=Star)](https://github.com/devgelo-labs/astro-starter-pro)
[![Clones](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fangelop47%2Fastro-starter-template%2Fmain%2F.github%2Fdata%2Fclones.json&query=%24.count&label=Clones&color=brightgreen&style=flat-square&logo=github)](https://github.com/devgelo-labs/astro-starter-pro)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](./LICENSE)
[![Astro](https://img.shields.io/badge/Astro-5.0-orange?style=flat-square&logo=astro)](https://astro.build/)
[![Tailwind](https://img.shields.io/badge/Tailwind-4.0-38B2AC?style=flat-square&logo=tailwind-css)](https://tailwindcss.com/)
[![Maintainer](https://img.shields.io/badge/maintainer-devgelo-purple?style=flat-square)](https://github.com/devgelo-labs)
<br>
<details open>
<summary>Table of Contents</summary>
- [Demo](#demo)
- [Features](#features)
- [Tech Stack](#tech-stack)
- [Project Structure](#project-structure)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
- [Content Management](#content-management)
- [Commands](#commands)
- [Support the Project](#support-the-project)
- [License](#license)
</details>
<br>
## Demo
📌 [https://astrostarterpro.com/](https://astrostarterpro.com/)
## Features
-**Dark & Light Mode**: Clean implementation.
-**Optimized SEO**: Automatic meta tags, Structured Data (JSON-LD), RSS Feed, Open Graph, Twitter Cards, and native Sitemap.
-**Native Scroll Animations**: High-performance, JS-light scroll reveal animations using `Intersection Observer`.
-**Clean Architecture**: Organized and scalable code.
-**Reusable Components**: Navbar, Footer, and modern Layouts with Tailwind v4.
<br>
<img alt="Image" src="./.github/astro-starter-pro.webp" />
<br>
<img alt="PageSpeed Insights Score 100/100" src="https://github.com/user-attachments/assets/541d4bfc-bcb9-4287-bd91-08564108d706" />
<br>
## Tech Stack
This template is built with modern, high-performance technologies:
- **[Astro 5](https://astro.build/)**: The web framework for building content-driven websites.
- **[Tailwind CSS 4](https://tailwindcss.com/)**: A utility-first CSS framework for rapid UI development.
- **[TypeScript](https://www.typescriptlang.org/)**: Strongly typed programming language that builds on JavaScript.
- **[MDX](https://mdxjs.com/)**: Markdown for the component era, allowing you to use JSX in your markdown content.
<br>
## Project Structure
A quick overview of the folder structure to help you understand where everything is located:
```text
/
├── public/ # Static assets (fonts, favicon, images outside of processing)
├── src/
│ ├── assets/ # Images and assets to be processed by Astro
│ ├── components/ # Reusable UI components (Navbar, Footer, SEO, etc.)
│ ├── config/ # Centralized site configuration (site.ts)
│ ├── content/ # Blog posts and content collections (Markdown/MDX)
│ ├── layouts/ # Base page layouts
│ ├── pages/ # File-based routing (pages and endpoints)
│ ├── styles/ # Global CSS and Tailwind directives
│ ├── types/ # TypeScript type definitions
│ └── content.config.ts # Astro Content Collections configuration
├── astro.config.mjs # Astro configuration file
└── tailwind.config.mjs # Tailwind CSS configuration
```
<br>
## Quick Start
To start with this project locally, clone the repository and install dependencies:
```bash
# Clone the repository
git clone https://github.com/devgelo-labs/astro-starter-pro.git
# If you like it, don't forget to leave a star! ⭐
cd astro-starter-pro
npm install
npm run dev
```
<br>
## Configuration
All global site information is managed in `src/config/site.ts`. Update this file with your data:
```typescript
// src/config/site.ts
import ogImage from "../assets/og-image.png";
export const siteConfig = {
name: "Astro Starter Pro",
description:
"Starter template optimized for SEO and performance. A solid foundation to start your projects with best practices.",
url: "https://astrostarterpro.com",
lang: "en",
locale: "en_US",
author: "Devgelo",
twitter: "@Devgelo",
ogImage: ogImage,
socialLinks: {
twitter: "https://twitter.com",
github: "https://github.com/devgelo-labs/astro-starter-pro",
discord: "https://discord.com",
},
navLinks: [
{ text: "Home", href: "/" },
{ text: "About", href: "/about" },
{ text: "Services", href: "/services" },
{ text: "Blog", href: "/blog" },
{ text: "Contact", href: "/contact" },
{ text: "Widgets", href: "/widgets" },
],
};
```
## Content Management
This template uses **Astro Content Collections** to manage blog posts.
To add a new blog post, simply create a new `.md` or `.mdx` file inside the `src/content/blog/` directory.
### Frontmatter Schema
Each blog post must include the following frontmatter at the top of the file:
```yaml
---
title: "Your Post Title"
description: "A brief summary of your post for SEO."
pubDate: 2024-03-20
author: "Author Name"
image: "/images/your-cover-image.webp" # Optional
tags: ["Astro", "Tailwind"] # Optional
category: "Web Development" # Optional
---
Your markdown or MDX content goes here...
```
<br>
## Commands
| Command | Action |
| :------------------ | :------------------------------------------------- |
| `npm run dev` | Starts the development server at `localhost:4321`. |
| `npm run build` | Generates the static site in the `dist/` folder. |
| `npm run preview` | Previews the production build locally. |
| `npm run lint` | Runs ESLint to ensure code quality. |
| `npm run format` | Formats code with Prettier. |
| `npm run fix` | Runs format and lint auto-fix. |
| `npm run check` | Runs astro check for diagnostics. |
| `npm run typecheck` | Verifies TypeScript types. |
<br>
## Support the Project
If you find this starter useful, please consider giving it a ⭐ on GitHub! It helps more people discover the project.
<br>
## License
This project is under the **MIT** license. See the [LICENSE](./LICENSE) file for more details.
---
Designed by [Devgelo Labs](https://github.com/devgelo-labs)
+39
View File
@@ -0,0 +1,39 @@
import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap";
import mdx from "@astrojs/mdx";
import tailwindcss from "@tailwindcss/vite";
import icon from "astro-icon";
import remarkReadingTime from "remark-reading-time";
export default defineConfig({
site: "https://astrostarterpro.com/",
integrations: [sitemap(), icon(), mdx()],
markdown: {
remarkPlugins: [
remarkReadingTime,
() => {
return function (tree, file) {
file.data.astro.frontmatter.minutesRead =
file.data.readingTime.minutes;
};
},
],
},
i18n: {
defaultLocale: "en",
locales: ["en", "es"],
routing: {
prefixDefaultLocale: false,
},
},
prefetch: {
prefetchAll: true,
defaultStrategy: "viewport",
},
build: {
inlineStylesheets: "always",
},
vite: {
plugins: [tailwindcss()],
},
});
+44
View File
@@ -0,0 +1,44 @@
import js from "@eslint/js";
import eslintPluginAstro from "eslint-plugin-astro";
import jsxA11y from "eslint-plugin-jsx-a11y";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
import globals from "globals";
export default tseslint.config(
{
ignores: ["dist/**", ".astro/**", "node_modules/**"],
},
js.configs.recommended,
...tseslint.configs.recommended,
...eslintPluginAstro.configs.recommended,
{
files: ["**/*.astro"],
plugins: {
"jsx-a11y": jsxA11y,
},
rules: {
"jsx-a11y/alt-text": "error",
"jsx-a11y/anchor-is-valid": "error",
"jsx-a11y/no-static-element-interactions": "off",
},
},
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
eslintConfigPrettier,
{
rules: {
"no-unused-vars": "warn",
"@typescript-eslint/no-unused-vars": [
"warn",
{ argsIgnorePattern: "^_" },
],
},
},
);
+12219
View File
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
{
"name": "astro-starter-pro",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"lint": "eslint . --fix",
"lint:check": "eslint .",
"format": "prettier --write .",
"fix": "npm run format && npm run lint",
"test": "vitest",
"check": "astro check",
"typecheck": "tsc --noEmit",
"build-preview": "npm run build && npm run preview",
"prepare": "husky"
},
"dependencies": {
"@astrojs/check": "^0.9.6",
"@astrojs/mdx": "^4.3.13",
"@astrojs/rss": "^4.0.15",
"@astrojs/sitemap": "^3.6.1",
"@iconify-json/lucide": "^1.2.86",
"@iconify-json/tabler": "^1.2.26",
"@tailwindcss/vite": "^4.1.18",
"@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1",
"astro": "^5.16.8",
"astro-icon": "^1.1.5",
"remark-reading-time": "^2.0.2",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^25.0.6",
"@typescript-eslint/parser": "^8.52.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-astro": "^1.5.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"globals": "^17.0.0",
"husky": "^9.1.7",
"lint-staged": "^16.3.2",
"prettier": "^3.7.4",
"prettier-plugin-astro": "^0.14.1",
"typescript-eslint": "^8.52.0",
"vitest": "^4.0.16"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,astro}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,mdx,css}": [
"prettier --write"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

+80
View File
@@ -0,0 +1,80 @@
---
import Tags from "@/components/ui/Tags.astro";
import { type CollectionEntry, render } from "astro:content";
export interface Props {
post: CollectionEntry<"blog">;
}
const { post } = Astro.props;
const { remarkPluginFrontmatter } = await render(post);
const readingTime = remarkPluginFrontmatter?.minutesRead
? `${Math.ceil(remarkPluginFrontmatter.minutesRead)} min read`
: "";
---
<article
class="reveal group relative flex flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-sm transition-all duration-300 hover:shadow-lg hover:shadow-primary/5"
>
<div class="relative h-48 w-full overflow-hidden">
{
post.data.image && (
<img
src={post.data.image}
alt={post.data.title}
class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
)
}
</div>
<div class="flex flex-1 flex-col p-6">
<div class="flex-1">
<p class="text-sm font-medium text-primary">
{post.data.author}
</p>
<a
href={`/blog/${post.id}`}
class="mt-2 block before:absolute before:inset-0 before:z-10"
>
<p class="text-xl font-semibold text-foreground group-hover:underline">
{post.data.title}
</p>
<p class="mt-3 text-base text-muted-foreground line-clamp-3">
{post.data.description}
</p>
</a>
</div>
<div class="mt-6 flex items-center">
<div class="shrink-0">
<span class="sr-only">
{post.data.author}
</span>
</div>
<div class="">
<div class="flex space-x-1 text-sm text-muted-foreground">
<time datetime={post.data.pubDate.toISOString()}>
{
post.data.pubDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
timeZone: "UTC",
})
}
</time>
{readingTime && <span aria-hidden="true">&bull;</span>}
{readingTime && <span>{readingTime}</span>}
</div>
</div>
</div>
{
post.data.tags && (
<div class="mt-4 relative z-20">
<Tags tags={post.data.tags} />
</div>
)
}
</div>
</article>
+45
View File
@@ -0,0 +1,45 @@
---
import { siteConfig } from "@/config/site";
---
<footer class="mt-20 border-t border-border bg-card">
<div class="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<div class="flex flex-col items-center justify-between gap-8 md:flex-row">
<div class="flex flex-col items-center gap-2 md:items-start">
<span class="text-lg font-bold text-foreground">{siteConfig.name}</span>
<p class="text-sm text-muted-foreground">
{siteConfig.description}
</p>
</div>
<div class="flex items-center gap-6">
<a
href={siteConfig.socialLinks.twitter}
target="_blank"
rel="noopener noreferrer"
class="text-muted-foreground transition-colors hover:text-primary"
>Twitter</a
>
<a
href={siteConfig.socialLinks.github}
target="_blank"
rel="noopener noreferrer"
class="text-muted-foreground transition-colors hover:text-primary"
>GitHub</a
>
<a
href={siteConfig.socialLinks.discord}
target="_blank"
rel="noopener noreferrer"
class="text-muted-foreground transition-colors hover:text-primary"
>Discord</a
>
</div>
</div>
<div
class="mt-8 border-t border-border pt-8 text-center text-xs text-muted-foreground"
>
&copy; {new Date().getFullYear()}
{siteConfig.author}. All rights reserved.
</div>
</div>
</footer>
+144
View File
@@ -0,0 +1,144 @@
---
import { siteConfig } from "@/config/site";
import ThemeToggle from "@/components/ui/ThemeToggle.astro";
---
<nav
class="fixed top-0 z-50 w-full border-b border-border bg-background md:bg-background/50 md:backdrop-blur"
>
<div
class="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8"
>
<div class="flex items-center gap-2">
<a
href="/"
class="text-xl font-bold tracking-tight text-foreground transition-colors hover:text-primary"
>
{siteConfig.name}
</a>
</div>
<!-- Desktop Menu -->
<div class="hidden md:block">
<div class="ml-10 flex items-baseline space-x-8">
{
siteConfig.navLinks.map((link) => (
<a
href={link.href}
class="text-sm font-medium text-foreground transition-colors hover:text-primary"
>
{link.text}
</a>
))
}
</div>
</div>
<div class="hidden md:flex items-center gap-4">
<a
href={siteConfig.socialLinks.github}
target="_blank"
rel="noopener noreferrer"
class="rounded-full bg-foreground/10 px-4 py-2 text-sm font-semibold text-foreground transition-all hover:bg-foreground/20"
>
GitHub
</a>
<ThemeToggle />
</div>
<!-- Mobile Menu Button -->
<div class="md:hidden flex items-center">
<button
id="mobile-menu-button"
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-foreground hover:text-white hover:bg-white/10 focus:outline-none transition-colors"
aria-controls="mobile-menu"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<!-- Icon when menu is closed -->
<svg
id="menu-icon-closed"
class="block h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
<!-- Icon when menu is open -->
<svg
id="menu-icon-open"
class="hidden h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
<div class="ml-2">
<ThemeToggle />
</div>
</div>
</div>
<div class="hidden md:hidden" id="mobile-menu">
<div
class="space-y-1 border-b border-border bg-background/90 px-2 pb-3 pt-2 sm:px-3"
>
{
siteConfig.navLinks.map((link) => (
<a
href={link.href}
class="block rounded-md px-3 py-2 text-base font-medium text-foreground transition-colors hover:bg-white/10 hover:text-primary"
>
{link.text}
</a>
))
}
<a
href={siteConfig.socialLinks.github}
target="_blank"
class="block rounded-md px-3 py-2 text-base font-medium text-blue-400 transition-colors hover:cursor-pointer hover:bg-blue-500/10 hover:text-blue-300"
>GitHub</a
>
</div>
</div>
</nav>
<script>
document.addEventListener("astro:page-load", () => {
const button = document.getElementById("mobile-menu-button");
const menu = document.getElementById("mobile-menu");
const iconClosed = document.getElementById("menu-icon-closed");
const iconOpen = document.getElementById("menu-icon-open");
// Remove old event listener if it exists to prevent multiple triggers
const newButton = button?.cloneNode(true);
if (newButton && button && button.parentNode) {
button.parentNode.replaceChild(newButton, button);
}
const finalButton = document.getElementById("mobile-menu-button");
finalButton?.addEventListener("click", () => {
const isExpanded = finalButton.getAttribute("aria-expanded") === "true";
finalButton.setAttribute("aria-expanded", String(!isExpanded));
menu?.classList.toggle("hidden");
iconClosed?.classList.toggle("hidden");
iconOpen?.classList.toggle("hidden");
});
});
</script>
+62
View File
@@ -0,0 +1,62 @@
---
import { siteConfig } from "@/config/site";
interface Props {
type: "WebSite" | "BlogPosting" | "BreadcrumbList";
data: {
title?: string;
description?: string;
image?: string;
pubDate?: Date;
author?: string;
url?: string;
};
}
const { type, data } = Astro.props;
let schema = {};
if (type === "WebSite") {
schema = {
"@context": "https://schema.org",
"@type": "WebSite",
name: siteConfig.name,
url: siteConfig.url,
description: siteConfig.description,
author: {
"@type": "Person",
name: siteConfig.author,
},
sameAs: [siteConfig.socialLinks.twitter, siteConfig.socialLinks.github],
};
} else if (type === "BlogPosting") {
schema = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: data.title,
description: data.description,
image: data.image,
datePublished: data.pubDate,
author: {
"@type": "Person",
name: data.author,
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": data.url,
},
publisher: {
"@type": "Organization",
name: siteConfig.name,
logo: {
"@type": "ImageObject",
// Placeholder, ideally should be a real logo URL
url: new URL("/favicon.svg", siteConfig.url).toString(),
},
},
};
}
---
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
+62
View File
@@ -0,0 +1,62 @@
---
import type { ImageMetadata } from "astro";
interface Props {
title?: string;
description?: string;
canonical?: string;
ogImage?: string | ImageMetadata;
ignoreTitleTemplate?: boolean;
}
import { siteConfig } from "@/config/site";
const {
title,
description = siteConfig.description,
canonical,
ogImage = siteConfig.ogImage,
ignoreTitleTemplate = false,
} = Astro.props;
const ogImageSrc = typeof ogImage === "string" ? ogImage : ogImage.src;
const fullTitle =
title && !ignoreTitleTemplate
? `${title} | ${siteConfig.name}`
: title || siteConfig.name;
const canonicalURL =
canonical ?? new URL(Astro.url.pathname, siteConfig.url).toString();
---
<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="canonical" href={canonicalURL} />
<meta name="generator" content={Astro.generator} />
<!-- Primary Meta Tags -->
<title>{fullTitle}</title>
<meta name="title" content={fullTitle} />
<meta name="description" content={description} />
<meta name="author" content={siteConfig.author} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(ogImageSrc, siteConfig.url)} />
<meta property="og:locale" content={siteConfig.locale} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={fullTitle} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(ogImageSrc, siteConfig.url)} />
{
siteConfig.twitter && (
<meta property="twitter:creator" content={siteConfig.twitter} />
)
}
+54
View File
@@ -0,0 +1,54 @@
---
interface Props {
variant?: "primary" | "secondary" | "tertiary";
size?: "sm" | "md" | "lg";
href?: string;
type?: "button" | "submit" | "reset";
class?: string;
"aria-label"?: string;
[key: string]: unknown;
}
const {
variant = "primary",
size = "md",
href,
type = "button",
class: className = "",
"aria-label": ariaLabel,
...rest
} = Astro.props;
const baseStyles =
"inline-flex items-center justify-center font-bold transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none rounded-xl";
const variants = {
primary:
"bg-foreground text-background hover:bg-foreground/90 transform hover:scale-105 shadow-lg shadow-primary/20",
secondary: "bg-muted text-foreground border border-border hover:bg-muted/80",
tertiary:
"bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground",
};
const sizes = {
sm: "px-4 py-2 text-sm",
md: "px-6 py-3 text-base",
lg: "px-8 py-4 text-lg",
};
const classes = [baseStyles, variants[variant], sizes[size], className].join(
" ",
);
const Tag = href ? "a" : "button";
---
<Tag
type={href ? undefined : type}
href={href}
class={classes}
aria-label={ariaLabel}
{...rest}
>
<slot />
</Tag>
+21
View File
@@ -0,0 +1,21 @@
---
interface Props {
class?: string;
href?: string;
}
const { class: className, href, ...rest } = Astro.props;
const Tag = href ? "a" : "div";
---
<Tag
href={href}
class:list={[
"bg-card p-8 rounded-2xl border border-border shadow-sm transition-all hover:border-primary/50",
className,
]}
{...rest}
>
<slot />
</Tag>
+59
View File
@@ -0,0 +1,59 @@
---
import Button from "./Button.astro";
interface Props {
action?: string;
method?: "GET" | "POST";
}
const { action = "#", method = "POST" } = Astro.props;
---
<form action={action} method={method} class="space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-foreground/80 mb-2"
>Name</label
>
<input
type="text"
name="name"
id="name"
autocomplete="name"
required
class="block w-full rounded-xl border-border bg-muted/50 px-4 py-3 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-primary/20 transition-all border outline-none focus:ring-4"
placeholder="Your name"
/>
</div>
<div>
<label for="email" class="block text-sm font-medium text-foreground/80 mb-2"
>Email</label
>
<input
type="email"
name="email"
id="email"
autocomplete="email"
required
class="block w-full rounded-xl border-border bg-muted/50 px-4 py-3 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-primary/20 transition-all border outline-none focus:ring-4"
placeholder="your@email.com"
/>
</div>
<div>
<label
for="message"
class="block text-sm font-medium text-foreground/80 mb-2">Message</label
>
<textarea
name="message"
id="message"
rows="4"
required
class="block w-full rounded-xl border-border bg-muted/50 px-4 py-3 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-primary/20 transition-all border outline-none focus:ring-4 resize-none"
placeholder="How can we help you?"></textarea>
</div>
<div>
<Button type="submit" size="lg" class="w-full sm:w-auto">
Send message
</Button>
</div>
</form>
+46
View File
@@ -0,0 +1,46 @@
---
import type { HeadlineProps } from "@/types/types";
const {
title = await Astro.slots.render("title"),
subtitle = await Astro.slots.render("subtitle"),
tagline,
classes = {},
titleAs = "h2",
} = Astro.props as HeadlineProps;
const {
container = "max-w-xl",
title: titleClass = "text-3xl md:text-4xl",
subtitle: subtitleClass = "text-xl",
tagline: taglineClass = "",
} = classes;
const Title = titleAs as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
---
{
(title || subtitle || tagline) && (
<div class={`mb-4 ${container}`}>
{tagline && (
<div
class={`inline-flex items-center gap-2 px-3 py-1 text-primary text-lg font-semibold ${taglineClass}`}
>
{tagline}
</div>
)}
{title && (
<Title
class={`font-bold tracking-tight text-foreground mb-4 ${titleClass}`}
set:html={title}
/>
)}
{subtitle && (
<p
class={`text-muted-foreground ${subtitleClass}`}
set:html={subtitle}
/>
)}
</div>
)
}
+46
View File
@@ -0,0 +1,46 @@
---
import type { Page } from "astro";
import type { CollectionEntry } from "astro:content";
import { Icon } from "astro-icon/components";
export interface Props {
page: Page<CollectionEntry<"blog">>;
class?: string;
}
const { page, class: className } = Astro.props;
const { currentPage, lastPage, url } = page;
const { prev, next } = url;
---
{
(prev || next) && (
<div class:list={["flex justify-between mt-12", className]}>
{prev ? (
<a
href={prev}
class="inline-flex items-center px-4 py-2 text-sm font-medium text-muted-foreground bg-card border border-border rounded-lg hover:bg-muted hover:text-foreground transition-colors"
>
<Icon name="tabler:arrow-left" class="w-4 h-4 mr-2" />
Previous
</a>
) : (
<span />
)}
<div class="flex items-center text-sm text-muted-foreground">
Página {currentPage} de {lastPage}
</div>
{next ? (
<a
href={next}
class="inline-flex items-center px-4 py-2 text-sm font-medium text-muted-foreground bg-card border border-border rounded-lg hover:bg-muted hover:text-foreground transition-colors"
>
Next
<Icon name="tabler:arrow-right" class="w-4 h-4 ml-2" />
</a>
) : (
<span />
)}
</div>
)
}
+25
View File
@@ -0,0 +1,25 @@
---
export interface Props {
tags: string[];
class?: string;
}
const { tags, class: className = "text-sm" } = Astro.props;
---
{
tags && Array.isArray(tags) && (
<ul class:list={["flex flex-wrap gap-2", className]}>
{tags.map((tag) => (
<li class="inline-block relative">
<a
href={`/blog/tags/${tag}`}
class="inline-block bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground px-3 py-1 rounded-full border border-border transition-colors duration-200"
>
#{tag}
</a>
</li>
))}
</ul>
)
}
+89
View File
@@ -0,0 +1,89 @@
---
---
<button
class="theme-toggle-btn relative rounded-full p-2 text-foreground transition-colors hover:bg-foreground/10 focus:outline-none"
aria-label="Toggle theme"
>
<!-- Sun icon -->
<svg
class="sun-icon hidden h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v2"></path>
<path d="M12 20v2"></path>
<path d="m4.93 4.93 1.41 1.41"></path>
<path d="m17.66 17.66 1.41 1.41"></path>
<path d="M2 12h2"></path>
<path d="M20 12h2"></path>
<path d="m6.34 17.66-1.41 1.41"></path>
<path d="m19.07 4.93-1.41 1.41"></path>
</svg>
<!-- Moon icon -->
<svg
class="moon-icon hidden h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
</svg>
</button>
<script>
document.addEventListener("astro:page-load", () => {
const updateIcons = () => {
const theme =
document.documentElement.getAttribute("data-theme") || "light";
const btns = document.querySelectorAll(".theme-toggle-btn");
btns.forEach((btn) => {
const sunIcon = btn.querySelector(".sun-icon");
const moonIcon = btn.querySelector(".moon-icon");
if (theme === "dark") {
sunIcon?.classList.remove("hidden");
moonIcon?.classList.add("hidden");
} else {
sunIcon?.classList.add("hidden");
moonIcon?.classList.remove("hidden");
}
});
};
// Initialize
updateIcons();
// Re-run on clicks
const btns = document.querySelectorAll(".theme-toggle-btn");
btns.forEach((btn) => {
// Clone to remove old event listeners and avoid multiple triggers
const newBtn = btn.cloneNode(true);
if (btn.parentNode) {
btn.parentNode.replaceChild(newBtn, btn);
}
newBtn.addEventListener("click", () => {
const currentTheme =
document.documentElement.getAttribute("data-theme");
const newTheme = currentTheme === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
updateIcons();
});
});
});
</script>
+53
View File
@@ -0,0 +1,53 @@
---
import WidgetWrapper from "./WidgetWrapper.astro";
import { Image } from "astro:assets";
import Headline from "@/components/ui/Headline.astro";
import type { ContentProps } from "@/types/types";
const {
title,
subtitle,
tagline,
description = [],
image,
imageAlt = "",
id,
isDark = false,
classes = {},
bg = await Astro.slots.render("bg"),
} = Astro.props as ContentProps;
const { container: containerClassProp = "", ...otherClasses } = classes;
---
<WidgetWrapper
id={id}
isDark={isDark}
containerClass={`max-w-4xl mx-auto px-4 ${containerClassProp}`}
bg={bg}
classes={otherClasses}
>
<div class="text-center">
<Headline
title={title}
subtitle={subtitle ||
(Array.isArray(description) ? description.join(" ") : description)}
tagline={tagline}
classes={{
container: "mb-8",
title: "text-4xl md:text-6xl font-bold mb-2",
subtitle: "text-lg text-muted-foreground",
}}
/>
{
image && (
<Image
src={image}
alt={imageAlt}
class="rounded-3xl shadow-2xl mx-auto mt-6 border border-border/50 aspect-video object-cover"
/>
)
}
<slot />
</div>
</WidgetWrapper>
+132
View File
@@ -0,0 +1,132 @@
---
import WidgetWrapper from "./WidgetWrapper.astro";
import { Image } from "astro:assets";
import { Icon } from "astro-icon/components";
import Headline from "@/components/ui/Headline.astro";
import Button from "@/components/ui/Button.astro";
import type { ContentProps } from "@/types/types";
const {
title,
subtitle,
tagline,
description = [],
image,
imageAlt = "",
actions = [],
isReversed = false,
id,
isDark = false,
classes = {},
bg = await Astro.slots.render("bg"),
items = [],
isAfterContent = false,
} = Astro.props as ContentProps;
const { container: containerClassProp = "", ...otherClasses } = classes;
---
<WidgetWrapper
id={id}
isDark={isDark}
containerClass={`max-w-7xl mx-auto ${containerClassProp}`}
bg={bg}
classes={otherClasses}
>
<div
class={`mx-auto max-w-7xl ${isReversed ? "md:flex-row-reverse" : ""} md:flex md:gap-16`}
>
<div class="md:basis-1/2 self-center text-left">
<Headline
title={title}
subtitle={subtitle ||
(Array.isArray(description) ? description.join(" ") : description)}
tagline={tagline}
titleAs="h2"
classes={{
container: "mb-8",
title:
"text-4xl md:text-5xl font-bold tracking-tighter mb-4 font-heading",
subtitle: "text-lg text-muted-foreground",
}}
/>
{/* Slot before items if not isAfterContent */}
{!isAfterContent && <slot />}
{
items && items.length > 0 && (
<div class="space-y-8 my-8">
{items.map(
({
title: titleItem,
description: descriptionItem,
icon,
iconClass,
}) => (
<div class="flex">
<div class="shrink-0">
<div
class={`flex items-center justify-center w-10 h-10 rounded-full ${iconClass ? iconClass : "bg-primary/10 text-primary"}`}
>
{icon ? (
<Icon name={icon} class="w-5 h-5" />
) : (
<Icon name="tabler:check" class="w-5 h-5" />
)}
</div>
</div>
<div class="ml-4">
{titleItem && (
<h3 class="text-lg font-medium leading-6">{titleItem}</h3>
)}
{descriptionItem && (
<p class="mt-2 text-muted-foreground">
{descriptionItem}
</p>
)}
</div>
</div>
),
)}
</div>
)
}
{
Array.isArray(actions) && actions.length > 0 && (
<div class="mt-8 flex flex-col sm:flex-row gap-4">
{actions.map((action) => (
<Button
href={action.href}
variant={action.variant as "primary" | "secondary"}
aria-label={action.ariaLabel}
class="w-full sm:w-auto"
>
{action.icon && (
<Icon name={action.icon} class="w-5 h-5 mr-2 -ml-1" />
)}
{action.text}
</Button>
))}
</div>
)
}
{/* Slot after items if isAfterContent */}
{isAfterContent && <slot />}
</div>
<div class="md:basis-1/2 self-center mt-10 md:mt-0" aria-hidden="true">
{
image && (
<div class="relative m-auto max-w-4xl">
<Image
src={image}
alt={imageAlt}
class="mx-auto w-full rounded-lg bg-gray-500 shadow-lg"
/>
</div>
)
}
</div>
</div>
</WidgetWrapper>
+56
View File
@@ -0,0 +1,56 @@
---
import WidgetWrapper from "./WidgetWrapper.astro";
import type { FeaturesProps } from "@/types/types";
import { Icon } from "astro-icon/components";
import Card from "@/components/ui/Card.astro";
import Headline from "@/components/ui/Headline.astro";
const {
features = [],
title,
subtitle,
tagline,
id,
isDark = false,
classes = {},
bg = await Astro.slots.render("bg"),
} = Astro.props as FeaturesProps;
const { container: containerClassProp = "", ...otherClasses } = classes;
---
<WidgetWrapper
id={id}
isDark={isDark}
containerClass={` ${containerClassProp}`}
bg={bg}
classes={otherClasses}
>
<Headline
title={title}
subtitle={subtitle}
tagline={tagline}
classes={{
container: "max-w-3xl mx-auto text-center",
}}
/>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{
features.map((feature) => (
<Card>
{feature.icon && (
<div
class={`w-12 h-12 rounded-lg flex items-center justify-center mb-6 ${feature.iconClass ? feature.iconClass : "bg-primary/10 text-primary"}`}
>
<Icon name={feature.icon} class="w-6 h-6" />
</div>
)}
<h3 class="text-xl font-bold text-foreground mb-3">
{feature.title}
</h3>
<p class="text-muted-foreground text-sm">{feature.description}</p>
</Card>
))
}
</div>
</WidgetWrapper>
+80
View File
@@ -0,0 +1,80 @@
---
import WidgetWrapper from "./WidgetWrapper.astro";
import type { HeroProps } from "@/types/types";
import Headline from "@/components/ui/Headline.astro";
import Button from "@/components/ui/Button.astro";
import { Icon } from "astro-icon/components";
const {
title = await Astro.slots.render("title"),
tagline,
description = await Astro.slots.render("description"),
actions = [],
id,
isDark = false,
classes = {},
bg = await Astro.slots.render("bg"),
animate = false,
} = Astro.props as HeroProps;
const { container: containerClassProp = "" } = classes;
---
<WidgetWrapper
id={id}
isDark={isDark}
containerClass={`min-h-[calc(100vh-72px)] flex flex-col justify-center ${containerClassProp}`}
bg={bg}
animate={animate}
>
<div class="text-center">
{
tagline && (
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-semibold mb-6">
<span class="relative flex h-2 w-2" aria-hidden="true">
<span class="absolute inline-flex h-full w-full rounded-full bg-primary/60 opacity-75 animate-ping motion-reduce:animate-none" />
<span class="relative inline-flex h-2 w-2 rounded-full bg-primary" />
</span>
{tagline}
</div>
)
}
{
(title || description) && (
<Headline
title={title}
subtitle={description}
titleAs="h1"
classes={{
container: "mb-0 max-w-none",
title:
"text-5xl md:text-7xl font-extrabold tracking-tight text-foreground mb-6",
subtitle: "max-w-2xl mx-auto text-xl text-muted-foreground mb-10",
}}
/>
)
}
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
{
Array.isArray(actions) &&
actions.length > 0 &&
actions.map((action) => (
<Button
href={action.href}
variant={action.variant as "primary" | "secondary"}
size="lg"
aria-label={action.ariaLabel}
class="w-full sm:w-auto"
>
{action.icon && (
<Icon name={action.icon} class="w-5 h-5 mr-2 -ml-1" />
)}
{action.text}
</Button>
))
}
</div>
</div>
<slot />
</WidgetWrapper>
+61
View File
@@ -0,0 +1,61 @@
---
import WidgetWrapper from "./WidgetWrapper.astro";
import type { ServiceListProps } from "@/types/types";
import Card from "@/components/ui/Card.astro";
import Headline from "@/components/ui/Headline.astro";
import { Icon } from "astro-icon/components";
const {
services = [],
title = "",
subtitle = "",
tagline = "",
id,
isDark = false,
classes = {},
bg = await Astro.slots.render("bg"),
} = Astro.props as ServiceListProps;
const { container: containerClassProp = "", ...otherClasses } = classes;
---
<WidgetWrapper
id={id}
isDark={isDark}
containerClass={` ${containerClassProp}`}
bg={bg}
classes={otherClasses}
>
<Headline
title={title}
subtitle={subtitle}
tagline={tagline}
classes={{
container: "max-w-3xl mx-auto text-center",
}}
/>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{
services.map((s) => (
<Card class="group relative overflow-hidden hover:bg-muted/50 hover:shadow-md">
<div class="absolute -right-4 -top-4 text-8xl opacity-10 transition-transform group-hover:scale-110 group-hover:rotate-12 select-none">
<Icon name={s.icon} class="w-32 h-32" />
</div>
<div class="text-left relative z-10">
<div class="text-4xl mb-6 text-primary">
<Icon name={s.icon} class="w-12 h-12" />
</div>
<h3 class="text-2xl font-bold text-foreground mb-4">{s.title}</h3>
<p class="text-muted-foreground leading-relaxed">{s.description}</p>
<button class="mt-8 text-primary font-semibold flex items-center gap-2 group/btn hover:underline transition-all">
View details
<span class="transition-transform group-hover/btn:translate-x-1">
</span>
</button>
</div>
</Card>
))
}
</div>
</WidgetWrapper>
+60
View File
@@ -0,0 +1,60 @@
---
import WidgetWrapper from "./WidgetWrapper.astro";
import type { ValuesProps } from "@/types/types";
import Card from "@/components/ui/Card.astro";
import Headline from "@/components/ui/Headline.astro";
import { Icon } from "astro-icon/components";
const {
items = [],
columns = 2,
title,
subtitle,
tagline,
id,
isDark = false,
classes = {},
bg = await Astro.slots.render("bg"),
} = Astro.props as ValuesProps;
const { container: containerClassProp = "", ...otherClasses } = classes;
const gridCols = {
1: "grid-cols-1",
2: "grid-cols-1 md:grid-cols-2",
3: "grid-cols-1 md:grid-cols-3",
4: "grid-cols-1 md:grid-cols-4",
};
---
<WidgetWrapper
id={id}
isDark={isDark}
containerClass={` ${containerClassProp}`}
bg={bg}
classes={otherClasses}
>
<Headline
title={title}
subtitle={subtitle}
tagline={tagline}
classes={{
container: "max-w-3xl mx-auto text-center",
}}
/>
<div class={`grid ${gridCols[columns]} gap-8 text-left`}>
{
items.map((item) => (
<Card>
{item.icon && (
<div class="mb-4 text-primary text-4xl">
<Icon name={item.icon} class="w-12 h-12" />
</div>
)}
<h3 class="text-xl font-bold text-foreground mb-2">{item.title}</h3>
<p class="text-sm text-muted-foreground">{item.description}</p>
</Card>
))
}
</div>
</WidgetWrapper>
@@ -0,0 +1,36 @@
---
import type { Widget } from "@/types/types";
const {
id,
isDark = false,
containerClass = "",
bg = "",
classes = {},
animate = true,
} = Astro.props as Widget;
const wrapperClasses = {
container: containerClass,
...classes,
};
---
<div
id={id}
class:list={[
"relative not-prose scroll-mt-[72px]",
bg,
{ dark: isDark },
{ reveal: animate },
]}
>
<div
class:list={[
"relative mx-auto max-w-7xl px-4 md:px-6 text-default py-8 md:py-12",
wrapperClasses.container,
]}
>
<slot />
</div>
</div>
+26
View File
@@ -0,0 +1,26 @@
import ogImage from "@/assets/og-image.png";
export const siteConfig = {
name: "Astro Starter Pro",
description:
"Starter template optimized for SEO and performance. A solid foundation to start your projects with best practices.",
url: "https://astrostarterpro.com",
lang: "en",
locale: "en_US",
author: "Devgelo",
twitter: "@Devgelo",
ogImage: ogImage,
socialLinks: {
twitter: "https://twitter.com",
github: "https://github.com/devgelo-labs/astro-starter-pro",
discord: "https://discord.com",
},
navLinks: [
{ text: "Home", href: "/" },
{ text: "About", href: "/about" },
{ text: "Services", href: "/services" },
{ text: "Blog", href: "/blog" },
{ text: "Contact", href: "/contact" },
{ text: "Widgets", href: "/widgets" },
],
};
+17
View File
@@ -0,0 +1,17 @@
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const blog = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
author: z.string(),
image: z.string().optional(),
tags: z.array(z.string()).optional(),
category: z.string().optional(),
}),
});
export const collections = { blog };
+23
View File
@@ -0,0 +1,23 @@
---
title: "Aenean Commodo Ligula Eget Dolor"
pubDate: "2026-01-18"
description: "Exploring the depths of layout variations with standard dummy text."
author: "Lorem"
category: "Tech"
tags: ["testing", "coding"]
image: "/blog/blog_post_1_1768848683359.webp"
---
# Aenean Commodo Ligula Eget Dolor
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
## Key Takeaways
Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim.
- **Donec pede justo**: Fringilla vel, aliquet nec, vulputate eget, arcu.
- **In enim justo**: Rhoncus ut, imperdiet a, venenatis vitae, justo.
- **Nullam dictum**: Felis eu pede mollis pretium. Integer tincidunt.
Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus.
+19
View File
@@ -0,0 +1,19 @@
---
title: "Cras Risus Ipsum"
pubDate: "2026-01-09"
description: "Why elegant code is as important as elegant design."
author: "Lorem"
category: "General"
tags: ["features", "welcome"]
image: "/blog/blog_post_10_1768850582328.webp"
---
# Cras Risus Ipsum
Faucibus ut, ullamcorper id, varius ac, leo. Morbi mollis tellus ac sapien. Nunc nec neque. Praesent congue erat at massa. Sed cursus turpis vitae tortor.
Donec posuere vulputate arcu. Phasellus accumsan cursus velit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed aliquam, nisi quis porttitor congue, elit erat euismod orci, ac placerat dolor lectus quis orci.
## Luxury in Code
Phasellus consectetuer vestibulum elit. Aenean tellus metus, bibendum sed, posuere ac, mattis non, nunc. Vestibulum fringilla pede sit amet augue. In turpis. Pellentesque posuere.
@@ -0,0 +1,19 @@
---
title: "Curabitur Ullamcorper Ultricies Nisi"
pubDate: "2026-01-15"
description: "Extensive reading on the topic of automated content generation."
author: "Lorem"
category: "Tech"
tags: ["tutorial", "testing"]
image: "/blog/blog_post_4_1768848735398.webp"
---
# Curabitur Ullamcorper Ultricies Nisi
Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus.
Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc.
## Deep Dive
Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue.
+21
View File
@@ -0,0 +1,21 @@
---
title: "Donec Quam Felis Ultricies Nec"
pubDate: "2026-01-17"
description: "A deep dive into the philosophy of placeholder text."
author: "Lorem"
category: "Astro"
tags: ["testing", "tutorial"]
image: "/blog/blog_post_2_1768848698871.webp"
---
# Donec Quam Felis Ultricies Nec
Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus.
> "Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum."
Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo.
## Why Do We Use It?
Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc.
@@ -0,0 +1,19 @@
---
title: "Fusce Vulputate Eleifend"
pubDate: "2026-01-11"
description: "Exploring the retro wave aesthetic in modern web interfaces."
author: "Lorem"
category: "Design"
tags: ["web-design", "news"]
image: "/blog/blog_post_8_1768850554501.webp"
---
# Fusce Vulputate Eleifend
Ut tellus dolor, dapibus eget, elementum vel, cursus eleifend, elit. Aenean auctor wisi et urna. Aliquam erat volutpat. Duis ac turpis. Integer rutrum ante eu lacus.
Vestibulum libero nisl, porta vel, scelerisque eget, malesuada at, neque. Vivamus eget nibh. Etiam cursus leo vel metus. Nulla facilisi. Aenean nec eros.
## Retro Vibes
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse sollicitudin velit sed leo. Ut pharetra augue nec augue. Nam elit agna, endrerit sit amet, tincidunt ac, viverra sed, nulla.
@@ -0,0 +1,27 @@
---
title: "Integer Tincidunt Cras"
pubDate: "2026-01-08"
description: "Building tiny worlds: Component-based architecture explained."
author: "Lorem"
category: "Tech"
tags: ["coding", "astro"]
image: "/blog/blog_post_11_1768850597259.webp"
---
# Integer Tincidunt Cras
Dapibus vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim.
```css
.tiny-world {
display: flex;
justify-content: center;
perspective: 1000px;
}
```
Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum.
## Microservices
Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus.
@@ -0,0 +1,21 @@
---
title: "Maecenas Tempus Tellus Eget"
pubDate: "2026-01-14"
description: "Structured chaos: Ordering the random words."
author: "Lorem"
category: "Tech"
tags: ["coding", "news"]
image: "/blog/blog_post_5_1768848750922.webp"
---
# Maecenas Tempus Tellus Eget
Condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem.
## Steps to Success
1. **Maecenas nec odio** et ante tincidunt tempus.
2. **Donec vitae sapien** ut libero venenatis faucibus.
3. **Nullam quis ante**. Etiam sit amet orci eget eros faucibus tincidunt.
Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc.
+25
View File
@@ -0,0 +1,25 @@
---
title: "Markdown Features"
pubDate: "2026-01-19"
description: "Exploring the capabilities of Markdown in our blog."
author: "Angelo Pescetto"
category: "Tech"
tags: ["markdown", "features", "astro"]
image: "/blog/markdown.webp"
---
# Markdown is Great
You can write **bold** text, _italic_ text, and even lists:
- Item 1
- Item 2
- Item 3
## Code Blocks
```javascript
console.log("Hello, world!");
```
Enjoy writing!
+19
View File
@@ -0,0 +1,19 @@
---
title: "Nam Quam Nunc Blandit Vel"
pubDate: "2026-01-13"
description: "Short, punchy, and direct to the point."
author: "Lorem"
category: "General"
tags: ["news", "tutorial"]
image: "/blog/blog_post_6_1768848765056.webp"
---
# Nam Quam Nunc Blandit Vel
Luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante.
Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna.
## Conclusion
Sed consequat, leo eget bibendum sodales, augue velit cursus nunc. **Donec quam felis**, ultricies nec, pellentesque eu, pretium quis, sem.
@@ -0,0 +1,27 @@
---
title: "Phasellus Viverra Nulla Ut Metus"
pubDate: "2026-01-16"
description: "Analyzing code blocks and italicized text in modern web design."
author: "Lorem"
category: "Design"
tags: ["coding", "astro"]
image: "/blog/blog_post_3_1768848720552.webp"
---
# Phasellus Viverra Nulla Ut Metus
Varius laoreet. Quisque rutrum. _Aenean imperdiet_. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui.
```javascript
function helloWorld() {
console.log("Lorem Ipsum!");
}
```
Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem.
## Technical Details
1. Maecenas nec odio et ante tincidunt tempus.
2. Donec vitae sapien ut libero venenatis faucibus.
3. Nullam quis ante.
@@ -0,0 +1,19 @@
---
title: "Suspendisse Potenti Nullam"
pubDate: "2026-01-12"
description: "Minimalist architecture and its influence on digital design patterns."
author: "Lorem"
category: "Design"
tags: ["web-design"]
image: "/blog/blog_post_7_1768850538178.webp"
---
# Suspendisse Potenti Nullam
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante.
> "Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo."
## Design Principles
Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis.
+19
View File
@@ -0,0 +1,19 @@
---
title: "Vestibulum Purus Quam"
pubDate: "2026-01-10"
description: "The art of macro photography and its relation to detail-oriented coding."
author: "Lorem"
category: "General"
tags: ["features"]
image: "/blog/blog_post_9_1768850568154.webp"
---
# Vestibulum Purus Quam
Scelerisque ut, mollis sed, nonummy id, metus. Nullam accumsan lorem in dui. Cras ultricies mi eu turpis hendrerit fringilla. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi consectetuer lacinia.
Nam pretium turpis et arcu. Duis arcu tortor, suscipit eget, imperdiet nec, imperdiet iaculis, ipsum. Sed aliquam ultrices mauris. Integer ante arcu, accumsan a, consectetuer eget, posuere ut, mauris.
## Clear Vision
Praesent adipiscing. Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui.
@@ -0,0 +1,19 @@
---
title: "Vivamus Elementum Semper"
pubDate: "2026-01-07"
description: "Next-gen interfaces and the future of dashboard design."
author: "Lorem"
category: "Tech"
tags: ["tutorial", "news"]
image: "/blog/blog_post_12_1768850610966.webp"
---
# Vivamus Elementum Semper
Nisi aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus.
Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi.
## Data Visualization
Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar.
+17
View File
@@ -0,0 +1,17 @@
---
title: "Welcome to the New Blog"
pubDate: "2026-01-19"
description: "This is the first post on our new Astro-powered blog."
author: "Angelo Pescetto"
category: "General"
tags: ["welcome", "community", "astro"]
image: "/blog/welcome.webp"
---
# Welcome!
We are excited to launch our new blog. Here we will share updates, tutorials, and more.
## Why Astro?
Astro is fast, flexible, and perfect for content-driven websites like this blog.
+165
View File
@@ -0,0 +1,165 @@
---
interface Props {
title?: string;
description?: string;
ogImage?: string;
canonical?: string;
metadata?: {
title?: string;
description?: string;
ogImage?: string;
canonical?: string;
ignoreTitleTemplate?: boolean;
};
}
import Seo from "@/components/seo/Seo.astro";
import Schema from "@/components/seo/Schema.astro";
import Navbar from "@/components/layout/Navbar.astro";
import Footer from "@/components/layout/Footer.astro";
import { ClientRouter } from "astro:transitions";
import "@/styles/global.css";
import SpeedInsights from "@vercel/speed-insights/astro";
import Analytics from "@vercel/analytics/astro";
import { siteConfig } from "@/config/site";
const { title, description, ogImage, canonical, metadata } = Astro.props;
const finalTitle = metadata?.title || title;
const finalDescription = metadata?.description || description;
const finalOgImage = metadata?.ogImage || ogImage;
const finalCanonical = metadata?.canonical || canonical;
const finalIgnoreTitleTemplate = metadata?.ignoreTitleTemplate || false;
---
<!doctype html>
<html lang={siteConfig.lang} class="scroll-smooth">
<head>
<Seo
title={finalTitle}
description={finalDescription}
ogImage={finalOgImage}
canonical={finalCanonical}
ignoreTitleTemplate={finalIgnoreTitleTemplate}
/>
<ClientRouter />
<Schema type="WebSite" data={{}} />
<slot name="head" />
<script is:inline>
const getTheme = () => {
if (
typeof localStorage !== "undefined" &&
localStorage.getItem("theme")
) {
return localStorage.getItem("theme");
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
};
const setTheme = () => {
const theme = getTheme();
if (theme === "light") {
document.documentElement.setAttribute("data-theme", "light");
} else {
document.documentElement.setAttribute("data-theme", "dark");
}
window.localStorage.setItem("theme", theme);
};
// Run immediately on first hard load
setTheme();
// Run before the new page renders during View Transitions
document.addEventListener("astro:after-swap", setTheme);
</script>
</head>
<body
class="bg-background text-foreground antialiased selection:bg-blue-500/30 dark:selection:text-blue-200"
>
<a
href="#main-content"
class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 z-50 rounded-md bg-primary px-4 py-2 text-white shadow-lg ring-2 ring-white"
>
Skip to content
</a>
<div class="relative flex min-h-screen flex-col overflow-x-hidden">
<!-- Background Glow -->
<div class="pointer-events-none fixed inset-0 z-0 hidden md:block">
<div
class="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(59,130,246,0.15)_0%,transparent_50%)]"
>
</div>
<div
class="absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,rgba(168,85,247,0.15)_0%,transparent_50%)]"
>
</div>
</div>
<Navbar />
<main id="main-content" class="relative z-10 grow pt-[72px]">
<slot />
</main>
<Footer />
</div>
<SpeedInsights />
<Analytics />
<script>
// Keep a global reference to prevent duplicate observers during View Transitions
let scrollObserver: IntersectionObserver | null = null;
const setupAnimations = () => {
// Disconnect previous observer if it exists (prevents ghost triggers)
if (scrollObserver) {
scrollObserver.disconnect();
}
// Instantly bypass scroll animations for elements already visible in viewport
// This solves the Astro View Transitions "double bounce" issue completely
document.querySelectorAll(".reveal:not(.active)").forEach((el) => {
if (el.getBoundingClientRect().top < window.innerHeight) {
// Temporarily disable transition during initial show
el.setAttribute(
"style",
"transition: none !important; transform: none !important;",
);
el.classList.add("active");
// Reactivate their explicit styles so CSS takes over next time
requestAnimationFrame(() => {
el.removeAttribute("style");
});
}
});
scrollObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// Add class synchronously to prevent frame delays
entry.target.classList.add("active");
observer.unobserve(entry.target);
}
});
},
{
root: null,
rootMargin: "0px",
threshold: 0.1,
},
);
const remainingElements = document.querySelectorAll(
".reveal:not(.active)",
);
remainingElements.forEach((el) => scrollObserver?.observe(el));
};
document.addEventListener("astro:page-load", setupAnimations);
document.addEventListener("astro:after-swap", setupAnimations);
</script>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import Hero from "@/components/widgets/Hero.astro";
const metadata = {
title: "404 - Page Not Found",
description: "The page you're looking for doesn't exist.",
ignoreTitleTemplate: true,
};
---
<BaseLayout {metadata}>
<Hero
title={`<h1 class="bg-linear-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent text-[12rem] font-extrabold tracking-tighter mb-4">404</h1>`}
description="We're sorry, we can't find the page you're looking for. It may have been moved or deleted."
actions={[
{
variant: "primary",
text: "Back to Home",
href: "/",
icon: "lucide:home",
},
{
variant: "secondary",
text: "Contact",
href: "/contact",
icon: "lucide:phone",
},
]}
/>
</BaseLayout>
+86
View File
@@ -0,0 +1,86 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import { getImage } from "astro:assets";
import Content2 from "@/components/widgets/Content2.astro";
import Values from "@/components/widgets/Values.astro";
import aboutImage from "@/assets/images/about-office.webp";
const metadata = {
title: "About us",
description:
"Learn more about our vision, values, and the team behind Astro Starter Pro.",
};
const values = [
{
title: "Innovation",
description:
"Curabitur dignissim, felis non sollicitudin molestie, urna mauris pellentesque velit, ut eleifend.",
},
{
title: "Commitment",
description:
"Aenean condimentum finibus mauris, a fermentum justo pan eget. Sed ultricies, neque quis.",
},
{
title: "Quality",
description:
"Donec sed orci tincidunt, aliquam nisl a, condimentum nunc. Nulla varius ex nec ante feugiat.",
},
{
title: "Transparency",
description:
"Phasellus tristique, elit dapibus cursus facilisis, lorem augue fringilla nunc, vitae fermentum.",
},
{
title: "Teamwork",
description:
"In hac habitasse platea dictumst. Vivamus adipiscing fermentum quam volutpat aliquam. Integer et.",
},
{
title: "Excellence",
description:
"Fusce at massa nec sapien auctor gravida in in tellus. Vivamus a tristique metus, et molestie.",
},
];
const optimizedImage = await getImage({
src: aboutImage,
format: "webp",
});
---
<BaseLayout {metadata}>
<link slot="head" rel="preload" href={optimizedImage.src} as="image" />
<Content2
title="Our Vision"
description={[
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
]}
tagline="About"
image={aboutImage}
imageAlt="Modern web development office"
actions={[
{
text: "Contact Us",
href: "/contact",
variant: "primary",
icon: "lucide:mail",
},
{
text: "View Details",
href: "#",
variant: "secondary",
icon: "lucide:arrow-right",
},
]}
/>
<Values
items={values}
columns={3}
tagline="Our Values"
title="What Defines Us"
subtitle="Principles that guide our daily work"
/>
</BaseLayout>
+54
View File
@@ -0,0 +1,54 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import { getCollection } from "astro:content";
import Headline from "@/components/ui/Headline.astro";
import Pagination from "@/components/ui/Pagination.astro";
import PostItem from "@/components/blog/PostItem.astro";
import type { GetStaticPathsOptions } from "astro";
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const blogEntries = await getCollection("blog");
const sortedPosts = blogEntries
.filter((post) => post.data && post.data.pubDate)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
return paginate(sortedPosts, { pageSize: 6 });
}
const { page } = Astro.props;
const blog = page.data;
const metadata = {
title: "Blog",
description: "Latest news and updates from our team.",
};
---
<BaseLayout metadata={metadata}>
<section class="relative px-4 py-8 sm:px-6 lg:px-8 md:py-12">
<div class="absolute inset-0 z-0">
<div
class="absolute inset-0 bg-linear-to-br from-blue-500/5 via-transparent to-pink-500/5"
>
</div>
</div>
<div class="relative mx-auto max-w-5xl">
<Headline
tagline="Blog"
title="Our Publications"
subtitle="Explore the latest news, tutorials, and articles from our team."
classes={{
container: "reveal mb-12 text-center",
title:
"font-heading mb-4 text-4xl font-bold tracking-tight text-foreground md:text-6xl",
subtitle: "mx-auto max-w-3xl text-xl text-muted-foreground",
}}
/>
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{blog.map((post) => <PostItem post={post} />)}
</div>
<Pagination page={page} />
</div>
</section>
</BaseLayout>
+77
View File
@@ -0,0 +1,77 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import Schema from "@/components/seo/Schema.astro";
import { getCollection, render } from "astro:content";
export async function getStaticPaths() {
const blog = await getCollection("blog");
return blog.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content, remarkPluginFrontmatter } = await render(post);
const { title, description, pubDate, author, image } = post.data;
const formattedDate = pubDate.toLocaleDateString("es-ES", {
year: "numeric",
month: "long",
day: "numeric",
});
const readingTime = remarkPluginFrontmatter?.minutesRead
? `${Math.ceil(remarkPluginFrontmatter.minutesRead)} min de lectura`
: "";
const metadata = {
title: title,
description: description,
ogImage: image,
};
---
<BaseLayout metadata={metadata}>
<Schema
type="BlogPosting"
data={{
title,
description,
image,
pubDate,
author,
url: Astro.url.href,
}}
/>
<section class="relative px-4 py-8 sm:px-6 lg:px-8 md:py-12">
<div class="relative mx-auto max-w-3xl">
<div class="reveal mb-10 text-center">
<p class="text-sm font-semibold tracking-wide text-primary uppercase">
{formattedDate} • {author}
{readingTime && ` • ${readingTime}`}
</p>
<h1
class="font-heading mt-2 text-3xl font-bold leading-tight tracking-tighter text-foreground sm:text-4xl md:text-5xl"
>
{title}
</h1>
</div>
{
image && (
<div class="reveal relative mb-8 h-96 w-full overflow-hidden rounded-xl shadow-lg">
<img src={image} alt={title} class="h-full w-full object-cover" />
</div>
)
}
<div
class="reveal prose prose-lg dark:prose-invert mx-auto prose-headings:text-foreground prose-a:text-primary hover:prose-a:underline prose-p:text-muted-foreground prose-strong:text-foreground prose-li:text-muted-foreground prose-code:text-primary"
>
<Content />
</div>
</div>
</section>
</BaseLayout>
+65
View File
@@ -0,0 +1,65 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import { getCollection } from "astro:content";
import Headline from "@/components/ui/Headline.astro";
import PostItem from "@/components/blog/PostItem.astro";
export async function getStaticPaths() {
const allPosts = await getCollection("blog");
const posts = allPosts.filter((post) => post.data && post.data.pubDate); // Ensure published
const categories = new Set();
posts.forEach((post) => {
if (post.data.category) {
categories.add(post.data.category);
}
});
return Array.from(categories).map((category) => {
return {
params: { category: category as string },
props: {
category,
posts: posts.filter((post) => post.data.category === category),
},
};
});
}
const { category, posts } = Astro.props;
const metadata = {
title: `Category: ${category}`,
description: `Articles in the ${category} category.`,
};
---
<BaseLayout metadata={metadata}>
<section class="relative px-4 py-24 sm:px-6 lg:px-8 md:py-32">
<div class="relative mx-auto max-w-5xl">
<Headline
tagline="Category"
title={category}
subtitle={`Showing ${posts.length} article${posts.length === 1 ? "" : "s"}.`}
classes={{
container: "mb-12 text-center",
title:
"font-heading mb-4 text-4xl font-bold tracking-tight text-foreground md:text-6xl capitalized",
subtitle: "mx-auto max-w-3xl text-xl text-muted-foreground",
}}
/>
<div class="mb-8 text-center">
<a href="/blog" class="text-primary hover:underline">← Back to blog</a>
</div>
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{
posts
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
.map((post) => <PostItem post={post} />)
}
</div>
</div>
</section>
</BaseLayout>
+63
View File
@@ -0,0 +1,63 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import { getCollection } from "astro:content";
import Headline from "@/components/ui/Headline.astro";
import PostItem from "@/components/blog/PostItem.astro";
export async function getStaticPaths() {
const allPosts = await getCollection("blog");
const posts = allPosts.filter((post) => post.data && post.data.pubDate); // Ensure published
const tags = new Set();
posts.forEach((post) => {
post.data.tags?.forEach((tag) => tags.add(tag));
});
return Array.from(tags).map((tag) => {
return {
params: { tag: tag as string },
props: {
tag,
posts: posts.filter((post) => post.data.tags?.includes(tag as string)),
},
};
});
}
const { tag, posts } = Astro.props;
const metadata = {
title: `Posts tagged with '${tag}'`,
description: `Explore our articles about ${tag}.`,
};
---
<BaseLayout metadata={metadata}>
<section class="relative px-4 py-24 sm:px-6 lg:px-8 md:py-32">
<div class="relative mx-auto max-w-5xl">
<Headline
tagline="Tag"
title={`#${tag}`}
subtitle={`Showing ${posts.length} article${posts.length === 1 ? "" : "s"}.`}
classes={{
container: "mb-12 text-center",
title:
"font-heading mb-4 text-4xl font-bold tracking-tight text-foreground md:text-6xl capitalized",
subtitle: "mx-auto max-w-3xl text-xl text-muted-foreground",
}}
/>
<div class="mb-8 text-center">
<a href="/blog" class="text-primary hover:underline">← Back to blog</a>
</div>
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{
posts
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
.map((post) => <PostItem post={post} />)
}
</div>
</div>
</section>
</BaseLayout>
+66
View File
@@ -0,0 +1,66 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import Features from "@/components/widgets/Features.astro";
import Content from "@/components/widgets/Content.astro";
import Form from "@/components/ui/Form.astro";
const metadata = {
title: "Contact",
description: "Get in touch with us to discuss your next project.",
};
const visionText = ["Get in touch with us to discuss your next project."];
---
<BaseLayout {metadata}>
<Content tagline="Contact" title="Contact" description={visionText} />
<div
class="reveal mx-auto max-w-xl bg-card border border-border rounded-3xl p-8 shadow-sm mb-12 md:mb-16"
>
<Form />
</div>
<Features
tagline="Contact Means"
title="We are here to help you"
subtitle="Choose the communication channel you prefer. We respond in less than 24 hours."
classes={{ container: "pt-0 md:pt-0" }}
features={[
{
title: "Phone",
description: "+123 456 7890",
icon: "lucide:phone",
iconClass: "bg-green-500/10 text-green-400",
},
{
title: "Email",
description: "contact@company.com",
icon: "lucide:mail",
iconClass: "bg-yellow-500/10 text-yellow-400",
},
{
title: "Location",
description: "City, Country",
icon: "lucide:map-pin",
iconClass: "bg-red-500/10 text-red-400",
},
{
title: "Personalized Attention",
description: "We understand your needs to offer tailored solutions.",
icon: "lucide:user-check",
iconClass: "bg-blue-500/10 text-blue-400",
},
{
title: "Technical Support",
description: "We resolve any doubt or technical incident quickly.",
icon: "lucide:wrench",
iconClass: "bg-purple-500/10 text-purple-400",
},
{
title: "Strategic Consulting",
description: "We advise you to ensure the success of your project.",
icon: "lucide:briefcase",
iconClass: "bg-pink-500/10 text-pink-400",
},
]}
/>
</BaseLayout>
+134
View File
@@ -0,0 +1,134 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import { siteConfig } from "@/config/site";
import Hero from "@/components/widgets/Hero.astro";
import Features from "@/components/widgets/Features.astro";
import devImg from "@/assets/images/dev_balanced.png";
import openSourceImg from "@/assets/images/open-source.png";
import Content2 from "@/components/widgets/Content2.astro";
const metadata = {
title: `${siteConfig.name} — The next step for your project`,
description:
"A modern and professional template for your web projects with Astro and Tailwind CSS. Boost your development with a solid foundation.",
ignoreTitleTemplate: true,
};
---
<BaseLayout {metadata}>
<Hero
title={`Starter Template for <br class="hidden md:block" /> <span class="bg-linear-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">Web Developers</span>`}
tagline="Astro v5 + Tailwind v4"
description={`${siteConfig.description}`}
actions={[
{
text: "Get Started",
href: "https://github.com/devgelo-labs/astro-starter-pro",
variant: "primary",
icon: "lucide:github",
},
{
text: "Our Services",
href: "/services",
variant: "secondary",
icon: "lucide:arrow-right",
ariaLabel: "Learn more about our services",
},
]}
/>
<Features
tagline="Features"
title="What's Included"
subtitle="A selection of tools and configurations to streamline your workflow."
features={[
{
title: "Optimized SEO",
description:
"Base configuration for Meta tags, Open Graph, and Sitemap generation.",
icon: "lucide:scan-search",
iconClass: "bg-blue-500/10 text-blue-400",
},
{
title: "High Performance",
description: "Lightweight structure for fast load times.",
icon: "lucide:zap",
iconClass: "bg-purple-500/10 text-purple-400",
},
{
title: "Tailwind v4",
description:
"Integrated configuration with the latest version of Tailwind CSS.",
icon: "lucide:palette",
iconClass: "bg-pink-500/10 text-pink-400",
},
]}
/>
<Content2
items={[
{
title: "Responsive Design",
description:
"Base configuration for Meta tags, Open Graph, and Sitemap generation.",
icon: "lucide:scan-search",
iconClass: "bg-blue-500/10 text-blue-400",
},
{
title: "High Performance",
description: "Lightweight structure for fast load times.",
icon: "lucide:zap",
iconClass: "bg-purple-500/10 text-purple-400",
},
{
title: "Tailwind v4",
description:
"Integrated configuration with the latest version of Tailwind CSS.",
icon: "lucide:palette",
iconClass: "bg-pink-500/10 text-pink-400",
},
]}
image={devImg}
imageAlt="Development"
>
<Fragment>
<h3 class="text-2xl font-bold tracking-tight sm:text-3xl mb-2">
Development
</h3>
We provide a solid foundation to start your projects with best practices.
</Fragment>
</Content2>
<Content2
items={[
{
title: "Open Source",
description:
"Developed under the MIT license, free for personal and commercial use.",
icon: "lucide:github",
iconClass: "bg-indigo-500/10 text-indigo-400",
},
{
title: "Community Driven",
description:
"Built with the feedback and contributions of the developer community.",
icon: "lucide:users",
iconClass: "bg-orange-500/10 text-orange-400",
},
{
title: "Regular Updates",
description:
"Continuously improved with the latest technologies and best practices.",
icon: "lucide:refresh-cw",
iconClass: "bg-teal-500/10 text-teal-400",
},
]}
image={openSourceImg}
imageAlt="Community and Open Source"
isReversed={true}
>
<Fragment>
<h3 class="text-2xl font-bold tracking-tight sm:text-3xl mb-2">
Open Source & Community
</h3>
Join the revolution of modern web development. This project is open for everyone
to use, learn from, and contribute to.
</Fragment>
</Content2>
</BaseLayout>
+17
View File
@@ -0,0 +1,17 @@
import type { APIRoute } from "astro";
import { siteConfig } from "@/config/site";
const robotsTxt = `
User-agent: *
Allow: /
Sitemap: ${new URL("sitemap-index.xml", siteConfig.url).href}
`.trim();
export const GET: APIRoute = () => {
return new Response(robotsTxt, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
},
});
};
+26
View File
@@ -0,0 +1,26 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import { siteConfig } from "@/config/site";
export async function GET(context) {
const blog = await getCollection("blog");
return rss({
title: siteConfig.name,
description: siteConfig.description,
site: context.site,
items: blog
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
.map((post) => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/blog/${post.id}/`,
content: post.body, // Optional: include full content
customData: post.data.author
? `<author>${post.data.author}</author>`
: undefined,
})),
customData: `<language>${siteConfig.locale}</language>`,
});
}
+128
View File
@@ -0,0 +1,128 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import ServiceList from "@/components/widgets/ServiceList.astro";
import Content from "@/components/widgets/Content.astro";
import Content2 from "@/components/widgets/Content2.astro";
import webDevImg from "@/assets/images/services/web-dev.webp";
import seoImg from "@/assets/images/services/seo.webp";
import consultingImg from "@/assets/images/services/consulting.webp";
const services = [
{
title: "Web Development",
description: "Static and dynamic sites built for the modern era.",
icon: "lucide:globe",
},
{
title: "SEO Optimization",
description: "We ensure your content is found by those looking for it.",
icon: "lucide:search",
},
{
title: "IT Consulting",
description: "Technical advice to scale your projects to the next level.",
icon: "lucide:rocket",
},
];
const metadata = {
title: "Services",
description:
"Discover our web development, SEO optimization, and technology consulting services.",
};
---
<BaseLayout {metadata}>
<Content
tagline="Services"
title="Our Services"
description={[
"Comprehensive solutions designed to maximize the potential of your digital presence.",
]}
/>
<ServiceList services={services} classes={{ container: "pt-0 md:pt-0" }} />
<Content
tagline="Web Development"
title="Modern Web Development"
description={[
"We specialize in building fast, secure, and scalable websites using the latest technologies. We transform your vision into an impactful digital experience.",
]}
classes={{ container: "pb-0 md:pb-0" }}
/>
<Content2
items={[
{
title: "Responsive Design",
description:
"Your site will look perfect on any device, from mobiles to desktop screens.",
},
{
title: "Optimized Performance",
description:
"Fast loading to improve user experience and SEO positioning.",
},
]}
image={webDevImg}
imageAlt="Modern Web Development"
classes={{ container: "pt-0 md:pt-0" }}
>
<Fragment>
<h3 class="text-2xl font-bold tracking-tight sm:text-3xl mb-2">
Modern Web Development
</h3>
We specialize in building fast, secure, and scalable websites using the latest
technologies. We transform your vision into an impactful digital experience.
</Fragment>
</Content2>
<Content2
isReversed={true}
items={[
{
title: "SEO Audit",
description:
"We deeply analyze your website to identify technical and content improvement opportunities.",
},
{
title: "Content Strategy",
description:
"We create relevant content plans that attract and retain your target audience.",
},
]}
image={seoImg}
imageAlt="SEO Strategies"
>
<Fragment>
<h3 class="text-2xl font-bold tracking-tight sm:text-3xl mb-2">
SEO Optimization
</h3>
We help your business get found by the right people. Our organic positioning
strategies are designed to improve your organic visibility.
</Fragment>
</Content2>
<Content2
items={[
{
title: "Digital Transformation",
description:
"We guide you in adopting new technologies to optimize your business processes.",
},
{
title: "Software Architecture",
description:
"We design robust and scalable systems prepared for future growth.",
},
]}
image={consultingImg}
imageAlt="Technology Consulting"
>
<Fragment>
<h3 class="text-2xl font-bold tracking-tight sm:text-3xl mb-2">
Technology Consulting
</h3>
We accompany you in critical decision-making. We evaluate your current infrastructure
and design personalized roadmaps to take your company to the next technological
level.
</Fragment>
</Content2>
</BaseLayout>
+231
View File
@@ -0,0 +1,231 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import Hero from "@/components/widgets/Hero.astro";
import Features from "@/components/widgets/Features.astro";
import Content from "@/components/widgets/Content.astro";
import Content2 from "@/components/widgets/Content2.astro";
import ServiceList from "@/components/widgets/ServiceList.astro";
import Values from "@/components/widgets/Values.astro";
import aboutImage from "@/assets/images/about-office.webp";
const heroProps = {
title: "Lorem Ipsum Dolor Sit Amet",
tagline: "Hero",
description:
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
actions: [
{
text: "Get Started",
href: "https://github.com/devgelo-labs/astro-starter-pro",
variant: "primary",
icon: "lucide:github",
},
{
text: "View Details",
href: "#",
variant: "secondary",
icon: "lucide:arrow-right",
},
],
};
const featuresProps = {
title: "Quis Nostrud Exercitation",
subtitle: "Ullamco laboris nisi ut aliquip ex ea commodo",
tagline: "Features",
features: [
{
title: "Lorem Ipsum",
description: "Dolor sit amet, consectetur adipiscing elit.",
icon: "lucide:box",
},
{
title: "Sed Do Eiusmod",
description: "Tempor incididunt ut labore et dolore magna aliqua.",
icon: "lucide:check",
},
{
title: "Ut Enim Ad",
description: "Minim veniam, quis nostrud exercitation ullamco.",
icon: "lucide:eye",
},
],
};
const contentProps = {
title: "Duis Aute Irure Dolor",
description: [
"In reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.",
],
tagline: "Content",
image: aboutImage,
imageAlt: "Placeholder image",
isReversed: false,
actions: [
{
text: "Call to Action",
href: "#",
variant: "primary",
icon: "lucide:github",
},
{
text: "View details",
href: "#",
variant: "secondary",
icon: "lucide:arrow-right",
},
],
};
const content2Props = {
title: "Duis Aute Irure Dolor",
description: [
"In reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.",
],
tagline: "Content2",
image: aboutImage,
imageAlt: "Placeholder image",
isReversed: false,
actions: [
{
text: "Call to Action",
href: "#",
variant: "primary",
icon: "lucide:github",
},
{
text: "View details",
href: "#",
variant: "secondary",
icon: "lucide:arrow-right",
},
],
};
const content2InvertedProps = {
items: [
{
title: "Open Source",
description:
"Developed under the MIT license, free for personal and commercial use.",
icon: "lucide:github",
iconClass: "bg-indigo-500/10 text-indigo-400",
},
{
title: "Community Driven",
description:
"Built with the feedback and contributions of the developer community.",
icon: "lucide:users",
iconClass: "bg-orange-500/10 text-orange-400",
},
{
title: "Regular Updates",
description:
"Continuously improved with the latest technologies and best practices.",
icon: "lucide:refresh-cw",
iconClass: "bg-teal-500/10 text-teal-400",
},
],
image: aboutImage,
imageAlt: "Community and Open Source",
isReversed: true,
actions: [
{
text: "Call to Action",
href: "#",
variant: "primary",
icon: "lucide:github",
},
{
text: "View details",
href: "#",
variant: "secondary",
icon: "lucide:arrow-right",
},
],
title: "Open Source",
subtitle:
"Built with the feedback and contributions of the developer community.",
tagline: "Content2",
};
const serviceListProps = {
services: [
{
title: "Voluptatem Accusantium",
description: "Doloremque laudantium, totam rem aperiam.",
icon: "lucide:rocket",
},
{
title: "Ipsa Quae Ab",
description: "Illo inventore veritatis et quasi architecto.",
icon: "lucide:lightbulb",
},
{
title: "Beatae Vitae Dicta",
description: "Sunt explicabo. Nemo enim ipsam voluptatem.",
icon: "lucide:wrench",
},
],
title: "Services that will help you achieve your goals",
subtitle: "What we offer",
tagline: "Services",
};
const valuesProps = {
items: [
{
title: "Corporate",
description:
"Curabitur dignissim, felis non sollicitudin molestie, urna mauris pellentesque velit, ut eleifend.",
},
{
title: "Creative",
description:
"Aenean condimentum finibus mauris, a fermentum justo pan eget. Sed ultricies, neque quis.",
},
{
title: "Startups",
description:
"Donec sed orci tincidunt, aliquam nisl a, condimentum nunc. Nulla varius ex nec ante feugiat.",
},
{
title: "SaaS",
description:
"Phasellus tristique, elit dapibus cursus facilisis, lorem augue fringilla nunc, vitae fermentum.",
},
{
title: "Education",
description:
"In hac habitasse platea dictumst. Vivamus adipiscing fermentum quam volutpat aliquam. Integer et.",
},
{
title: "Real Estate",
description:
"Fusce at massa nec sapien auctor gravida in in tellus. Vivamus a tristique metus, et molestie.",
},
],
columns: 3 as const,
tagline: "Values",
title: "What defines us",
subtitle: "Principles that guide our daily work",
};
const metadata = {
title: "Widgets - Astro Starter Pro",
description: "Showcase of available components and widgets.",
ignoreTitleTemplate: true,
};
---
<BaseLayout {metadata}>
<Hero {...heroProps} />
<Features {...featuresProps} />
<Content {...contentProps} />
<Content2 {...content2Props} />
<Content2 {...content2InvertedProps} />
<ServiceList {...serviceListProps} />
<Values {...valuesProps} />
</BaseLayout>
+65
View File
@@ -0,0 +1,65 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-border: var(--border);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-card: var(--card);
}
:root {
--background: #ffffff;
--foreground: #0a0a0a;
--primary: #0254d8;
--border: #e5e7eb;
--muted: #f4f4f5;
--muted-foreground: #52525b;
--card: #ffffff;
}
:root[data-theme="dark"] {
--background: #030303;
--foreground: #e5e7eb;
--primary: #8bbcff;
--border: #262626;
--muted: #171717;
--muted-foreground: #a1a1aa;
--card: #0a0a0a;
}
/* Base styles using variables */
@layer base {
body {
background-color: var(--background);
color: var(--foreground);
}
:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
}
/* Animations - Reveal on Scroll */
.reveal {
opacity: 0;
transform: translateY(30px);
transition:
opacity 0.6s ease-out,
transform 0.6s ease-out;
will-change: opacity, transform;
}
.reveal.active {
opacity: 1;
transform: translateY(0);
}
/* Prevent double-bounce conflicts when Astro View Transitions is actively animating */
:root[data-astro-transition] .reveal {
transition: none !important;
}
+74
View File
@@ -0,0 +1,74 @@
export interface CallToAction {
text: string;
href: string;
variant?: "primary" | "secondary" | "link";
icon?: string;
ariaLabel?: string;
}
export interface Feature {
title: string;
description: string;
icon?: string;
iconClass?: string;
}
export type Value = Feature;
export interface Service {
title: string;
description: string;
icon: string;
}
export interface NavLink {
text: string;
href: string;
}
export interface Widget {
id?: string;
isDark?: boolean;
bg?: string;
containerClass?: string;
classes?: Record<string, string>;
animate?: boolean;
}
export interface HeadlineProps extends Widget {
title?: string;
subtitle?: string;
tagline?: string;
titleAs?: string;
}
export interface HeroProps extends HeadlineProps {
description?: string; // override or additional? Hero has description, Headline has subtitle. Hero has actions.
actions?: string | CallToAction[];
image?: ImageMetadata | string; // Just in case, though checked Hero.astro and it uses slots mostly or props.
}
export interface FeaturesProps extends HeadlineProps {
features?: Feature[];
columns?: number; // Values has columns
}
export interface ContentProps extends HeadlineProps {
content?: string;
image?: ImageMetadata;
imageAlt?: string;
items?: Feature[];
isReversed?: boolean;
isAfterContent?: boolean;
description?: string[]; // Adding back description as string array for compatibility
actions?: string | CallToAction[];
}
export interface ServiceListProps extends HeadlineProps {
services?: Service[];
}
export interface ValuesProps extends HeadlineProps {
items?: Value[];
columns?: 1 | 2 | 3 | 4;
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}