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.
@@ -0,0 +1,176 @@
1
+ ---
2
+ import BaseLayout from '@/layouts/BaseLayout.astro';
3
+ import Navbar from '@/components/landing/Navbar.astro';
4
+ import LandingFooter from '@/components/landing/LandingFooter.astro';
5
+ import BlogCard from '@/components/blog/BlogCard.astro';
6
+ import Button from '@/components/ui/Button.astro';
7
+ import Icon from '@/components/ui/Icon.astro';
8
+ import { getCollection } from 'astro:content';
9
+ import { locales, defaultLocale, isValidLocale, type Locale } from '@/i18n/config';
10
+ import { useTranslations } from '@/i18n/index';
11
+
12
+ export function getStaticPaths() {
13
+ // Only generate paths for non-default locales
14
+ return locales
15
+ .filter((lang) => lang !== defaultLocale)
16
+ .map((lang) => ({
17
+ params: { lang },
18
+ }));
19
+ }
20
+
21
+ const { lang } = Astro.params;
22
+
23
+ if (!lang || !isValidLocale(lang)) {
24
+ return Astro.redirect('/blog');
25
+ }
26
+
27
+ const locale = lang as Locale;
28
+ const t = useTranslations(locale);
29
+
30
+ // Get all published posts
31
+ const allPosts = await getCollection('blog', ({ data }) => {
32
+ return import.meta.env.PROD ? data.draft !== true : true;
33
+ });
34
+
35
+ // Filter by current locale and sort by date
36
+ const posts = allPosts
37
+ .filter((post) => post.data.locale === locale)
38
+ .sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf());
39
+
40
+ // Separate featured posts (shown in hero section)
41
+ const featuredPosts = posts.filter((post) => post.data.featured);
42
+ // Non-featured posts for the main listing
43
+ const nonFeaturedPosts = posts.filter((post) => !post.data.featured);
44
+ // If ALL posts are featured, show them all in the main listing too (to avoid empty section)
45
+ const regularPosts = nonFeaturedPosts.length > 0 ? nonFeaturedPosts : posts;
46
+
47
+ const getPostUrl = (postId: string) => {
48
+ // Remove locale prefix from id (e.g., "es/bienvenido-a-velocity" -> "bienvenido-a-velocity")
49
+ const slug = postId.replace(`${locale}/`, '');
50
+ return `/${locale}/blog/${slug}`;
51
+ };
52
+ ---
53
+
54
+ <BaseLayout
55
+ title={t('blog.title')}
56
+ description={t('blog.description')}
57
+ lang={locale}
58
+ >
59
+ <Navbar slot="header" />
60
+
61
+ <!-- Hero Section with decorative background -->
62
+ <section class="relative overflow-hidden bg-background-secondary border-b border-border">
63
+ <!-- Decorative grid pattern -->
64
+ <div class="absolute inset-0 bg-grid-pattern [mask-image:linear-gradient(to_bottom,white_50%,transparent)] pointer-events-none opacity-30"></div>
65
+
66
+ <!-- Decorative blur blob -->
67
+ <div class="absolute top-0 left-1/4 w-96 h-96 bg-brand-200/20 dark:bg-brand-800/10 rounded-full blur-3xl pointer-events-none"></div>
68
+
69
+ <div class="relative mx-auto max-w-6xl px-6 py-20 md:py-28">
70
+ <div class="max-w-3xl">
71
+ <div class="mb-4 inline-flex items-center rounded-full border border-border bg-background px-3 py-1 shadow-sm">
72
+ <Icon name="book" size="sm" class="text-brand-500 mr-2" />
73
+ <span class="text-xs font-medium text-foreground-secondary">{t('blog.allPosts')}</span>
74
+ </div>
75
+
76
+ <h1 class="font-display text-4xl font-bold tracking-tight text-foreground md:text-5xl lg:text-6xl mb-6">
77
+ {t('blog.title')}
78
+ </h1>
79
+
80
+ <p class="text-xl text-foreground-muted leading-relaxed">
81
+ {t('blog.description')}
82
+ </p>
83
+ </div>
84
+ </div>
85
+ </section>
86
+
87
+ <!-- Featured Posts Section -->
88
+ {featuredPosts.length > 0 && (
89
+ <section class="py-16 bg-background">
90
+ <div class="mx-auto max-w-6xl px-6">
91
+ <h2 class="font-display text-2xl font-bold text-foreground mb-8 flex items-center gap-2">
92
+ <svg class="h-6 w-6 text-brand-500" fill="currentColor" viewBox="0 0 20 20">
93
+ <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
94
+ </svg>
95
+ {t('blog.featured')}
96
+ </h2>
97
+
98
+ <div class="grid gap-6 md:grid-cols-2">
99
+ {featuredPosts.map((post) => (
100
+ <BlogCard
101
+ title={post.data.title}
102
+ description={post.data.description}
103
+ href={getPostUrl(post.id)}
104
+ publishedAt={post.data.publishedAt}
105
+ tags={post.data.tags}
106
+ author={post.data.author}
107
+ image={post.data.image}
108
+ featured={true}
109
+ />
110
+ ))}
111
+ </div>
112
+ </div>
113
+ </section>
114
+ )}
115
+
116
+ <!-- All Posts Section -->
117
+ <section class="py-16 bg-background-secondary">
118
+ <div class="mx-auto max-w-6xl px-6">
119
+ {(featuredPosts.length > 0 && nonFeaturedPosts.length > 0) && (
120
+ <h2 class="font-display text-2xl font-bold text-foreground mb-8">
121
+ {t('blog.allPosts')}
122
+ </h2>
123
+ )}
124
+
125
+ {regularPosts.length > 0 ? (
126
+ <div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
127
+ {regularPosts.map((post) => (
128
+ <BlogCard
129
+ title={post.data.title}
130
+ description={post.data.description}
131
+ href={getPostUrl(post.id)}
132
+ publishedAt={post.data.publishedAt}
133
+ tags={post.data.tags}
134
+ author={post.data.author}
135
+ image={post.data.image}
136
+ />
137
+ ))}
138
+ </div>
139
+ ) : posts.length === 0 ? (
140
+ <div class="text-center py-16">
141
+ <div class="mx-auto w-16 h-16 rounded-full bg-background flex items-center justify-center mb-4">
142
+ <Icon name="file-text" size="lg" class="text-foreground-muted" />
143
+ </div>
144
+ <p class="text-foreground-muted text-lg">{t('blog.noPosts')}</p>
145
+ </div>
146
+ ) : null}
147
+ </div>
148
+ </section>
149
+
150
+ <!-- Newsletter CTA Section -->
151
+ <section class="py-20 bg-surface-invert text-on-invert">
152
+ <div class="mx-auto max-w-3xl px-6 text-center">
153
+ <h2 class="font-display text-3xl font-bold mb-4">
154
+ {t('blog.subscribe')}
155
+ </h2>
156
+ <p class="text-on-invert-secondary text-lg mb-8">
157
+ {t('blog.subscribeDescription')}
158
+ </p>
159
+
160
+ <form class="flex flex-col sm:flex-row gap-4 max-w-md mx-auto">
161
+ <input
162
+ type="email"
163
+ placeholder={t('blog.emailPlaceholder')}
164
+ class="flex-1 h-12 px-4 rounded-md bg-surface-invert-secondary border border-border-invert text-on-invert placeholder:text-on-invert-muted focus:outline-none focus:ring-2 focus:ring-brand-500"
165
+ required
166
+ />
167
+ <Button variant="primary" size="lg" type="submit">
168
+ {t('blog.subscribeButton')}
169
+ <Icon name="arrow-right" size="sm" />
170
+ </Button>
171
+ </form>
172
+ </div>
173
+ </section>
174
+
175
+ <LandingFooter slot="footer" />
176
+ </BaseLayout>
@@ -0,0 +1,36 @@
1
+ ---
2
+ import BlogLayout from '@/layouts/BlogLayout.astro';
3
+ import { getCollection, render } from 'astro:content';
4
+ import { defaultLocale } from '@/i18n/config';
5
+
6
+ const locale = defaultLocale;
7
+
8
+ export async function getStaticPaths() {
9
+ const posts = await getCollection('blog', ({ data }) => {
10
+ return data.locale === 'en' && (import.meta.env.PROD ? data.draft !== true : true);
11
+ });
12
+
13
+ return posts.map((post) => ({
14
+ params: { slug: post.id.replace('en/', '') },
15
+ props: { post },
16
+ }));
17
+ }
18
+
19
+ const { post } = Astro.props;
20
+ const { Content } = await render(post);
21
+ ---
22
+
23
+ <BlogLayout
24
+ title={post.data.title}
25
+ description={post.data.description}
26
+ publishedAt={post.data.publishedAt}
27
+ updatedAt={post.data.updatedAt}
28
+ author={post.data.author}
29
+ image={post.data.image}
30
+ imageAlt={post.data.imageAlt}
31
+ tags={post.data.tags}
32
+ slug={post.id}
33
+ locale={locale}
34
+ >
35
+ <Content />
36
+ </BlogLayout>
@@ -0,0 +1,160 @@
1
+ ---
2
+ import BaseLayout from '@/layouts/BaseLayout.astro';
3
+ import Navbar from '@/components/landing/Navbar.astro';
4
+ import LandingFooter from '@/components/landing/LandingFooter.astro';
5
+ import BlogCard from '@/components/blog/BlogCard.astro';
6
+ import Button from '@/components/ui/Button.astro';
7
+ import Icon from '@/components/ui/Icon.astro';
8
+ import { getCollection } from 'astro:content';
9
+ import { defaultLocale } from '@/i18n/config';
10
+ import { useTranslations } from '@/i18n/index';
11
+
12
+ const locale = defaultLocale;
13
+ const t = useTranslations(locale);
14
+
15
+ // Get all published posts
16
+ const allPosts = await getCollection('blog', ({ data }) => {
17
+ return import.meta.env.PROD ? data.draft !== true : true;
18
+ });
19
+
20
+ // Filter by default locale and sort by date
21
+ const posts = allPosts
22
+ .filter((post) => post.data.locale === locale)
23
+ .sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf());
24
+
25
+ // Separate featured posts (shown in hero section)
26
+ const featuredPosts = posts.filter((post) => post.data.featured);
27
+ // Non-featured posts for the main listing
28
+ const nonFeaturedPosts = posts.filter((post) => !post.data.featured);
29
+ // If ALL posts are featured, show them all in the main listing too (to avoid empty section)
30
+ const regularPosts = nonFeaturedPosts.length > 0 ? nonFeaturedPosts : posts;
31
+
32
+ const getPostUrl = (postId: string) => {
33
+ // Remove locale prefix from id (e.g., "en/welcome-to-velocity" -> "welcome-to-velocity")
34
+ const slug = postId.replace(`${locale}/`, '');
35
+ return `/blog/${slug}`;
36
+ };
37
+ ---
38
+
39
+ <BaseLayout
40
+ title={t('blog.title')}
41
+ description={t('blog.description')}
42
+ >
43
+ <Navbar slot="header" />
44
+
45
+ <!-- Hero Section with decorative background -->
46
+ <section class="relative overflow-hidden bg-background-secondary border-b border-border">
47
+ <!-- Decorative grid pattern -->
48
+ <div class="absolute inset-0 bg-grid-pattern [mask-image:linear-gradient(to_bottom,white_50%,transparent)] pointer-events-none opacity-30"></div>
49
+
50
+ <!-- Decorative blur blob -->
51
+ <div class="absolute top-0 left-1/4 w-96 h-96 bg-brand-200/20 dark:bg-brand-800/10 rounded-full blur-3xl pointer-events-none"></div>
52
+
53
+ <div class="relative mx-auto max-w-6xl px-6 py-20 md:py-28">
54
+ <div class="max-w-3xl">
55
+ <div class="mb-4 inline-flex items-center rounded-full border border-border bg-background px-3 py-1 shadow-sm">
56
+ <Icon name="book" size="sm" class="text-brand-500 mr-2" />
57
+ <span class="text-xs font-medium text-foreground-secondary">{t('blog.allPosts')}</span>
58
+ </div>
59
+
60
+ <h1 class="font-display text-4xl font-bold tracking-tight text-foreground md:text-5xl lg:text-6xl mb-6">
61
+ {t('blog.title')}
62
+ </h1>
63
+
64
+ <p class="text-xl text-foreground-muted leading-relaxed">
65
+ {t('blog.description')}
66
+ </p>
67
+ </div>
68
+ </div>
69
+ </section>
70
+
71
+ <!-- Featured Posts Section -->
72
+ {featuredPosts.length > 0 && (
73
+ <section class="py-16 bg-background">
74
+ <div class="mx-auto max-w-6xl px-6">
75
+ <h2 class="font-display text-2xl font-bold text-foreground mb-8 flex items-center gap-2">
76
+ <svg class="h-6 w-6 text-brand-500" fill="currentColor" viewBox="0 0 20 20">
77
+ <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
78
+ </svg>
79
+ {t('blog.featured')}
80
+ </h2>
81
+
82
+ <div class="grid gap-6 md:grid-cols-2">
83
+ {featuredPosts.map((post) => (
84
+ <BlogCard
85
+ title={post.data.title}
86
+ description={post.data.description}
87
+ href={getPostUrl(post.id)}
88
+ publishedAt={post.data.publishedAt}
89
+ tags={post.data.tags}
90
+ author={post.data.author}
91
+ image={post.data.image}
92
+ featured={true}
93
+ />
94
+ ))}
95
+ </div>
96
+ </div>
97
+ </section>
98
+ )}
99
+
100
+ <!-- All Posts Section -->
101
+ <section class="py-16 bg-background-secondary">
102
+ <div class="mx-auto max-w-6xl px-6">
103
+ {(featuredPosts.length > 0 && nonFeaturedPosts.length > 0) && (
104
+ <h2 class="font-display text-2xl font-bold text-foreground mb-8">
105
+ {t('blog.allPosts')}
106
+ </h2>
107
+ )}
108
+
109
+ {regularPosts.length > 0 ? (
110
+ <div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
111
+ {regularPosts.map((post) => (
112
+ <BlogCard
113
+ title={post.data.title}
114
+ description={post.data.description}
115
+ href={getPostUrl(post.id)}
116
+ publishedAt={post.data.publishedAt}
117
+ tags={post.data.tags}
118
+ author={post.data.author}
119
+ image={post.data.image}
120
+ />
121
+ ))}
122
+ </div>
123
+ ) : posts.length === 0 ? (
124
+ <div class="text-center py-16">
125
+ <div class="mx-auto w-16 h-16 rounded-full bg-background flex items-center justify-center mb-4">
126
+ <Icon name="file-text" size="lg" class="text-foreground-muted" />
127
+ </div>
128
+ <p class="text-foreground-muted text-lg">{t('blog.noPosts')}</p>
129
+ </div>
130
+ ) : null}
131
+ </div>
132
+ </section>
133
+
134
+ <!-- Newsletter CTA Section -->
135
+ <section class="py-20 bg-surface-invert text-on-invert">
136
+ <div class="mx-auto max-w-3xl px-6 text-center">
137
+ <h2 class="font-display text-3xl font-bold mb-4">
138
+ {t('blog.subscribe')}
139
+ </h2>
140
+ <p class="text-on-invert-secondary text-lg mb-8">
141
+ {t('blog.subscribeDescription')}
142
+ </p>
143
+
144
+ <form class="flex flex-col sm:flex-row gap-4 max-w-md mx-auto">
145
+ <input
146
+ type="email"
147
+ placeholder={t('blog.emailPlaceholder')}
148
+ class="flex-1 h-12 px-4 rounded-md bg-surface-invert-secondary border border-border-invert text-on-invert placeholder:text-on-invert-muted focus:outline-none focus:ring-2 focus:ring-brand-500"
149
+ required
150
+ />
151
+ <Button variant="primary" size="lg" type="submit">
152
+ {t('blog.subscribeButton')}
153
+ <Icon name="arrow-right" size="sm" />
154
+ </Button>
155
+ </form>
156
+ </div>
157
+ </section>
158
+
159
+ <LandingFooter slot="footer" />
160
+ </BaseLayout>