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
```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).
TODO: Update README

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 formatDate from '@/utils/format-date';
import type { lang } from '@/i18n/utils';
const locales = {
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',
},
};
import { footerLocales } from '@/i18n/footerLocales';
import VerticalLinkButton from '@/components/vertical-link-button';
type Props = {
lang: lang;
};
export default function Footer({ lang }: Props) {
const rssUrl =
lang == 'en'
? 'https://juancman.dev/feed.xml'
: 'https://juancman.dev/es/feed.xml';
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>
<p>
{locales[lang].developed_by}
<strong className='font-bold text-primary'>juancmandev</strong>
</p>
<p>
{locales[lang].build_handcrafted}
{footerLocales[lang].built_with}
<Button
asChild
size={null}
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
href='https://astro.build/'
@ -50,38 +30,20 @@ export default function Footer({ lang }: Props) {
</Button>
</p>
<p>
{locales[lang].last_build}: {formatDate(new Date(), lang)}.
{footerLocales[lang].last_build}: {formatDate(new Date(), lang)}.
</p>
</section>
<section className='w-max mx-auto flex items-center gap-12'>
<Button
asChild
size={null}
variant='link'
className='flex flex-col justify-center'
<VerticalLinkButton
href={`https://juancman.dev/${lang === 'es' && lang}feed.xml`}
>
<a
target='_blank'
href='https://git.juancman.dev/juancmandev/website'
>
<Code className='w-6' />
Source Code
</a>
</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>
<RssIcon className='w-6' />
RSS feed
</VerticalLinkButton>
<VerticalLinkButton href='https://git.juancman.dev/juancmandev/website'>
<Code className='w-6' />
Source Code
</VerticalLinkButton>
</section>
</footer>
);

View File

@ -28,11 +28,9 @@ const { lang } = Astro.props;
---
<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
class='px-4 sm:px-0 flex w-full max-w-[65ch] items-center justify-between mx-auto'
>
<div class='w-full max-w-[800px] mx-auto flex items-center justify-between'>
<section class='flex max-w-max'>
<LinkButton
href={lang === 'en' ? '/' : '/es'}
@ -52,7 +50,7 @@ const { lang } = Astro.props;
/>
</LinkButton>
</section>
<section class='flex items-center gap-2'>
<section class='flex items-center gap-4'>
<LinkButton
variant='link'
href={locales[lang].to}

View File

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

View File

