From 80e7f567af0e47bb4d01f63b8de05ed144bb6c9f Mon Sep 17 00:00:00 2001 From: Jaron Date: Sat, 22 Aug 2026 12:43:00 -0500 Subject: [PATCH] writing folders working --- app/components/WritingList.vue | 93 +++++++ app/pages/writing/[...slug].vue | 252 ++++++++++-------- app/pages/writing/index.vue | 74 +---- .../frontend_developers_are_underpaid.md | 0 4 files changed, 240 insertions(+), 179 deletions(-) create mode 100644 app/components/WritingList.vue rename content/writing/{ => 2026}/frontend_developers_are_underpaid.md (100%) diff --git a/app/components/WritingList.vue b/app/components/WritingList.vue new file mode 100644 index 0000000..c1ae5a7 --- /dev/null +++ b/app/components/WritingList.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/app/pages/writing/[...slug].vue b/app/pages/writing/[...slug].vue index 42867dc..f905f17 100644 --- a/app/pages/writing/[...slug].vue +++ b/app/pages/writing/[...slug].vue @@ -12,130 +12,167 @@ const articleUrl = `${SITE_URL}${route.path}`; const webpageId = `${articleUrl}#webpage`; const articleId = `${articleUrl}#article`; const blogId = `${SITE_URL}/writing#blog`; -const contentPath = - '/' + - (Array.isArray(route.params.slug) +const slug = computed(() => + Array.isArray(route.params.slug) ? route.params.slug.join('/') - : String(route.params.slug ?? '')); + : String(route.params.slug ?? ''), +); +const contentPath = computed(() => '/' + slug.value); +const folderName = computed( + () => slug.value.split('/').filter(Boolean).at(-1) ?? slug.value, +); -const { data: page } = await useAsyncData(route.path, () => { - return queryCollection('writing').path(contentPath).first(); -}); +const { data } = await useAsyncData( + () => `writing:${route.path}`, + async () => { + const all = await queryCollection('writing').order('date', 'DESC').all(); + const visible = all.filter((page) => import.meta.dev || !page.demo); + const article = visible.find( + (page) => + page.path === contentPath.value || page.stem === slug.value, + ); -if (!page.value) { + if (article) { + return { kind: 'article' as const, article, pages: [] }; + } + + const prefix = contentPath.value.endsWith('/') + ? contentPath.value + : `${contentPath.value}/`; + const pages = visible.filter((page) => page.path.startsWith(prefix)); + + if (!pages.length) return null; + return { kind: 'folder' as const, article: null, pages }; + }, + { watch: [() => route.path] }, +); + +if (!data.value) { throw createError({ statusCode: 404, statusMessage: 'Page Not Found' }); } -const articleTitle = page.value.title; -const articleDescription = page.value.description; +const page = computed(() => data.value?.article ?? null); +const isFolder = computed(() => data.value?.kind === 'folder'); +const articleTitle = page.value?.title ?? folderName.value; +const articleDescription = page.value?.description ?? ''; useSeoMeta({ title: articleTitle, ogTitle: articleTitle, - description: articleDescription, - ogDescription: articleDescription, + description: articleDescription || articleTitle, + ogDescription: articleDescription || articleTitle, ogUrl: articleUrl, - ogType: 'article', + ogType: isFolder.value ? 'website' : 'article', ogImage: DEFAULT_IMAGE, ogImageAlt: articleTitle, twitterCard: 'summary', twitterTitle: articleTitle, - twitterDescription: articleDescription, + twitterDescription: articleDescription || articleTitle, twitterImage: DEFAULT_IMAGE, - articlePublishedTime: new Date(page.value.date).toISOString(), - articleModifiedTime: new Date(page.value.date).toISOString(), - articleAuthor: [SITE_NAME], + articlePublishedTime: page.value + ? new Date(page.value.date).toISOString() + : undefined, + articleModifiedTime: page.value + ? new Date(page.value.date).toISOString() + : undefined, + articleAuthor: page.value ? [SITE_NAME] : undefined, }); useHead({ link: [{ rel: 'canonical', href: articleUrl }], - script: [ - { - key: 'json-ld-article', - type: 'application/ld+json', - innerHTML: JSON.stringify({ - '@context': 'https://schema.org', - '@graph': [ - { - '@type': 'WebPage', - '@id': webpageId, - url: articleUrl, - name: articleTitle, - description: articleDescription, - inLanguage: 'en-US', - isPartOf: { '@id': websiteId }, - breadcrumb: { '@id': `${articleUrl}#breadcrumb` }, - mainEntity: { '@id': articleId }, - }, - { - '@type': 'Blog', - '@id': blogId, - url: `${SITE_URL}/writing`, - name: 'Jaron Pate Writing', - inLanguage: 'en-US', - isPartOf: { '@id': websiteId }, - publisher: { - '@type': 'Person', - '@id': personId, - name: SITE_NAME, - }, - }, - { - '@type': 'BlogPosting', - '@id': articleId, - url: articleUrl, - headline: articleTitle, - description: articleDescription, - datePublished: new Date(page.value.date).toISOString(), - dateModified: new Date(page.value.date).toISOString(), - image: { - '@type': 'ImageObject', - '@id': `${articleUrl}#article-image`, - url: DEFAULT_IMAGE, - }, - inLanguage: 'en-US', - isPartOf: { '@id': blogId }, - mainEntityOfPage: { '@id': webpageId }, - author: { - '@type': 'Person', - '@id': personId, - name: SITE_NAME, - url: SITE_URL, - }, - publisher: { - '@type': 'Person', - '@id': personId, - name: SITE_NAME, - }, - }, - { - '@type': 'BreadcrumbList', - '@id': `${articleUrl}#breadcrumb`, - itemListElement: [ - { - '@type': 'ListItem', - position: 1, - name: 'Home', - item: SITE_URL, - }, - { - '@type': 'ListItem', - position: 2, - name: 'Writing', - item: `${SITE_URL}/writing`, - }, - { - '@type': 'ListItem', - position: 3, - name: articleTitle, - item: articleUrl, - }, - ], - }, - ], - }), - }, - ], + script: page.value + ? [ + { + key: 'json-ld-article', + type: 'application/ld+json', + innerHTML: JSON.stringify({ + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'WebPage', + '@id': webpageId, + url: articleUrl, + name: articleTitle, + description: articleDescription, + inLanguage: 'en-US', + isPartOf: { '@id': websiteId }, + breadcrumb: { '@id': `${articleUrl}#breadcrumb` }, + mainEntity: { '@id': articleId }, + }, + { + '@type': 'Blog', + '@id': blogId, + url: `${SITE_URL}/writing`, + name: 'Jaron Pate Writing', + inLanguage: 'en-US', + isPartOf: { '@id': websiteId }, + publisher: { + '@type': 'Person', + '@id': personId, + name: SITE_NAME, + }, + }, + { + '@type': 'BlogPosting', + '@id': articleId, + url: articleUrl, + headline: articleTitle, + description: articleDescription, + datePublished: new Date( + page.value.date, + ).toISOString(), + dateModified: new Date( + page.value.date, + ).toISOString(), + image: { + '@type': 'ImageObject', + '@id': `${articleUrl}#article-image`, + url: DEFAULT_IMAGE, + }, + inLanguage: 'en-US', + isPartOf: { '@id': blogId }, + mainEntityOfPage: { '@id': webpageId }, + author: { + '@type': 'Person', + '@id': personId, + name: SITE_NAME, + url: SITE_URL, + }, + publisher: { + '@type': 'Person', + '@id': personId, + name: SITE_NAME, + }, + }, + { + '@type': 'BreadcrumbList', + '@id': `${articleUrl}#breadcrumb`, + itemListElement: [ + { + '@type': 'ListItem', + position: 1, + name: 'Home', + item: SITE_URL, + }, + { + '@type': 'ListItem', + position: 2, + name: 'Writing', + item: `${SITE_URL}/writing`, + }, + { + '@type': 'ListItem', + position: 3, + name: articleTitle, + item: articleUrl, + }, + ], + }, + ], + }), + }, + ] + : [], }); const formatDate = (date: Date | string) => { @@ -155,8 +192,8 @@ definePageMeta({ diff --git a/app/pages/writing/index.vue b/app/pages/writing/index.vue index f89f084..0c949e3 100644 --- a/app/pages/writing/index.vue +++ b/app/pages/writing/index.vue @@ -113,15 +113,6 @@ useHead({ ], }); -const formatDate = (dateString: string | Date) => { - return new Date(dateString).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - timeZone: 'UTC', - }); -}; - definePageMeta({ layout: false, }); @@ -134,70 +125,7 @@ definePageMeta({ description="Thoughts I've had here and there. Maybe you'll find something helpful or interesting here." >
- -

No thoughts...head empty

+
- - diff --git a/content/writing/frontend_developers_are_underpaid.md b/content/writing/2026/frontend_developers_are_underpaid.md similarity index 100% rename from content/writing/frontend_developers_are_underpaid.md rename to content/writing/2026/frontend_developers_are_underpaid.md