create-velocity-astro 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -9
- package/dist/index.js +126 -42
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
- package/templates/base/src/pages/404.astro +29 -0
- package/templates/base/src/pages/blog/[...slug].astro +32 -0
- package/templates/base/src/pages/blog/index.astro +58 -0
- package/templates/base/src/pages/index.astro +36 -0
- package/templates/i18n/src/components/blog/RelatedPosts.astro +73 -0
- package/templates/i18n/src/content/blog/en/welcome-to-velocity.mdx +40 -0
- package/templates/i18n/src/content/blog/es/bienvenido-a-velocity.mdx +40 -0
- package/templates/i18n/src/content/blog/fr/bienvenue-sur-velocity.mdx +40 -0
- package/templates/i18n/src/i18n/translations/en.ts +15 -0
- package/templates/i18n/src/i18n/translations/es.ts +15 -0
- package/templates/i18n/src/i18n/translations/fr.ts +15 -0
- package/templates/i18n/src/layouts/BlogLayout.astro +133 -0
- package/templates/i18n/src/pages/[lang]/blog/[...slug].astro +55 -0
- package/templates/i18n/src/pages/[lang]/blog/index.astro +176 -0
- package/templates/i18n/src/pages/blog/[...slug].astro +36 -0
- package/templates/i18n/src/pages/blog/index.astro +160 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
import PageLayout from '@/layouts/PageLayout.astro';
|
|
3
|
+
import { getCollection } from 'astro:content';
|
|
4
|
+
|
|
5
|
+
const posts = await getCollection('blog', ({ data }) => !data.draft);
|
|
6
|
+
const sortedPosts = posts.sort(
|
|
7
|
+
(a, b) => new Date(b.data.publishDate).getTime() - new Date(a.data.publishDate).getTime()
|
|
8
|
+
);
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<PageLayout
|
|
12
|
+
title="Blog"
|
|
13
|
+
description="Read our latest articles and updates."
|
|
14
|
+
>
|
|
15
|
+
<section class="py-16 md:py-24">
|
|
16
|
+
<div class="container mx-auto px-4">
|
|
17
|
+
<h1 class="text-3xl font-bold tracking-tight sm:text-4xl mb-8">Blog</h1>
|
|
18
|
+
|
|
19
|
+
{sortedPosts.length === 0 ? (
|
|
20
|
+
<div class="text-center py-12">
|
|
21
|
+
<p class="text-muted-foreground">No posts yet. Create your first post in</p>
|
|
22
|
+
<code class="bg-muted px-2 py-1 rounded text-sm mt-2 inline-block">
|
|
23
|
+
src/content/blog/
|
|
24
|
+
</code>
|
|
25
|
+
</div>
|
|
26
|
+
) : (
|
|
27
|
+
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
28
|
+
{sortedPosts.map((post) => (
|
|
29
|
+
<article class="group">
|
|
30
|
+
<a href={`/blog/${post.id}`} class="block">
|
|
31
|
+
{post.data.image && (
|
|
32
|
+
<img
|
|
33
|
+
src={post.data.image}
|
|
34
|
+
alt={post.data.title}
|
|
35
|
+
class="aspect-video w-full rounded-lg object-cover mb-4"
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
<h2 class="text-xl font-semibold group-hover:text-primary transition-colors">
|
|
39
|
+
{post.data.title}
|
|
40
|
+
</h2>
|
|
41
|
+
<p class="mt-2 text-muted-foreground line-clamp-2">
|
|
42
|
+
{post.data.description}
|
|
43
|
+
</p>
|
|
44
|
+
<time class="mt-2 text-sm text-muted-foreground block">
|
|
45
|
+
{new Date(post.data.publishDate).toLocaleDateString('en-US', {
|
|
46
|
+
year: 'numeric',
|
|
47
|
+
month: 'long',
|
|
48
|
+
day: 'numeric',
|
|
49
|
+
})}
|
|
50
|
+
</time>
|
|
51
|
+
</a>
|
|
52
|
+
</article>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
</PageLayout>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
import PageLayout from '@/layouts/PageLayout.astro';
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<PageLayout
|
|
6
|
+
title="Welcome"
|
|
7
|
+
description="Welcome to your new Velocity site. Start building something amazing."
|
|
8
|
+
>
|
|
9
|
+
<section class="py-24 md:py-32">
|
|
10
|
+
<div class="container mx-auto px-4 text-center">
|
|
11
|
+
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
|
12
|
+
Welcome to <span class="text-primary">Velocity</span>
|
|
13
|
+
</h1>
|
|
14
|
+
<p class="mt-6 text-lg text-muted-foreground max-w-2xl mx-auto">
|
|
15
|
+
Your new Astro site is ready. Edit this page at
|
|
16
|
+
<code class="bg-muted px-2 py-1 rounded text-sm">src/pages/index.astro</code>
|
|
17
|
+
</p>
|
|
18
|
+
<div class="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
|
|
19
|
+
<a
|
|
20
|
+
href="/blog"
|
|
21
|
+
class="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90"
|
|
22
|
+
>
|
|
23
|
+
View Blog
|
|
24
|
+
</a>
|
|
25
|
+
<a
|
|
26
|
+
href="https://docs.astro.build"
|
|
27
|
+
target="_blank"
|
|
28
|
+
rel="noopener noreferrer"
|
|
29
|
+
class="inline-flex items-center justify-center rounded-md border border-input bg-background px-6 py-3 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
|
30
|
+
>
|
|
31
|
+
Astro Docs
|
|
32
|
+
</a>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
</PageLayout>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BlogCard from './BlogCard.astro';
|
|
3
|
+
import { getCollection } from 'astro:content';
|
|
4
|
+
import { defaultLocale, type Locale } from '@/i18n/config';
|
|
5
|
+
import { useTranslations } from '@/i18n/index';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
currentSlug: string;
|
|
9
|
+
tags: string[];
|
|
10
|
+
locale?: Locale;
|
|
11
|
+
maxPosts?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { currentSlug, tags, locale = defaultLocale, maxPosts = 3 } = Astro.props;
|
|
15
|
+
const t = useTranslations(locale);
|
|
16
|
+
|
|
17
|
+
// Get all published posts in the same locale
|
|
18
|
+
const allPosts = await getCollection('blog', ({ data }) => {
|
|
19
|
+
return data.locale === locale && (import.meta.env.PROD ? data.draft !== true : true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Filter to find posts with matching tags
|
|
23
|
+
const relatedPosts = allPosts
|
|
24
|
+
.filter((post) => {
|
|
25
|
+
// Exclude current post
|
|
26
|
+
if (post.id === currentSlug || post.id.endsWith(`/${currentSlug}`)) return false;
|
|
27
|
+
|
|
28
|
+
// Must have at least one matching tag
|
|
29
|
+
if (!tags.length) return false;
|
|
30
|
+
return post.data.tags.some((tag) => tags.includes(tag));
|
|
31
|
+
})
|
|
32
|
+
// Sort by number of matching tags, then by date
|
|
33
|
+
.sort((a, b) => {
|
|
34
|
+
const aMatches = a.data.tags.filter((tag) => tags.includes(tag)).length;
|
|
35
|
+
const bMatches = b.data.tags.filter((tag) => tags.includes(tag)).length;
|
|
36
|
+
|
|
37
|
+
if (bMatches !== aMatches) return bMatches - aMatches;
|
|
38
|
+
return b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf();
|
|
39
|
+
})
|
|
40
|
+
.slice(0, maxPosts);
|
|
41
|
+
|
|
42
|
+
// Generate URLs for each post (locale-aware)
|
|
43
|
+
const getPostUrl = (postId: string) => {
|
|
44
|
+
const slug = postId.replace(`${locale}/`, '');
|
|
45
|
+
// Default locale doesn't have prefix
|
|
46
|
+
if (locale === defaultLocale) {
|
|
47
|
+
return `/blog/${slug}`;
|
|
48
|
+
}
|
|
49
|
+
return `/${locale}/blog/${slug}`;
|
|
50
|
+
};
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
{relatedPosts.length > 0 && (
|
|
54
|
+
<section class="border-t border-border pt-12 mt-12">
|
|
55
|
+
<h2 class="font-display text-2xl font-bold text-foreground mb-8">
|
|
56
|
+
{t('blog.relatedPosts')}
|
|
57
|
+
</h2>
|
|
58
|
+
|
|
59
|
+
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
60
|
+
{relatedPosts.map((post) => (
|
|
61
|
+
<BlogCard
|
|
62
|
+
title={post.data.title}
|
|
63
|
+
description={post.data.description}
|
|
64
|
+
href={getPostUrl(post.id)}
|
|
65
|
+
publishedAt={post.data.publishedAt}
|
|
66
|
+
tags={post.data.tags}
|
|
67
|
+
author={post.data.author}
|
|
68
|
+
image={post.data.image}
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
)}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Welcome to Velocity"
|
|
3
|
+
description: "Get started with Velocity, the modern Astro boilerplate with everything you need to build fast, beautiful websites."
|
|
4
|
+
publishedAt: 2026-01-28
|
|
5
|
+
author: "Southwell Media"
|
|
6
|
+
tags: ["astro", "tutorial", "getting-started"]
|
|
7
|
+
featured: true
|
|
8
|
+
locale: en
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Welcome to Velocity
|
|
12
|
+
|
|
13
|
+
Velocity is a production-ready starter template built on **Astro 6** and **Tailwind CSS v4**. It's designed to help you build fast, beautiful, and accessible websites with minimal setup time.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- 🚀 **Astro 6** - The latest version with all stable features
|
|
18
|
+
- 🎨 **Tailwind CSS v4** - CSS-first design token system
|
|
19
|
+
- 🌍 **Internationalization** - Built-in i18n with 3 languages
|
|
20
|
+
- 🔍 **SEO Optimized** - Complete meta tags, OG images, and structured data
|
|
21
|
+
- 🌙 **Dark Mode** - Automatic theme detection and toggle
|
|
22
|
+
- ♿ **Accessible** - WCAG 2.2 AA compliant components
|
|
23
|
+
|
|
24
|
+
## Getting Started
|
|
25
|
+
|
|
26
|
+
1. Clone the repository
|
|
27
|
+
2. Install dependencies with `pnpm install`
|
|
28
|
+
3. Start the dev server with `pnpm dev`
|
|
29
|
+
|
|
30
|
+
That's it! You're ready to start building.
|
|
31
|
+
|
|
32
|
+
## Customization
|
|
33
|
+
|
|
34
|
+
Velocity is designed to be easily customizable. Start by editing:
|
|
35
|
+
|
|
36
|
+
- `src/config/site.config.ts` - Site metadata
|
|
37
|
+
- `src/styles/tokens/colors.css` - Brand colors
|
|
38
|
+
- `src/i18n/translations/` - Translations
|
|
39
|
+
|
|
40
|
+
Happy building! 🎉
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Bienvenido a Velocity"
|
|
3
|
+
description: "Comienza con Velocity, el boilerplate moderno de Astro con todo lo que necesitas para construir sitios web rápidos y hermosos."
|
|
4
|
+
publishedAt: 2026-01-28
|
|
5
|
+
author: "Southwell Media"
|
|
6
|
+
tags: ["astro", "tutorial", "inicio"]
|
|
7
|
+
featured: true
|
|
8
|
+
locale: es
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Bienvenido a Velocity
|
|
12
|
+
|
|
13
|
+
Velocity es una plantilla lista para producción construida con **Astro 6** y **Tailwind CSS v4**. Está diseñada para ayudarte a construir sitios web rápidos, hermosos y accesibles con un tiempo de configuración mínimo.
|
|
14
|
+
|
|
15
|
+
## Características
|
|
16
|
+
|
|
17
|
+
- 🚀 **Astro 6** - La última versión con todas las características estables
|
|
18
|
+
- 🎨 **Tailwind CSS v4** - Sistema de tokens de diseño CSS-first
|
|
19
|
+
- 🌍 **Internacionalización** - i18n integrado con 3 idiomas
|
|
20
|
+
- 🔍 **Optimizado para SEO** - Meta tags completos, imágenes OG y datos estructurados
|
|
21
|
+
- 🌙 **Modo Oscuro** - Detección automática de tema y toggle
|
|
22
|
+
- ♿ **Accesible** - Componentes conformes a WCAG 2.2 AA
|
|
23
|
+
|
|
24
|
+
## Comenzando
|
|
25
|
+
|
|
26
|
+
1. Clona el repositorio
|
|
27
|
+
2. Instala las dependencias con `pnpm install`
|
|
28
|
+
3. Inicia el servidor de desarrollo con `pnpm dev`
|
|
29
|
+
|
|
30
|
+
¡Eso es todo! Estás listo para comenzar a construir.
|
|
31
|
+
|
|
32
|
+
## Personalización
|
|
33
|
+
|
|
34
|
+
Velocity está diseñado para ser fácilmente personalizable. Comienza editando:
|
|
35
|
+
|
|
36
|
+
- `src/config/site.config.ts` - Metadatos del sitio
|
|
37
|
+
- `src/styles/tokens/colors.css` - Colores de marca
|
|
38
|
+
- `src/i18n/translations/` - Traducciones
|
|
39
|
+
|
|
40
|
+
¡Feliz construcción! 🎉
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Bienvenue sur Velocity"
|
|
3
|
+
description: "Commencez avec Velocity, le boilerplate Astro moderne avec tout ce dont vous avez besoin pour créer des sites web rapides et magnifiques."
|
|
4
|
+
publishedAt: 2026-01-28
|
|
5
|
+
author: "Southwell Media"
|
|
6
|
+
tags: ["astro", "tutoriel", "démarrage"]
|
|
7
|
+
featured: true
|
|
8
|
+
locale: fr
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Bienvenue sur Velocity
|
|
12
|
+
|
|
13
|
+
Velocity est un template prêt pour la production construit avec **Astro 6** et **Tailwind CSS v4**. Il est conçu pour vous aider à créer des sites web rapides, beaux et accessibles avec un temps de configuration minimal.
|
|
14
|
+
|
|
15
|
+
## Fonctionnalités
|
|
16
|
+
|
|
17
|
+
- 🚀 **Astro 6** - La dernière version avec toutes les fonctionnalités stables
|
|
18
|
+
- 🎨 **Tailwind CSS v4** - Système de tokens de design CSS-first
|
|
19
|
+
- 🌍 **Internationalisation** - i18n intégré avec 3 langues
|
|
20
|
+
- 🔍 **Optimisé pour le SEO** - Meta tags complets, images OG et données structurées
|
|
21
|
+
- 🌙 **Mode Sombre** - Détection automatique du thème et basculement
|
|
22
|
+
- ♿ **Accessible** - Composants conformes à WCAG 2.2 AA
|
|
23
|
+
|
|
24
|
+
## Premiers pas
|
|
25
|
+
|
|
26
|
+
1. Clonez le dépôt
|
|
27
|
+
2. Installez les dépendances avec `pnpm install`
|
|
28
|
+
3. Démarrez le serveur de développement avec `pnpm dev`
|
|
29
|
+
|
|
30
|
+
C'est tout ! Vous êtes prêt à commencer à construire.
|
|
31
|
+
|
|
32
|
+
## Personnalisation
|
|
33
|
+
|
|
34
|
+
Velocity est conçu pour être facilement personnalisable. Commencez par éditer :
|
|
35
|
+
|
|
36
|
+
- `src/config/site.config.ts` - Métadonnées du site
|
|
37
|
+
- `src/styles/tokens/colors.css` - Couleurs de marque
|
|
38
|
+
- `src/i18n/translations/` - Traductions
|
|
39
|
+
|
|
40
|
+
Bonne construction ! 🎉
|
|
@@ -48,6 +48,21 @@ export const en = {
|
|
|
48
48
|
success: 'Message sent successfully!',
|
|
49
49
|
error: 'Failed to send message. Please try again.',
|
|
50
50
|
},
|
|
51
|
+
|
|
52
|
+
// Blog
|
|
53
|
+
blog: {
|
|
54
|
+
title: 'Blog',
|
|
55
|
+
description: 'Latest articles and updates',
|
|
56
|
+
allPosts: 'All posts',
|
|
57
|
+
featured: 'Featured',
|
|
58
|
+
noPosts: 'No posts found',
|
|
59
|
+
relatedPosts: 'Related Posts',
|
|
60
|
+
backToBlog: 'Back to Blog',
|
|
61
|
+
subscribe: 'Subscribe',
|
|
62
|
+
subscribeDescription: 'Get the latest articles and updates delivered to your inbox.',
|
|
63
|
+
emailPlaceholder: 'Enter your email',
|
|
64
|
+
subscribeButton: 'Subscribe',
|
|
65
|
+
},
|
|
51
66
|
} as const;
|
|
52
67
|
|
|
53
68
|
export type TranslationKeys = typeof en;
|
|
@@ -50,4 +50,19 @@ export const es: TranslationKeys = {
|
|
|
50
50
|
success: '¡Mensaje enviado con éxito!',
|
|
51
51
|
error: 'Error al enviar el mensaje. Por favor, inténtalo de nuevo.',
|
|
52
52
|
},
|
|
53
|
+
|
|
54
|
+
// Blog
|
|
55
|
+
blog: {
|
|
56
|
+
title: 'Blog',
|
|
57
|
+
description: 'Últimos artículos y actualizaciones',
|
|
58
|
+
allPosts: 'Todos los artículos',
|
|
59
|
+
featured: 'Destacados',
|
|
60
|
+
noPosts: 'No se encontraron artículos',
|
|
61
|
+
relatedPosts: 'Artículos relacionados',
|
|
62
|
+
backToBlog: 'Volver al Blog',
|
|
63
|
+
subscribe: 'Suscríbete',
|
|
64
|
+
subscribeDescription: 'Recibe los últimos artículos y actualizaciones en tu correo.',
|
|
65
|
+
emailPlaceholder: 'Introduce tu correo',
|
|
66
|
+
subscribeButton: 'Suscribirse',
|
|
67
|
+
},
|
|
53
68
|
} as const;
|
|
@@ -50,4 +50,19 @@ export const fr: TranslationKeys = {
|
|
|
50
50
|
success: 'Message envoyé avec succès !',
|
|
51
51
|
error: "Échec de l'envoi du message. Veuillez réessayer.",
|
|
52
52
|
},
|
|
53
|
+
|
|
54
|
+
// Blog
|
|
55
|
+
blog: {
|
|
56
|
+
title: 'Blog',
|
|
57
|
+
description: 'Derniers articles et mises à jour',
|
|
58
|
+
allPosts: 'Tous les articles',
|
|
59
|
+
featured: 'À la une',
|
|
60
|
+
noPosts: 'Aucun article trouvé',
|
|
61
|
+
relatedPosts: 'Articles connexes',
|
|
62
|
+
backToBlog: 'Retour au Blog',
|
|
63
|
+
subscribe: "S'abonner",
|
|
64
|
+
subscribeDescription: 'Recevez les derniers articles et mises à jour dans votre boîte mail.',
|
|
65
|
+
emailPlaceholder: 'Entrez votre e-mail',
|
|
66
|
+
subscribeButton: "S'abonner",
|
|
67
|
+
},
|
|
53
68
|
} as const;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from './BaseLayout.astro';
|
|
3
|
+
import Navbar from '@/components/landing/Navbar.astro';
|
|
4
|
+
import LandingFooter from '@/components/landing/LandingFooter.astro';
|
|
5
|
+
import Breadcrumbs from '@/components/seo/Breadcrumbs.astro';
|
|
6
|
+
import ArticleHero from '@/components/blog/ArticleHero.astro';
|
|
7
|
+
import ShareButtons from '@/components/blog/ShareButtons.astro';
|
|
8
|
+
import RelatedPosts from '@/components/blog/RelatedPosts.astro';
|
|
9
|
+
import { defaultLocale, type Locale } from '@/i18n/config';
|
|
10
|
+
import { useTranslations } from '@/i18n/index';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
publishedAt: Date;
|
|
16
|
+
updatedAt?: Date;
|
|
17
|
+
author?: string;
|
|
18
|
+
image?: string;
|
|
19
|
+
imageAlt?: string;
|
|
20
|
+
tags?: string[];
|
|
21
|
+
slug?: string;
|
|
22
|
+
locale?: Locale;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
title,
|
|
27
|
+
description,
|
|
28
|
+
publishedAt,
|
|
29
|
+
updatedAt,
|
|
30
|
+
author = 'Team',
|
|
31
|
+
image,
|
|
32
|
+
imageAlt,
|
|
33
|
+
tags = [],
|
|
34
|
+
slug = '',
|
|
35
|
+
locale = defaultLocale,
|
|
36
|
+
} = Astro.props;
|
|
37
|
+
|
|
38
|
+
const t = useTranslations(locale);
|
|
39
|
+
const ogImage = image || `/og${Astro.url.pathname}.png`;
|
|
40
|
+
|
|
41
|
+
// Build breadcrumbs with locale-aware URLs
|
|
42
|
+
const blogUrl = locale === defaultLocale ? '/blog' : `/${locale}/blog`;
|
|
43
|
+
const homeUrl = locale === defaultLocale ? '/' : `/${locale}`;
|
|
44
|
+
|
|
45
|
+
const breadcrumbs = [
|
|
46
|
+
{ label: t('nav.home'), href: homeUrl },
|
|
47
|
+
{ label: t('nav.blog'), href: blogUrl },
|
|
48
|
+
{ label: title },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// Get full URL for sharing
|
|
52
|
+
const fullUrl = new URL(Astro.url.pathname, Astro.site).toString();
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<BaseLayout
|
|
56
|
+
title={title}
|
|
57
|
+
description={description}
|
|
58
|
+
image={ogImage}
|
|
59
|
+
imageAlt={imageAlt || title}
|
|
60
|
+
lang={locale}
|
|
61
|
+
article={{
|
|
62
|
+
publishedTime: publishedAt,
|
|
63
|
+
modifiedTime: updatedAt,
|
|
64
|
+
authors: [author],
|
|
65
|
+
tags,
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<Navbar slot="header" />
|
|
69
|
+
|
|
70
|
+
<!-- Article Hero -->
|
|
71
|
+
<ArticleHero
|
|
72
|
+
title={title}
|
|
73
|
+
description={description}
|
|
74
|
+
publishedAt={publishedAt}
|
|
75
|
+
updatedAt={updatedAt}
|
|
76
|
+
author={author}
|
|
77
|
+
tags={tags}
|
|
78
|
+
image={image}
|
|
79
|
+
imageAlt={imageAlt}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<article class={`py-12 ${image ? 'pt-24' : ''}`}>
|
|
83
|
+
<div class="mx-auto max-w-4xl px-6">
|
|
84
|
+
<!-- Breadcrumbs -->
|
|
85
|
+
<Breadcrumbs items={breadcrumbs} class="mb-8" />
|
|
86
|
+
|
|
87
|
+
<!-- Article Content -->
|
|
88
|
+
<div class="prose prose-lg max-w-none prose-headings:font-display prose-headings:font-bold prose-a:text-brand-600 hover:prose-a:text-brand-500 prose-code:text-brand-600 prose-code:bg-brand-50 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-pre:bg-background-secondary prose-pre:border prose-pre:border-border dark:prose-invert dark:prose-a:text-brand-400 dark:hover:prose-a:text-brand-300 dark:prose-code:text-brand-400 dark:prose-code:bg-brand-900/30">
|
|
89
|
+
<slot />
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Article Footer -->
|
|
93
|
+
<footer class="mt-12 pt-8 border-t border-border">
|
|
94
|
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-6">
|
|
95
|
+
<!-- Back link -->
|
|
96
|
+
<a
|
|
97
|
+
href={blogUrl}
|
|
98
|
+
class="inline-flex items-center gap-2 text-sm font-medium text-brand-600 hover:text-brand-500 dark:text-brand-400 dark:hover:text-brand-300 transition-colors"
|
|
99
|
+
>
|
|
100
|
+
<svg
|
|
101
|
+
class="h-4 w-4"
|
|
102
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
103
|
+
fill="none"
|
|
104
|
+
viewBox="0 0 24 24"
|
|
105
|
+
stroke="currentColor"
|
|
106
|
+
stroke-width="2"
|
|
107
|
+
>
|
|
108
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
109
|
+
</svg>
|
|
110
|
+
{t('blog.backToBlog')}
|
|
111
|
+
</a>
|
|
112
|
+
|
|
113
|
+
<!-- Share buttons -->
|
|
114
|
+
<ShareButtons title={title} url={fullUrl} />
|
|
115
|
+
</div>
|
|
116
|
+
</footer>
|
|
117
|
+
</div>
|
|
118
|
+
</article>
|
|
119
|
+
|
|
120
|
+
<!-- Related Posts -->
|
|
121
|
+
{tags.length > 0 && (
|
|
122
|
+
<div class="mx-auto max-w-6xl px-6 pb-16">
|
|
123
|
+
<RelatedPosts
|
|
124
|
+
currentSlug={slug}
|
|
125
|
+
tags={tags}
|
|
126
|
+
locale={locale}
|
|
127
|
+
maxPosts={3}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
<LandingFooter slot="footer" />
|
|
133
|
+
</BaseLayout>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BlogLayout from '@/layouts/BlogLayout.astro';
|
|
3
|
+
import { getCollection, render } from 'astro:content';
|
|
4
|
+
import { locales, defaultLocale, isValidLocale, type Locale } from '@/i18n/config';
|
|
5
|
+
|
|
6
|
+
export async function getStaticPaths() {
|
|
7
|
+
const paths: { params: { lang: string; slug: string }; props: { post: any } }[] = [];
|
|
8
|
+
|
|
9
|
+
// Get all posts for non-default locales
|
|
10
|
+
for (const locale of locales) {
|
|
11
|
+
if (locale === defaultLocale) continue; // Skip default locale (handled by /blog/[...slug].astro)
|
|
12
|
+
|
|
13
|
+
const posts = await getCollection('blog', ({ data }) => {
|
|
14
|
+
return data.locale === locale && (import.meta.env.PROD ? data.draft !== true : true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
for (const post of posts) {
|
|
18
|
+
paths.push({
|
|
19
|
+
params: {
|
|
20
|
+
lang: locale,
|
|
21
|
+
slug: post.id.replace(`${locale}/`, ''),
|
|
22
|
+
},
|
|
23
|
+
props: { post },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return paths;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { lang } = Astro.params;
|
|
32
|
+
const { post } = Astro.props;
|
|
33
|
+
|
|
34
|
+
if (!lang || !isValidLocale(lang)) {
|
|
35
|
+
return Astro.redirect('/blog');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const locale = lang as Locale;
|
|
39
|
+
const { Content } = await render(post);
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<BlogLayout
|
|
43
|
+
title={post.data.title}
|
|
44
|
+
description={post.data.description}
|
|
45
|
+
publishedAt={post.data.publishedAt}
|
|
46
|
+
updatedAt={post.data.updatedAt}
|
|
47
|
+
author={post.data.author}
|
|
48
|
+
image={post.data.image}
|
|
49
|
+
imageAlt={post.data.imageAlt}
|
|
50
|
+
tags={post.data.tags}
|
|
51
|
+
slug={post.id}
|
|
52
|
+
locale={locale}
|
|
53
|
+
>
|
|
54
|
+
<Content />
|
|
55
|
+
</BlogLayout>
|