Migrate to Astro V5 (#22)

* fix video url

* update deploy script for using rsync

* remove delete from script

* translate resources page

* fix images dimensions and favicon

* add css optimization plugin and improve images

* update project dependencies

* refactor lang

* post translated to spanish

* fix metadata

* update dependencies

* update dependencies

* update dependencies

* update to Astro 5.x

* format index.astro

* Migrate content layer to Astro V5
This commit is contained in:
Juan Manzanero 2025-02-01 17:33:00 -06:00 committed by GitHub
parent 9363bf7a20
commit 4f0e80b988
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 2265 additions and 2034 deletions

View File

@ -1,28 +1,34 @@
import { defineConfig } from "astro/config"; import { defineConfig } from 'astro/config';
import react from "@astrojs/react"; import react from '@astrojs/react';
import tailwind from "@astrojs/tailwind"; import tailwind from '@astrojs/tailwind';
import mdx from "@astrojs/mdx"; import mdx from '@astrojs/mdx';
import rehypePrettyCode from "rehype-pretty-code"; import rehypePrettyCode from 'rehype-pretty-code';
import rehypeSlug from "rehype-slug"; import rehypeSlug from 'rehype-slug';
import sitemap from "@astrojs/sitemap"; import sitemap from '@astrojs/sitemap';
import playformInline from "@playform/inline"; import playformInline from '@playform/inline';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: "https://juancman.dev/", site: 'https://juancman.dev/',
integrations: [sitemap(), react(), tailwind({ integrations: [
sitemap(),
react(),
tailwind({
applyBaseStyles: false, applyBaseStyles: false,
}), mdx({ }),
mdx({
syntaxHighlight: false, syntaxHighlight: false,
rehypePlugins: [ rehypePlugins: [
rehypeSlug, rehypeSlug,
[ [
rehypePrettyCode, rehypePrettyCode,
{ {
theme: "catppuccin-mocha", theme: 'catppuccin-mocha',
}, },
], ],
], ],
}), playformInline()], }),
playformInline(),
],
}); });

View File