@ -1,7 +1,6 @@
import LinkButton from '@/components/link-button';
import {
NotebookText,
BriefcaseBusiness,
MonitorPlay,
Newspaper,
PocketKnife,
@ -20,7 +19,6 @@ export const navItems: TNavItem[] = [
type: 'blog',
icon: <NotebookText />,
},
{ type: 'portfolio', icon: <BriefcaseBusiness /> },
{
type: 'videos',
icon: <MonitorPlay />,
@ -51,7 +49,7 @@ export default function Navigation(props: Props) {
const t = useTranslations(props.lang as any);
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>
<ul className='list-none p-0 flex flex-wrap gap-4'>
{navItems.map((navItem, index) => (

View File

@ -1,31 +1,19 @@
import formatDate from '@/utils/format-date';
import { Button } from '@/components/ui/button';
type Props = {
id: string;
date: Date | string;
title: string;
type: 'blog' | 'portfolio' | 'videos';
lang: string;
};
export default function PostItem(props: Props) {
export default function PostItem(props: TPostItem) {
return (
<Button
asChild
size={null}
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
className='no-underline'
href={
props.lang === 'en'
? `/${props.type}/${props.id}`
: `/es/${props.type}/${[props.id]}`
}
href={`/${props.lang === 'es' && '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)}
</span>
<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()),
author: z.string(),
rss: z.boolean(),
draft: z.boolean({}).optional(),
draft: z.boolean().optional(),
archived: z.boolean().optional(),
});
const blog = defineCollection({
@ -18,14 +19,6 @@ const blog = defineCollection({
schema: contentSchema,
});
const portfolio = defineCollection({
loader: glob({
pattern: '**/[^_]*.{md,mdx}',
base: './src/content/portfolio',
}),
schema: contentSchema,
});
const pages = defineCollection({
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/content/pages' }),
schema: z.object({
@ -41,7 +34,6 @@ const videos = defineCollection({
export const collections = {
blog,
portfolio,
pages,
videos,
};

View File

@ -9,11 +9,6 @@ author: Juan Manzanero
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.
## 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>
<body>
<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 />
</main>
<Navigation lang={lang} />

View File

@ -6,7 +6,7 @@ import Layout from '@/layouts/Layout.astro';
title='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>
</div>
</Layout>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,9 +12,6 @@ const markdownParser = new MarkdownIt();
const imagesBlog = import.meta.glob<{ default: ImageMetadata }>(
'/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) {
const items: RSSFeedItem[] = [];
@ -29,16 +26,6 @@ export async function GET(context: any) {
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) {
const body = markdownParser.render(post.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({
xmlns: { atom: 'http://www.w3.org/2005/Atom' },
title: 'juancmandev',

View File

@ -1,6 +1,6 @@
---
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 Layout from '@/layouts/Layout.astro';
import { sortContentByDate } from '@/utils/sorts';
@ -26,28 +26,11 @@ const allEsPosts = allPosts.map((post) => {
sortContentByDate(allEsPosts);
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);
---
<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>
<p>
Soy <strong class='text-primary'>juancmandev</strong>; <strong
@ -63,24 +46,10 @@ const lang = getLangFromUrl(Astro.url);
</p>
<section>
<h2>Últimos posts</h2>
<ul class='mt-0 p-0 list-none'>
{
last3Blogs.map(
(blogpost) =>
blogpost && (
<li class='p-0'>
<PostItem
type='blog'
lang={lang}
id={blogpost.id}
date={blogpost.data.date}
title={blogpost.data.title}
/>
</li>
)
)
}
</ul>
<PostItemList
items={last3Blogs}
lang={lang}
/>
<LinkButton
variant='secondary'
href='/es/blog'
@ -88,32 +57,5 @@ const lang = getLangFromUrl(Astro.url);
>Más posts</LinkButton
>
</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>
</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}
description={data.description}
>
<article class='prose prose-invert'>
<article class='prose prose-invert max-w-[800px] mx-auto'>
<h1>{data.title}</h1>
<Content components={{ ...components }} />
<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 Layout from '@/layouts/Layout.astro';
import { sortContentByDate } from '@/utils/sorts';
@ -20,23 +20,12 @@ const lang = getLangFromUrl(Astro.url);
---
<Layout {...pageData}>
<section class='prose prose-invert'>
<div class='prose prose-invert max-w-[800px] mx-auto'>
<h1>{pageData.title}</h1>
<p>{pageData.description}</p>
</section>
<ul class='mt-4 flex flex-col gap-4'>
{
allVideos.map((video: any) => (
<li>
<PostItem
lang={lang}
type='videos'
id={video.id.split('.')[0]}
date={video.data.date!}
title={video.data.title!}
/>
</li>
))
}
</ul>
<PostItemList
items={allVideos}
lang={lang}
/>
</div>
</Layout>

View File

@ -12,9 +12,6 @@ const markdownParser = new MarkdownIt();
const imagesBlog = import.meta.glob<{ default: ImageMetadata }>(
'/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) {
const items: RSSFeedItem[] = [];
@ -29,16 +26,6 @@ export async function GET(context: any) {
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) {
const body = markdownParser.render(post.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({
xmlns: { atom: 'http://www.w3.org/2005/Atom' },
title: 'juancmandev',

View File

@ -2,9 +2,10 @@
import Layout from '@/layouts/Layout.astro';
import LinkButton from '@/components/link-button';
import { getCollection } from 'astro:content';
import PostItem from '@/components/post-item';
import { sortContentByDate } from '@/utils/sorts';
import { getLangFromUrl } from '@/i18n/utils';
import PostItemList from '@/components/post-items-list';
import Random from '@/components/random.astro';
const pageData = {
title: 'juancmandev',
@ -26,31 +27,15 @@ const allEnPosts = allPosts.map((post) => {
sortContentByDate(allEnPosts);
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);
---
<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>
<!-- <Random /> -->
<p>
I am <strong class='text-primary'>juancmandev</strong>; <strong
I am <strong class='text-primary'>Juan Manzanero</strong>; <strong
>Web Developer</strong
>, <strong>Linux</strong> enthusiast, and <strong>privacy</strong> defender.
</p>
@ -61,21 +46,10 @@ const lang = getLangFromUrl(Astro.url);
</p>
<section>
<h2>Latest posts</h2>
<ul class='mt-0 p-0 list-none'>
{
last3Blogs.map((blogpost: any) => (
<li class='p-0'>
<PostItem
type='blog'
lang={lang}
id={blogpost.id}
date={blogpost.data.date!}
title={blogpost.data.title!}
/>
</li>
))
}
</ul>
<PostItemList
items={last3Blogs}
lang={lang}
/>
<LinkButton
variant='secondary'
href='/blog'
@ -83,32 +57,5 @@ const lang = getLangFromUrl(Astro.url);
>More posts</LinkButton
>
</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>
</Layout>

View File

@ -14,7 +14,7 @@ const data = await pb.collection('microblogs').getFullList({
title='Microblog'
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>
<p>Short-format writing.</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: *
Allow: /
Sitemap: ${new URL('sitemap-index.xml', import.meta.env.SITE).href}
Sitemap: ${new URL('sitemap-index.xml', 'https://juancman.dev').href}
`.trim();
export const GET: APIRoute = () => {

View File

@ -9,6 +9,9 @@
body {
@apply bg-background text-foreground;
}
.prose {
@apply max-w-none;
}
[data-rehype-pretty-code-figure] {
@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;
};