Rework layout and refactors (#4)

Reviewed-on: https://git.juancman.dev/juancmandev/website/pulls/4
Co-authored-by: juancmandev <juancmandev@protonmail.com>
Co-committed-by: juancmandev <juancmandev@protonmail.com>
This commit is contained in:
juancmandev 2025-03-14 00:56:54 -04:00 committed by Juan Manzanero
parent 9b4a54f702
commit b4447f0e38
47 changed files with 161 additions and 636 deletions

View File

@ -1,54 +1 @@
# Astro Starter Kit: Basics TODO: Update README
```sh
npm create astro@latest -- --template basics
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
│ └── favicon.svg
├── src/
│ ├── components/
│ │ └── Card.astro
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@ -2,44 +2,24 @@ import { Code, RssIcon } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import formatDate from '@/utils/format-date'; import formatDate from '@/utils/format-date';
import type { lang } from '@/i18n/utils'; import type { lang } from '@/i18n/utils';
import { footerLocales } from '@/i18n/footerLocales';
const locales = { import VerticalLinkButton from '@/components/vertical-link-button';
en: {
developed_by: 'Developed by ',
build_handcrafted: 'Built handcrafted with ',
last_build: 'Last build',
},
es: {
developed_by: 'Desarrollado por ',
build_handcrafted: 'Construido a mano con ',
last_build: 'Última build',
},
};
type Props = { type Props = {
lang: lang; lang: lang;
}; };
export default function Footer({ lang }: Props) { export default function Footer({ lang }: Props) {
const rssUrl =
lang == 'en'
? 'https://juancman.dev/feed.xml'
: 'https://juancman.dev/es/feed.xml';
return ( return (
<footer className='border-t border-secondary px-4 py-12 text-center text-sm md:px-16 prose prose-invert min-w-full'> <footer className='px-2 pt-14 pb-28 text-center lg:px-0 prose prose-invert min-w-full'>
<section> <section>
<p> <p>
{locales[lang].developed_by} {footerLocales[lang].built_with}
<strong className='font-bold text-primary'>juancmandev</strong>
</p>
<p>
{locales[lang].build_handcrafted}
<Button <Button
asChild asChild
size={null} size={null}
variant='link' variant='link'
className='m-0 p-0 text-base no-underline hover:underline' className='m-0 p-0 text-base visited:text-purple-500'
> >
<a <a
href='https://astro.build/' href='https://astro.build/'
@ -50,38 +30,20 @@ export default function Footer({ lang }: Props) {
</Button> </Button>
</p> </p>
<p> <p>
{locales[lang].last_build}: {formatDate(new Date(), lang)}. {footerLocales[lang].last_build}: {formatDate(new Date(), lang)}.
</p> </p>
</section> </section>
<section className='w-max mx-auto flex items-center gap-12'> <section className='w-max mx-auto flex items-center gap-12'>
<Button <VerticalLinkButton
asChild href={`https://juancman.dev/${lang === 'es' && lang}feed.xml`}
size={null}
variant='link'
className='flex flex-col justify-center'
> >
<a <RssIcon className='w-6' />
target='_blank' RSS feed
href='https://git.juancman.dev/juancmandev/website' </VerticalLinkButton>
> <VerticalLinkButton href='https://git.juancman.dev/juancmandev/website'>
<Code className='w-6' /> <Code className='w-6' />
Source Code Source Code
</a> </VerticalLinkButton>
</Button>
<Button
asChild
size={null}
variant='link'
className='flex flex-col justify-center'
>
<a
target='_blank'
href={rssUrl}
>
<RssIcon className='w-6' />
RSS feed
</a>
</Button>
</section> </section>
</footer> </footer>
); );

View File

@ -28,11 +28,9 @@ const { lang } = Astro.props;
--- ---
<header <header
class='py-2 fixed top-0 z-50 flex w-full items-center justify-between border-b border-secondary backdrop-blur-lg' class='py-2 px-2 lg:px-0 fixed top-0 z-50 flex w-full items-center justify-between border-b border-secondary backdrop-blur-lg'
> >
<div <div class='w-full max-w-[800px] mx-auto flex items-center justify-between'>
class='px-4 sm:px-0 flex w-full max-w-[65ch] items-center justify-between mx-auto'
>
<section class='flex max-w-max'> <section class='flex max-w-max'>
<LinkButton <LinkButton
href={lang === 'en' ? '/' : '/es'} href={lang === 'en' ? '/' : '/es'}
@ -52,7 +50,7 @@ const { lang } = Astro.props;
/> />
</LinkButton> </LinkButton>
</section> </section>
<section class='flex items-center gap-2'> <section class='flex items-center gap-4'>
<LinkButton <LinkButton
variant='link' variant='link'
href={locales[lang].to} href={locales[lang].to}