@ -10,21 +10,21 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.7.0", "@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^3.1.9", "@astrojs/mdx": "^4.0.8",
"@astrojs/react": "^3.6.2", "@astrojs/react": "^4.2.0",
"@astrojs/rss": "^4.0.9", "@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.2.1", "@astrojs/sitemap": "^3.2.1",
"@astrojs/tailwind": "^5.1.2", "@astrojs/tailwind": "^5.1.5",
"@playform/inline": "^0.1.0", "@playform/inline": "^0.1.1",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.1",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.16",
"@types/react": "^18.3.12", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.5",
"astro": "^4.16.10", "astro": "^5.2.3",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.3",
"lucide-react": "^0.396.0", "lucide-react": "^0.396.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"marked": "^13.0.3", "marked": "^13.0.3",
@ -33,12 +33,12 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"rehype-pretty-code": "^0.13.2", "rehype-pretty-code": "^0.13.2",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"sanitize-html": "^2.13.1", "sanitize-html": "^2.14.0",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.6.0",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.6.3" "typescript": "^5.7.3"
}, },
"devDependencies": { "devDependencies": {
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",

2503
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,18 @@
import { Code, RssIcon } from "lucide-react"; 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';
const locales = { const locales = {
en: { en: {
developed_by: "Developed by ", developed_by: 'Developed by ',
build_handcrafted: "Built handcrafted with ", build_handcrafted: 'Built handcrafted with ',
last_build: "Last build", last_build: 'Last build',
}, },
es: { es: {
developed_by: "Desarrollado por ", developed_by: 'Desarrollado por ',
build_handcrafted: "Construido a mano con ", build_handcrafted: 'Construido a mano con ',
last_build: "Última build", last_build: 'Última build',
}, },
}; };
@ -22,26 +22,29 @@ type Props = {
export default function Footer({ lang }: Props) { export default function Footer({ lang }: Props) {
const rssUrl = const rssUrl =
lang == "en" lang == 'en'
? "https://juancman.dev/feed.xml" ? 'https://juancman.dev/feed.xml'
: "https://juancman.dev/es/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='border-t border-secondary px-4 py-12 text-center text-sm md:px-16 prose prose-invert min-w-full'>
<section> <section>
<p> <p>
{locales[lang].developed_by} {locales[lang].developed_by}
<strong className="font-bold text-primary">juancmandev</strong> <strong className='font-bold text-primary'>juancmandev</strong>
</p> </p>
<p> <p>
{locales[lang].build_handcrafted} {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 no-underline hover:underline'
>
<a
href='https://astro.build/'
target='_blank'
> >
<a href="https://astro.build/" target="_blank">
Astro Astro
</a> </a>
</Button> </Button>
@ -50,26 +53,32 @@ export default function Footer({ lang }: Props) {
{locales[lang].last_build}: {formatDate(new Date(), lang)}. {locales[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 <Button
asChild asChild
size={null} size={null}
variant="link" variant='link'
className="flex flex-col justify-center" className='flex flex-col justify-center'
> >
<a target="_blank" href="https://github.com/juancmandev/website"> <a
<Code className="w-6" /> target='_blank'
href='https://github.com/juancmandev/website'
>
<Code className='w-6' />
Source Code Source Code
</a> </a>
</Button> </Button>
<Button <Button
asChild asChild
size={null} size={null}
variant="link" variant='link'
className="flex flex-col justify-center" className='flex flex-col justify-center'
> >
<a target="_blank" href={rssUrl}> <a
<RssIcon className="w-6" /> target='_blank'
href={rssUrl}
>
<RssIcon className='w-6' />
RSS feed RSS feed
</a> </a>
</Button> </Button>

View File

@ -1,9 +1,9 @@
--- ---
import logo from "@/assets/logo.png"; import logo from '@/assets/logo.png';
import { Image } from "astro:assets"; import { Image } from 'astro:assets';
import LinkButton from "@/components/link-button"; import LinkButton from '@/components/link-button';
import { ChevronUp, Compass } from "lucide-react"; import { ChevronUp, Compass } from 'lucide-react';
import type { lang } from "@/i18n/utils"; import type { lang } from '@/i18n/utils';
type Props = { type Props = {
lang: lang; lang: lang;
@ -11,16 +11,16 @@ type Props = {
const locales = { const locales = {
en: { en: {
to: "/es", to: '/es',
switch_language: "🇲🇽", switch_language: '🇲🇽',
top: "Top", top: 'Top',
navigation: "Navigation", navigation: 'Navigation',
}, },
es: { es: {
to: "/", to: '/',
switch_language: "🇺🇸", switch_language: '🇺🇸',
top: "Arriba", top: 'Arriba',
navigation: "Navevación", navigation: 'Navevación',
}, },
}; };
@ -28,44 +28,52 @@ 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 fixed top-0 z-50 flex w-full items-center justify-between border-b border-secondary backdrop-blur-lg'
> >
<div <div
class="px-4 sm:px-0 flex w-full max-w-[65ch] items-center justify-between mx-auto" 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'}
size="icon" size='icon'
variant="link" variant='link'
className="rounded-full px-0" className='rounded-full px-0'
> >
<Image <Image
src={logo} src={logo}
width={80} width={80}
height={80} height={80}
loading="eager" loading='eager'
decoding="sync" decoding='sync'
fetchpriority="high" fetchpriority='high'
class="w-auto h-auto aspect-square" class='w-auto h-auto aspect-square'
alt="juancmandev logo" alt='juancmandev logo'
/> />
</LinkButton> </LinkButton>
</section> </section>
<section class="flex items-center gap-2"> <section class='flex items-center gap-2'>
<LinkButton <LinkButton
variant="link" variant='link'
href={locales[lang].to} href={locales[lang].to}
className="p-0 gap-1 text-base" className='p-0 gap-1 text-base'
> >
{locales[lang].switch_language} {locales[lang].switch_language}
</LinkButton> </LinkButton>
<LinkButton variant="link" className="p-0 gap-0.5" href="#"> <LinkButton
<ChevronUp className="w-5" /> variant='link'
className='p-0 gap-0.5'
href='#'
>
<ChevronUp className='w-5' />
{locales[lang].top} {locales[lang].top}
</LinkButton> </LinkButton>
<LinkButton variant="link" className="p-0 gap-0.5" href="#navigation"> <LinkButton
<Compass className="w-5" /> variant='link'
className='p-0 gap-0.5'
href='#navigation'
>
<Compass className='w-5' />
{locales[lang].navigation} {locales[lang].navigation}
</LinkButton> </LinkButton>
</section> </section>

View File

@ -1,20 +1,20 @@
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
title?: string; title?: string;
href: string; href: string;
variant?: variant?:
| "default" | 'default'
| "destructive" | 'destructive'
| "outline" | 'outline'
| "secondary" | 'secondary'
| "ghost" | 'ghost'
| "link" | 'link'
| null | null
| undefined; | undefined;
className?: string; className?: string;
size?: "default" | "sm" | "lg" | "icon" | null | undefined; size?: 'default' | 'sm' | 'lg' | 'icon' | null | undefined;
}; };
export default function LinkButton(props: Props) { export default function LinkButton(props: Props) {

View File

@ -1,23 +1,23 @@
--- ---
import { Image } from "astro:assets"; import { Image } from 'astro:assets';
const props = Astro.props; const props = Astro.props;
--- ---
<Image <Image
id="img" id='img'
decoding="async" decoding='async'
src={props.src} src={props.src}
alt={props.alt} alt={props.alt}
width={props.width} width={props.width}
height={props.height} height={props.height}
class="w-auto h-auto rounded-md aspect-auto object-cover" class='w-auto h-auto rounded-md aspect-auto object-cover'
/> />
<script> <script>
const image = document.getElementById("img")!; const image = document.getElementById('img')!;
image && image.setAttribute("loading", "eager"); image && image.setAttribute('loading', 'eager');
image && image.setAttribute("decoding", "sync"); image && image.setAttribute('decoding', 'sync');
image && image.setAttribute("fetchpriority", "high"); image && image.setAttribute('fetchpriority', 'high');
</script> </script>

View File

@ -3,9 +3,16 @@ type TAnchor = {
} & React.HTMLAttributes<HTMLAnchorElement>; } & React.HTMLAttributes<HTMLAnchorElement>;
export default function CustomAnchor(props: TAnchor) { export default function CustomAnchor(props: TAnchor) {
return props.href.startsWith("/") || props.href.startsWith("#") ? ( return props.href.startsWith('/') || props.href.startsWith('#') ? (
<a {...props} className="inline-flex outline-ring" /> <a
{...props}
className='inline-flex outline-ring'
/>
) : ( ) : (
<a {...props} className="inline-flex outline-ring" target="_blank" /> <a
{...props}
className='inline-flex outline-ring'
target='_blank'
/>
); );
} }

View File

@ -1,5 +1,5 @@
import AstroImage from "@/components/mdx/astro-image.astro"; import AstroImage from '@/components/mdx/astro-image.astro';
import CustomAnchor from "@/components/mdx/custom-anchor"; import CustomAnchor from '@/components/mdx/custom-anchor';
const components = { const components = {
img: AstroImage, img: AstroImage,

View File

@ -1,7 +1,7 @@
--- ---
import { marked } from "marked"; import { marked } from 'marked';
import formatDate from "@/utils/format-date"; import formatDate from '@/utils/format-date';
import { getLangFromUrl } from "@/i18n/utils"; import { getLangFromUrl } from '@/i18n/utils';
const props = Astro.props; const props = Astro.props;
const content = marked.parse(props.content); const content = marked.parse(props.content);
@ -9,22 +9,22 @@ const content = marked.parse(props.content);
const lang = getLangFromUrl(Astro.url); const lang = getLangFromUrl(Astro.url);
--- ---
<article class="rounded-md border px-4 py-2"> <article class='rounded-md border px-4 py-2'>
<header class="mb-2"> <header class='mb-2'>
<section class="flex items-center justify-between text-sm"> <section class='flex items-center justify-between text-sm'>
<span class="font-light"> <span class='font-light'>
{formatDate(new Date(props.published), lang)}{" "} {formatDate(new Date(props.published), lang)}{' '}
</span> </span>
<span class="text-sm font-thin"> <span class='text-sm font-thin'>
{new Date(props.published).toLocaleTimeString()} {new Date(props.published).toLocaleTimeString()}
</span> </span>
</section> </section>
<section class="mt-1"> <section class='mt-1'>
{ {
props && props &&
props.expand.tags && props.expand.tags &&
props?.expand.tags.map( props?.expand.tags.map(
(tag: any) => tag && <span class="text-sm">#{tag.name} </span>, (tag: any) => tag && <span class='text-sm'>#{tag.name} </span>
) )
} }
</section> </section>

View File

@ -1,4 +1,4 @@
import LinkButton from "@/components/link-button"; import LinkButton from '@/components/link-button';
import { import {
NotebookText, NotebookText,
BriefcaseBusiness, BriefcaseBusiness,
@ -7,8 +7,8 @@ import {
PocketKnife, PocketKnife,
Info, Info,
Mail, Mail,
} from "lucide-react"; } from 'lucide-react';
import { useTranslations, type lang } from "@/i18n/utils"; import { useTranslations, type lang } from '@/i18n/utils';
type TNavItem = { type TNavItem = {
type: string; type: string;
@ -17,28 +17,28 @@ type TNavItem = {
export const navItems: TNavItem[] = [ export const navItems: TNavItem[] = [
{ {
type: "blog", type: 'blog',
icon: <NotebookText />, icon: <NotebookText />,
}, },
{ type: "portfolio", icon: <BriefcaseBusiness /> }, { type: 'portfolio', icon: <BriefcaseBusiness /> },
{ {
type: "videos", type: 'videos',
icon: <MonitorPlay />, icon: <MonitorPlay />,
}, },
{ {
type: "microblog", type: 'microblog',
icon: <Newspaper />, icon: <Newspaper />,
}, },
{ {
type: "resources", type: 'resources',
icon: <PocketKnife />, icon: <PocketKnife />,
}, },
{ {
type: "about", type: 'about',
icon: <Info />, icon: <Info />,
}, },
{ {
type: "contact", type: 'contact',
icon: <Mail />, icon: <Mail />,
}, },
]; ];
@ -51,15 +51,18 @@ 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='px-4 sm:px-0 max-w-[65ch] mx-auto prose prose-invert pt-5 pb-20'>
<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) => (
<li key={index} className="m-0 p-0"> <li
key={index}
className='m-0 p-0'
>
<LinkButton <LinkButton
variant="link" variant='link'
href={t(`${navItem.type}.to` as any)} href={t(`${navItem.type}.to` as any)}
className="p-0 text-base gap-1" className='p-0 text-base gap-1'
> >
{navItem.icon} {navItem.icon}
{t(`${navItem.type}.label` as any)} {t(`${navItem.type}.label` as any)}

View File

@ -1,11 +1,11 @@
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 = { type Props = {
slug: string; id: string;
date: Date | string; date: Date | string;
title: string; title: string;
type: "blog" | "portfolio" | "videos"; type: 'blog' | 'portfolio' | 'videos';
lang: string; lang: string;
}; };
@ -14,21 +14,21 @@ export default function PostItem(props: Props) {
<Button <Button
asChild asChild
size={null} size={null}
variant="link" variant='link'
className="px-4 whitespace-normal py-2 hover:no-underline focus:no-underline flex flex-col items-start italic border border-secondary hover:border-foreground focus:border-foreground transition-colors rounded-md" className='px-4 whitespace-normal py-2 hover:no-underline focus:no-underline flex flex-col items-start italic border border-secondary hover:border-foreground focus:border-foreground transition-colors rounded-md'
> >
<a <a
className="no-underline" className='no-underline'
href={ href={
props.lang === "en" props.lang === 'en'
? `/${props.type}/${props.slug}` ? `/${props.type}/${props.id}`
: `/es/${props.type}/${[props.slug]}` : `/es/${props.type}/${[props.id]}`
} }
> >
<span className="text-sm font-light no-underline"> <span className='text-sm font-light no-underline'>
{formatDate(props.date, props.lang)} {formatDate(props.date, props.lang)}
</span> </span>
<span className="text-primary text-underline text-lg font-semibold underline"> <span className='text-primary text-underline text-lg font-semibold underline'>
{props.title} {props.title}
</span> </span>
</a> </a>

View File

@ -1,35 +1,35 @@
import * as React from "react"; import * as React from 'react';
import { Slot } from "@radix-ui/react-slot"; import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90", 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground", 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: "bg-secondary text-foreground hover:bg-secondary/60", secondary: 'bg-secondary text-foreground hover:bg-secondary/60',
ghost: "shadow-none hover:bg-foreground/10", ghost: 'shadow-none hover:bg-foreground/10',
link: "shadow-none underline-offset-4 hover:underline focus-within:underline", link: 'shadow-none underline-offset-4 hover:underline focus-within:underline',
}, },
size: { size: {
default: "h-10 px-4 py-2", default: 'h-10 px-4 py-2',
sm: "h-9 rounded-md px-3", sm: 'h-9 rounded-md px-3',
lg: "h-11 rounded-md px-8", lg: 'h-11 rounded-md px-8',
icon: "h-10 w-10", icon: 'h-10 w-10',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
},
}, },
}
); );
export interface ButtonProps export interface ButtonProps
@ -40,7 +40,7 @@ export interface ButtonProps
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"; const Comp = asChild ? Slot : 'button';
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
@ -48,8 +48,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{...props} {...props}
/> />
); );
}, }
); );
Button.displayName = "Button"; Button.displayName = 'Button';
export { Button, buttonVariants }; export { Button, buttonVariants };

View File

@ -1,4 +1,5 @@
import { defineCollection, z } from "astro:content"; import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const contentSchema = z.object({ const contentSchema = z.object({
title: z.string(), title: z.string(),
@ -13,17 +14,20 @@ const contentSchema = z.object({
}); });
const blog = defineCollection({ const blog = defineCollection({
type: "content", loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/content/blog' }),
schema: contentSchema, schema: contentSchema,
}); });
const portfolio = defineCollection({ const portfolio = defineCollection({
type: "content", loader: glob({
pattern: '**/[^_]*.{md,mdx}',
base: './src/content/portfolio',
}),
schema: contentSchema, schema: contentSchema,
}); });
const pages = defineCollection({ const pages = defineCollection({
type: "content", loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/content/pages' }),
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),
description: z.string(), description: z.string(),
@ -31,13 +35,13 @@ const pages = defineCollection({
}); });
const videos = defineCollection({ const videos = defineCollection({
type: "content", loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/content/videos' }),
schema: contentSchema schema: contentSchema,
}) });
export const collections = { export const collections = {
blog, blog,
portfolio, portfolio,
pages, pages,
videos videos,
}; };

View File

@ -1,44 +1,44 @@
export const languages = { export const languages = {
en: "English", en: 'English',
es: "Español", es: 'Español',
}; };
export const defaultLang = "en"; export const defaultLang = 'en';
export const showDefaultLang = false; export const showDefaultLang = false;
export const ui = { export const ui = {
en: { en: {
navigation: "Navigation", navigation: 'Navigation',
"blog.label": "Blog", 'blog.label': 'Blog',
"blog.to": "/blog", 'blog.to': '/blog',
"portfolio.label": "Portfolio", 'portfolio.label': 'Portfolio',
"portfolio.to": "/portfolio", 'portfolio.to': '/portfolio',
"videos.label": "Videos", 'videos.label': 'Videos',
"videos.to": "/es/videos", 'videos.to': '/es/videos',
"microblog.label": "Microblog", 'microblog.label': 'Microblog',
"microblog.to": "/microblog", 'microblog.to': '/microblog',
"resources.label": "Resources", 'resources.label': 'Resources',
"resources.to": "/resources", 'resources.to': '/resources',
"about.label": "About", 'about.label': 'About',
"about.to": "/about", 'about.to': '/about',
"contact.label": "Contact", 'contact.label': 'Contact',
"contact.to": "/contact", 'contact.to': '/contact',
}, },
es: { es: {
navigation: "Navegación", navigation: 'Navegación',
"blog.label": "Blog", 'blog.label': 'Blog',
"blog.to": "/es/blog", 'blog.to': '/es/blog',
"portfolio.label": "Portfolio", 'portfolio.label': 'Portfolio',
"portfolio.to": "/es/portfolio", 'portfolio.to': '/es/portfolio',
"videos.label": "Videos", 'videos.label': 'Videos',
"videos.to": "/es/videos", 'videos.to': '/es/videos',
"microblog.label": "Microblog", 'microblog.label': 'Microblog',
"microblog.to": "/microblog", 'microblog.to': '/microblog',
"resources.label": "Recursos", 'resources.label': 'Recursos',
"resources.to": "/es/recursos", 'resources.to': '/es/recursos',
"about.label": "Acerca de", 'about.label': 'Acerca de',
"about.to": "/es/acerca-de", 'about.to': '/es/acerca-de',
"contact.label": "Contacto", 'contact.label': 'Contacto',
"contact.to": "/es/contacto", 'contact.to': '/es/contacto',
}, },
} as const; } as const;

View File

@ -1,9 +1,11 @@
import { ui, defaultLang, showDefaultLang } from "@/i18n/ui"; import { ui, defaultLang, showDefaultLang } from '@/i18n/ui';
export type lang = 'en' | 'es';
export type lang = "en" | "es"; export type lang = "en" | "es";
export function getLangFromUrl(url: URL) { export function getLangFromUrl(url: URL) {
const [, lang] = url.pathname.split("/"); const [, lang] = url.pathname.split('/');
if (lang in ui) return lang as keyof typeof ui; if (lang in ui) return lang as keyof typeof ui;
return defaultLang; return defaultLang;
} }

View File

@ -1,9 +1,9 @@
--- ---
import Header from "@/components/header.astro"; import Header from '@/components/header.astro';
import Navigation from "@/components/navigation"; import Navigation from '@/components/navigation';
import Footer from "@/components/footer"; import Footer from '@/components/footer';
import { getLangFromUrl } from "@/i18n/utils"; import { getLangFromUrl } from '@/i18n/utils';
import "@/styles/globals.css"; import '@/styles/globals.css';
interface Props { interface Props {
title: string; title: string;
@ -16,37 +16,54 @@ const { title, description } = Astro.props;
<html lang={lang}> <html lang={lang}>
<head> <head>
<meta charset="UTF-8" /> <meta charset='UTF-8' />
<meta name="description" content={description} /> <meta
<meta name="viewport" content="width=device-width" /> name='description'
<link rel="icon" type="image/png" href="/logo.png" sizes="16x16" /> content={description}
{ />
lang === "en" && ( <meta
name='viewport'
content='width=device-width'
/>
<link <link
rel="alternate" rel='icon'
title="juancmandev" type='image/png'
type="application/rss+xml" href='/logo.png'
href={new URL("feed.xml", Astro.site)} sizes='16x16'
/>
{
lang === 'en' && (
<link
rel='alternate'
title='juancmandev'
type='application/rss+xml'
href={new URL('feed.xml', Astro.site)}
/> />
) )
} }
{ {
lang === "es" && ( lang === 'es' && (
<link <link
rel="alternate" rel='alternate'
title="juancmandev" title='juancmandev'
type="application/rss+xml" type='application/rss+xml'
href={new URL("feed.xml", `${Astro.site}/es/`)} href={new URL('feed.xml', `${Astro.site}/es/`)}
/> />
) )
} }
<link rel="sitemap" href="/sitemap-index.xml" /> <link
<meta name="generator" content={Astro.generator} /> rel='sitemap'
href='/sitemap-index.xml'
/>
<meta
name='generator'
content={Astro.generator}
/>
<title>{title}</title> <title>{title}</title>
</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='px-4 sm:px-0 max-w-[65ch] pt-28 pb-5 mx-auto'>
<slot /> <slot />
</main> </main>
<Navigation lang={lang} /> <Navigation lang={lang} />

View File

@ -1,5 +1,5 @@
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from 'clsx';
import { twMerge } from "tailwind-merge"; import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));

View File

@ -1,9 +1,12 @@
--- ---
import Layout from "@/layouts/Layout.astro"; import Layout from '@/layouts/Layout.astro';
--- ---
<Layout title="Not found" description="Error 404: Not found."> <Layout
<div class="prose prose-invert"> title='Not found'
<h1 class="">Error 404: Not found</h1> description='Error 404: Not found.'
>
<div class='prose prose-invert'>
<h1 class=''>Error 404: Not found</h1>
</div> </div>
</Layout> </Layout>

View File

@ -1,28 +1,38 @@
--- ---
import Layout from "@/layouts/Layout.astro"; import Layout from '@/layouts/Layout.astro';
import { getCollection } from "astro:content"; import components from '@/components/mdx/wrapper';
import components from "@/components/mdx/wrapper"; import { getLangFromUrl } from '@/i18n/utils';
import type { CollectionEntry } from "astro:content"; import { getCollection, getEntry, render } from 'astro:content';
interface Props {
page: CollectionEntry<"pages">;
}
export async function getStaticPaths() { export async function getStaticPaths() {
const allPages = await getCollection("pages"); const allPages = await getCollection('pages');
const filterEnPages = allPages.map((page) => {
const [lang, id] = page.id.split('/');
return allPages.map((page: CollectionEntry<"pages">) => ({ if (lang === 'en')
params: { slug: page.slug }, return {
props: { page }, ...page,
id: id.split('.')[0],
};
else null;
});
return filterEnPages.map((page) => ({
params: { slug: page?.id },
})); }));
} }
const { page } = Astro.props; const lang = getLangFromUrl(Astro.url);
const { Content } = await page.render(); const { slug } = Astro.params;
const project = await getEntry('pages', `${lang}/${slug}`)!;
const { Content, remarkPluginFrontmatter: data } = await render(project);
--- ---
<Layout {...page.data}> <Layout
<article class="prose prose-invert"> title={data.title}
description={data.description}
>
<article class='prose prose-invert'>
<Content components={{ ...components }} /> <Content components={{ ...components }} />
</article> </article>
</Layout> </Layout>

View File

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

View File

@ -1,23 +1,23 @@
--- ---
import PostItem from "@/components/post-item"; import PostItem from '@/components/post-item';
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';
import { getCollection } from "astro:content"; import { getCollection } from 'astro:content';
const pageData = { const pageData = {
title: "Blog", title: 'Blog',
description: "Long format about thoughts and other topics.", description: 'Long format about thoughts and other topics.',
}; };
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true); const allPosts = await getCollection('blog', ({ data }) => data.draft !== true);
const filterEnPosts = allPosts.map((post) => { const filterEnPosts = allPosts.map((post) => {
const [lang, ...slug] = post.slug.split("/"); const [lang, id] = post.id.split('/');
if (lang === "en") if (lang === 'en')
return { return {
...post, ...post,
slug: slug.toString(), id: id.split('.')[0],
}; };
else null; else null;
}); });
@ -27,25 +27,25 @@ const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<section class="prose prose-invert"> <section class='prose prose-invert'>
<h1>{pageData.title}</h1> <h1>{pageData.title}</h1>
<p>{pageData.description}</p> <p>{pageData.description}</p>
</section> </section>
<ul class="mt-4 flex flex-col gap-4"> <ul class='mt-4 flex flex-col gap-4'>
{ {
filterEnPosts.map( filterEnPosts.map(
(blogpost) => (blogpost) =>
blogpost && ( blogpost && (
<li> <li>
<PostItem <PostItem
type="blog" type='blog'
lang={lang} lang={lang}
slug={blogpost.slug} id={blogpost.id}
date={blogpost.data.date!} date={blogpost.data.date!}
title={blogpost.data.title!} title={blogpost.data.title!}
/> />
</li> </li>
), )
) )
} }
</ul> </ul>

View File

@ -1,28 +1,38 @@
--- ---
import Layout from "@/layouts/Layout.astro"; import Layout from '@/layouts/Layout.astro';
import { getCollection } from "astro:content"; import components from '@/components/mdx/wrapper';
import components from "@/components/mdx/wrapper"; import { getLangFromUrl } from '@/i18n/utils';
import type { CollectionEntry } from "astro:content"; import { getCollection, getEntry, render } from 'astro:content';
interface Props {
page: CollectionEntry<"pages">;
}
export async function getStaticPaths() { export async function getStaticPaths() {
const allPages = await getCollection("pages"); const allPages = await getCollection('pages');
const filterEsPages = allPages.map((page) => {
const [lang, id] = page.id.split('/');
return allPages.map((page: CollectionEntry<"pages">) => ({ if (lang === 'es')
params: { slug: page.slug }, return {
props: { page }, ...page,
id: id.split('.')[0],
};
else null;
});
return filterEsPages.map((page) => ({
params: { slug: page?.id },
})); }));
} }
const { page } = Astro.props; const lang = getLangFromUrl(Astro.url);
const { Content } = await page.render(); const { slug } = Astro.params;
const page = await getEntry('pages', `${lang}/${slug}`)!;
const { Content, remarkPluginFrontmatter: data } = await render(page);
--- ---
<Layout {...page.data}> <Layout
<article class="prose prose-invert"> title={data.title}
description={data.description}
>
<article class='prose prose-invert'>
<Content components={{ ...components }} /> <Content components={{ ...components }} />
</article> </article>
</Layout> </Layout>

View File

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

View File

@ -1,23 +1,23 @@
--- ---
import PostItem from "@/components/post-item"; import PostItem from '@/components/post-item';
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';
import { getCollection } from "astro:content"; import { getCollection } from 'astro:content';
const pageData = { const pageData = {
title: "Blog", title: 'Blog',
description: "Formato largo sobre pensamientos y otros temas.", description: 'Formato largo sobre pensamientos y otros temas.',
}; };
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true); const allPosts = await getCollection('blog', ({ data }) => data.draft !== true);
const filterEsPosts = allPosts.map((post) => { const filterEsPosts = allPosts.map((post) => {
const [lang, ...slug] = post.slug.split("/"); const [lang, id] = post.id.split('/');
if (lang === "es") if (lang === 'es')
return { return {
...post, ...post,
slug: slug.toString(), id: id.split('.')[0],
}; };
else null; else null;
}); });
@ -27,25 +27,25 @@ const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<section class="prose prose-invert"> <section class='prose prose-invert'>
<h1>{pageData.title}</h1> <h1>{pageData.title}</h1>
<p>{pageData.description}</p> <p>{pageData.description}</p>
</section> </section>
<ul class="mt-4 flex flex-col gap-4"> <ul class='mt-4 flex flex-col gap-4'>
{ {
filterEsPosts.map( filterEsPosts.map(
(post) => (post) =>
post && ( post && (
<li> <li>
<PostItem <PostItem
type="blog" type='blog'
lang={lang} lang={lang}
slug={post?.slug} id={post.id}
date={post?.data.date!} date={post.data.date}
title={post?.data.title!} title={post.data.title}
/> />
</li> </li>
), )
) )
} }
</ul> </ul>

View File

@ -1,54 +1,54 @@
import rss from "@astrojs/rss"; import rss from '@astrojs/rss';
import type { RSSFeedItem } from "@astrojs/rss"; import type { RSSFeedItem } from '@astrojs/rss';
import { getCollection } from "astro:content"; import { getCollection } from 'astro:content';
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from 'sanitize-html';
import MarkdownIt from "markdown-it"; import MarkdownIt from 'markdown-it';
import { parse as htmlParser } from "node-html-parser"; import { parse as htmlParser } from 'node-html-parser';
import { getImage } from "astro:assets"; import { getImage } from 'astro:assets';
import type { ImageMetadata } from "astro"; import type { ImageMetadata } from 'astro';
const markdownParser = new MarkdownIt(); 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 }>( const imagesPortfolio = import.meta.glob<{ default: ImageMetadata }>(
"/src/assets/portfolio/**/**/*.{jpeg,jpg,png,gif,webp}" '/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[] = [];
const blog = await getCollection( const blog = await getCollection(
"blog", 'blog',
({ data }) => data.draft !== true && data.rss === true ({ data }) => data.draft !== true && data.rss === true
); );
const filterBlog = blog.filter((post) => { const filterBlog = blog.filter((post) => {
const [lang] = post.slug.split("/"); const [lang] = post.id.split('/');
return lang === "es" && post; return lang === 'es' && post;
}); });
const portfolio = await getCollection( const portfolio = await getCollection(
"portfolio", 'portfolio',
({ data }) => data.draft !== true && data.rss === true ({ data }) => data.draft !== true && data.rss === true
); );
const filterPortfolio = portfolio.filter((project) => { const filterPortfolio = portfolio.filter((project) => {
const [lang] = project.slug.split("/"); const [lang] = project.id.split('/');
return lang === "es" && project; 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);
const images = html.querySelectorAll("img"); const images = html.querySelectorAll('img');
for await (const img of images) { for await (const img of images) {
const src = img.getAttribute("src")!; const src = img.getAttribute('src')!;
if (src.startsWith("@/")) { if (src.startsWith('@/')) {
const prefixRemoved = src.replace("@/", ""); const prefixRemoved = src.replace('@/', '');
const imagePathPrefix = `/src/${prefixRemoved}`; const imagePathPrefix = `/src/${prefixRemoved}`;
const imagePath = await imagesBlog[imagePathPrefix]?.()?.then( const imagePath = await imagesBlog[imagePathPrefix]?.()?.then(
(res: any) => res.default (res: any) => res.default
@ -57,14 +57,14 @@ export async function GET(context: any) {
if (imagePath) { if (imagePath) {
const optimizedImg = await getImage({ src: imagePath }); const optimizedImg = await getImage({ src: imagePath });
img.setAttribute( img.setAttribute(
"src", 'src',
context.site + optimizedImg.src.replace("/", "") context.site + optimizedImg.src.replace('/', '')
); );
} }
} else if (src.startsWith("/images")) { } else if (src.startsWith('/images')) {
img.setAttribute("src", context.site + src.replace("/", "")); img.setAttribute('src', context.site + src.replace('/', ''));
} else { } else {
throw Error("src unknown"); throw Error('src unknown');
} }
} }
@ -72,23 +72,23 @@ export async function GET(context: any) {
title: post.data.title, title: post.data.title,
pubDate: post.data.date, pubDate: post.data.date,
description: post.data.description, description: post.data.description,
link: `/blog/${post.slug}/`, link: `/blog/${post.id.split('.')[0]}/`,
content: sanitizeHtml(html.toString(), { content: sanitizeHtml(html.toString(), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]), allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
}), }),
}); });
} }
for await (const project of filterPortfolio) { for await (const project of filterPortfolio) {
const body = markdownParser.render(project.body); const body = markdownParser.render(project.body!);
const html = htmlParser.parse(body); const html = htmlParser.parse(body);
const images = html.querySelectorAll("img"); const images = html.querySelectorAll('img');
for await (const img of images) { for await (const img of images) {
const src = img.getAttribute("src")!; const src = img.getAttribute('src')!;
if (src.startsWith("@/")) { if (src.startsWith('@/')) {
const prefixRemoved = src.replace("@/", ""); const prefixRemoved = src.replace('@/', '');
const imagePathPrefix = `/src/${prefixRemoved}`; const imagePathPrefix = `/src/${prefixRemoved}`;
const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then( const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then(
(res: any) => res.default (res: any) => res.default
@ -97,15 +97,15 @@ export async function GET(context: any) {
if (imagePath) { if (imagePath) {
const optimizedImg = await getImage({ src: imagePath }); const optimizedImg = await getImage({ src: imagePath });
img.setAttribute( img.setAttribute(
"src", 'src',
context.site + optimizedImg.src.replace("/", "") context.site + optimizedImg.src.replace('/', '')
); );
} }
} else if (src.startsWith("/images")) { } else if (src.startsWith('/images')) {
// images starting with `/images/` is the public dir // images starting with `/images/` is the public dir
img.setAttribute("src", context.site + src.replace("/", "")); img.setAttribute('src', context.site + src.replace('/', ''));
} else { } else {
throw Error("src unknown"); throw Error('src unknown');
} }
} }
@ -113,27 +113,27 @@ export async function GET(context: any) {
title: project.data.title, title: project.data.title,
pubDate: project.data.date, pubDate: project.data.date,
description: project.data.description, description: project.data.description,
link: `/portfolio/${project.slug}/`, link: `/portfolio/${project.id.split('.')[0]}/`,
content: sanitizeHtml(html.toString(), { content: sanitizeHtml(html.toString(), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]), 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',
description: "Bienvenido a mi dominio, extraño.", description: 'Bienvenido a mi dominio, extraño.',
site: `${context.site}es/`, site: `${context.site}es/`,
customData: [ customData: [
"<language>es-mx</language>", '<language>es-mx</language>',
`<image> `<image>
<url>https://juancman.dev/logo.png</url> <url>https://juancman.dev/logo.png</url>
<title>juancmandev</title> <title>juancmandev</title>
<link>https://juancman.dev</link> <link>https://juancman.dev</link>
</image>`, </image>`,
`<atom:link href="${context.site}es/feed.xml" rel="self" type="application/rss+xml"/>`, `<atom:link href="${context.site}es/feed.xml" rel="self" type="application/rss+xml"/>`,
].join(""), ].join(''),
items, items,
trailingSlash: false, trailingSlash: false,
}); });

View File

@ -1,25 +1,25 @@
--- ---
import LinkButton from "@/components/link-button"; import LinkButton from '@/components/link-button';
import PostItem from "@/components/post-item"; import PostItem from '@/components/post-item';
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';
import { getCollection } from "astro:content"; import { getCollection } from 'astro:content';
const pageData = { const pageData = {
title: "juancmandev", title: 'juancmandev',
description: description:
"Bienvenido a mi dominio, extraño. Soy juancmandev; Desarrollador Web, entusiasta de Linux, y defensor de la privacidad.", 'Bienvenido a mi dominio, extraño. Soy juancmandev; Desarrollador Web, entusiasta de Linux, y defensor de la privacidad.',
}; };
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true); const allPosts = await getCollection('blog', ({ data }) => data.draft !== true);
const allEsPosts = allPosts.map((post) => { const allEsPosts = allPosts.map((post) => {
const [lang, ...slug] = post.slug.split("/"); const [lang, id] = post.id.split('/');
if (lang === "es") if (lang !== 'en')
return { return {
...post, ...post,
slug: slug.toString(), id: id.split('.')[0],
}; };
else null; else null;
}); });
@ -27,16 +27,16 @@ sortContentByDate(allEsPosts);
const last3Blogs = allEsPosts.slice(0, 3); const last3Blogs = allEsPosts.slice(0, 3);
const allProjects = await getCollection( const allProjects = await getCollection(
"portfolio", 'portfolio',
({ data }) => data.draft !== true, ({ data }) => data.draft !== true
); );
const allEnProjects = allProjects.map((project) => { const allEnProjects = allProjects.map((project) => {
const [lang, ...slug] = project.slug.split("/"); const [lang, id] = project.id.split('/');
if (lang === "es") if (lang !== 'en')
return { return {
...project, ...project,
slug: slug.toString(), id: id.split('.')[0],
}; };
else null; else null;
}); });
@ -47,10 +47,10 @@ const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<div class="prose prose-invert"> <div class='prose prose-invert'>
<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
>Desarrollador Web</strong >Desarrollador Web</strong
>, entusiasta de <strong>Linux</strong> y defensor de la <strong >, entusiasta de <strong>Linux</strong> y defensor de la <strong
>privacidad.</strong >privacidad.</strong
@ -63,52 +63,56 @@ 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"> <ul class='mt-0 p-0 list-none'>
{ {
last3Blogs.map( last3Blogs.map(
(blogpost) => (blogpost) =>
blogpost && ( blogpost && (
<li class="p-0"> <li class='p-0'>
<PostItem <PostItem
type="blog" type='blog'
lang={lang} lang={lang}
slug={blogpost?.slug} id={blogpost.id}
date={blogpost.data.date} date={blogpost.data.date}
title={blogpost.data.title} title={blogpost.data.title}
/> />
</li> </li>
), )
) )
} }
</ul> </ul>
<LinkButton variant="secondary" href="/es/blog" className="no-underline" <LinkButton
variant='secondary'
href='/es/blog'
className='no-underline'
>Más posts</LinkButton >Más posts</LinkButton
> >
</section> </section>
<section> <section>
<h2>Últimos proyectos</h2> <h2>Últimos proyectos</h2>
<ul class="mt-0 p-0 list-none"> <ul class='mt-0 p-0 list-none'>
{ {
last3Projects.map( last3Projects.map(
(project) => (project) =>
project && ( project && (
<li class="p-0"> <li class='p-0'>
<PostItem <PostItem
lang={lang} lang={lang}
type="portfolio" type='portfolio'
slug={project.slug} id={project.id}
date={project.data.date!} date={project.data.date!}
title={project.data.title!} title={project.data.title!}
/> />
</li> </li>
), )
) )
} }
</ul> </ul>
<LinkButton <LinkButton
variant="secondary" variant='secondary'
href="/es/portfolio" href='/es/portfolio'
className="no-underline">Más proyectos</LinkButton className='no-underline'
>Más proyectos</LinkButton
> >
</section> </section>
</div> </div>

View File

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

View File

@ -1,26 +1,26 @@
--- ---
import PostItem from "@/components/post-item"; import PostItem from '@/components/post-item';
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';
import { getCollection } from "astro:content"; import { getCollection } from 'astro:content';
const pageData = { const pageData = {
title: "Portfolio", title: 'Portfolio',
description: "Revisa mis proyectos.", description: 'Revisa mis proyectos.',
}; };
const allProjects = await getCollection( const allProjects = await getCollection(
"portfolio", 'portfolio',
({ data }) => data.draft !== true, ({ data }) => data.draft !== true
); );
const allEsProjects = allProjects.map((project) => { const allEsProjects = allProjects.map((project) => {
const [lang, ...slug] = project.slug.split("/"); const [lang, id] = project.id.split('/');
if (lang === "es") if (lang === 'es')
return { return {
...project, ...project,
slug: slug.toString(), id: id.split('.')[0],
}; };
else null; else null;
}); });
@ -30,11 +30,11 @@ const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<section class="prose prose-invert"> <section class='prose prose-invert'>
<h1>{pageData.title}</h1> <h1>{pageData.title}</h1>
<p>{pageData.description}</p> <p>{pageData.description}</p>
</section> </section>
<ul class="mt-4 flex flex-col gap-4"> <ul class='mt-4 flex flex-col gap-4'>
{ {
allEsProjects.map( allEsProjects.map(
(project) => (project) =>
@ -42,13 +42,13 @@ const lang = getLangFromUrl(Astro.url);
<li> <li>
<PostItem <PostItem
lang={lang} lang={lang}
type="portfolio" type='portfolio'
slug={project.slug} id={project.id}
date={project.data.date!} date={project.data.date!}
title={project.data.title!} title={project.data.title!}
/> />
</li> </li>
), )
) )
} }
</ul> </ul>

View File

@ -1,41 +1,46 @@
--- ---
import Layout from "@/layouts/Layout.astro"; import Layout from '@/layouts/Layout.astro';
import { getCollection } from "astro:content"; import components from '@/components/mdx/wrapper';
import components from "@/components/mdx/wrapper"; import formatDate from '@/utils/format-date';
import formatDate from "@/utils/format-date"; import { getLangFromUrl } from '@/i18n/utils';
import type { CollectionEntry } from "astro:content"; import { getCollection, getEntry, render } from 'astro:content';
import { getLangFromUrl } from "@/i18n/utils";
interface Props {
project: CollectionEntry<"videos">;
}
export async function getStaticPaths() { export async function getStaticPaths() {
const allProjects = await getCollection( const allVideos = await getCollection(
"videos", 'videos',
({ data }) => data.draft !== true, ({ data }) => data.draft !== true
); );
const formatVideos = allVideos.map((video) => {
const [id] = video.id.split('/');
return allProjects.map((project) => ({ return {
params: { slug: project.slug }, ...video,
props: { project }, id: id.split('.')[0],
};
});
return formatVideos.map((video) => ({
params: { slug: video.id },
})); }));
} }
const { project } = Astro.props;
const { Content } = await project.render();
const lang = getLangFromUrl(Astro.url); const lang = getLangFromUrl(Astro.url);
const { slug } = Astro.params;
const video = await getEntry('videos', slug)!;
const { Content, remarkPluginFrontmatter: data } = await render(video);
--- ---
<Layout title={project.data.title} description={project.data.description}> <Layout
<article class="prose prose-invert"> title={data.title}
<h1>{project.data.title}</h1> description={data.description}
>
<article class='prose prose-invert'>
<h1>{data.title}</h1>
<Content components={{ ...components }} /> <Content components={{ ...components }} />
<hr /> <hr />
<p> <p>
<strong>Posted: </strong> <strong>Posted: </strong>
{project.data.date && formatDate(new Date(project.data.date), lang)} {data.date && formatDate(new Date(data.date), lang)}
</p> </p>
</article> </article>
</Layout> </Layout>

View File

@ -1,18 +1,18 @@
--- ---
import PostItem from "@/components/post-item"; import PostItem from '@/components/post-item';
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';
import { getCollection } from "astro:content"; import { getCollection } from 'astro:content';
const pageData = { const pageData = {
title: "Videos", title: 'Videos',
description: "Guiones de los videos de mi canal de YouTube.", description: 'Guiones de los videos de mi canal de YouTube.',
}; };
const allVideos = await getCollection( const allVideos = await getCollection(
"videos", 'videos',
({ data }) => data.draft !== true, ({ data }) => data.draft !== true
); );
sortContentByDate(allVideos); sortContentByDate(allVideos);
@ -20,18 +20,18 @@ const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<section class="prose prose-invert"> <section class='prose prose-invert'>
<h1>{pageData.title}</h1> <h1>{pageData.title}</h1>
<p>{pageData.description}</p> <p>{pageData.description}</p>
</section> </section>
<ul class="mt-4 flex flex-col gap-4"> <ul class='mt-4 flex flex-col gap-4'>
{ {
allVideos.map((video: any) => ( allVideos.map((video: any) => (
<li> <li>
<PostItem <PostItem
lang={lang} lang={lang}
type="videos" type='videos'
slug={video.slug} id={video.id.split('.')[0]}
date={video.data.date!} date={video.data.date!}
title={video.data.title!} title={video.data.title!}
/> />

View File

@ -1,54 +1,54 @@
import rss from "@astrojs/rss"; import rss from '@astrojs/rss';
import type { RSSFeedItem } from "@astrojs/rss"; import type { RSSFeedItem } from '@astrojs/rss';
import { getCollection } from "astro:content"; import { getCollection } from 'astro:content';
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from 'sanitize-html';
import MarkdownIt from "markdown-it"; import MarkdownIt from 'markdown-it';
import { parse as htmlParser } from "node-html-parser"; import { parse as htmlParser } from 'node-html-parser';
import { getImage } from "astro:assets"; import { getImage } from 'astro:assets';
import type { ImageMetadata } from "astro"; import type { ImageMetadata } from 'astro';
const markdownParser = new MarkdownIt(); 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 }>( const imagesPortfolio = import.meta.glob<{ default: ImageMetadata }>(
"/src/assets/portfolio/**/**/*.{jpeg,jpg,png,gif,webp}" '/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[] = [];
const blog = await getCollection( const blog = await getCollection(
"blog", 'blog',
({ data }) => data.draft !== true && data.rss === true ({ data }) => data.draft !== true && data.rss === true
); );
const filterBlog = blog.filter((post) => { const filterBlog = blog.filter((post) => {
const [lang] = post.slug.split("/"); const [lang] = post.id.split('/');
return lang !== "es" && post; return lang !== 'es' && post;
}); });
const portfolio = await getCollection( const portfolio = await getCollection(
"portfolio", 'portfolio',
({ data }) => data.draft !== true && data.rss === true ({ data }) => data.draft !== true && data.rss === true
); );
const filterPortfolio = portfolio.filter((project) => { const filterPortfolio = portfolio.filter((project) => {
const [lang] = project.slug.split("/"); const [lang] = project.id.split('/');
return lang !== "es" && project; 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);
const images = html.querySelectorAll("img"); const images = html.querySelectorAll('img');
for await (const img of images) { for await (const img of images) {
const src = img.getAttribute("src")!; const src = img.getAttribute('src')!;
if (src.startsWith("@/")) { if (src.startsWith('@/')) {
const prefixRemoved = src.replace("@/", ""); const prefixRemoved = src.replace('@/', '');
const imagePathPrefix = `/src/${prefixRemoved}`; const imagePathPrefix = `/src/${prefixRemoved}`;
const imagePath = await imagesBlog[imagePathPrefix]?.()?.then( const imagePath = await imagesBlog[imagePathPrefix]?.()?.then(
(res: any) => res.default (res: any) => res.default
@ -57,14 +57,14 @@ export async function GET(context: any) {
if (imagePath) { if (imagePath) {
const optimizedImg = await getImage({ src: imagePath }); const optimizedImg = await getImage({ src: imagePath });
img.setAttribute( img.setAttribute(
"src", 'src',
context.site + optimizedImg.src.replace("/", "") context.site + optimizedImg.src.replace('/', '')
); );
} }
} else if (src.startsWith("/images")) { } else if (src.startsWith('/images')) {
img.setAttribute("src", context.site + src.replace("/", "")); img.setAttribute('src', context.site + src.replace('/', ''));
} else { } else {
throw Error("src unknown"); throw Error('src unknown');
} }
} }
@ -72,23 +72,23 @@ export async function GET(context: any) {
title: post.data.title, title: post.data.title,
pubDate: post.data.date, pubDate: post.data.date,
description: post.data.description, description: post.data.description,
link: `/blog/${post.slug}/`, link: `/blog/${post.id.split('.')[0]}/`,
content: sanitizeHtml(html.toString(), { content: sanitizeHtml(html.toString(), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]), allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
}), }),
}); });
} }
for await (const project of filterPortfolio) { for await (const project of filterPortfolio) {
const body = markdownParser.render(project.body); const body = markdownParser.render(project.body!);
const html = htmlParser.parse(body); const html = htmlParser.parse(body);
const images = html.querySelectorAll("img"); const images = html.querySelectorAll('img');
for await (const img of images) { for await (const img of images) {
const src = img.getAttribute("src")!; const src = img.getAttribute('src')!;
if (src.startsWith("@/")) { if (src.startsWith('@/')) {
const prefixRemoved = src.replace("@/", ""); const prefixRemoved = src.replace('@/', '');
const imagePathPrefix = `/src/${prefixRemoved}`; const imagePathPrefix = `/src/${prefixRemoved}`;
const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then( const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then(
(res: any) => res.default (res: any) => res.default
@ -97,15 +97,15 @@ export async function GET(context: any) {
if (imagePath) { if (imagePath) {
const optimizedImg = await getImage({ src: imagePath }); const optimizedImg = await getImage({ src: imagePath });
img.setAttribute( img.setAttribute(
"src", 'src',
context.site + optimizedImg.src.replace("/", "") context.site + optimizedImg.src.replace('/', '')
); );
} }
} else if (src.startsWith("/images")) { } else if (src.startsWith('/images')) {
// images starting with `/images/` is the public dir // images starting with `/images/` is the public dir
img.setAttribute("src", context.site + src.replace("/", "")); img.setAttribute('src', context.site + src.replace('/', ''));
} else { } else {
throw Error("src unknown"); throw Error('src unknown');
} }
} }
@ -113,27 +113,27 @@ export async function GET(context: any) {
title: project.data.title, title: project.data.title,
pubDate: project.data.date, pubDate: project.data.date,
description: project.data.description, description: project.data.description,
link: `/portfolio/${project.slug}/`, link: `/portfolio/${project.id.split('.')[0]}/`,
content: sanitizeHtml(html.toString(), { content: sanitizeHtml(html.toString(), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]), 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',
description: "Welcome to my domain, stranger.", description: 'Welcome to my domain, stranger.',
site: context.site, site: context.site,
customData: [ customData: [
"<language>en-us</language>", '<language>en-us</language>',
`<image> `<image>
<url>https://juancman.dev/logo.png</url> <url>https://juancman.dev/logo.png</url>
<title>juancmandev</title> <title>juancmandev</title>
<link>https://juancman.dev</link> <link>https://juancman.dev</link>
</image>`, </image>`,
`<atom:link href="${context.site}feed.xml" rel="self" type="application/rss+xml"/>`, `<atom:link href="${context.site}feed.xml" rel="self" type="application/rss+xml"/>`,
].join(""), ].join(''),
items, items,
trailingSlash: false, trailingSlash: false,
}); });

View File

@ -1,25 +1,25 @@
--- ---
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 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';
const pageData = { const pageData = {
title: "juancmandev", title: 'juancmandev',
description: description:
"Welcome to my domain, stranger. I am juancmandev; Web Developer, Linux enthusiast, and privacy defender.", 'Welcome to my domain, stranger. I am juancmandev; Web Developer, Linux enthusiast, and privacy defender.',
}; };
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true); const allPosts = await getCollection('blog', ({ data }) => data.draft !== true);
const allEnPosts = allPosts.map((post) => { const allEnPosts = allPosts.map((post) => {
const [lang, ...slug] = post.slug.split("/"); const [lang, id] = post.id.split('/');
if (lang !== "es") if (lang !== 'es')
return { return {
...post, ...post,
slug: slug.toString(), id: id.split('.')[0],
}; };
else null; else null;
}); });
@ -27,16 +27,16 @@ sortContentByDate(allEnPosts);
const last3Blogs = allEnPosts.slice(0, 3); const last3Blogs = allEnPosts.slice(0, 3);
const allProjects = await getCollection( const allProjects = await getCollection(
"portfolio", 'portfolio',
({ data }) => data.draft !== true, ({ data }) => data.draft !== true
); );
const allEnProjects = allProjects.map((project) => { const allEnProjects = allProjects.map((project) => {
const [lang, ...slug] = project.slug.split("/"); const [lang, id] = project.id.split('/');
if (lang !== "es") if (lang !== 'es')
return { return {
...project, ...project,
slug: slug.toString(), id: id.split('.')[0],
}; };
else null; else null;
}); });
@ -47,10 +47,10 @@ const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<div class="prose prose-invert"> <div class='prose prose-invert'>
<h1 class="text-primary">Welcome to my domain, stranger.</h1> <h1 class='text-primary'>Welcome to my domain, stranger.</h1>
<p> <p>
I am <strong class="text-primary">juancmandev</strong>; <strong I am <strong class='text-primary'>juancmandev</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,14 +61,14 @@ 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"> <ul class='mt-0 p-0 list-none'>
{ {
last3Blogs.map((blogpost: any) => ( last3Blogs.map((blogpost: any) => (
<li class="p-0"> <li class='p-0'>
<PostItem <PostItem
type="blog" type='blog'
lang={lang} lang={lang}
slug={blogpost.slug} id={blogpost.id}
date={blogpost.data.date!} date={blogpost.data.date!}
title={blogpost.data.title!} title={blogpost.data.title!}
/> />
@ -76,31 +76,37 @@ const lang = getLangFromUrl(Astro.url);
)) ))
} }
</ul> </ul>
<LinkButton variant="secondary" href="/blog" className="no-underline" <LinkButton
variant='secondary'
href='/blog'
className='no-underline'
>More posts</LinkButton >More posts</LinkButton
> >
</section> </section>
<section> <section>
<h2>Latest projects</h2> <h2>Latest projects</h2>
<ul class="mt-0 p-0 list-none"> <ul class='mt-0 p-0 list-none'>
{ {
last3Projects.map( last3Projects.map(
(project) => (project) =>
project && ( project && (
<li class="p-0"> <li class='p-0'>
<PostItem <PostItem
lang={lang} lang={lang}
type="portfolio" type='portfolio'
slug={project.slug} id={project.id}
date={project.data.date!} date={project.data.date!}
title={project.data.title!} title={project.data.title!}
/> />
</li> </li>
), )
) )
} }
</ul> </ul>
<LinkButton variant="secondary" href="/portfolio" className="no-underline" <LinkButton
variant='secondary'
href='/portfolio'
className='no-underline'
>More projects</LinkButton >More projects</LinkButton
> >
</section> </section>

View File

@ -1,24 +1,24 @@
--- ---
import MicroblogItem from "@/components/microblog-item.astro"; import MicroblogItem from '@/components/microblog-item.astro';
import Layout from "@/layouts/Layout.astro"; import Layout from '@/layouts/Layout.astro';
import { createServerClient } from "@/utils/pocketbase"; import { createServerClient } from '@/utils/pocketbase';
const pb = createServerClient(import.meta.env.SECRET_POCKETBASE_API_URL); const pb = createServerClient(import.meta.env.SECRET_POCKETBASE_API_URL);
const data = await pb.collection("microblogs").getFullList({ const data = await pb.collection('microblogs').getFullList({
expand: "tags", expand: 'tags',
sort: "-published", sort: '-published',
}); });
--- ---
<Layout <Layout
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'>
<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>
<ul class="mx-auto p-0 mt-10 flex flex-col gap-10 list-none"> <ul class='mx-auto p-0 mt-10 flex flex-col gap-10 list-none'>
{ {
data.map((item: any) => ( data.map((item: any) => (
<li> <li>

View File

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

View File

@ -1,26 +1,26 @@
--- ---
import PostItem from "@/components/post-item"; import PostItem from '@/components/post-item';
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';
import { getCollection } from "astro:content"; import { getCollection } from 'astro:content';
const pageData = { const pageData = {
title: "Portfolio", title: 'Portfolio',
description: "Check my projects.", description: 'Check my projects.',
}; };
const allProjects = await getCollection( const allProjects = await getCollection(
"portfolio", 'portfolio',
({ data }) => data.draft !== true, ({ data }) => data.draft !== true
); );
const allEnProjects = allProjects.map((project) => { const allEnProjects = allProjects.map((project) => {
const [lang, ...slug] = project.slug.split("/"); const [lang, id] = project.id.split('/');
if (lang === "en") if (lang === 'en')
return { return {
...project, ...project,
slug: slug.toString(), id: id.split('.')[0],
}; };
else null; else null;
}); });
@ -30,11 +30,11 @@ const lang = getLangFromUrl(Astro.url);
--- ---
<Layout {...pageData}> <Layout {...pageData}>
<section class="prose prose-invert"> <section class='prose prose-invert'>
<h1>{pageData.title}</h1> <h1>{pageData.title}</h1>
<p>{pageData.description}</p> <p>{pageData.description}</p>
</section> </section>
<ul class="mt-4 flex flex-col gap-4"> <ul class='mt-4 flex flex-col gap-4'>
{ {
allEnProjects.map( allEnProjects.map(
(project) => (project) =>
@ -42,13 +42,13 @@ const lang = getLangFromUrl(Astro.url);
<li> <li>
<PostItem <PostItem
lang={lang} lang={lang}
type="portfolio" type='portfolio'
slug={project.slug} id={project.id}
date={project.data.date!} date={project.data.date!}
title={project.data.title!} title={project.data.title!}
/> />
</li> </li>
), )
) )
} }
</ul> </ul>

View File

@ -1,16 +1,16 @@
import type { APIRoute } from "astro"; import type { APIRoute } from 'astro';
const robotsTxt = ` 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', import.meta.env.SITE).href}
`.trim(); `.trim();
export const GET: APIRoute = () => { export const GET: APIRoute = () => {
return new Response(robotsTxt, { return new Response(robotsTxt, {
headers: { headers: {
"Content-Type": "text/plain; charset=utf-8", 'Content-Type': 'text/plain; charset=utf-8',
}, },
}); });
}; };

View File

@ -1,30 +1,30 @@
const months = [ const months = [
"January", 'January',
"February", 'February',
"March", 'March',
"April", 'April',
"May", 'May',
"June", 'June',
"July", 'July',
"August", 'August',
"September", 'September',
"October", 'October',
"November", 'November',
"December", 'December',
]; ];
const meses = [ const meses = [
"Enero", 'Enero',
"Febrero", 'Febrero',
"Marzo", 'Marzo',
"Abril", 'Abril',
"Mayo", 'Mayo',
"Junio", 'Junio',
"Julio", 'Julio',
"Agosto", 'Agosto',
"Septiembre", 'Septiembre',
"Octubre", 'Octubre',
"Noviembre", 'Noviembre',
"Diciembre", 'Diciembre',
]; ];
export default function formatDate(date: Date | string, lang: string) { export default function formatDate(date: Date | string, lang: string) {
@ -34,7 +34,7 @@ export default function formatDate(date: Date | string, lang: string) {
const day = newDate.getDate(); const day = newDate.getDate();
const year = newDate.getFullYear(); const year = newDate.getFullYear();
return lang !== "es" return lang !== 'es'
? `${month} ${day}, ${year}` ? `${month} ${day}, ${year}`
: `${day} de ${mes} del ${year}`; : `${day} de ${mes} del ${year}`;
} }

View File

@ -1,10 +1,10 @@
import PocketBase from "pocketbase"; import PocketBase from 'pocketbase';
import type { RecordService } from "pocketbase"; import type { RecordService } from 'pocketbase';
export enum Collections { export enum Collections {
Microblogs = "microblogs", Microblogs = 'microblogs',
Tags = "tags", Tags = 'tags',
Users = "users", Users = 'users',
} }
export type IsoDateString = string; export type IsoDateString = string;
@ -62,19 +62,19 @@ export type CollectionResponses = {
}; };
export type TypedPocketBase = PocketBase & { export type TypedPocketBase = PocketBase & {
collection(idOrName: "microblogs"): RecordService<MicroblogsResponse>; collection(idOrName: 'microblogs'): RecordService<MicroblogsResponse>;
collection(idOrName: "tags"): RecordService<TagsResponse>; collection(idOrName: 'tags'): RecordService<TagsResponse>;
collection(idOrName: "users"): RecordService<UsersResponse>; collection(idOrName: 'users'): RecordService<UsersResponse>;
}; };
export function createServerClient(url: string) { export function createServerClient(url: string) {
if (!url) { if (!url) {
throw new Error("Pocketbase API url not defined !"); throw new Error('Pocketbase API url not defined !');
} }
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
throw new Error( throw new Error(
"This method is only supposed to call from the Server environment", 'This method is only supposed to call from the Server environment'
); );
} }

View File

@ -1,6 +1,6 @@
export function sortContentByDate(array: any[]) { export function sortContentByDate(array: any[]) {
array.sort( array.sort(
(a: any, b: any) => (a: any, b: any) =>
Date.parse(b.data.date.toString()) - Date.parse(a.data.date.toString()), Date.parse(b.data.date.toString()) - Date.parse(a.data.date.toString())
); );
} }

View File

@ -1,61 +1,61 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: ["class"], darkMode: ['class'],
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
prefix: "", prefix: '',
theme: { theme: {
fontFamily: { fontFamily: {
sans: ["Helvetica", "Arial", "sans-serif"], sans: ['Helvetica', 'Arial', 'sans-serif'],
}, },
container: { container: {
center: true, center: true,
padding: "2rem", padding: '2rem',
screens: { screens: {
"2xl": "1400px", '2xl': '1400px',
}, },
}, },
extend: { extend: {
colors: { colors: {
border: "#eee", border: '#eee',
input: "#00adb5", input: '#00adb5',
ring: "#00adb5", ring: '#00adb5',
background: "#222831", background: '#222831',
foreground: "#eee", foreground: '#eee',
primary: { primary: {
DEFAULT: "#00adb5", DEFAULT: '#00adb5',
foreground: "#000", foreground: '#000',
}, },
secondary: { secondary: {
DEFAULT: "#393e46", DEFAULT: '#393e46',
foreground: "#eee", foreground: '#eee',
}, },
destructive: { destructive: {
DEFAULT: "#ff2e63", DEFAULT: '#ff2e63',
foreground: "#eee", foreground: '#eee',
}, },
muted: { muted: {
DEFAULT: "#393e46", DEFAULT: '#393e46',
foreground: "#eee", foreground: '#eee',
}, },
accent: { accent: {
DEFAULT: "#00adb5", DEFAULT: '#00adb5',
foreground: "#eee", foreground: '#eee',
}, },
popover: { popover: {
DEFAULT: "#393e46", DEFAULT: '#393e46',
foreground: "#eee", foreground: '#eee',
}, },
card: { card: {
DEFAULT: "#393e46", DEFAULT: '#393e46',
foreground: "#eee", foreground: '#eee',
}, },
}, },
borderRadius: { borderRadius: {
lg: "8px", lg: '8px',
md: "4px", md: '4px',
sm: "2px", sm: '2px',
}, },
}, },
}, },
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
}; };