From 6d7403c6cdc807968cf6d929cc24cf97187230b2 Mon Sep 17 00:00:00 2001 From: Tanner Heffner Date: Fri, 19 Jan 2024 22:08:34 -0800 Subject: [PATCH 1/4] stub out gallery page list --- .github/ISSUE_TEMPLATE/new-gallery-post.md | 18 +++ src/lib/content.js | 129 +++++++++++------- src/lib/types.d.ts | 25 +++- src/lib/utils.js | 56 ++++++++ src/routes/(main)/blog/+page.js | 3 +- src/routes/(main)/blogroll/+page.svx | 1 + src/routes/(main)/gallery/+page.js | 16 +++ src/routes/(main)/gallery/+page.svelte | 32 +++++ src/routes/(main)/gallery/[slug]/+page.js | 0 src/routes/(main)/gallery/[slug]/+page.svelte | 0 src/routes/(main)/resume/+page.svelte | 1 - src/routes/api/listContent.json/+server.js | 5 +- src/routes/api/listGallery.json/+server.js | 44 ++++++ 13 files changed, 268 insertions(+), 62 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/new-gallery-post.md create mode 100644 src/routes/(main)/gallery/+page.js create mode 100644 src/routes/(main)/gallery/+page.svelte create mode 100644 src/routes/(main)/gallery/[slug]/+page.js create mode 100644 src/routes/(main)/gallery/[slug]/+page.svelte create mode 100644 src/routes/api/listGallery.json/+server.js diff --git a/.github/ISSUE_TEMPLATE/new-gallery-post.md b/.github/ISSUE_TEMPLATE/new-gallery-post.md new file mode 100644 index 0000000..b0650ef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-gallery-post.md @@ -0,0 +1,18 @@ +--- +name: New Gallery post +about: Template with front matter for gallery posts +title: '' +labels: Gallery +assignees: '' + +--- + +*** swap these for dashes for front matter +title: "post title" +date: 2022-01-16 00:00:00 +description: "will show underneath title on list page" +image: upload cover image here +alt: image alt text +*** + +post content goes here, underneath diff --git a/src/lib/content.js b/src/lib/content.js index c79b9c3..2fe4554 100644 --- a/src/lib/content.js +++ b/src/lib/content.js @@ -1,6 +1,5 @@ import { compile } from 'mdsvex'; import { dev } from '$app/environment'; -import grayMatter from 'gray-matter'; import fetch from 'node-fetch'; import { GH_USER_REPO, @@ -8,11 +7,8 @@ import { GH_PUBLISHED_TAGS, REPO_OWNER } from './siteConfig'; -import { slugify, readingTime } from './utils' +import { slugify, readingTime, baseIssueContent } from './utils' import parse from 'parse-link-header'; -import { remark } from 'remark'; -import remarkParse from 'remark-parse'; -import remarkStringify from 'remark-stringify'; import rehypeStringify from 'rehype-stringify'; import rehypeSlug from 'rehype-slug'; import rehypeAutoLink from 'rehype-autolink-headings'; @@ -31,6 +27,7 @@ const rehypePlugins = [ ]; let allBlogposts = []; +let allGalleries = []; // let etag = null // todo - implmement etag header export async function listContent() { @@ -71,7 +68,7 @@ export async function listContent() { // issue.labels.some((label) => GH_PUBLISHED_TAGS.includes(label.name)) && APPROVED_POSTERS_GH_USERNAME.includes(issue.user.login) ) { - _allBlogposts.push(parseIssue(issue)); + _allBlogposts.push(parseBlogIssue(issue)); } } ); @@ -83,6 +80,58 @@ export async function listContent() { return _allBlogposts; } +export async function listContentFromIssues(label) { + let allContentWithLabel = [] + let next = null; + + const authheader = process.env.GH_TOKEN && { + Authorization: `token ${process.env.GH_TOKEN}` + }; + + let url = + `https://api.github.com/repos/${GH_USER_REPO}/issues?` + + new URLSearchParams({ + state: 'all', + labels: label, + per_page: '100', + }); + + // pull issues created by owner only if allowed author = repo owner + if (APPROVED_POSTERS_GH_USERNAME.length === 1 && APPROVED_POSTERS_GH_USERNAME[0] === REPO_OWNER) { + url += '&' + new URLSearchParams({ creator: REPO_OWNER }); + } + + do { + const res = await fetch(next?.url ?? url, { + headers: authheader + }); + + const issues = await res.json(); + if ('message' in issues && res.status > 400) + throw new Error(res.status + ' ' + res.statusText + '\n' + (issues && issues.message)); + + issues.forEach((issue) => { + if (APPROVED_POSTERS_GH_USERNAME.includes(issue.user.login)) { + // Add additional label page types here: + switch (label) { + case 'Gallery': + allContentWithLabel.push(parseGalleryIssue(issue)); + break; + case 'Published': + default: + allContentWithLabel.push(parseBlogIssue(issue)); + break; + } + } + }); + const headers = parse(res.headers.get('Link')); + next = headers && headers.next; + } while (next) + + allContentWithLabel.sort((a, b) => b.date.valueOf() - a.date.valueOf()); // use valueOf to make TS happy https://stackoverflow.com/a/60688789/1106414 + return allContentWithLabel +} + export async function getContent(slug) { // get all blogposts if not already done - or in development if (dev || allBlogposts.length === 0) { @@ -175,38 +224,17 @@ export async function getContent(slug) { return { ...blogpost, content }; } else { - throw new Error('Blogpost not found for slug: ' + slug); + throw new Error('Issue not found for slug: ' + slug); } } /** * @param {import('./types').GithubIssue} issue - * @returns {import('./types').ContentItem} + * @returns {import('./types').BlogItem} */ -function parseIssue(issue) { - const src = issue.body; - const { content, data } = grayMatter(src); - let title = data.title ?? issue.title; - let slug; - if (data.slug) { - slug = data.slug; - } else { - slug = slugify(title); - } - let description = data.description ?? content.trim().split('\n')[0]; - // extract plain text from markdown - description = remark() - .use(remarkParse) - .use(remarkStringify) - .processSync(description) - .toString(); - description = description.replace(/\n/g, ' '); - // strip html - description = description.replace(/<[^>]*>?/gm, ''); - // strip markdown - description = description.replace(/[[\]]/gm, ''); - // strip markdown - description = description.replace(/[[\]]/gm, ''); +function parseBlogIssue(issue) { + const base = baseIssueContent(issue); + const data = base.frontmatter; // you may wish to use a truncation approach like this instead... // let description = (data.content.length > 300) ? data.content.slice(0, 300) + '...' : data.content @@ -218,27 +246,26 @@ function parseIssue(issue) { // console.log(slug, tags); return { - type: 'blog', // futureproof in case you want to add other types of content - issueNumber: issue.number, - content, - frontmatter: data, - title, - subtitle: data.subtitle, - description, - category: data.category?.toLowerCase() || 'blog', + ...base, + type: 'blog', + category: data.category?.toLowerCase() || 'note', tags, image: data.image ?? data.cover_image, - canonical: data.canonical, // for canonical URLs of something published elsewhere - slug: slug.toString().toLowerCase(), date: new Date(data.date ?? issue.created_at), - readingTime: readingTime(content), - ghMetadata: { - issueUrl: issue.html_url, - commentsUrl: issue.comments_url, - title: issue.title, - created_at: issue.created_at, - updated_at: issue.updated_at, - reactions: issue.reactions - } + readingTime: readingTime(base.content), }; } + +function parseGalleryIssue(issue) { + const base = baseIssueContent(issue); + const data = base.frontmatter; + + return { + ...base, + slug: `gallery/${data.title.toLowerCase()}`, + type: 'gallery', + image: data.image ?? data.cover_image, + alt: data.alt, + date: new Date(data.date ?? issue.created_at), + } +} diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts index 08a30e8..5e12cf5 100644 --- a/src/lib/types.d.ts +++ b/src/lib/types.d.ts @@ -1,5 +1,4 @@ -export type ContentItem = { - type: 'blog'; +export type BaseContentItem = { content: string; frontmatter: { [key: string]: string; @@ -7,15 +6,31 @@ export type ContentItem = { title: string; subtitle: string; description: string; - category: string; - tags: string[]; - image: string; canonical: string; slug: string; date: Date; ghMetadata: GHMetadata; +} + +export type BlogItem = BaseContentItem & { + type: 'blog'; + category: string; + tags: string[]; + image: string; + readingTime: string; }; +export type GalleryItem = BaseContentItem & { + type: 'gallery'; + images: GalleryImage[] +} + +export type GalleryImage = { + src: string; + alt: string; + size: string; +} + export type GHMetadata = { issueUrl: string; commentsUrl: string; diff --git a/src/lib/utils.js b/src/lib/utils.js index 18587f6..f68ff9e 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -1,3 +1,8 @@ +import grayMatter from 'gray-matter'; +import { remark } from 'remark'; +import remarkParse from 'remark-parse'; +import remarkStringify from 'remark-stringify'; + /** * @param {string} text * @returns {string} @@ -22,3 +27,54 @@ export function slugify(text) { .replace(/--+/g, '-') // Replace multiple hyphen with single hyphen .replace(/(^-|-$)/g, ''); // Remove leading or trailing hyphen } + + +/** + * All pages built from github issue should contain this data at minimum + * + * @param {import('./types').GithubIssue} issue + * @returns {import('./types').BaseContentItem} + */ +export function baseIssueContent(issue) { + const src = issue.body; + const { content, data } = grayMatter(src); + let title = data.title ?? issue.title; + let slug; + if (data.slug) { + slug = data.slug; + } else { + slug = slugify(title); + } + + let description = data.description ?? content.trim().split('\n')[0]; + // extract plain text from markdown + description = remark() + .use(remarkParse) + .use(remarkStringify) + .processSync(description) + .toString(); + description = description.replace(/\n/g, ' '); + // strip html + description = description.replace(/<[^>]*>?/gm, ''); + // strip markdown + description = description.replace(/[[\]]/gm, ''); + // strip markdown + description = description.replace(/[[\]]/gm, ''); + + return { + frontmatter: data, + issueNumber: issue.number, + slug: slug, + title, + description, + content, + ghMetadata: { + issueUrl: issue.html_url, + commentsUrl: issue.comments_url, + title: issue.title, + created_at: issue.created_at, + updated_at: issue.updated_at, + reactions: issue.reactions + } + } +} diff --git a/src/routes/(main)/blog/+page.js b/src/routes/(main)/blog/+page.js index f35f233..adbbc8b 100644 --- a/src/routes/(main)/blog/+page.js +++ b/src/routes/(main)/blog/+page.js @@ -2,8 +2,7 @@ import { error } from '@sveltejs/kit'; // export const prerender = true; // turned off so it refreshes quickly export async function load({ setHeaders, fetch }) { const res = await fetch(`/api/listContent.json`); - // alternate strategy https://www.davidwparker.com/posts/how-to-make-an-rss-feed-in-sveltekit - // Object.entries(import.meta.glob('./*.md')).map(async ([path, page]) => { + if (res.status > 400) { throw error(res.status, await res.text()) } diff --git a/src/routes/(main)/blogroll/+page.svx b/src/routes/(main)/blogroll/+page.svx index ff92874..606101b 100644 --- a/src/routes/(main)/blogroll/+page.svx +++ b/src/routes/(main)/blogroll/+page.svx @@ -10,6 +10,7 @@ - [randi bolt](https://www.rbolt.me/) - [nug doug](https://darkdell.net) +- [mike crittenden](https://critter.blog) - [defector](https://defector.com) - [bright side](https://www.brightsideofthesun.com/) - [fujichia](https://www.fujichia.com/) diff --git a/src/routes/(main)/gallery/+page.js b/src/routes/(main)/gallery/+page.js new file mode 100644 index 0000000..f72efef --- /dev/null +++ b/src/routes/(main)/gallery/+page.js @@ -0,0 +1,16 @@ +import { error } from '@sveltejs/kit'; +// export const prerender = true; // turned off so it refreshes quickly +export async function load({ setHeaders, fetch }) { + const res = await fetch(`/api/listGallery.json`); + + if (res.status > 400) { + throw error(res.status, await res.text()) + } + + /** @type {import('$lib/types').GalleryItem[]} */ + const items = await res.json(); + setHeaders({ + 'Cache-Control': 'public, max-age=60' // 1 minute + }) + return {items} +} diff --git a/src/routes/(main)/gallery/+page.svelte b/src/routes/(main)/gallery/+page.svelte new file mode 100644 index 0000000..a65df96 --- /dev/null +++ b/src/routes/(main)/gallery/+page.svelte @@ -0,0 +1,32 @@ + + + + heffner.dev | adventures + + + + + + +
+ +

Gallery

+

+ details, photos, ephemera from past adventures +

+
+ + {#each items as trip} + + {trip.alt} + {trip.description} + + + {/each} + +
diff --git a/src/routes/(main)/gallery/[slug]/+page.js b/src/routes/(main)/gallery/[slug]/+page.js new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/(main)/gallery/[slug]/+page.svelte b/src/routes/(main)/gallery/[slug]/+page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/(main)/resume/+page.svelte b/src/routes/(main)/resume/+page.svelte index 001ebcd..577f687 100644 --- a/src/routes/(main)/resume/+page.svelte +++ b/src/routes/(main)/resume/+page.svelte @@ -1,5 +1,4 @@ diff --git a/src/routes/api/listContent.json/+server.js b/src/routes/api/listContent.json/+server.js index 38b4d66..f855d3f 100644 --- a/src/routes/api/listContent.json/+server.js +++ b/src/routes/api/listContent.json/+server.js @@ -1,11 +1,10 @@ -// import { json } from '@sveltejs/kit'; -import { listContent } from '$lib/content'; +import { listContentFromIssues } from '$lib/content'; /** * @type {import('./$types').RequestHandler} */ export async function GET({ setHeaders }) { - const list = await listContent(); + const list = await listContentFromIssues('Published'); setHeaders({ 'Cache-Control': `max-age=0, s-maxage=${60}` // 1 minute.. for now }); diff --git a/src/routes/api/listGallery.json/+server.js b/src/routes/api/listGallery.json/+server.js new file mode 100644 index 0000000..5d99523 --- /dev/null +++ b/src/routes/api/listGallery.json/+server.js @@ -0,0 +1,44 @@ +import { listContentFromIssues } from '$lib/content'; + +/** + * @type {import('./$types').RequestHandler} + */ +export async function GET({ setHeaders }) { + const list = await listContentFromIssues('Gallery'); + + // const list = [ + // { + // name: 'Japan', + // date: 'April 2023', + // description: 'traveled all over the country for a month with randi', + // slug: '/gallery/japan', + // image: 'http://placekitten.com/400/400', + // alt: 'japan alt' + // }, + // { + // name: 'Morocco', + // date: 'September 2018', + // description: 'visited my sister during her peace corps mission', + // slug: '/gallery/morocco', + // image: 'http://placekitten.com/400/400', + // alt: 'morocco alt' + // }, + // { + // name: 'Costa Rica', + // date: 'December 2017', + // description: 'two weeks of desayuno tipica with the boys', + // slug: '/gallery/costa-rica', + // image: 'http://placekitten.com/400/400', + // alt: 'cr alt' + // }, + // ]; + + setHeaders({ + 'Cache-Control': `max-age=0, s-maxage=${60}` // 1 minute.. for now + }); + return new Response(JSON.stringify(list), { + headers: { + 'content-type': 'application/json; charset=utf-8' + } + }); +} From f38100c65e585c6b4cd964f08c054668d045cd96 Mon Sep 17 00:00:00 2001 From: Tanner Heffner Date: Fri, 19 Jan 2024 22:26:58 -0800 Subject: [PATCH 2/4] clean up mobile menu --- src/lib/components/MobileMenu.svelte | 96 ++++++++++++---------------- src/routes/(main)/+layout.svelte | 1 + src/routes/(main)/blogroll/+page.svx | 5 +- 3 files changed, 44 insertions(+), 58 deletions(-) diff --git a/src/lib/components/MobileMenu.svelte b/src/lib/components/MobileMenu.svelte index 352915e..04743d0 100644 --- a/src/lib/components/MobileMenu.svelte +++ b/src/lib/components/MobileMenu.svelte @@ -12,6 +12,41 @@ }, 300); } } + + export const links = [ + { + name: 'Home', + href: '/' + }, + { + name: 'Posts', + href: '/blog' + }, + { + name: 'Work', + href: '/work' + }, + { + name: 'Gallery', + href: '/gallery' + }, + { + name: 'Blogroll', + href: '/blogroll' + }, + { + name: 'About', + href: '/about' + }, + { + name: 'Github', + href: 'https://github.com/tjheffner' + }, + { + name: 'Tweets', + href: 'https://twitter.com/foodpyramids' + }, + ] diff --git a/src/routes/(main)/+layout.svelte b/src/routes/(main)/+layout.svelte index 9b6e865..eb75480 100644 --- a/src/routes/(main)/+layout.svelte +++ b/src/routes/(main)/+layout.svelte @@ -53,6 +53,7 @@
Posts Work + Gallery About
diff --git a/src/routes/(main)/blogroll/+page.svx b/src/routes/(main)/blogroll/+page.svx index 606101b..6972d06 100644 --- a/src/routes/(main)/blogroll/+page.svx +++ b/src/routes/(main)/blogroll/+page.svx @@ -8,9 +8,10 @@

blogroll

-- [randi bolt](https://www.rbolt.me/) +here's some other cool blogs + - [nug doug](https://darkdell.net) - [mike crittenden](https://critter.blog) - [defector](https://defector.com) -- [bright side](https://www.brightsideofthesun.com/) +- [bright side of the sun](https://www.brightsideofthesun.com/) - [fujichia](https://www.fujichia.com/) From 269eba350c82d4001a75f60ebe1dcb40b7fef183 Mon Sep 17 00:00:00 2001 From: Tanner Heffner Date: Sat, 20 Jan 2024 00:33:52 -0800 Subject: [PATCH 3/4] individual gallery pages work + some cleanup --- src/lib/content.js | 254 ++++-------------- src/lib/types.d.ts | 2 +- src/lib/utils.js | 98 +++++++ src/routes/(main)/+page.svelte | 4 +- src/routes/(main)/[slug]/+page.js | 11 +- src/routes/(main)/about/+page.svx | 29 +- src/routes/(main)/gallery/+page.svelte | 2 +- src/routes/(main)/gallery/[slug]/+page.js | 21 ++ src/routes/(main)/gallery/[slug]/+page.svelte | 86 ++++++ .../{blog => gallery}/[slug].json/+server.js | 0 .../api/getContent/[slug].json/+server.js | 20 ++ 11 files changed, 298 insertions(+), 229 deletions(-) rename src/routes/api/{blog => gallery}/[slug].json/+server.js (100%) create mode 100644 src/routes/api/getContent/[slug].json/+server.js diff --git a/src/lib/content.js b/src/lib/content.js index 2fe4554..1eb4a9d 100644 --- a/src/lib/content.js +++ b/src/lib/content.js @@ -1,4 +1,3 @@ -import { compile } from 'mdsvex'; import { dev } from '$app/environment'; import fetch from 'node-fetch'; import { @@ -7,79 +6,20 @@ import { GH_PUBLISHED_TAGS, REPO_OWNER } from './siteConfig'; -import { slugify, readingTime, baseIssueContent } from './utils' +import { slugify, readingTime, baseIssueContent, formatContent } from './utils' import parse from 'parse-link-header'; -import rehypeStringify from 'rehype-stringify'; -import rehypeSlug from 'rehype-slug'; -import rehypeAutoLink from 'rehype-autolink-headings'; - -const remarkPlugins = undefined; -const rehypePlugins = [ - rehypeStringify, - rehypeSlug, - [ - rehypeAutoLink, - { - behavior: 'wrap', - properties: { class: 'hover:text-yellow-100 no-underline' } - } - ] -]; let allBlogposts = []; let allGalleries = []; -// let etag = null // todo - implmement etag header - -export async function listContent() { - // use a diff var so as to not have race conditions while fetching - // TODO: make sure to handle this better when doing etags or cache restore - - /** @type {import('./types').ContentItem[]} */ - let _allBlogposts = []; - let next = null; - let limit = 0; // just a failsafe against infinite loop - feel free to remove - const authheader = process.env.GH_TOKEN && { - Authorization: `token ${process.env.GH_TOKEN}` - }; - let url = - `https://api.github.com/repos/${GH_USER_REPO}/issues?` + - new URLSearchParams({ - state: 'all', - labels: GH_PUBLISHED_TAGS.toString(), - per_page: '100', - }); - // pull issues created by owner only if allowed author = repo owner - if (APPROVED_POSTERS_GH_USERNAME.length === 1 && APPROVED_POSTERS_GH_USERNAME[0] === REPO_OWNER) { - url += '&' + new URLSearchParams({ creator: REPO_OWNER }); - } - do { - const res = await fetch(next?.url ?? url, { - headers: authheader - }); - - const issues = await res.json(); - if ('message' in issues && res.status > 400) - throw new Error(res.status + ' ' + res.statusText + '\n' + (issues && issues.message)); - issues.forEach( - /** @param {import('./types').GithubIssue} issue */ - (issue) => { - if ( - // labels check not needed anymore as we have set the labels param in github api - // issue.labels.some((label) => GH_PUBLISHED_TAGS.includes(label.name)) && - APPROVED_POSTERS_GH_USERNAME.includes(issue.user.login) - ) { - _allBlogposts.push(parseBlogIssue(issue)); - } - } - ); - const headers = parse(res.headers.get('Link')); - next = headers && headers.next; - } while (next && limit++ < 1000); // just a failsafe against infinite loop - feel free to remove - _allBlogposts.sort((a, b) => b.date.valueOf() - a.date.valueOf()); // use valueOf to make TS happy https://stackoverflow.com/a/60688789/1106414 - allBlogposts = _allBlogposts; - return _allBlogposts; -} - +let allPosts = []; + +/* + * Gets all github issues with a provided label. + * + * PAGETYPE: 'LABEL' + * Blog posts: 'Published' + * Gallery pages: 'Gallery' + */ export async function listContentFromIssues(label) { let allContentWithLabel = [] let next = null; @@ -112,16 +52,7 @@ export async function listContentFromIssues(label) { issues.forEach((issue) => { if (APPROVED_POSTERS_GH_USERNAME.includes(issue.user.login)) { - // Add additional label page types here: - switch (label) { - case 'Gallery': - allContentWithLabel.push(parseGalleryIssue(issue)); - break; - case 'Published': - default: - allContentWithLabel.push(parseBlogIssue(issue)); - break; - } + allContentWithLabel.push(parseIssue(issue, label)) } }); const headers = parse(res.headers.get('Link')); @@ -132,140 +63,67 @@ export async function listContentFromIssues(label) { return allContentWithLabel } +// searches the list of content returned and matches based on slug export async function getContent(slug) { - // get all blogposts if not already done - or in development - if (dev || allBlogposts.length === 0) { + // get all posts if not already done - or in development + if (dev || allPosts.length === 0) { console.log('loading allBlogposts'); - allBlogposts = await listContent(); + allBlogposts = await listContentFromIssues('Published'); + allGalleries = await listContentFromIssues('Gallery'); + allPosts = [...allBlogposts, ...allGalleries]; console.log('loaded ' + allBlogposts.length + ' blogposts'); - if (!allBlogposts.length) + console.log('loaded ' + allGalleries.length + ' galleries'); + console.log('loaded ' + allPosts.length + ' posts from issues'); + + if (!allPosts.length) throw new Error( - 'failed to load blogposts for some reason. check token' + process.env.GH_TOKEN + 'failed to load posts from github issues for some reason. check token' + process.env.GH_TOKEN ); } - if (!allBlogposts.length) throw new Error('no blogposts'); - // find the blogpost that matches this slug - const blogpost = allBlogposts.find((post) => post.slug === slug); - if (blogpost) { - const blogbody = blogpost.content - .replace(/\n{% youtube (.*?) %}/g, (_, x) => { - // https://stackoverflow.com/a/27728417/1106414 - function youtube_parser(url) { - var rx = - /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/; - return url.match(rx)[1]; - } - const videoId = x.startsWith('https://') ? youtube_parser(x) : x; - return ``; - }) - .replace(/\n{% (tweet|twitter) (.*?) %}/g, (_, _2, x) => { - const url = x.startsWith('https://twitter.com/') ? x : `https://twitter.com/x/status/${x}`; - return ` - - - `; - }); - - // compile it with mdsvex - const content = ( - await compile(blogbody, { - remarkPlugins, - // @ts-ignore - rehypePlugins - }) - ).code - // https://github.com/pngwn/MDsveX/issues/392 - .replace(/>{@html ``}<\/pre>/g, ''); + if (!allPosts.length) throw new Error('no posts'); + // find the issue that matches this slug + const post = allPosts.find((p) => p.slug === slug); + if (post) { + const content = await formatContent(post.content); - return { ...blogpost, content }; + return { ...post, content }; } else { throw new Error('Issue not found for slug: ' + slug); } } -/** - * @param {import('./types').GithubIssue} issue - * @returns {import('./types').BlogItem} - */ -function parseBlogIssue(issue) { +// format github issue into object that page type expects. +// work pages are loaded using localContent.js for .svx files, not github issues +function parseIssue(issue, label) { const base = baseIssueContent(issue); const data = base.frontmatter; - // you may wish to use a truncation approach like this instead... - // let description = (data.content.length > 300) ? data.content.slice(0, 300) + '...' : data.content - - /** @type {string[]} */ - let tags = []; - if (data.tags) tags = Array.isArray(data.tags) ? data.tags : [data.tags]; - tags = tags.map((tag) => tag.toLowerCase()); - // console.log(slug, tags); - - return { - ...base, - type: 'blog', - category: data.category?.toLowerCase() || 'note', - tags, - image: data.image ?? data.cover_image, - date: new Date(data.date ?? issue.created_at), - readingTime: readingTime(base.content), - }; -} + let post; -function parseGalleryIssue(issue) { - const base = baseIssueContent(issue); - const data = base.frontmatter; + switch (label) { + case 'Gallery': + post = { + type: 'gallery', + ...base, + alt: data.alt, + } + break; + case 'Published': + default: + let tags = []; + if (data.tags) tags = Array.isArray(data.tags) ? data.tags : [data.tags]; + tags = tags.map((tag) => tag.toLowerCase()); + + post = { + type: 'blog', + ...base, + category: data.category?.toLowerCase() || 'note', + tags, + readingTime: readingTime(base.content), + } - return { - ...base, - slug: `gallery/${data.title.toLowerCase()}`, - type: 'gallery', - image: data.image ?? data.cover_image, - alt: data.alt, - date: new Date(data.date ?? issue.created_at), + break; } + + return post } diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts index 5e12cf5..aed263e 100644 --- a/src/lib/types.d.ts +++ b/src/lib/types.d.ts @@ -10,13 +10,13 @@ export type BaseContentItem = { slug: string; date: Date; ghMetadata: GHMetadata; + image: string; } export type BlogItem = BaseContentItem & { type: 'blog'; category: string; tags: string[]; - image: string; readingTime: string; }; diff --git a/src/lib/utils.js b/src/lib/utils.js index f68ff9e..72e0c27 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -1,7 +1,24 @@ import grayMatter from 'gray-matter'; +import { compile } from 'mdsvex'; import { remark } from 'remark'; import remarkParse from 'remark-parse'; import remarkStringify from 'remark-stringify'; +import rehypeStringify from 'rehype-stringify'; +import rehypeSlug from 'rehype-slug'; +import rehypeAutoLink from 'rehype-autolink-headings'; + +const remarkPlugins = undefined; +const rehypePlugins = [ + rehypeStringify, + rehypeSlug, + [ + rehypeAutoLink, + { + behavior: 'wrap', + properties: { class: 'hover:text-yellow-100 no-underline' } + } + ] +]; /** * @param {string} text @@ -68,6 +85,8 @@ export function baseIssueContent(issue) { title, description, content, + image: data.image ?? data.cover_image, + date: new Date(data.date ?? issue.created_at), ghMetadata: { issueUrl: issue.html_url, commentsUrl: issue.comments_url, @@ -78,3 +97,82 @@ export function baseIssueContent(issue) { } } } + +export async function formatContent(content) { + const formatted = content + .replace(/\n{% youtube (.*?) %}/g, (_, x) => { + // https://stackoverflow.com/a/27728417/1106414 + function youtube_parser(url) { + var rx = + /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/; + return url.match(rx)[1]; + } + const videoId = x.startsWith('https://') ? youtube_parser(x) : x; + return ``; + }) + .replace(/\n{% (tweet|twitter) (.*?) %}/g, (_, _2, x) => { + const url = x.startsWith('https://twitter.com/') ? x : `https://twitter.com/x/status/${x}`; + return ` + + + `; + }); + + // compile it with mdsvex + const output = ( + await compile(formatted, { + remarkPlugins, + // @ts-ignore + rehypePlugins + }) + ).code + // https://github.com/pngwn/MDsveX/issues/392 + .replace(/>{@html ``}<\/pre>/g, ''); + + return output +} diff --git a/src/routes/(main)/+page.svelte b/src/routes/(main)/+page.svelte index 3097909..059174c 100644 --- a/src/routes/(main)/+page.svelte +++ b/src/routes/(main)/+page.svelte @@ -30,14 +30,14 @@

- welcome to my page + welcome to my page 🌊

This site is perpetually under construction but coming along :)

-

It's mostly a place for me to post recipes I like, with the occasional technical post or personal blog mixed in. Some work-related stuff can be found here too, but that's not what I want to talk about.

+

It's mostly a place for me to post recipes I like and travel photos, with the occasional technical post or personal blog mixed in. Some work-related stuff can be found here too, but that's not what I want to talk about.

Thanks for stopping by, check out the links that work. ✌️

diff --git a/src/routes/(main)/[slug]/+page.js b/src/routes/(main)/[slug]/+page.js index d0b3ca5..6a4cda8 100644 --- a/src/routes/(main)/[slug]/+page.js +++ b/src/routes/(main)/[slug]/+page.js @@ -10,15 +10,22 @@ export async function load({ params, fetch, setHeaders }) { } let res = null; - res = await fetch(`/api/blog/${slug}.json`); + res = await fetch(`/api/getContent/${slug}.json`); if (res.status > 400) { throw error(res.status, await res.text()); } + const json = await res.json() + + // because [slug] is a catchall, it gets the gallery pages too. redirect them. + if (json.type === 'gallery') { + throw redirect(308, `/gallery/${json.slug}`) + } + setHeaders({ 'Cache-Control': 'public, max-age=60' }); return { - json: await res.json(), + json, slug, REPO_URL }; diff --git a/src/routes/(main)/about/+page.svx b/src/routes/(main)/about/+page.svx index 40801ea..629a2fd 100644 --- a/src/routes/(main)/about/+page.svx +++ b/src/routes/(main)/about/+page.svx @@ -1,5 +1,6 @@ @@ -13,7 +14,7 @@

about

## the site -this site is the online home of tanner heffner. it's part [blog](/blog), [portfolio](/work), playground, [resume](/resume), and all [digital garden](https://joelhooks.com/digital-garden), using [swyxkit](https://swyxkit.netlify.app/about) as my starting point. [this post](/heffdotdev-technical-details) explains more about the tools i chose and why. +this site is the online home of tanner heffner. it's part [blog](/blog), [portfolio](/work), [photo gallery](/gallery), [playground](/christmas), [resume](/resume), and all [digital garden](https://joelhooks.com/digital-garden), using [swyxkit](https://swyxkit.netlify.app/about) as my starting point. [this post](/heffdotdev-technical-details) explains more about the tools i chose and why. you'll find technical snippets & longer blogs, cooking recipes, old projects both professional and personal, and potentially much more. we're just getting started watering this garden :) @@ -21,27 +22,5 @@ you'll find technical snippets & longer blogs, cooking recipes, old projects bot i do a lot of different stuff. when i'm not staring at screens for work and fun, i like to get outside in the pnw. -here's a list of my favorite disc golf courses around portland, or. -if you're in the area you might catch me out here! - -
    -
  • Dabney
  • -
  • Pier on an empty day
  • -
  • Ft. Stevens
  • -
  • Milo
  • -
  • Blue Lake on a still day
  • -
  • Stub Stewart
  • -
  • Horning's
  • -
  • Champoeg
  • -
  • Timber
  • -
  • McCormick
  • -
  • Rooster Rock
  • -
  • Pier on a busy day
  • -
  • Leveritch
  • -
  • ...
  • -
  • Blue Lake on a windy day
  • -
  • Trojan
  • -
  • Eco Park
  • -
- -i'm hoping to play Buxton soon! +i'm interested in cooking, DIY, sustainability, +gardening, disc golf, and a whole lot more. diff --git a/src/routes/(main)/gallery/+page.svelte b/src/routes/(main)/gallery/+page.svelte index a65df96..d7ef233 100644 --- a/src/routes/(main)/gallery/+page.svelte +++ b/src/routes/(main)/gallery/+page.svelte @@ -23,7 +23,7 @@ {#each items as trip} - {trip.alt} + {trip.alt} {trip.description} diff --git a/src/routes/(main)/gallery/[slug]/+page.js b/src/routes/(main)/gallery/[slug]/+page.js index e69de29..e68b476 100644 --- a/src/routes/(main)/gallery/[slug]/+page.js +++ b/src/routes/(main)/gallery/[slug]/+page.js @@ -0,0 +1,21 @@ +import { error, redirect } from '@sveltejs/kit'; +import { REPO_URL } from '$lib/siteConfig'; + +export const csr = true; // https://github.com/sveltejs/kit/pull/6446 +export async function load({ params, url, fetch, setHeaders }) { + const slug = params.slug; + + let res = null; + res = await fetch(`/api/gallery/${slug}.json`); + if (res.status > 400) { + throw error(res.status, await res.text()); + } + setHeaders({ + 'Cache-Control': 'public, max-age=60' + }); + return { + json: await res.json(), + slug, + REPO_URL + }; +} diff --git a/src/routes/(main)/gallery/[slug]/+page.svelte b/src/routes/(main)/gallery/[slug]/+page.svelte index e69de29..4f66430 100644 --- a/src/routes/(main)/gallery/[slug]/+page.svelte +++ b/src/routes/(main)/gallery/[slug]/+page.svelte @@ -0,0 +1,86 @@ + + + + {json.title} + + + + + + + + + + + + {#if json.image} + + + + {:else} + + + + {/if} + + + Back + + + +
+
+ +
+ +
+
diff --git a/src/routes/api/blog/[slug].json/+server.js b/src/routes/api/gallery/[slug].json/+server.js similarity index 100% rename from src/routes/api/blog/[slug].json/+server.js rename to src/routes/api/gallery/[slug].json/+server.js diff --git a/src/routes/api/getContent/[slug].json/+server.js b/src/routes/api/getContent/[slug].json/+server.js new file mode 100644 index 0000000..b071857 --- /dev/null +++ b/src/routes/api/getContent/[slug].json/+server.js @@ -0,0 +1,20 @@ +import { getContent } from '$lib/content'; +import { error } from '@sveltejs/kit'; + +/** + * @type {import('@sveltejs/kit').RequestHandler} + */ +export async function GET({ params }) { + const { slug } = params; + let data; + try { + data = await getContent(slug); + return new Response(JSON.stringify(data), { + headers: { + 'Cache-Control': `max-age=0, s-maxage=${60}` // 1 minute.. for now + } + }); + } catch (err) { + throw error(404, err.message); + } +} From c07def59951ff13e613da71a90a3f6d2d66a2aaf Mon Sep 17 00:00:00 2001 From: Tanner Heffner Date: Sat, 20 Jan 2024 00:42:24 -0800 Subject: [PATCH 4/4] update rss and sitemap --- src/routes/(main)/about/+page.svx | 1 - src/routes/rss.xml/+server.js | 4 ++-- src/routes/sitemap.xml/+server.js | 18 +++++++++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/routes/(main)/about/+page.svx b/src/routes/(main)/about/+page.svx index 629a2fd..b03b512 100644 --- a/src/routes/(main)/about/+page.svx +++ b/src/routes/(main)/about/+page.svx @@ -1,6 +1,5 @@ diff --git a/src/routes/rss.xml/+server.js b/src/routes/rss.xml/+server.js index 05aa0e6..aa9c818 100644 --- a/src/routes/rss.xml/+server.js +++ b/src/routes/rss.xml/+server.js @@ -2,7 +2,7 @@ import RSS from 'rss'; import { SITE_TITLE, SITE_URL } from '$lib/siteConfig'; import { remark } from 'remark'; import remarkHTML from 'remark-html'; -import { listContent } from '$lib/content'; +import { listContentFromIssues } from '$lib/content'; // Reference: https://github.com/sveltejs/kit/blob/master/examples/hn.svelte.dev/src/routes/%5Blist%5D/rss.js /** @type {import('@sveltejs/kit').RequestHandler} */ @@ -13,7 +13,7 @@ export async function GET({ fetch }) { feed_url: SITE_URL + '/rss.xml' }); - const allBlogs = await listContent(fetch); + const allBlogs = await listContentFromIssues('Published'); allBlogs.forEach((post) => { // extract HTML from markdown const htmlDescription = remark() diff --git a/src/routes/sitemap.xml/+server.js b/src/routes/sitemap.xml/+server.js index d289513..0ee4b56 100644 --- a/src/routes/sitemap.xml/+server.js +++ b/src/routes/sitemap.xml/+server.js @@ -1,12 +1,13 @@ import { SITE_URL } from '$lib/siteConfig'; -import { listContent } from '$lib/content'; +import { listContentFromIssues } from '$lib/content'; import { fetchMarkdownPosts } from '$lib/localContent' /** @type {import('@sveltejs/kit').RequestHandler} */ export async function GET({ fetch }) { - const posts = await listContent(fetch); + const posts = await listContentFromIssues('Published'); + const galleries = await listContentFromIssues('Gallery'); const projects = await fetchMarkdownPosts() - const pages = [`about`, 'resume', 'blogroll', 'christmas', 'blog', 'work']; + const pages = ['about', 'resume', 'blogroll', 'christmas', 'blog', 'work']; const body = sitemap(posts, projects, pages); return new Response(body, { @@ -47,6 +48,17 @@ const sitemap = (posts, projects, pages) => ` ` ).join('')} + ${galleries + .map((gallery) => + post.isPrivate + ? null + : ` + + ${SITE_URL}/gallery/${gallery.slug} + ${gallery.ghMetadata.updated_at ? gallery.ghMetadata.updated_at.substring(0, 10) : gallery.ghMetadata.created_at.substring(0, 10)} + + ` + ).join('')} ${projects .map((project) => project.isPrivate