Compare commits

..

3 Commits

Author SHA1 Message Date
jaron 7c4a878c12 Merge branch 'main' of github.com:jaronpate/folio 2026-09-10 00:46:09 -05:00
jaron b095e0451d fix date off by one 2026-09-10 00:45:36 -05:00
jaron a788c71edf hide projects too 2026-09-10 00:26:59 -05:00
12 changed files with 61 additions and 38 deletions
+5 -12
View File
@@ -1,20 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { contentDateAttr, formatContentDate } from '~/utils/dates';
defineProps<{ defineProps<{
pages: Array<{ pages: Array<{
path: string; path: string;
title: string; title: string;
date: string | Date; date: string;
}>; }>;
}>(); }>();
const formatDate = (dateString: string | Date) => {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
};
</script> </script>
<template> <template>
@@ -27,8 +20,8 @@ const formatDate = (dateString: string | Date) => {
> >
<span class="listing__title">{{ page.title }}</span> <span class="listing__title">{{ page.title }}</span>
<span class="listing__rule" aria-hidden="true"></span> <span class="listing__rule" aria-hidden="true"></span>
<time :datetime="new Date(page.date).toISOString()">{{ <time :datetime="contentDateAttr(page.date)">{{
formatDate(page.date) formatContentDate(page.date)
}}</time> }}</time>
</NuxtLink> </NuxtLink>
</template> </template>
+5 -2
View File
@@ -10,6 +10,7 @@ import {
isExternalHref, isExternalHref,
projectEntryHref, projectEntryHref,
projectOgImage, projectOgImage,
sortProjects,
} from '~/utils/projects'; } from '~/utils/projects';
const route = useRoute(); const route = useRoute();
@@ -30,7 +31,7 @@ const folderName = computed(
const { data } = await useAsyncData( const { data } = await useAsyncData(
() => `projects:${route.path}`, () => `projects:${route.path}`,
async () => { async () => {
const all = await queryCollection('projects').order('title', 'ASC').all(); const all = await queryCollection('projects').all();
const visible = all.filter((page) => import.meta.dev || !page.hidden); const visible = all.filter((page) => import.meta.dev || !page.hidden);
const project = visible.find( const project = visible.find(
(page) => (page) =>
@@ -44,7 +45,9 @@ const { data } = await useAsyncData(
const prefix = contentPath.value.endsWith('/') const prefix = contentPath.value.endsWith('/')
? contentPath.value ? contentPath.value
: `${contentPath.value}/`; : `${contentPath.value}/`;
const pages = visible.filter((page) => page.path.startsWith(prefix)); const pages = sortProjects(
visible.filter((page) => page.path.startsWith(prefix)),
);
if (!pages.length) return null; if (!pages.length) return null;
return { kind: 'folder' as const, project: null, pages }; return { kind: 'folder' as const, project: null, pages };
+8 -3
View File
@@ -6,11 +6,16 @@ import {
personId, personId,
websiteId, websiteId,
} from '~/utils/site'; } from '~/utils/site';
import { isExternalHref, projectEntryHref } from '~/utils/projects'; import {
isExternalHref,
projectEntryHref,
sortProjects,
} from '~/utils/projects';
const { data: pages } = await useAsyncData('project-pages', async () => { const { data: pages } = await useAsyncData('project-pages', async () => {
const all = await queryCollection('projects').order('title', 'ASC').all(); const all = await queryCollection('projects').all();
return import.meta.dev ? all : all.filter((page) => !page.hidden); const visible = all.filter((page) => !page.hidden);
return sortProjects(visible);
}); });
const projectsUrl = `${SITE_URL}/projects`; const projectsUrl = `${SITE_URL}/projects`;
+3 -2
View File
@@ -6,6 +6,7 @@ import {
} from '~/utils/fonts'; } from '~/utils/fonts';
import { BG_THEMES } from '~/utils/themes'; import { BG_THEMES } from '~/utils/themes';
import { SITE_URL } from '~/utils/site'; import { SITE_URL } from '~/utils/site';
import { sortProjects } from '~/utils/projects';
const TOKEN_NAMES = [ const TOKEN_NAMES = [
'--bg', '--bg',
@@ -51,8 +52,8 @@ const { data: writingPages } = await useAsyncData(
const { data: projectPages } = await useAsyncData( const { data: projectPages } = await useAsyncData(
'styleguide-projects', 'styleguide-projects',
async () => { async () => {
const all = await queryCollection('projects').order('title', 'ASC').all(); const all = await queryCollection('projects').all();
return all.filter((page) => !page.hidden).slice(0, 4); return sortProjects(all.filter((page) => !page.hidden)).slice(0, 4);
}, },
); );
+6 -17
View File
@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { contentDateAttr, formatContentDate } from '~/utils/dates';
import { import {
DEFAULT_IMAGE, DEFAULT_IMAGE,
SITE_NAME, SITE_NAME,
@@ -54,19 +55,11 @@ if (!data.value) {
const page = computed(() => data.value?.article ?? null); const page = computed(() => data.value?.article ?? null);
const isFolder = computed(() => data.value?.kind === 'folder'); const isFolder = computed(() => data.value?.kind === 'folder');
const articleTitle = page.value?.title ?? folderName.value; const articleTitle = page.value?.title ?? folderName.value;
const formatDate = (date: Date | string) => {
return new Date(date).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
};
const articleDescription = page.value const articleDescription = page.value
? page.value.description ? page.value.description
: `Writing from ${folderName.value}`; : `Writing from ${folderName.value}`;
const layoutDescription = page.value const layoutDescription = page.value
? formatDate(page.value.date) ? formatContentDate(page.value.date, 'long')
: articleDescription; : articleDescription;
useSeoMeta({ useSeoMeta({
@@ -83,10 +76,10 @@ useSeoMeta({
twitterDescription: articleDescription, twitterDescription: articleDescription,
twitterImage: DEFAULT_IMAGE, twitterImage: DEFAULT_IMAGE,
articlePublishedTime: page.value articlePublishedTime: page.value
? new Date(page.value.date).toISOString() ? contentDateAttr(page.value.date)
: undefined, : undefined,
articleModifiedTime: page.value articleModifiedTime: page.value
? new Date(page.value.date).toISOString() ? contentDateAttr(page.value.date)
: undefined, : undefined,
articleAuthor: page.value ? [SITE_NAME] : undefined, articleAuthor: page.value ? [SITE_NAME] : undefined,
}); });
@@ -131,12 +124,8 @@ useHead({
url: articleUrl, url: articleUrl,
headline: articleTitle, headline: articleTitle,
description: articleDescription, description: articleDescription,
datePublished: new Date( datePublished: contentDateAttr(page.value.date),
page.value.date, dateModified: contentDateAttr(page.value.date),
).toISOString(),
dateModified: new Date(
page.value.date,
).toISOString(),
image: { image: {
'@type': 'ImageObject', '@type': 'ImageObject',
'@id': `${articleUrl}#article-image`, '@id': `${articleUrl}#article-image`,
+1 -1
View File
@@ -9,7 +9,7 @@ import {
const { data: pages } = await useAsyncData('writing-pages', async () => { const { data: pages } = await useAsyncData('writing-pages', async () => {
const all = await queryCollection('writing').order('date', 'DESC').all(); const all = await queryCollection('writing').order('date', 'DESC').all();
return import.meta.dev ? all : all.filter((page) => !page.hidden); return all.filter((page) => !page.hidden);
}); });
const writingUrl = `${SITE_URL}/writing`; const writingUrl = `${SITE_URL}/writing`;
+16
View File
@@ -0,0 +1,16 @@
const YMD = /^(\d{4})-(\d{2})-(\d{2})/;
const HAS_TZ = /(?:Z|[+-]\d{2}:?\d{2})$/i;
export function formatContentDate(value: string, month: 'short' | 'long' = 'short') {
const match = YMD.exec(value);
if (!match) return '';
return new Date(Date.UTC(+match[1], +match[2] - 1, +match[3], 12)).toLocaleDateString(
'en-US',
{ month, day: 'numeric', year: 'numeric', timeZone: 'UTC' },
);
}
export function contentDateAttr(value: string) {
if (HAS_TZ.test(value)) return new Date(value).toISOString();
return YMD.exec(value)?.[0] ?? '';
}
+12
View File
@@ -5,6 +5,18 @@ export function projectEntryHref(page: {
return page.href || `/projects${page.path}`; return page.href || `/projects${page.path}`;
} }
export function sortProjects<T extends { title: string; date?: string | null }>(
pages: T[],
) {
return [...pages].sort((a, b) => {
if (!a.date && !b.date) return a.title.localeCompare(b.title);
if (!a.date) return 1;
if (!b.date) return -1;
if (a.date !== b.date) return b.date.localeCompare(a.date);
return a.title.localeCompare(b.title);
});
}
export function isExternalHref(href: string) { export function isExternalHref(href: string) {
return /^https?:\/\//i.test(href); return /^https?:\/\//i.test(href);
} }
+2 -1
View File
@@ -10,7 +10,7 @@ export default defineContentConfig({
}, },
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),
date: z.coerce.date(), date: z.string(),
description: z.string(), description: z.string(),
hidden: z.boolean().optional(), hidden: z.boolean().optional(),
}), }),
@@ -26,6 +26,7 @@ export default defineContentConfig({
description: z.string(), description: z.string(),
href: z.string().optional(), href: z.string().optional(),
hidden: z.boolean().optional(), hidden: z.boolean().optional(),
date: z.string().optional(),
image: z.string().optional(), image: z.string().optional(),
}), }),
}), }),
+1
View File
@@ -2,4 +2,5 @@
title: Mystwright title: Mystwright
description: An AI-driven mystery detective game. Generate, investigate, solve. description: An AI-driven mystery detective game. Generate, investigate, solve.
href: https://mystwright.com/ href: https://mystwright.com/
hidden: true
--- ---
+1
View File
@@ -2,6 +2,7 @@
title: Unicord 🦄 title: Unicord 🦄
description: A Discord bot SDK that works like a web server. description: A Discord bot SDK that works like a web server.
# href: https://unicord.hat.fish # href: https://unicord.hat.fish
date: 2025-01-12
--- ---
I wanted writing a Discord bot to feel like writing a small web app. Register a handler, get a request, send a reply. *Bang!...bot done.* This became particularly interesting once bots could be served over HTTP. I wanted writing a Discord bot to feel like writing a small web app. Register a handler, get a request, send a reply. *Bang!...bot done.* This became particularly interesting once bots could be served over HTTP.
@@ -2,6 +2,7 @@
title: AI and the Barrier of Entry title: AI and the Barrier of Entry
date: 2023-03-25T12:00:00.000Z date: 2023-03-25T12:00:00.000Z
description: We are witnessing the next major wave of technological innovation. And it's never been easier to be a part of it. description: We are witnessing the next major wave of technological innovation. And it's never been easier to be a part of it.
hidden: true
--- ---
To be fair the term itself, **AI**, has had its meaning stretched pretty thin in the last few years. :margin-note[(Artificial Intelligence. The label has been doing too much work.)] It's been used to describe everything from a simple chatbot to a self driving car. That use is extremely broad. It doesn't really describe the space we think of as "AI". To be fair the term itself, **AI**, has had its meaning stretched pretty thin in the last few years. :margin-note[(Artificial Intelligence. The label has been doing too much work.)] It's been used to describe everything from a simple chatbot to a self driving car. That use is extremely broad. It doesn't really describe the space we think of as "AI".