Initial commit
@@ -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
|
||||
|
After Width: | Height: | Size: 26 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
|
||||
@@ -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/
|
||||
@@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"plugins": ["prettier-plugin-astro"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.astro",
|
||||
"options": {
|
||||
"parser": "astro"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
[](https://github.com/devgelo-labs/astro-starter-pro)
|
||||
[](https://github.com/devgelo-labs/astro-starter-pro)
|
||||
[](./LICENSE)
|
||||
[](https://astro.build/)
|
||||
[](https://tailwindcss.com/)
|
||||
[](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)
|
||||
@@ -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()],
|
||||
},
|
||||
});
|
||||
@@ -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: "^_" },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 105 KiB |
@@ -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 |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 521 KiB |
|
After Width: | Height: | Size: 550 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 410 KiB |
@@ -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">•</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>
|
||||
@@ -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"
|
||||
>
|
||||
© {new Date().getFullYear()}
|
||||
{siteConfig.author}. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -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>
|
||||
@@ -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)} />
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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>`,
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||