feat: Add articles in www
This commit is contained in:
8 files changed
+348
-11
No files matched your search
@@ -1,5 +1,7 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { StorybookConfig } from '@storybook/react-vite';
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
import { mergeConfig } from 'vite';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
stories: [
|
stories: [
|
||||||
@@ -25,14 +27,16 @@ const config: StorybookConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async viteFinal(config) {
|
async viteFinal(config) {
|
||||||
config.resolve = config.resolve || {};
|
return mergeConfig(config, {
|
||||||
config.resolve.alias = {
|
plugins: [tailwindcss()],
|
||||||
...config.resolve.alias,
|
resolve: {
|
||||||
'#': path.resolve(__dirname, '../src'),
|
alias: {
|
||||||
'@ermnvldmr/stl': path.resolve(__dirname, '../../../packages/stl/src'),
|
'#': path.resolve(__dirname, '../src'),
|
||||||
'@ermnvldmr/ui': path.resolve(__dirname, '../../../packages/ui/src'),
|
'@ermnvldmr/stl': path.resolve(__dirname, '../../../packages/stl/src'),
|
||||||
};
|
'@ermnvldmr/ui': path.resolve(__dirname, '../../../packages/ui/src'),
|
||||||
return config;
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
title: "Hello World"
|
||||||
|
description: "My first article using the new content system."
|
||||||
|
createdDate: 2026-01-31
|
||||||
|
tags: ["test", "general"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Hello World
|
||||||
|
|
||||||
|
This is a test article.
|
||||||
|
|
||||||
|
## Section 1
|
||||||
|
|
||||||
|
Some content here.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const hello = "world";
|
||||||
|
console.log(hello);
|
||||||
|
```
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { glob } from 'astro/loaders';
|
||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
|
const articles = defineCollection({
|
||||||
|
// Load all markdown and MDX files from the content/articles directory
|
||||||
|
loader: glob({ pattern: "**/*.{md,mdx}", base: "./content/articles" }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
// Astro handles date strings in frontmatter, but we use coerce to be safe
|
||||||
|
createdDate: z.coerce.date(),
|
||||||
|
updatedDate: z.coerce.date().optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
image: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { articles };
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { ArticleLayout } from './ArticleLayout';
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata for the ArticleLayout component stories.
|
||||||
|
*/
|
||||||
|
const meta: Meta<typeof ArticleLayout> = {
|
||||||
|
title: 'WWW/Layouts/ArticleLayout',
|
||||||
|
component: ArticleLayout,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
args: {
|
||||||
|
title: 'Building a Modern Content System with Astro 5',
|
||||||
|
description:
|
||||||
|
"A deep dive into building a scalable, file-based content architecture using Astro's new Content Layer API and MDX.",
|
||||||
|
createdDate: new Date('2026-01-31'),
|
||||||
|
tags: ['engineering', 'astro', 'react'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
type Story = StoryObj<typeof ArticleLayout>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default story showcasing various HTML elements to verify typography.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<ArticleLayout {...args}>
|
||||||
|
<h2>Introduction</h2>
|
||||||
|
<p>
|
||||||
|
Astro 5 introduces the Content Layer API, a powerful way to fetch content from any source,
|
||||||
|
including local files, remote APIs, and CMSs. In this article, we'll explore how to set
|
||||||
|
up a robust system for articles and projects.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
"The best way to manage content is to not manage it at all." — Someone, probably.
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Let's look at some code. Our new <code>content.config.ts</code> uses the{' '}
|
||||||
|
<code>glob</code> loader to pull in MDX files from outside the <code>src</code> directory:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code>{`// services/www/src/content.config.ts
|
||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
import { glob } from 'astro/loaders';
|
||||||
|
|
||||||
|
const articles = defineCollection({
|
||||||
|
loader: glob({ pattern: "**/*.mdx", base: "./content/articles" }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
createdDate: z.date(),
|
||||||
|
updatedDate: z.date().optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { articles };`}</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h3>Why Astro 5?</h3>
|
||||||
|
<p>
|
||||||
|
Astro's zero-JS by default approach is perfect for content-heavy sites. By moving our
|
||||||
|
content outside of the <code>src</code> directory, we keep our project clean and organized.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Better performance:</strong> Only what you need is shipped.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Improved developer experience:</strong> Fast HMR and great tooling.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Type safety:</strong> Zod schemas validate your frontmatter.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Visual Media</h3>
|
||||||
|
<p>
|
||||||
|
We can also include images and videos. The layout ensures they are responsive and fit the
|
||||||
|
grid.
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted flex aspect-video items-center justify-center rounded-lg border-2 border-dashed">
|
||||||
|
<span className="text-muted-foreground">Placeholder for Image/Video</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Conclusion: It's a great time to be a web developer.</p>
|
||||||
|
</ArticleLayout>
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { cn } from '@ermnvldmr/stl';
|
||||||
|
import { VStack, HStack } from '@ermnvldmr/ui';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the ArticleLayout component.
|
||||||
|
*/
|
||||||
|
export interface ArticleLayoutProps {
|
||||||
|
/** The title of the article. */
|
||||||
|
title: string;
|
||||||
|
/** A short description or lead paragraph for the article. */
|
||||||
|
description?: string;
|
||||||
|
/** The date the article was created. */
|
||||||
|
createdDate: Date;
|
||||||
|
/** The date the article was last updated. */
|
||||||
|
updatedDate?: Date;
|
||||||
|
/** An array of tags associated with the article. */
|
||||||
|
tags?: string[];
|
||||||
|
/** The content of the article. */
|
||||||
|
children: React.ReactNode;
|
||||||
|
/** Optional additional class names for the article container. */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArticleLayout provides a structured container for long-form content.
|
||||||
|
* It uses the project's base typography styles and provides a hero section
|
||||||
|
* for metadata like title, date, and tags.
|
||||||
|
*
|
||||||
|
* @param props - The component props.
|
||||||
|
* @param props.title
|
||||||
|
* @param props.description
|
||||||
|
* @param props.createdDate
|
||||||
|
* @param props.updatedDate
|
||||||
|
* @param props.tags
|
||||||
|
* @param props.children
|
||||||
|
* @param props.className
|
||||||
|
* @returns A React element representing the article layout.
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <ArticleLayout
|
||||||
|
* title="Hello World"
|
||||||
|
* createdDate={new Date()}
|
||||||
|
* tags={['hello', 'world']}
|
||||||
|
* >
|
||||||
|
* <p>This is my first article.</p>
|
||||||
|
* </ArticleLayout>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const ArticleLayout: React.FC<ArticleLayoutProps> = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
createdDate,
|
||||||
|
updatedDate,
|
||||||
|
tags,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const formattedDate = createdDate.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayDate = updatedDate
|
||||||
|
? `Updated ${updatedDate.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}`
|
||||||
|
: formattedDate;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className={cn('mx-auto max-w-3xl px-6 py-12 md:py-20', className)}>
|
||||||
|
<header className="pb-12">
|
||||||
|
<VStack gap={4}>
|
||||||
|
<HStack className="text-muted-foreground text-sm font-medium" gap={4}>
|
||||||
|
<time dateTime={createdDate.toISOString()}>{displayDate}</time>
|
||||||
|
{tags && tags.length > 0 && (
|
||||||
|
<HStack className="flex-wrap" gap={2}>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="before:text-muted-foreground/50 before:mr-0.5 before:content-['#']"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<h1 className="leading-tight">{title}</h1>
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<p className="text-muted-foreground text-l leading-relaxed italic">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="article-content">{children}</section>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import { getCollection, render } from 'astro:content';
|
||||||
|
|
||||||
|
import { ArticleLayout } from '#layouts/ArticleLayout/ArticleLayout';
|
||||||
|
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
|
||||||
|
import '#global.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate paths for all articles in the collection.
|
||||||
|
* The slug will include the year directory (e.g., "2026/test-article").
|
||||||
|
*/
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const articles = await getCollection('articles');
|
||||||
|
return articles.map((entry) => ({
|
||||||
|
params: { slug: entry.id },
|
||||||
|
props: { entry },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props;
|
||||||
|
const { Content } = await render(entry);
|
||||||
|
const { title, description, createdDate, updatedDate, tags } = entry.data;
|
||||||
|
---
|
||||||
|
|
||||||
|
<IndexLayout title={title} description={description ?? ''}>
|
||||||
|
<ArticleLayout
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
createdDate={createdDate}
|
||||||
|
updatedDate={updatedDate}
|
||||||
|
tags={tags}
|
||||||
|
>
|
||||||
|
<Content />
|
||||||
|
</ArticleLayout>
|
||||||
|
</IndexLayout>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
|
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
|
||||||
|
import { SITE_TITLE } from '#shared/constants';
|
||||||
|
import '#global.css';
|
||||||
|
|
||||||
|
const articles = (await getCollection('articles')).sort(
|
||||||
|
(a, b) => b.data.createdDate.valueOf() - a.data.createdDate.valueOf()
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<IndexLayout title={`Articles | ${SITE_TITLE}`} description="Browse all articles and projects.">
|
||||||
|
<main class="mx-auto max-w-3xl px-6 py-20">
|
||||||
|
<h1 class="mb-12">Articles</h1>
|
||||||
|
<ul class="space-y-12">
|
||||||
|
{
|
||||||
|
articles.map((article) => (
|
||||||
|
<li>
|
||||||
|
<a href={`/articles/${article.id}`} class="group block no-underline">
|
||||||
|
<article class="space-y-3">
|
||||||
|
<time class="text-muted-foreground text-sm font-medium">
|
||||||
|
{article.data.createdDate.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
<h2 class="group-hover:text-primary mt-0 border-none pb-0 text-3xl transition-colors">
|
||||||
|
{article.data.title}
|
||||||
|
</h2>
|
||||||
|
{article.data.description && (
|
||||||
|
<p class="text-muted-foreground line-clamp-3 text-lg leading-relaxed">
|
||||||
|
{article.data.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{article.data.tags && (
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{article.data.tags.map((tag) => (
|
||||||
|
<span class="text-muted-foreground text-xs before:content-['#']">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
</IndexLayout>
|
||||||
@@ -14,13 +14,16 @@ import '#global.css';
|
|||||||
It will be further configured.
|
It will be further configured.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
For the source code and support please refer to <a href="https://github.com/deytenit"
|
For the source code and support please refer to <a href="https://github.com/deytenit/ermnvldmr.com/"
|
||||||
>github.com</a
|
>ermnvldmr.com monorepo</a
|
||||||
>.
|
>.
|
||||||
<br />
|
<br />
|
||||||
Pictures of mine are available at <a href="https://instagram.com/deytenit">instagram.com</a
|
Pictures of mine are available at <a href="https://ermnvldmr.com/instagram">@deytenit</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
Check out my <a href="/articles">thoughts and articles on various matters</a>.
|
||||||
|
</p>
|
||||||
<p><em>Thank you for visiting!</em></p>
|
<p><em>Thank you for visiting!</em></p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in new issue
Block a user