View File

@ -11,7 +11,7 @@ export default function CustomAnchor(props: TAnchor) {
) : ( ) : (
<a <a
{...props} {...props}
className='inline-flex outline-ring visited:text-purple-600' className='inline-flex outline-ring visited:text-purple-500'
target='_blank' target='_blank'
/> />
); );

View File

@ -1,7 +1,6 @@
import LinkButton from '@/components/link-button'; import LinkButton from '@/components/link-button';
import { import {
NotebookText, NotebookText,
BriefcaseBusiness,
MonitorPlay, MonitorPlay,
Newspaper, Newspaper,
PocketKnife, PocketKnife,
@ -20,7 +19,6 @@ export const navItems: TNavItem[] = [
type: 'blog', type: 'blog',
icon: <NotebookText />, icon: <NotebookText />,
}, },
{ type: 'portfolio', icon: <BriefcaseBusiness /> },
{ {
type: 'videos', type: 'videos',
icon: <MonitorPlay />, icon: <MonitorPlay />,
@ -51,7 +49,7 @@ export default function Navigation(props: Props) {
const t = useTranslations(props.lang as any); const t = useTranslations(props.lang as any);
return ( return (
<nav className='px-4 sm:px-0 max-w-[65ch] mx-auto prose prose-invert pt-5 pb-20'> <nav className='prose prose-invert max-w-[800px] mx-auto px-2 lg:px-0 py-16'>
<h2 id='navigation'>{t('navigation')}</h2> <h2 id='navigation'>{t('navigation')}</h2>
<ul className='list-none p-0 flex flex-wrap gap-4'> <ul className='list-none p-0 flex flex-wrap gap-4'>
{navItems.map((navItem, index) => ( {navItems.map((navItem, index) => (

View File

@ -1,31 +1,19 @@
import formatDate from '@/utils/format-date'; import formatDate from '@/utils/format-date';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
type Props = { export default function PostItem(props: TPostItem) {
id: string;
date: Date | string;
title: string;
type: 'blog' | 'portfolio' | 'videos';
lang: string;
};
export default function PostItem(props: Props) {
return ( return (
<Button <Button
asChild asChild
size={null} size={null}
variant='link' variant='link'
className='group hover:no-underline focus:no-underline text-foreground visited:text-purple-600 px-4 whitespace-normal py-2 flex flex-col items-start italic border border-secondary hover:border-foreground focus:border-foreground transition-colors rounded-md' className='group p-0 hover:no-underline focus:no-underline text-foreground visited:text-purple-500 whitespace-normal flex flex-col items-start italic transition-colors rounded-md'
> >
<a <a
className='no-underline' className='no-underline'
href={ href={`/${props.lang === 'es' && 'es'}/${props.type}/${props.id}`}
props.lang === 'en'
? `/${props.type}/${props.id}`
: `/es/${props.type}/${[props.id]}`
}
> >
<span className='text-foreground text-sm font-light no-underline'> <span className='text-foreground text-sm font-light no-underline'>
{formatDate(props.date, props.lang)} {formatDate(props.date, props.lang)}
</span> </span>
<span className='text-lg font-semibold group-hover:underline group-focus:underline'> <span className='text-lg font-semibold group-hover:underline group-focus:underline'>

View File

@ -0,0 +1,28 @@
import PostItem from '@/components/post-item';
import type { CollectionEntry } from 'astro:content';
type Props = {
items: any;
lang: string;
};
export default function PostItemList(props: Props) {
return (
<ul>
{props.items.map(
(item: CollectionEntry<'blog' | 'videos'>) =>
item && (
<li key={item.id}>
<PostItem
type={item.collection}
lang={props.lang}
id={item.id}
date={item.data.date!}
title={item.data.title!}
/>
</li>
)
)}
</ul>
);
}

View File

@ -0,0 +1,21 @@
---
import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import { readdirSync } from 'fs';
const images = import.meta.glob<{ default: ImageMetadata }>(
'/src/assets/random/*.{jpeg,jpg,png,gif}'
);
const imagePaths = readdirSync('./src/assets/random/');
const randomIndex = Math.floor(Math.random() * (imagePaths.length - 0));
const randomImage = `/src/assets/random/${imagePaths[randomIndex]}`;
---
<Image
decoding='sync'
loading='eager'
src={images[randomImage]()}
alt='image'
width={300}
class='w-auto h-auto rounded-md aspect-auto object-cover mx-auto'
/>

View File

@ -0,0 +1,24 @@
import { Button } from './ui/button';
type Props = {
href?: string;
children: React.ReactNode;
};
export default function VerticalLinkButton(props: Props) {
return (
<Button
asChild
size={null}
variant='link'
className='flex flex-col justify-center visited:text-purple-500'
>
<a
target={props.href?.startsWith('h') ? '_blank' : ''}
href={props.href}
>
{props.children}
</a>
</Button>
);
}

View File

@ -10,7 +10,8 @@ const contentSchema = z.object({
tags: z.array(z.string()), tags: z.array(z.string()),
author: z.string(), author: z.string(),
rss: z.boolean(), rss: z.boolean(),
draft: z.boolean({}).optional(), draft: z.boolean().optional(),
archived: z.boolean().optional(),
}); });
const blog = defineCollection({ const blog = defineCollection({
@ -18,14 +19,6 @@ const blog = defineCollection({
schema: contentSchema, schema: contentSchema,
}); });
const portfolio = defineCollection({
loader: glob({
pattern: '**/[^_]*.{md,mdx}',
base: './src/content/portfolio',
}),
schema: contentSchema,
});
const pages = defineCollection({ const pages = defineCollection({
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/content/pages' }), loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/content/pages' }),
schema: z.object({ schema: z.object({
@ -41,7 +34,6 @@ const videos = defineCollection({
export const collections = { export const collections = {
blog, blog,
portfolio,
pages, pages,
videos, videos,
}; };

View File

@ -9,11 +9,6 @@ author: Juan Manzanero
rss: true rss: true
--- ---
![Newspapers](@/assets/blog/a-better-way-for-consuming-content/banner.webp) _Photo by
[Ashni](https://unsplash.com/@ashni_ahlawat?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)
on
[Unsplash](https://unsplash.com/photos/text-ePWaAwUn80k?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)_
Get your news without visiting websites with algorithms that shows content that you don't want to see. Get your news without visiting websites with algorithms that shows content that you don't want to see.
## Algorithms that Dictates What You See ## Algorithms that Dictates What You See

12
src/i18n/footerLocales.ts Normal file
View File

@ -0,0 +1,12 @@
export const footerLocales = {
en: {
developed_by: 'Developed by ',
built_with: 'Built with ',
last_build: 'Last build',
},
es: {
developed_by: 'Desarrollado por ',
built_with: 'Construido con ',
last_build: 'Última build',
},
};

View File

@ -63,7 +63,7 @@ const { title, description } = Astro.props;
</head> </head>
<body> <body>
<Header lang={lang} /> <Header lang={lang} />
<main class='px-4 sm:px-0 max-w-[65ch] pt-28 pb-5 mx-auto'> <main class='width-global px-2 lg:px-0 pt-32'>
<slot /> <slot />
</main> </main>
<Navigation lang={lang} /> <Navigation lang={lang} />

View File

@ -6,7 +6,7 @@ import Layout from '@/layouts/Layout.astro';
title='Not found' title='Not found'
description='Error 404: Not found.' description='Error 404: Not found.'
> >
<div class='prose prose-invert'> <div class='prose prose-invert max-w-[800px] mx-auto'>
<h1 class=''>Error 404: Not found</h1> <h1 class=''>Error 404: Not found</h1>
</div> </div>
</Layout> </Layout>

View File

@ -32,7 +32,7 @@ const { Content, remarkPluginFrontmatter: data } = await render(project);
title={data.title} title={data.title}
description={data.description} description={data.description}
> >
<article class='prose prose-invert'> <article class='prose prose-invert max-w-[800px] mx-auto'>
<Content components={{ ...components }} /> <Content components={{ ...components }} />
</article> </article>
</Layout> </Layout>

View File

@ -36,7 +36,7 @@ const { Content, remarkPluginFrontmatter: data } = await render(post);
title={data.title} title={data.title}
description={data.description} description={data.description}
> >
<article class='prose prose-invert'> <article class='prose prose-invert max-w-[800px] mx-auto'>
<h1>{data.title}</h1> <h1>{data.title}</h1>
<Content components={{ ...components }} /> <Content components={{ ...components }} />
<hr /> <hr />

View File

@ -1,5 +1,5 @@
--- ---
import PostItem from '@/components/post-item'; import PostItemList from '@/components/post-items-list';
import { getLangFromUrl } from '@/i18n/utils'; import { getLangFromUrl } from '@/i18n/utils';
import Layout from '@/layouts/Layout.astro'; import Layout from '@/layouts/Layout.astro';
import { sortContentByDate } from '@/utils/sorts'; import { sortContentByDate } from '@/utils/sorts';
@ -27,26 +27,12 @@ const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<section class='prose prose-invert'> <div class='prose prose-invert max-w-[800px] mx-auto'>
<h1>{pageData.title}</h1> <h1>{pageData.title}</h1>
<p>{pageData.description}</p> <p>{pageData.description}</p>
</section> <PostItemList
<ul class='mt-4 flex flex-col gap-4'> items={filterEnPosts}
{ lang={lang}
filterEnPosts.map( />
(blogpost) => </div>
blogpost && (
<li>
<PostItem
type='blog'
lang={lang}
id={blogpost.id}
date={blogpost.data.date!}
title={blogpost.data.title!}
/>
</li>
)
)
}
</ul>
</Layout> </Layout>

View File

@ -32,7 +32,7 @@ const { Content, remarkPluginFrontmatter: data } = await render(page);
title={data.title} title={data.title}
description={data.description} description={data.description}
> >
<article class='prose prose-invert'> <article class='prose prose-invert max-w-[800px] mx-auto'>
<Content components={{ ...components }} /> <Content components={{ ...components }} />
</article> </article>
</Layout> </Layout>

View File

@ -36,7 +36,7 @@ const { Content, remarkPluginFrontmatter: data } = await render(blog);
title={data.title} title={data.title}
description={data.description} description={data.description}
> >
<article class='prose prose-invert'> <article class='prose prose-invert max-w-[800px] mx-auto'>
<h1>{data.title}</h1> <h1>{data.title}</h1>
<Content components={{ ...components }} /> <Content components={{ ...components }} />
<hr /> <hr />

View File

@ -27,7 +27,7 @@ const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<section class='prose prose-invert'> <section class='prose prose-invert max-w-[800px] mx-auto'>
<h1>{pageData.title}</h1> <h1>{pageData.title}</h1>
<p>{pageData.description}</p> <p>{pageData.description}</p>
</section> </section>

View File

@ -12,9 +12,6 @@ const markdownParser = new MarkdownIt();
const imagesBlog = import.meta.glob<{ default: ImageMetadata }>( const imagesBlog = import.meta.glob<{ default: ImageMetadata }>(
'/src/assets/blog/**/**/*.{jpeg,jpg,png,gif,webp}' '/src/assets/blog/**/**/*.{jpeg,jpg,png,gif,webp}'
); );
const imagesPortfolio = import.meta.glob<{ default: ImageMetadata }>(
'/src/assets/portfolio/**/**/*.{jpeg,jpg,png,gif,webp}'
);
export async function GET(context: any) { export async function GET(context: any) {
const items: RSSFeedItem[] = []; const items: RSSFeedItem[] = [];
@ -29,16 +26,6 @@ export async function GET(context: any) {
return lang === 'es' && post; return lang === 'es' && post;
}); });
const portfolio = await getCollection(
'portfolio',
({ data }) => data.draft !== true && data.rss === true
);
const filterPortfolio = portfolio.filter((project) => {
const [lang] = project.id.split('/');
return lang === 'es' && project;
});
for await (const post of filterBlog) { for await (const post of filterBlog) {
const body = markdownParser.render(post.body!); const body = markdownParser.render(post.body!);
const html = htmlParser.parse(body); const html = htmlParser.parse(body);
@ -79,47 +66,6 @@ export async function GET(context: any) {
}); });
} }
for await (const project of filterPortfolio) {
const body = markdownParser.render(project.body!);
const html = htmlParser.parse(body);
const images = html.querySelectorAll('img');
for await (const img of images) {
const src = img.getAttribute('src')!;
if (src.startsWith('@/')) {
const prefixRemoved = src.replace('@/', '');
const imagePathPrefix = `/src/${prefixRemoved}`;
const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then(
(res: any) => res.default
);
if (imagePath) {
const optimizedImg = await getImage({ src: imagePath });
img.setAttribute(
'src',
context.site + optimizedImg.src.replace('/', '')
);
}
} else if (src.startsWith('/images')) {
// images starting with `/images/` is the public dir
img.setAttribute('src', context.site + src.replace('/', ''));
} else {
throw Error('src unknown');
}
}
items.push({
title: project.data.title,
pubDate: project.data.date,
description: project.data.description,
link: `/portfolio/${project.id.split('.')[0]}/`,
content: sanitizeHtml(html.toString(), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
}),
});
}
return rss({ return rss({
xmlns: { atom: 'http://www.w3.org/2005/Atom' }, xmlns: { atom: 'http://www.w3.org/2005/Atom' },
title: 'juancmandev', title: 'juancmandev',

View File

@ -1,6 +1,6 @@
--- ---
import LinkButton from '@/components/link-button'; import LinkButton from '@/components/link-button';
import PostItem from '@/components/post-item'; import PostItemList from '@/components/post-items-list';
import { getLangFromUrl } from '@/i18n/utils'; import { getLangFromUrl } from '@/i18n/utils';
import Layout from '@/layouts/Layout.astro'; import Layout from '@/layouts/Layout.astro';
import { sortContentByDate } from '@/utils/sorts'; import { sortContentByDate } from '@/utils/sorts';
@ -26,28 +26,11 @@ const allEsPosts = allPosts.map((post) => {
sortContentByDate(allEsPosts); sortContentByDate(allEsPosts);
const last3Blogs = allEsPosts.slice(0, 3); const last3Blogs = allEsPosts.slice(0, 3);
const allProjects = await getCollection(
'portfolio',
({ data }) => data.draft !== true
);
const allEnProjects = allProjects.map((project) => {
const [lang, id] = project.id.split('/');
if (lang !== 'en')
return {
...project,
id: id.split('.')[0],
};
else null;
});
sortContentByDate(allEnProjects);
const last3Projects = allEnProjects.slice(0, 3);
const lang = getLangFromUrl(Astro.url); const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<div class='prose prose-invert'> <div class='prose prose-invert max-w-[800px] mx-auto'>
<h1 class='text-primary'>Bienvenido a mi dominio, extraño.</h1> <h1 class='text-primary'>Bienvenido a mi dominio, extraño.</h1>
<p> <p>
Soy <strong class='text-primary'>juancmandev</strong>; <strong Soy <strong class='text-primary'>juancmandev</strong>; <strong
@ -63,24 +46,10 @@ const lang = getLangFromUrl(Astro.url);
</p> </p>
<section> <section>
<h2>Últimos posts</h2> <h2>Últimos posts</h2>
<ul class='mt-0 p-0 list-none'> <PostItemList
{ items={last3Blogs}
last3Blogs.map( lang={lang}
(blogpost) => />
blogpost && (
<li class='p-0'>
<PostItem
type='blog'
lang={lang}
id={blogpost.id}
date={blogpost.data.date}
title={blogpost.data.title}
/>
</li>
)
)
}
</ul>
<LinkButton <LinkButton
variant='secondary' variant='secondary'
href='/es/blog' href='/es/blog'
@ -88,32 +57,5 @@ const lang = getLangFromUrl(Astro.url);
>Más posts</LinkButton >Más posts</LinkButton
> >
</section> </section>
<section>
<h2>Últimos proyectos</h2>
<ul class='mt-0 p-0 list-none'>
{
last3Projects.map(
(project) =>
project && (
<li class='p-0'>
<PostItem
lang={lang}
type='portfolio'
id={project.id}
date={project.data.date!}
title={project.data.title!}
/>
</li>
)
)
}
</ul>
<LinkButton
variant='secondary'
href='/es/portfolio'
className='no-underline'
>Más proyectos</LinkButton
>
</section>
</div> </div>
</Layout> </Layout>

View File

@ -1,48 +0,0 @@
---
import Layout from '@/layouts/Layout.astro';
import components from '@/components/mdx/wrapper';
import formatDate from '@/utils/format-date';
import { getLangFromUrl } from '@/i18n/utils';
import { getCollection, getEntry, render } from 'astro:content';
export async function getStaticPaths() {
const allProjects = await getCollection(
'portfolio',
({ data }) => data.draft !== true
);
const filterEnProjects = allProjects.map((project) => {
const [lang, id] = project.id.split('/');
if (lang === 'es')
return {
...project,
id: id.split('.')[0],
};
else null;
});
return filterEnProjects.map((project) => ({
params: { slug: project?.id },
}));
}
const lang = getLangFromUrl(Astro.url);
const { slug } = Astro.params;
const project = await getEntry('portfolio', `${lang}/${slug}`)!;
const { Content, remarkPluginFrontmatter: data } = await render(project);
---
<Layout
title={data.title}
description={data.description}
>
<article class='prose prose-invert'>
<h1>{data.title}</h1>
<Content components={{ ...components }} />
<hr />
<p>
<strong>Publicado: </strong>
{data.date && formatDate(new Date(data.date), lang)}
</p>
</article>
</Layout>

View File

@ -1,55 +0,0 @@
---
import PostItem from '@/components/post-item';
import { getLangFromUrl } from '@/i18n/utils';
import Layout from '@/layouts/Layout.astro';
import { sortContentByDate } from '@/utils/sorts';
import { getCollection } from 'astro:content';
const pageData = {
title: 'Portfolio',
description: 'Revisa mis proyectos.',
};
const allProjects = await getCollection(
'portfolio',
({ data }) => data.draft !== true
);
const allEsProjects = allProjects.map((project) => {
const [lang, id] = project.id.split('/');
if (lang === 'es')
return {
...project,
id: id.split('.')[0],
};
else null;
});
sortContentByDate(allEsProjects);
const lang = getLangFromUrl(Astro.url);
---
<Layout {...pageData}>
<section class='prose prose-invert'>
<h1>{pageData.title}</h1>
<p>{pageData.description}</p>
</section>
<ul class='mt-4 flex flex-col gap-4'>
{
allEsProjects.map(
(project) =>
project && (
<li>
<PostItem
lang={lang}
type='portfolio'
id={project.id}
date={project.data.date!}
title={project.data.title!}
/>
</li>
)
)
}
</ul>
</Layout>

View File

@ -34,7 +34,7 @@ const { Content, remarkPluginFrontmatter: data } = await render(video);
title={data.title} title={data.title}
description={data.description} description={data.description}
> >
<article class='prose prose-invert'> <article class='prose prose-invert max-w-[800px] mx-auto'>
<h1>{data.title}</h1> <h1>{data.title}</h1>
<Content components={{ ...components }} /> <Content components={{ ...components }} />
<hr /> <hr />

View File

@ -1,5 +1,5 @@
--- ---
import PostItem from '@/components/post-item'; import PostItemList from '@/components/post-items-list';
import { getLangFromUrl } from '@/i18n/utils'; import { getLangFromUrl } from '@/i18n/utils';
import Layout from '@/layouts/Layout.astro'; import Layout from '@/layouts/Layout.astro';
import { sortContentByDate } from '@/utils/sorts'; import { sortContentByDate } from '@/utils/sorts';
@ -20,23 +20,12 @@ const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<section class='prose prose-invert'> <div class='prose prose-invert max-w-[800px] mx-auto'>
<h1>{pageData.title}</h1> <h1>{pageData.title}</h1>
<p>{pageData.description}</p> <p>{pageData.description}</p>
</section> <PostItemList
<ul class='mt-4 flex flex-col gap-4'> items={allVideos}
{ lang={lang}
allVideos.map((video: any) => ( />
<li> </div>
<PostItem
lang={lang}
type='videos'
id={video.id.split('.')[0]}
date={video.data.date!}
title={video.data.title!}
/>
</li>
))
}
</ul>
</Layout> </Layout>

View File

@ -12,9 +12,6 @@ const markdownParser = new MarkdownIt();
const imagesBlog = import.meta.glob<{ default: ImageMetadata }>( const imagesBlog = import.meta.glob<{ default: ImageMetadata }>(
'/src/assets/blog/**/**/*.{jpeg,jpg,png,gif,webp}' '/src/assets/blog/**/**/*.{jpeg,jpg,png,gif,webp}'
); );
const imagesPortfolio = import.meta.glob<{ default: ImageMetadata }>(
'/src/assets/portfolio/**/**/*.{jpeg,jpg,png,gif,webp}'
);
export async function GET(context: any) { export async function GET(context: any) {
const items: RSSFeedItem[] = []; const items: RSSFeedItem[] = [];
@ -29,16 +26,6 @@ export async function GET(context: any) {
return lang !== 'es' && post; return lang !== 'es' && post;
}); });
const portfolio = await getCollection(
'portfolio',
({ data }) => data.draft !== true && data.rss === true
);
const filterPortfolio = portfolio.filter((project) => {
const [lang] = project.id.split('/');
return lang !== 'es' && project;
});
for await (const post of filterBlog) { for await (const post of filterBlog) {
const body = markdownParser.render(post.body!); const body = markdownParser.render(post.body!);
const html = htmlParser.parse(body); const html = htmlParser.parse(body);
@ -79,47 +66,6 @@ export async function GET(context: any) {
}); });
} }
for await (const project of filterPortfolio) {
const body = markdownParser.render(project.body!);
const html = htmlParser.parse(body);
const images = html.querySelectorAll('img');
for await (const img of images) {
const src = img.getAttribute('src')!;
if (src.startsWith('@/')) {
const prefixRemoved = src.replace('@/', '');
const imagePathPrefix = `/src/${prefixRemoved}`;
const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then(
(res: any) => res.default
);
if (imagePath) {
const optimizedImg = await getImage({ src: imagePath });
img.setAttribute(
'src',
context.site + optimizedImg.src.replace('/', '')
);
}
} else if (src.startsWith('/images')) {
// images starting with `/images/` is the public dir
img.setAttribute('src', context.site + src.replace('/', ''));
} else {
throw Error('src unknown');
}
}
items.push({
title: project.data.title,
pubDate: project.data.date,
description: project.data.description,
link: `/portfolio/${project.id.split('.')[0]}/`,
content: sanitizeHtml(html.toString(), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
}),
});
}
return rss({ return rss({
xmlns: { atom: 'http://www.w3.org/2005/Atom' }, xmlns: { atom: 'http://www.w3.org/2005/Atom' },
title: 'juancmandev', title: 'juancmandev',

View File

@ -2,9 +2,10 @@
import Layout from '@/layouts/Layout.astro'; import Layout from '@/layouts/Layout.astro';
import LinkButton from '@/components/link-button'; import LinkButton from '@/components/link-button';
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
import PostItem from '@/components/post-item';
import { sortContentByDate } from '@/utils/sorts'; import { sortContentByDate } from '@/utils/sorts';
import { getLangFromUrl } from '@/i18n/utils'; import { getLangFromUrl } from '@/i18n/utils';
import PostItemList from '@/components/post-items-list';
import Random from '@/components/random.astro';
const pageData = { const pageData = {
title: 'juancmandev', title: 'juancmandev',
@ -26,31 +27,15 @@ const allEnPosts = allPosts.map((post) => {
sortContentByDate(allEnPosts); sortContentByDate(allEnPosts);
const last3Blogs = allEnPosts.slice(0, 3); const last3Blogs = allEnPosts.slice(0, 3);
const allProjects = await getCollection(
'portfolio',
({ data }) => data.draft !== true
);
const allEnProjects = allProjects.map((project) => {
const [lang, id] = project.id.split('/');
if (lang !== 'es')
return {
...project,
id: id.split('.')[0],
};
else null;
});
sortContentByDate(allEnProjects);
const last3Projects = allEnProjects.slice(0, 3);
const lang = getLangFromUrl(Astro.url); const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<div class='prose prose-invert'> <div class='prose prose-invert max-w-[800px] mx-auto'>
<h1 class='text-primary'>Welcome to my domain, stranger.</h1> <h1 class='text-primary'>Welcome to my domain, stranger.</h1>
<!-- <Random /> -->
<p> <p>
I am <strong class='text-primary'>juancmandev</strong>; <strong I am <strong class='text-primary'>Juan Manzanero</strong>; <strong
>Web Developer</strong >Web Developer</strong
>, <strong>Linux</strong> enthusiast, and <strong>privacy</strong> defender. >, <strong>Linux</strong> enthusiast, and <strong>privacy</strong> defender.
</p> </p>
@ -61,21 +46,10 @@ const lang = getLangFromUrl(Astro.url);
</p> </p>
<section> <section>
<h2>Latest posts</h2> <h2>Latest posts</h2>
<ul class='mt-0 p-0 list-none'> <PostItemList
{ items={last3Blogs}
last3Blogs.map((blogpost: any) => ( lang={lang}
<li class='p-0'> />
<PostItem
type='blog'
lang={lang}
id={blogpost.id}
date={blogpost.data.date!}
title={blogpost.data.title!}
/>
</li>
))
}
</ul>
<LinkButton <LinkButton
variant='secondary' variant='secondary'
href='/blog' href='/blog'
@ -83,32 +57,5 @@ const lang = getLangFromUrl(Astro.url);
>More posts</LinkButton >More posts</LinkButton
> >
</section> </section>
<section>
<h2>Latest projects</h2>
<ul class='mt-0 p-0 list-none'>
{
last3Projects.map(
(project) =>
project && (
<li class='p-0'>
<PostItem
lang={lang}
type='portfolio'
id={project.id}
date={project.data.date!}
title={project.data.title!}
/>
</li>
)
)
}
</ul>
<LinkButton
variant='secondary'
href='/portfolio'
className='no-underline'
>More projects</LinkButton
>
</section>
</div> </div>
</Layout> </Layout>

View File

@ -14,7 +14,7 @@ const data = await pb.collection('microblogs').getFullList({
title='Microblog' title='Microblog'
description='Short-format writing. Instead of using shitty social media.' description='Short-format writing. Instead of using shitty social media.'
> >
<div class='prose prose-invert'> <div class='prose prose-invert max-w-[800px] mx-auto'>
<h1>Microblog</h1> <h1>Microblog</h1>
<p>Short-format writing.</p> <p>Short-format writing.</p>
<p>Instead of using shitty social media.</p> <p>Instead of using shitty social media.</p>

View File

@ -1,48 +0,0 @@
---
import Layout from '@/layouts/Layout.astro';
import components from '@/components/mdx/wrapper';
import formatDate from '@/utils/format-date';
import { getLangFromUrl } from '@/i18n/utils';
import { getCollection, getEntry, render } from 'astro:content';
export async function getStaticPaths() {
const allProjects = await getCollection(
'portfolio',
({ data }) => data.draft !== true
);
const filterEnProjects = allProjects.map((project) => {
const [lang, id] = project.id.split('/');
if (lang === 'en')
return {
...project,
id: id.split('.')[0],
};
else null;
});
return filterEnProjects.map((project) => ({
params: { slug: project?.id },
}));
}
const lang = getLangFromUrl(Astro.url);
const { slug } = Astro.params;
const project = await getEntry('portfolio', `${lang}/${slug}`)!;
const { Content, remarkPluginFrontmatter: data } = await render(project);
---
<Layout
title={data.title}
description={data.description}
>
<article class='prose prose-invert'>
<h1>{data.title}</h1>
<Content components={{ ...components }} />
<hr />
<p>
<strong>Posted: </strong>
{data.date && formatDate(new Date(data.date), lang)}
</p>
</article>
</Layout>

View File

@ -1,55 +0,0 @@
---
import PostItem from '@/components/post-item';
import { getLangFromUrl } from '@/i18n/utils';
import Layout from '@/layouts/Layout.astro';
import { sortContentByDate } from '@/utils/sorts';
import { getCollection } from 'astro:content';
const pageData = {
title: 'Portfolio',
description: 'Check my projects.',
};
const allProjects = await getCollection(
'portfolio',
({ data }) => data.draft !== true
);
const allEnProjects = allProjects.map((project) => {
const [lang, id] = project.id.split('/');
if (lang === 'en')
return {
...project,
id: id.split('.')[0],
};
else null;
});
sortContentByDate(allEnProjects);
const lang = getLangFromUrl(Astro.url);
---
<Layout {...pageData}>
<section class='prose prose-invert'>
<h1>{pageData.title}</h1>
<p>{pageData.description}</p>
</section>
<ul class='mt-4 flex flex-col gap-4'>
{
allEnProjects.map(
(project) =>
project && (
<li>
<PostItem
lang={lang}
type='portfolio'
id={project.id}
date={project.data.date!}
title={project.data.title!}
/>
</li>
)
)
}
</ul>
</Layout>

View File

@ -4,7 +4,7 @@ const robotsTxt = `
User-agent: * User-agent: *
Allow: / Allow: /
Sitemap: ${new URL('sitemap-index.xml', import.meta.env.SITE).href} Sitemap: ${new URL('sitemap-index.xml', 'https://juancman.dev').href}
`.trim(); `.trim();
export const GET: APIRoute = () => { export const GET: APIRoute = () => {

View File

@ -9,6 +9,9 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
.prose {
@apply max-w-none;
}
[data-rehype-pretty-code-figure] { [data-rehype-pretty-code-figure] {
@apply bg-[#1e1e2e] rounded-md pt-4; @apply bg-[#1e1e2e] rounded-md pt-4;
} }

7
src/types/post-item.ts Normal file
View File

@ -0,0 +1,7 @@
type TPostItem = {
id: string;
date: Date | string;
title: string;
type: 'blog' | 'portfolio' | 'videos';
lang: string;
};