[READ-ONLY] Mirror of https://github.com/flo-bit/blog-template. minimalistic astro blog template flo-bit.dev/blog-template/
astro blog template
0

Configure Feed

Select the types of activity you want to include in your feed.

refactor

Florian (Mar 23, 2025, 6:47 PM +0100) b9801c2a 63431dd5

+563 -610
+1 -1
.github/workflows/deploy.yml
··· 5 5 branches: [main] 6 6 workflow_dispatch: 7 7 schedule: 8 - - cron: '0 */12 * * *' # every 12 hours 8 + - cron: "0 */12 * * *" # every 12 hours 9 9 10 10 permissions: 11 11 contents: read
-146
.pages.yml
··· 1 - media: 2 - input: src/assets 3 - output: ../../assets 4 - 5 - content: 6 - - name: config 7 - label: Website configuration 8 - path: src/config.json 9 - type: file 10 - fields: 11 - - name: SITE_TITLE 12 - label: Title 13 - type: string 14 - - name: SITE_DESCRIPTION 15 - label: Description 16 - type: string 17 - - name: SITE_FAVICON 18 - label: Favicon 19 - type: string 20 - - name: NAME 21 - label: Your name 22 - type: string 23 - - name: POSTS_PER_PAGE 24 - label: Posts per page 25 - type: number 26 - default: 5 27 - - name: MANUAL_DARK_MODE 28 - label: Show dark mode toggle 29 - type: boolean 30 - default: true 31 - - name: SEARCH_ENABLED 32 - label: Enable search 33 - type: boolean 34 - default: true 35 - - name: BASE_COLOR 36 - label: Base color 37 - type: select 38 - options: 39 - values: [slate, stone, gray, neutral, zinc] 40 - - name: ACCENT_COLOR 41 - label: Accent color 42 - type: select 43 - options: 44 - values: 45 - [ 46 - red, 47 - orange, 48 - amber, 49 - yellow, 50 - lime, 51 - green, 52 - emerald, 53 - teal, 54 - cyan, 55 - sky, 56 - blue, 57 - indigo, 58 - violet, 59 - purple, 60 - fuchsia, 61 - pink, 62 - rose, 63 - ] 64 - - name: SOCIAL_LINKS 65 - label: Social links 66 - type: object 67 - fields: 68 - - name: FACEBOOK_URL 69 - label: Facebook URL 70 - type: string 71 - - name: TWITTER_URL 72 - label: Twitter URL 73 - type: string 74 - - name: INSTAGRAM_URL 75 - label: Instagram URL 76 - type: string 77 - - name: LINKEDIN_URL 78 - label: LinkedIn URL 79 - type: string 80 - - name: GITHUB_URL 81 - label: GitHub URL 82 - type: string 83 - - name: EMAIL 84 - label: Email 85 - type: string 86 - - name: YOUTUBE_URL 87 - label: YouTube URL 88 - type: string 89 - - name: SUBSTACK_URL 90 - label: Substack URL 91 - type: string 92 - 93 - - name: about 94 - label: About page 95 - path: src/content/info/about.md 96 - type: file 97 - fields: 98 - - name: body 99 - label: Body 100 - type: rich-text 101 - 102 - - name: description 103 - label: Description 104 - path: src/content/info/description.md 105 - type: file 106 - fields: 107 - - name: body 108 - label: Body 109 - type: rich-text 110 - 111 - - name: posts 112 - label: Posts 113 - path: src/content/blog 114 - type: collection 115 - filename: "{primary}.md" 116 - view: 117 - fields: [title, published, description, pubDate] 118 - primary: title 119 - sort: [date, title] 120 - default: 121 - search: "" 122 - sort: title 123 - order: desc 124 - fields: 125 - - name: title 126 - label: Title 127 - type: string 128 - - name: description 129 - label: Description 130 - type: string 131 - - name: published 132 - label: Published 133 - type: boolean 134 - default: false 135 - - name: pubDate 136 - label: Creation date 137 - type: date 138 - options: 139 - format: MMM dd yyyy 140 - min: 2023-12-24 141 - - name: heroImage 142 - label: Hero Image 143 - type: image 144 - - name: body 145 - label: Body 146 - type: rich-text
+2 -2
README.md
··· 36 36 37 37 1. Fork this repository by clicking on "Use template" (note: this repository per default uses github actions which are only free for public repositories). 38 38 39 - 2. In your repository settings, set up github pages to deploy using github actions (*SETTINGS* -> *PAGES* -> *SOURCE*: **Github Actions**) 39 + 2. In your repository settings, set up github pages to deploy using github actions (_SETTINGS_ -> _PAGES_ -> _SOURCE_: **Github Actions**) 40 40 41 - 3. Set up your blog info in `src/config.json` (most importantly change `SITE` to your deployment url, e.g. for github pages `https://<your-github-username>.github.io/` and `BASE` to your base path, e.g. for github pages `/<your-repo-name>`) 41 + 3. Set up your blog info in `src/config.ts` (most importantly change `SITE` to your deployment url, e.g. for github pages `https://<your-github-username>.github.io/` and `BASE` to your base path, e.g. for github pages `/<your-repo-name>`) 42 42 43 43 4. Your blog should be live in about 1 minute at `https://<your-github-username>.github.io/<your-repo-name>` 44 44
+27 -24
astro.config.ts
··· 1 1 // @ts-check 2 2 import { defineConfig } from "astro/config"; 3 3 import { resolve } from "path"; 4 - import remarkMath from "remark-math" 5 - import rehypeMathjax from "rehype-mathjax" 4 + import remarkMath from "remark-math"; 5 + import rehypeMathjax from "rehype-mathjax"; 6 6 7 7 import mdx from "@astrojs/mdx"; 8 8 import sitemap from "@astrojs/sitemap"; ··· 10 10 import svelte from "@astrojs/svelte"; 11 11 import { pagefind } from "vite-plugin-pagefind"; 12 12 13 - import { BASE, SITE } from "./src/config.json"; 13 + import { BASE, SITE } from "./src/config.ts"; 14 14 15 - import customEmbeds from 'astro-custom-embeds'; 15 + import customEmbeds from "astro-custom-embeds"; 16 16 17 - import { transformerMetaHighlight, transformerNotationHighlight } from '@shikijs/transformers' 17 + import { 18 + transformerMetaHighlight, 19 + transformerNotationHighlight, 20 + } from "@shikijs/transformers"; 18 21 19 - import LinkCardEmbed from './src/embeds/link-card/embed' 20 - import YoutubeEmbed from './src/embeds/youtube/embed' 22 + import LinkCardEmbed from "./src/embeds/link-card/embed"; 23 + import YoutubeEmbed from "./src/embeds/youtube/embed"; 21 24 import ExcalidrawEmbed from "./src/embeds/excalidraw/embed"; 22 25 23 26 // https://astro.build/config ··· 40 43 rollupOptions: { 41 44 external: [BASE + "/pagefind/pagefind.js"], 42 45 }, 43 - } 46 + }, 44 47 }, 45 48 46 - integrations: [customEmbeds({ 47 - embeds: [ 48 - ExcalidrawEmbed, 49 - YoutubeEmbed, 50 - LinkCardEmbed, 51 - ] 52 - }), mdx(), sitemap(), tailwind(), svelte()], 49 + integrations: [ 50 + customEmbeds({ 51 + embeds: [ExcalidrawEmbed, YoutubeEmbed, LinkCardEmbed], 52 + }), 53 + mdx(), 54 + sitemap(), 55 + tailwind(), 56 + svelte(), 57 + ], 53 58 54 59 markdown: { 55 60 shikiConfig: { ··· 62 67 dark: "github-dark", 63 68 }, 64 69 defaultColor: false, 65 - transformers: [transformerMetaHighlight(), transformerNotationHighlight()], 66 - wrap: true 70 + transformers: [ 71 + transformerMetaHighlight(), 72 + transformerNotationHighlight(), 73 + ], 74 + wrap: true, 67 75 }, 68 76 69 - remarkPlugins: [ 70 - remarkMath 71 - ], 72 - rehypePlugins: [ 73 - rehypeMathjax 74 - ] 77 + remarkPlugins: [remarkMath], 78 + rehypePlugins: [rehypeMathjax], 75 79 }, 76 80 77 81 prefetch: { ··· 80 84 site: SITE, 81 85 base: BASE, 82 86 output: "static", 83 - 84 87 });
+21
src/colors.ts
··· 25 25 pink: "prose-pink", 26 26 rose: "prose-rose", 27 27 }; 28 + 29 + export type AccentColor = 30 + | "red" 31 + | "orange" 32 + | "amber" 33 + | "yellow" 34 + | "lime" 35 + | "green" 36 + | "emerald" 37 + | "teal" 38 + | "cyan" 39 + | "sky" 40 + | "blue" 41 + | "indigo" 42 + | "violet" 43 + | "purple" 44 + | "fuchsia" 45 + | "pink" 46 + | "rose"; 47 + 48 + export type BaseColor = "slate" | "stone" | "gray" | "neutral" | "zinc";
+1 -1
src/components/BaseHead.astro
··· 1 1 --- 2 - import { BASE, SITE_FAVICON } from "../config.json"; 2 + import { BASE, SITE_FAVICON } from "../config.ts"; 3 3 4 4 interface Props { 5 5 title: string;
+1 -1
src/components/BigBlogEntry.astro
··· 1 1 --- 2 - import { BASE, SHOW_IMAGES } from "../config.json"; 2 + import { BASE, SHOW_IMAGES } from "../config.ts"; 3 3 import FormattedDate from "./FormattedDate.astro"; 4 4 import Tag from "./Tag.astro"; 5 5 import HeroImage from "./HeroImage.astro";
+1 -1
src/components/BlogEntry.astro
··· 1 1 --- 2 - import { BASE, SHOW_IMAGES } from "../config.json"; 2 + import { BASE, SHOW_IMAGES } from "../config.ts"; 3 3 import FormattedDate from "./FormattedDate.astro"; 4 4 import Tag from "./Tag.astro"; 5 5 import HeroImage from "./HeroImage.astro";
+3 -3
src/components/Footer.astro
··· 1 1 --- 2 - import { BASE, NAME, SOCIAL_LINKS } from "../config.json"; 2 + import { BASE, LICENSE, NAME, SOCIAL_LINKS } from "../config.ts"; 3 3 4 4 const today = new Date(); 5 5 --- ··· 231 231 } 232 232 </div> 233 233 <p 234 - class="mt-8 text-center text-sm/6 text-base-600 dark:text-base-400 md:order-1 md:mt-0" 234 + class="mt-8 text-center text-sm/6 text-base-600 dark:text-base-400 md:order-1 md:mt-0 text-pretty" 235 235 > 236 236 &copy; {today.getFullYear()} 237 - {NAME}. All rights reserved. 237 + {NAME}. {LICENSE} 238 238 </p> 239 239 </div> 240 240 </footer>
+5 -3
src/components/Header.astro
··· 5 5 SEARCH_ENABLED, 6 6 SITE_FAVICON, 7 7 SITE_NAME, 8 - } from "../config.json"; 8 + SITE_TITLE, 9 + SHOW_FAVICON_IN_HEADER, 10 + } from "../config.ts"; 9 11 import ThemeToggle from "./ThemeToggle.astro"; 10 12 import HeaderLink from "./HeaderLink.astro"; 11 13 import CodeCopyButton from "./CodeCopyButton.astro"; ··· 26 28 <span class="sr-only">go to home</span> 27 29 28 30 <div class="text-3xl flex items-center gap-x-2"> 29 - {SITE_FAVICON} 31 + {SHOW_FAVICON_IN_HEADER ? SITE_FAVICON : null} 30 32 <span 31 33 class="hidden md:block font-bold text-base text-base-950 dark:text-base-50" 32 - >{SITE_NAME}</span 34 + >{SITE_NAME || SITE_TITLE}</span 33 35 > 34 36 </div> 35 37 </a>
+1 -1
src/components/Pagination.astro
··· 1 1 --- 2 - import { BASE } from "../config.json"; 2 + import { BASE } from "../config.ts"; 3 3 import PaginationNumber from "./PaginationNumber.astro"; 4 4 5 5 type Props = {
+1 -1
src/components/PaginationNumber.astro
··· 1 1 --- 2 - import { BASE } from "../config.json"; 2 + import { BASE } from "../config.ts"; 3 3 4 4 const { index, active, tag } = Astro.props; 5 5 ---
+1 -1
src/components/Tag.astro
··· 1 1 --- 2 - import { BASE } from "../config.json"; 2 + import { BASE } from "../config.ts"; 3 3 const { tag } = Astro.props; 4 4 --- 5 5
+1 -2
src/components/bluesky/BlueskyLikes.astro
··· 1 1 --- 2 - import { BLUESKY_IDENTIFIER } from "../../config.json"; 2 + import { BLUESKY_IDENTIFIER, SITE } from "../../config.ts"; 3 3 import { getCollection } from "astro:content"; 4 4 import { atUriToPostUri, getComments, getLikes } from "./utils"; 5 5 import Likes from "./Likes.svelte"; 6 6 import Comments from "./Comments.svelte"; 7 - import { SITE } from "../../config.json"; 8 7 9 8 let posts = await getCollection("posts"); 10 9
+21 -21
src/components/bluesky/relative-time/action.ts
··· 1 - import type { Callback } from './render' 2 - import { register, unregister } from './state' 1 + import type { Callback } from "./render"; 2 + import { register, unregister } from "./state"; 3 3 4 4 export interface Options { 5 - date: Date | number 6 - locale?: string 7 - live?: boolean 5 + date: Date | number; 6 + locale?: string; 7 + live?: boolean; 8 8 } 9 9 10 10 export function relativeTime(node: HTMLElement, options: Options) { 11 - const callback: Callback = ({ text }) => (node.textContent = text) 11 + const callback: Callback = ({ text }) => (node.textContent = text); 12 12 13 - function init(options: Options) { 14 - const date = options.date 15 - const locale = options.locale || navigator.language 16 - const live = (options.live = true) 13 + function init(options: Options) { 14 + const date = options.date; 15 + const locale = options.locale || navigator.language; 16 + const live = (options.live = true); 17 17 18 - register(node, date, locale, live, callback) 19 - } 18 + register(node, date, locale, live, callback); 19 + } 20 20 21 - init(options) 21 + init(options); 22 22 23 - return { 24 - update(options: Options) { 25 - init(options) 26 - }, 27 - destroy() { 28 - unregister(node) 29 - }, 30 - } 23 + return { 24 + update(options: Options) { 25 + init(options); 26 + }, 27 + destroy() { 28 + unregister(node); 29 + }, 30 + }; 31 31 }
+10 -7
src/components/bluesky/relative-time/formatter.ts
··· 1 1 // keep a cache of formatter per locale, to avoid re-creating them (GC) 2 - const formatters = new Map<string, Intl.RelativeTimeFormat>() 2 + const formatters = new Map<string, Intl.RelativeTimeFormat>(); 3 3 4 4 // get the Intl.RelativeTimeFormat formatter for the given locale 5 5 export function getFormatter(locale: string) { 6 - if (formatters.has(locale)) { 7 - return formatters.get(locale)! 8 - } 9 - const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'always', style: 'narrow' }) 10 - formatters.set(locale, formatter) 11 - return formatter 6 + if (formatters.has(locale)) { 7 + return formatters.get(locale)!; 8 + } 9 + const formatter = new Intl.RelativeTimeFormat(locale, { 10 + numeric: "always", 11 + style: "narrow", 12 + }); 13 + formatters.set(locale, formatter); 14 + return formatter; 12 15 }
+4 -4
src/components/bluesky/relative-time/index.ts
··· 1 - export * from './action' 2 - export type { Callback } from './render' 3 - export { register, unregister } from './state' 4 - export { default as default } from './RelativeTime.svelte' 1 + export * from "./action"; 2 + export type { Callback } from "./render"; 3 + export { register, unregister } from "./state"; 4 + export { default as default } from "./RelativeTime.svelte";
+56 -38
src/components/bluesky/relative-time/render.ts
··· 1 - export type Callback = (result: { seconds: number; count: number; units: Intl.RelativeTimeFormatUnit; text: string }) => void 1 + export type Callback = (result: { 2 + seconds: number; 3 + count: number; 4 + units: Intl.RelativeTimeFormatUnit; 5 + text: string; 6 + }) => void; 2 7 3 8 export interface RenderState { 4 - date: Date | number 5 - callback: Callback 6 - formatter: Intl.RelativeTimeFormat 9 + date: Date | number; 10 + callback: Callback; 11 + formatter: Intl.RelativeTimeFormat; 7 12 } 8 13 9 14 // Array reprsenting one minute, hour, day, week, month, etc in seconds 10 - const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity] 15 + const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]; 11 16 12 17 // Array equivalent to the above but in the string representation of the units 13 - const formatUnits: Intl.RelativeTimeFormatUnit[] = ['seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years'] 18 + const formatUnits: Intl.RelativeTimeFormatUnit[] = [ 19 + "seconds", 20 + "minutes", 21 + "hours", 22 + "days", 23 + "weeks", 24 + "months", 25 + "years", 26 + ]; 14 27 15 28 // function to render relative time into 16 29 export function render(state: RenderState, now: number) { 17 - const { date, callback, formatter } = state 30 + const { date, callback, formatter } = state; 18 31 19 - // Allow dates or times to be passed 20 - const timeMs = typeof date === 'number' ? date : date.getTime() 32 + // Allow dates or times to be passed 33 + const timeMs = typeof date === "number" ? date : date.getTime(); 21 34 22 - // Get the amount of seconds between the given date and now 23 - const delta = timeMs - now 24 - const seconds = Math.round(delta / 1000) 35 + // Get the amount of seconds between the given date and now 36 + const delta = timeMs - now; 37 + const seconds = Math.round(delta / 1000); 25 38 26 - // Grab the ideal cutoff unit 27 - const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(seconds)) 39 + // Grab the ideal cutoff unit 40 + const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(seconds)); 28 41 29 - // units 30 - const units = formatUnits[unitIndex] 42 + // units 43 + const units = formatUnits[unitIndex]; 31 44 32 - // Get the divisor to divide from the seconds. E.g. if our unit is 'day' our divisor 33 - // is one day in seconds, so we can divide our seconds by this to get the # of days 34 - const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1 45 + // Get the divisor to divide from the seconds. E.g. if our unit is 'day' our divisor 46 + // is one day in seconds, so we can divide our seconds by this to get the # of days 47 + const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1; 35 48 36 - // count of units 37 - const count = Math.round(seconds / divisor) 49 + // count of units 50 + const count = Math.round(seconds / divisor); 38 51 39 - // Intl.RelativeTimeFormat do its magic 40 - callback({ seconds: seconds, count, units, text: formatter.format(count, units) }) 52 + // Intl.RelativeTimeFormat do its magic 53 + callback({ 54 + seconds: seconds, 55 + count, 56 + units, 57 + text: formatter.format(count, units), 58 + }); 41 59 42 - // calculate time to next update, taking account rounding (e.g. it goes from 2 minutes to 1 minute at 90 seconds) 43 - // and also the changeover from units (59 seconds shouldn't show as 1 minute, so at 61 seconds we schedule the next 44 - // update for 1 second time) 60 + // calculate time to next update, taking account rounding (e.g. it goes from 2 minutes to 1 minute at 90 seconds) 61 + // and also the changeover from units (59 seconds shouldn't show as 1 minute, so at 61 seconds we schedule the next 62 + // update for 1 second time) 45 63 46 - const divisorMs = divisor * 1000 64 + const divisorMs = divisor * 1000; 47 65 48 - let updateIn 66 + let updateIn; 49 67 50 - if (unitIndex) { 51 - updateIn = divisorMs / 2 - (Math.abs(delta) % divisorMs) 52 - if (updateIn < 0) { 53 - updateIn += divisorMs 54 - } 55 - } else { 56 - updateIn = divisorMs - (Math.abs(delta) % divisorMs) 57 - } 68 + if (unitIndex) { 69 + updateIn = divisorMs / 2 - (Math.abs(delta) % divisorMs); 70 + if (updateIn < 0) { 71 + updateIn += divisorMs; 72 + } 73 + } else { 74 + updateIn = divisorMs - (Math.abs(delta) % divisorMs); 75 + } 58 76 59 - const updateAt = now + updateIn 77 + const updateAt = now + updateIn; 60 78 61 - return updateAt 79 + return updateAt; 62 80 }
+43 -37
src/components/bluesky/relative-time/state.ts
··· 1 - import { getFormatter } from './formatter' 2 - import { render } from './render' 3 - import type { Callback, RenderState } from './render' 1 + import { getFormatter } from "./formatter"; 2 + import { render } from "./render"; 3 + import type { Callback, RenderState } from "./render"; 4 4 5 5 interface UpdateState extends RenderState { 6 - update: number 6 + update: number; 7 7 } 8 8 9 9 // keep track of each instance 10 - const instances = new Map<Object, UpdateState>() 10 + const instances = new Map<Object, UpdateState>(); 11 11 12 12 // we use a single timer for efficiency and to keep updates in sync 13 - let updateInterval: number | NodeJS.Timeout 13 + let updateInterval: number | NodeJS.Timeout; 14 14 15 15 // register or update instance 16 - export function register(instance: Object, date: Date | number, locale: string, live: boolean, callback: Callback) { 17 - // get the formatter for the given locale, we do this here so we don't keep having to look it up on each tick 18 - const formatter = getFormatter(locale) 16 + export function register( 17 + instance: Object, 18 + date: Date | number, 19 + locale: string, 20 + live: boolean, 21 + callback: Callback, 22 + ) { 23 + // get the formatter for the given locale, we do this here so we don't keep having to look it up on each tick 24 + const formatter = getFormatter(locale); 19 25 20 - // create state to render 21 - const state = { date, callback, formatter } 26 + // create state to render 27 + const state = { date, callback, formatter }; 22 28 23 - // initial render is immediate, so works for SSR 24 - const update = render(state, Date.now()) 29 + // initial render is immediate, so works for SSR 30 + const update = render(state, Date.now()); 25 31 26 - // if it's to update live, we keep a track and schedule the next update 27 - if (live) { 28 - instances.set(instance, { ...state, update }) 29 - } else { 30 - instances.delete(instance) 31 - } 32 + // if it's to update live, we keep a track and schedule the next update 33 + if (live) { 34 + instances.set(instance, { ...state, update }); 35 + } else { 36 + instances.delete(instance); 37 + } 32 38 33 - // start the clock ticking if there are any live instances 34 - if (instances.size) { 35 - updateInterval = 36 - updateInterval || 37 - setInterval(() => { 38 - const now = Date.now() 39 - for (const state of instances.values()) { 40 - if (state.update <= now) { 41 - state.update = render(state, now) 42 - } 43 - } 44 - }, 1000) 45 - } 39 + // start the clock ticking if there are any live instances 40 + if (instances.size) { 41 + updateInterval = 42 + updateInterval || 43 + setInterval(() => { 44 + const now = Date.now(); 45 + for (const state of instances.values()) { 46 + if (state.update <= now) { 47 + state.update = render(state, now); 48 + } 49 + } 50 + }, 1000); 51 + } 46 52 } 47 53 48 54 export function unregister(instance: Object) { 49 - instances.delete(instance) 50 - if (instances.size === 0) { 51 - clearInterval(updateInterval) 52 - updateInterval = 0 53 - } 55 + instances.delete(instance); 56 + if (instances.size === 0) { 57 + clearInterval(updateInterval); 58 + updateInterval = 0; 59 + } 54 60 }
+15 -15
src/components/bluesky/types.ts
··· 1 1 import { 2 - AppBskyFeedPost, 3 - AppBskyFeedDefs, 4 - AppBskyEmbedRecord, 5 - AppBskyEmbedExternal, 6 - AppBskyEmbedImages, 7 - AppBskyEmbedRecordWithMedia, 8 - AppBskyEmbedVideo, 9 - } from '@atproto/api'; 2 + AppBskyFeedPost, 3 + AppBskyFeedDefs, 4 + AppBskyEmbedRecord, 5 + AppBskyEmbedExternal, 6 + AppBskyEmbedImages, 7 + AppBskyEmbedRecordWithMedia, 8 + AppBskyEmbedVideo, 9 + } from "@atproto/api"; 10 10 11 11 export interface Post extends AppBskyFeedDefs.PostView { 12 - record: AppBskyFeedPost.Record; 12 + record: AppBskyFeedPost.Record; 13 13 } 14 14 15 15 export type EmbedView = 16 - | AppBskyEmbedRecord.View 17 - | AppBskyEmbedImages.View 18 - | AppBskyEmbedVideo.View 19 - | AppBskyEmbedExternal.View 20 - | AppBskyEmbedRecordWithMedia.View 21 - | { [k: string]: unknown; $type: string }; 16 + | AppBskyEmbedRecord.View 17 + | AppBskyEmbedImages.View 18 + | AppBskyEmbedVideo.View 19 + | AppBskyEmbedExternal.View 20 + | AppBskyEmbedRecordWithMedia.View 21 + | { [k: string]: unknown; $type: string };
+169 -166
src/components/bluesky/utils.ts
··· 1 1 import { 2 - AppBskyEmbedExternal, 3 - AppBskyEmbedImages, 4 - AppBskyEmbedRecord, 5 - AppBskyEmbedRecordWithMedia, 6 - AppBskyEmbedVideo, 7 - AppBskyFeedDefs, 8 - AtpAgent, 9 - RichText, 10 - } from '@atproto/api'; 11 - import type { Post } from './types'; 2 + AppBskyEmbedExternal, 3 + AppBskyEmbedImages, 4 + AppBskyEmbedRecord, 5 + AppBskyEmbedRecordWithMedia, 6 + AppBskyEmbedVideo, 7 + AppBskyFeedDefs, 8 + AtpAgent, 9 + RichText, 10 + } from "@atproto/api"; 11 + import type { Post } from "./types"; 12 12 13 13 const escapeMap: Record<string, string> = { 14 - '&': '&amp;', 15 - '<': '&lt;', 16 - '>': '&gt;', 17 - '"': '&quot;', 18 - "'": '&#39;', 14 + "&": "&amp;", 15 + "<": "&lt;", 16 + ">": "&gt;", 17 + '"': "&quot;", 18 + "'": "&#39;", 19 19 }; 20 20 21 21 export const escapeHTML = (str?: string) => 22 - str?.replace(/[&<>"']/g, (match) => escapeMap[match] || match) ?? ''; 22 + str?.replace(/[&<>"']/g, (match) => escapeMap[match] || match) ?? ""; 23 23 24 24 export function renderPostAsHtml(post?: AppBskyFeedDefs.PostView | Post) { 25 - if (!post) { 26 - return ''; 27 - } 28 - const rt = new RichText(post.record as any); 29 - let html = ''; 25 + if (!post) { 26 + return ""; 27 + } 28 + const rt = new RichText(post.record as any); 29 + let html = ""; 30 30 31 - const preLink = `<a target="_blank" rel="noopener noreferrer nofollow" class="text-accent-500 hover:text-accent-600 dark:hover:text-accent-400"`; 32 - for (const segment of rt.segments()) { 33 - if (segment.isLink()) { 34 - html += `${preLink} href="${escapeHTML( 35 - segment.link?.uri 36 - )}">${escapeHTML(segment.text)}</a>`; 37 - } else if (segment.isMention()) { 38 - html += `${preLink} href="https://bsky.app/profile/${escapeHTML( 39 - segment.mention?.did 40 - )}">${escapeHTML(segment.text)}</a>`; 41 - } else if (segment.isTag()) { 42 - html += `${preLink} href="https://bsky.app/hastag/${escapeHTML( 43 - segment.tag?.tag 44 - )}">#${escapeHTML(segment.tag?.tag)}</a>`; 45 - } else { 46 - html += escapeHTML(segment.text); 47 - } 48 - } 49 - // replace new lines with <br> 50 - html = html.replace(/\n/g, '<br>'); 51 - return html; 31 + const preLink = `<a target="_blank" rel="noopener noreferrer nofollow" class="text-accent-500 hover:text-accent-600 dark:hover:text-accent-400"`; 32 + for (const segment of rt.segments()) { 33 + if (segment.isLink()) { 34 + html += `${preLink} href="${escapeHTML( 35 + segment.link?.uri, 36 + )}">${escapeHTML(segment.text)}</a>`; 37 + } else if (segment.isMention()) { 38 + html += `${preLink} href="https://bsky.app/profile/${escapeHTML( 39 + segment.mention?.did, 40 + )}">${escapeHTML(segment.text)}</a>`; 41 + } else if (segment.isTag()) { 42 + html += `${preLink} href="https://bsky.app/hastag/${escapeHTML( 43 + segment.tag?.tag, 44 + )}">#${escapeHTML(segment.tag?.tag)}</a>`; 45 + } else { 46 + html += escapeHTML(segment.text); 47 + } 48 + } 49 + // replace new lines with <br> 50 + html = html.replace(/\n/g, "<br>"); 51 + return html; 52 52 } 53 53 54 54 export function viewRecordToPostView( 55 - viewRecord: AppBskyEmbedRecord.ViewRecord 55 + viewRecord: AppBskyEmbedRecord.ViewRecord, 56 56 ): AppBskyFeedDefs.PostView { 57 - const { value, embeds, ...rest } = viewRecord; 58 - return { 59 - ...rest, 60 - $type: 'app.bsky.feed.defs#postView', 61 - record: value, 62 - embed: embeds?.[0], 63 - } as AppBskyFeedDefs.PostView; 57 + const { value, embeds, ...rest } = viewRecord; 58 + return { 59 + ...rest, 60 + $type: "app.bsky.feed.defs#postView", 61 + record: value, 62 + embed: embeds?.[0], 63 + } as AppBskyFeedDefs.PostView; 64 64 } 65 65 66 66 export function viewRecordToEmbed( 67 - viewRecord: AppBskyEmbedRecord.ViewRecord, 68 - allowNestedQuotes = false 67 + viewRecord: AppBskyEmbedRecord.ViewRecord, 68 + allowNestedQuotes = false, 69 69 ) { 70 - const { embed } = viewRecordToPostView(viewRecord); 70 + const { embed } = viewRecordToPostView(viewRecord); 71 71 72 - if (allowNestedQuotes) { 73 - return embed; 74 - } else { 75 - if ( 76 - AppBskyEmbedImages.isView(embed) || 77 - AppBskyEmbedExternal.isView(embed) || 78 - AppBskyEmbedVideo.isView(embed) 79 - ) { 80 - return embed; 81 - } else if ( 82 - AppBskyEmbedRecordWithMedia.isView(embed) && 83 - (AppBskyEmbedImages.isView(embed.media) || 84 - AppBskyEmbedExternal.isView(embed.media) || 85 - AppBskyEmbedVideo.isView(embed.media)) 86 - ) { 87 - return embed.media; 88 - } 89 - } 90 - return undefined; 72 + if (allowNestedQuotes) { 73 + return embed; 74 + } else { 75 + if ( 76 + AppBskyEmbedImages.isView(embed) || 77 + AppBskyEmbedExternal.isView(embed) || 78 + AppBskyEmbedVideo.isView(embed) 79 + ) { 80 + return embed; 81 + } else if ( 82 + AppBskyEmbedRecordWithMedia.isView(embed) && 83 + (AppBskyEmbedImages.isView(embed.media) || 84 + AppBskyEmbedExternal.isView(embed.media) || 85 + AppBskyEmbedVideo.isView(embed.media)) 86 + ) { 87 + return embed.media; 88 + } 89 + } 90 + return undefined; 91 91 } 92 92 93 93 const agent = new AtpAgent({ 94 - service: 'https://public.api.bsky.app', 94 + service: "https://public.api.bsky.app", 95 95 }); 96 96 97 97 export async function resolvePost( 98 - postUrl: string | Post | AppBskyFeedDefs.PostView 98 + postUrl: string | Post | AppBskyFeedDefs.PostView, 99 99 ): Promise<Post | undefined> { 100 - let atUri; 100 + let atUri; 101 101 102 - if (typeof postUrl === 'object') { 103 - return postUrl as Post; 104 - } 102 + if (typeof postUrl === "object") { 103 + return postUrl as Post; 104 + } 105 105 106 - if (postUrl.startsWith('at:')) { 107 - atUri = postUrl; 108 - } else { 109 - if (!postUrl.startsWith('https://bsky.app/')) { 110 - return undefined; 111 - } 112 - const urlParts = new URL(postUrl).pathname.split('/'); 113 - let did = urlParts[2]!; 114 - const postId = urlParts[4]!; 115 - if (!did || !postId) { 116 - return undefined; 117 - } 118 - if (!did.startsWith('did:')) { 119 - try { 120 - const handleResolution = await agent.resolveHandle({ handle: did }); 121 - if (!handleResolution.data.did) { 122 - return undefined; 123 - } 124 - did = handleResolution.data.did; 125 - } catch (e: any) { 126 - console.error( 127 - `[error] astro-embed` + '\n ' + (e?.message ?? e) 128 - ); 129 - return undefined; 130 - } 131 - } 106 + if (postUrl.startsWith("at:")) { 107 + atUri = postUrl; 108 + } else { 109 + if (!postUrl.startsWith("https://bsky.app/")) { 110 + return undefined; 111 + } 112 + const urlParts = new URL(postUrl).pathname.split("/"); 113 + let did = urlParts[2]!; 114 + const postId = urlParts[4]!; 115 + if (!did || !postId) { 116 + return undefined; 117 + } 118 + if (!did.startsWith("did:")) { 119 + try { 120 + const handleResolution = await agent.resolveHandle({ handle: did }); 121 + if (!handleResolution.data.did) { 122 + return undefined; 123 + } 124 + did = handleResolution.data.did; 125 + } catch (e: any) { 126 + console.error( 127 + `[error] astro-embed` + "\n " + (e?.message ?? e), 128 + ); 129 + return undefined; 130 + } 131 + } 132 132 133 - atUri = `at://${did}/app.bsky.feed.post/${postId}`; 134 - } 133 + atUri = `at://${did}/app.bsky.feed.post/${postId}`; 134 + } 135 135 136 - try { 137 - const hydratedPost = await agent.getPosts({ uris: [atUri] }); 138 - return hydratedPost.data.posts[0] as unknown as Post; 139 - } catch (e: any) { 140 - console.error(`[error] astro-embed` + '\n ' + (e?.message ?? e)); 141 - return undefined; 142 - } 136 + try { 137 + const hydratedPost = await agent.getPosts({ uris: [atUri] }); 138 + return hydratedPost.data.posts[0] as unknown as Post; 139 + } catch (e: any) { 140 + console.error(`[error] astro-embed` + "\n " + (e?.message ?? e)); 141 + return undefined; 142 + } 143 143 } 144 144 145 145 export function atUriToPostUri(atUri: string) { 146 - const [, , did, , postId] = atUri.split('/'); 147 - return `https://bsky.app/profile/${did}/post/${postId}`; 146 + const [, , did, , postId] = atUri.split("/"); 147 + return `https://bsky.app/profile/${did}/post/${postId}`; 148 148 } 149 149 150 150 export function atUriToStarterPackUri(atUri: string) { 151 - const [, , did, , packId] = atUri.split('/'); 152 - return `https://bsky.app/starter-pack/${did}/${packId}`; 151 + const [, , did, , packId] = atUri.split("/"); 152 + return `https://bsky.app/starter-pack/${did}/${packId}`; 153 153 } 154 154 155 155 export function atUriToListUri(atUri: string) { 156 - const [, , did, , listId] = atUri.split('/'); 157 - return `https://bsky.app/profile/${did}/lists/${listId}`; 156 + const [, , did, , listId] = atUri.split("/"); 157 + return `https://bsky.app/profile/${did}/lists/${listId}`; 158 158 } 159 159 160 160 export function starterPackOgImage(uri: string) { 161 - const [, , did, , packId] = uri.split('/'); 162 - return `https://ogcard.cdn.bsky.app/start/${did}/${packId}`; 161 + const [, , did, , packId] = uri.split("/"); 162 + return `https://ogcard.cdn.bsky.app/start/${did}/${packId}`; 163 163 } 164 164 165 165 export async function getLikes(uri: string, getMore = false) { 166 - let allLikes = []; 167 - 168 - let likesData, cursor; 169 - do { 170 - let getLikesURL = `https://public.api.bsky.app/xrpc/app.bsky.feed.getLikes?uri=${uri}&limit=100`; 171 - 172 - if (cursor) { 173 - getLikesURL += `&cursor=${cursor}`; 174 - } 175 - const likes = await fetch(getLikesURL); 176 - 177 - likesData = await likes.json(); 178 - cursor = likesData.cursor; 179 - allLikes.push(...likesData.likes); 180 - } while (cursor && getMore); 181 - 182 - return allLikes; 166 + let allLikes = []; 167 + 168 + let likesData, cursor; 169 + do { 170 + let getLikesURL = `https://public.api.bsky.app/xrpc/app.bsky.feed.getLikes?uri=${uri}&limit=100`; 171 + 172 + if (cursor) { 173 + getLikesURL += `&cursor=${cursor}`; 174 + } 175 + const likes = await fetch(getLikesURL); 176 + 177 + likesData = await likes.json(); 178 + cursor = likesData.cursor; 179 + allLikes.push(...likesData.likes); 180 + } while (cursor && getMore); 181 + 182 + return allLikes; 183 183 } 184 184 185 185 export async function getPost(uri: string) { 186 - try { 187 - const hydratedPost = await agent.getPosts({ uris: [uri] }); 188 - return hydratedPost.data.posts[0] as unknown as Post; 189 - } catch (e: any) { 190 - console.error(`[error] astro-embed` + '\n ' + (e?.message ?? e)); 191 - return undefined; 192 - } 186 + try { 187 + const hydratedPost = await agent.getPosts({ uris: [uri] }); 188 + return hydratedPost.data.posts[0] as unknown as Post; 189 + } catch (e: any) { 190 + console.error(`[error] astro-embed` + "\n " + (e?.message ?? e)); 191 + return undefined; 192 + } 193 193 } 194 194 195 195 export async function getUserPosts(actor: string) { 196 - const getPostURL = `https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${actor}&limit=100`; 197 - const posts = await fetch(getPostURL); 198 - const postsData = await posts.json(); 199 - return postsData.feed; 196 + const getPostURL = `https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${actor}&limit=100`; 197 + const posts = await fetch(getPostURL); 198 + const postsData = await posts.json(); 199 + return postsData.feed; 200 200 } 201 201 202 202 export async function getComments(uri: string) { 203 - const getCommentsURL = `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${uri}&depth=10`; 204 - const comments = await fetch(getCommentsURL); 205 - const commentsData = await comments.json(); 206 - const replies = commentsData.thread.replies; 207 - return replies.sort((a: any, b: any) => new Date(b.post.record.createdAt).getTime() - new Date(a.post.record.createdAt).getTime()); 203 + const getCommentsURL = `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${uri}&depth=10`; 204 + const comments = await fetch(getCommentsURL); 205 + const commentsData = await comments.json(); 206 + const replies = commentsData.thread.replies; 207 + return replies.sort( 208 + (a: any, b: any) => 209 + new Date(b.post.record.createdAt).getTime() - 210 + new Date(a.post.record.createdAt).getTime(), 211 + ); 208 212 } 209 - 210 213 211 214 export function numberToHumanReadable(number: number) { 212 - if(number < 1000) { 213 - return number; 214 - } 215 - if(number < 1000000) { 216 - return `${Math.floor(number / 1000)}k`; 217 - } 218 - return `${Math.floor(number / 1000000)}m`; 215 + if (number < 1000) { 216 + return number; 217 + } 218 + if (number < 1000000) { 219 + return `${Math.floor(number / 1000)}k`; 219 220 } 221 + return `${Math.floor(number / 1000000)}m`; 222 + } 220 223 221 - export function getCommentCount(comments: { replies?: any}[]) { 224 + export function getCommentCount(comments: { replies?: any }[]) { 222 225 // recursively check for replies and add them up 223 226 let count = comments.length; 224 - for(const comment of comments) { 225 - if(comment.replies?.length) { 227 + for (const comment of comments) { 228 + if (comment.replies?.length) { 226 229 count += getCommentCount(comment.replies); 227 230 } 228 231 } 229 232 return count; 230 - } 233 + }
+1 -1
src/components/search/CommandPalette.svelte
··· 1 1 <script lang="ts"> 2 - import { BASE } from "../../config.json"; 2 + import { BASE } from "../../config.ts"; 3 3 4 4 import { fade, slide } from "svelte/transition"; 5 5 import type { Pagefind } from "vite-plugin-pagefind/types";
-28
src/config.json
··· 1 - { 2 - "SITE": "https://flo-bit.dev", 3 - "BASE": "/blog-template", 4 - "SITE_TITLE": "Blog template", 5 - "SITE_NAME": "blog template", 6 - "SITE_DESCRIPTION": "Welcome to my website!", 7 - "SITE_FAVICON": "🙃", 8 - "NAME": "flo-bit", 9 - "POSTS_PER_PAGE": 5, 10 - "MANUAL_DARK_MODE": true, 11 - "SEARCH_ENABLED": true, 12 - "SHOW_IMAGES": true, 13 - "BASE_COLOR": "stone", 14 - "ACCENT_COLOR": "rose", 15 - "SOCIAL_LINKS": { 16 - "FACEBOOK_URL": "", 17 - "TWITTER_URL": "", 18 - "GITHUB_URL": "https://github.com/flo-bit", 19 - "INSTAGRAM_URL": "", 20 - "LINKEDIN_URL": "", 21 - "YOUTUBE_URL": "", 22 - "SUBSTACK_URL": "", 23 - "EMAIL": "flo.bit.dev@gmail.com", 24 - "BLUESKY_URL": "https://bsky.app/profile/flo-bit.dev", 25 - "SHOW_RSS": true 26 - }, 27 - "BLUESKY_IDENTIFIER": "flo-bit.dev" 28 - }
+69
src/config.ts
··· 1 + import type { AccentColor, BaseColor } from "./colors"; 2 + 3 + // if deployed to github pages, set to https://<your-github-username>.github.io/ 4 + export const SITE = "https://flo-bit.dev"; 5 + 6 + // if deployed to github pages, set to '/<your-repo-name>' 7 + // EXCEPT if repo name is <your-github-username>.github.io 8 + // in that case set to '/' 9 + export const BASE = "/blog-template"; 10 + 11 + // will be used for the the title and meta tags and in the header (if SITE_NAME is left blank) 12 + export const SITE_TITLE = "Blog template"; 13 + 14 + // will be used in the meta tags (and for example shown in search results) 15 + export const SITE_DESCRIPTION = "Welcome to my website!"; 16 + 17 + // will be used as the icon in the header and the favicon 18 + export const SITE_FAVICON = "🙃"; 19 + 20 + // will be used in the footer as the name of the author (c) <YEAR> <NAME> - LICENSE 21 + export const NAME = "flo-bit"; 22 + 23 + // will be used in the footer as the license of the content (e.g. "All right reserved" or "CC-BY-SA 4.0") 24 + export const LICENSE = "All rights reserved."; 25 + 26 + // will be used to identify your bluesky account, so that likes and comments can be shown on your posts 27 + export const BLUESKY_IDENTIFIER = "flo-bit.dev"; 28 + 29 + // will be used to set the base color of the blog 30 + export const BASE_COLOR: BaseColor = "stone"; 31 + 32 + // will be used to set the accent color of the blog 33 + export const ACCENT_COLOR: AccentColor = "rose"; 34 + 35 + export const SOCIAL_LINKS: { 36 + FACEBOOK_URL?: string; 37 + TWITTER_URL?: string; 38 + GITHUB_URL?: string; 39 + INSTAGRAM_URL?: string; 40 + LINKEDIN_URL?: string; 41 + YOUTUBE_URL?: string; 42 + SUBSTACK_URL?: string; 43 + EMAIL?: string; 44 + BLUESKY_URL?: string; 45 + SHOW_RSS?: boolean; 46 + } = { 47 + SHOW_RSS: true, 48 + FACEBOOK_URL: "https://www.facebook.com/flo-bit.dev", 49 + TWITTER_URL: "https://twitter.com/flo_bit", 50 + GITHUB_URL: "https://github.com/flo-bit", 51 + INSTAGRAM_URL: "https://www.instagram.com/flo_bit", 52 + }; 53 + // if true, will show theme toggle in header (otherwise theme is automatically detected and can't be changed by the readers) 54 + export const MANUAL_DARK_MODE = true; 55 + 56 + // if true, will enable the search functionality 57 + export const SEARCH_ENABLED = true; 58 + 59 + // if true, will show images in the posts 60 + export const SHOW_IMAGES = true; 61 + 62 + // will be used to set the number of posts per page 63 + export const POSTS_PER_PAGE = 5; 64 + 65 + // will be shown in the header, if left blank will instead show the SITE_TITLE 66 + export const SITE_NAME = ""; 67 + 68 + // if true, will show the SITE_FAVICON in the header 69 + export const SHOW_FAVICON_IN_HEADER = true;
+7 -8
src/content.config.ts
··· 1 1 import { defineCollection, z } from "astro:content"; 2 - import { glob } from 'astro/loaders'; 2 + import { glob } from "astro/loaders"; 3 3 4 4 const blog = defineCollection({ 5 - loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/content/blog/" }), 6 - // Type-check frontmatter using a schema 7 - schema: z.object({ 5 + loader: glob({ pattern: "**/[^_]*.{md,mdx}", base: "./src/content/blog/" }), 6 + // Type-check frontmatter using a schema 7 + schema: z.object({ 8 8 // title of the blog post, don't repeat this in the markdown part 9 9 title: z.string(), 10 10 ··· 14 14 disableComments: z.boolean().optional(), 15 15 16 16 disableLikes: z.boolean().optional(), 17 - 17 + 18 18 // date published 19 19 pubDate: z.coerce.date(), 20 20 ··· 41 41 42 42 // whether to use the hero image as the og image (instead of the default `/src/assets/background.png`) 43 43 useHeroAsOGImage: z.boolean().optional(), 44 - 44 + 45 45 // wether to show title and short description in the og image 46 46 noTextInOGImage: z.boolean().optional(), 47 47 }), 48 48 }); 49 49 50 50 import { authorFeedLoader } from "@ascorbic/bluesky-loader"; 51 - import { BLUESKY_IDENTIFIER } from "./config.json"; 52 - 51 + import { BLUESKY_IDENTIFIER } from "./config.ts"; 53 52 54 53 const posts = defineCollection({ 55 54 loader: authorFeedLoader({
+5 -5
src/content/blog/comments-via-bluesky.mdx
··· 10 10 11 11 ## How it works 12 12 13 - Set the `BLUESKY_IDENTIFIER` in your `src/config.json` file to your bluesky handle (without the `@`). 13 + Set the `BLUESKY_IDENTIFIER` in your `src/config.ts` file to your bluesky handle (without the `@`). 14 14 15 15 Then just post a link to your blog post on bluesky and comments will be shown on your blog posts, looking something like this: 16 16 17 17 <div class="max-w-md rounded-xl overflow-hidden border border-base-200 dark:border-base-800 shadow-lg not-prose"> 18 - ![showing comments]($assets/posts/comments.png) 18 + ![showing comments]($assets/posts/comments.png) 19 19 </div> 20 20 21 21 Note that no "add comment" link is shown if there is no post on bluesky linking to your blog post. 22 22 23 - Comments are both server-side rendered on build and updated on the client when you navigate to the page 23 + Comments are both server-side rendered on build and updated on the client when you navigate to the page 24 24 (so they work without javascript, but might be a bit outdated). 25 25 26 - If you post a link to your blog post on bluesky, likes will also be shown (see [likes via bluesky](../likes-via-bluesky)). 26 + If you post a link to your blog post on bluesky, likes will also be shown (see [likes via bluesky](../likes-via-bluesky)). 27 27 If you don't want to show likes, you can disable them using the `disableLikes` option in your post options. 28 28 29 29 ```yml 30 30 disableLikes: true 31 31 ``` 32 32 33 - See a live example below: 33 + See a live example below:
+1 -1
src/content/blog/configuring-the-blog.mdx
··· 6 6 tags: ["setup"] 7 7 --- 8 8 9 - Change the values in `src/config.json` to configure the blog to your liking, see below for more information. 9 + Change the values in `src/config.ts` to configure the blog to your liking, see below for more information. 10 10 11 11 ## SITE 12 12
+1 -1
src/content/blog/features.mdx
··· 19 19 - ✅ Tag your posts 20 20 - ✅ Super easy to deploy as a static site 21 21 - ✅ Includes some prebuilt components for you to use 22 - - ✅ Likes and comments via bluesky 22 + - ✅ Likes and comments via bluesky
+2 -2
src/content/blog/how-to-use.mdx
··· 8 8 9 9 1. Fork [the repository of this blog](https://github.com/flo-bit/blog-template) (note: this repository per default uses github actions which are only free for public repositories) 10 10 11 - 2. In your repository settings, set up github pages to deploy using github actions (*SETTINGS* -> *PAGES* -> *SOURCE*: **Github Actions**) 11 + 2. In your repository settings, set up github pages to deploy using github actions (_SETTINGS_ -> _PAGES_ -> _SOURCE_: **Github Actions**) 12 12 13 - 3. Set up your blog info in `src/config.json` (see [all options](../configuring-the-blog)) 13 + 3. Set up your blog info in `src/config.ts` (see [all options](../configuring-the-blog)) 14 14 15 15 4. Your blog should be live in about 1 minute at `https://<your-github-username>.github.io/<your-repo-name>` 16 16
+5 -5
src/content/blog/likes-via-bluesky.mdx
··· 13 13 14 14 ## How it works 15 15 16 - Set the `BLUESKY_IDENTIFIER` in your `src/config.json` file to your bluesky handle (without the `@`). 16 + Set the `BLUESKY_IDENTIFIER` in your `src/config.ts` file to your bluesky handle (without the `@`). 17 17 18 18 Then just post a link to your blog post on bluesky and likes will be shown on your blog posts, looking something like this: 19 19 20 20 <div class="max-w-md rounded-xl overflow-hidden border border-base-200 dark:border-base-800 shadow-lg not-prose"> 21 - ![showing likes]($assets/posts/likes.png) 21 + ![showing likes]($assets/posts/likes.png) 22 22 </div> 23 23 24 24 Note that no like link is shown if there is no post on bluesky linking to your blog post. 25 25 26 - Likes are both server-side rendered on build and updated on the client when you navigate to the page 26 + Likes are both server-side rendered on build and updated on the client when you navigate to the page 27 27 (so they work without javascript, but might be a bit outdated). 28 28 29 - If you post a link to your blog post on bluesky, comments will also be shown (see [comments via bluesky](../comments-via-bluesky)). 29 + If you post a link to your blog post on bluesky, comments will also be shown (see [comments via bluesky](../comments-via-bluesky)). 30 30 If you don't want to show comments, you can disable them using the `disableComments` option in your post options. 31 31 32 32 ```yml 33 33 disableComments: true 34 34 ``` 35 35 36 - See a live example below: 36 + See a live example below:
+5 -5
src/content/blog/showing-code.mdx
··· 6 6 tags: ["code", "markdown"] 7 7 --- 8 8 9 - 10 9 this is some `inline code` 11 10 12 11 this is some multiline code: ··· 18 17 19 18 const b = "this is a string"; 20 19 21 - console.log(b);// [!code highlight] 20 + console.log(b); // [!code highlight] 22 21 23 22 function test() { 24 - const test = "this is a very long string, that should hopefully wrap onto multiple lines"; // [!code highlight] 23 + const test = 24 + "this is a very long string, that should hopefully wrap onto multiple lines"; // [!code highlight] 25 25 26 - console.log(test); 26 + console.log(test); 27 27 } 28 - ``` 28 + ```
-1
src/content/blog/showing-embeds.mdx
··· 37 37 ``` 38 38 39 39 :excalidraw[/src/assets/excalidraw-demo.svg] 40 -
+7 -3
src/content/info/about.mdx
··· 2 2 3 3 Get the [source code here](https://github.com/flo-bit/blog-template) 4 4 5 - Minimalistic but opinionated blog template using [astro](https://astro.build/) and [svelte](https://svelte.dev/). 6 - aims to be super easy to deploy and use, with a focus on performance and SEO, ease-of-use and design 5 + Minimalistic but opinionated blog template using [astro](https://astro.build/) and [svelte](https://svelte.dev/). 6 + aims to be super easy to deploy and use, with a focus on performance and SEO, ease-of-use and design 7 7 (see all [features here](/blog-template/posts/features)). 8 8 9 - This blog doubles as a tutorial on how to use this template, start by [setting up your github repo](/blog-template/posts/how-to-use), 9 + This blog doubles as a tutorial on how to use this template, start by [setting up your github repo](/blog-template/posts/how-to-use), 10 10 then [add some content](/blog-template/posts/adding-content). For more information read about [supported markdown features here](/blog-template/posts/markdown-style-guide). 11 + 12 + If you want to, I'd appreciate it if you add something like this here (not required): 13 + 14 + > This blog was created with [this astro blog template](https://github.com/flo-bit/blog-template) by [flo-bit](https://flo-bit.dev)
+1 -1
src/embeds/excalidraw/embed.ts
··· 9 9 importPath: "src/embeds", 10 10 }; 11 11 12 - export default ExcalidrawEmbed; 12 + export default ExcalidrawEmbed;
+6 -6
src/embeds/excalidraw/matcher.ts
··· 1 1 export default function urlMatcher(url: string): string | undefined { 2 - // should be an absolute path to an svg file 3 - if (url.startsWith("/src/") && url.endsWith(".svg")) { 4 - return url; 5 - } 6 - return undefined; 7 - } 2 + // should be an absolute path to an svg file 3 + if (url.startsWith("/src/") && url.endsWith(".svg")) { 4 + return url; 5 + } 6 + return undefined; 7 + }
-1
src/embeds/index.ts
··· 1 - 2 1 import LinkCard from "./link-card/LinkCard.astro"; 3 2 import YouTube from "./youtube/YouTube.astro"; 4 3 import Excalidraw from "./excalidraw/Excalidraw.astro";
+2 -2
src/embeds/matcher.ts
··· 1 - import YoutubeMatcher from './youtube/matcher.ts'; 2 - import LinkMatcher from './link-card/matcher.ts'; 1 + import YoutubeMatcher from "./youtube/matcher.ts"; 2 + import LinkMatcher from "./link-card/matcher.ts"; 3 3 export { YoutubeMatcher, LinkMatcher };
+7 -7
src/embeds/youtube/embed.ts
··· 2 2 import YoutubeMatcher from "./matcher"; 3 3 4 4 const YoutubeEmbed: EmbedsOption = { 5 - componentName: 'YouTube', 6 - urlArgument: 'id', 7 - urlMatcher: YoutubeMatcher, 8 - directiveName: 'youtube', 9 - importPath: 'src/embeds', 10 - } 5 + componentName: "YouTube", 6 + urlArgument: "id", 7 + urlMatcher: YoutubeMatcher, 8 + directiveName: "youtube", 9 + importPath: "src/embeds", 10 + }; 11 11 12 - export default YoutubeEmbed; 12 + export default YoutubeEmbed;
+3 -3
src/embeds/youtube/matcher.ts
··· 1 1 // Thanks to eleventy-plugin-youtube-embed 2 2 // https://github.com/gfscott/eleventy-plugin-youtube-embed/blob/main/lib/extractMatches.js 3 3 const urlPattern = 4 - /(?=(\s*))\1(?:<a [^>]*?>)??(?=(\s*))\2(?:https?:\/\/)??(?:w{3}\.)??(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|shorts\/)??([A-Za-z0-9-_]{11})(?:[^\s<>]*)(?=(\s*))\4(?:<\/a>)??(?=(\s*))\5/; 4 + /(?=(\s*))\1(?:<a [^>]*?>)??(?=(\s*))\2(?:https?:\/\/)??(?:w{3}\.)??(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|shorts\/)??([A-Za-z0-9-_]{11})(?:[^\s<>]*)(?=(\s*))\4(?:<\/a>)??(?=(\s*))\5/; 5 5 6 6 /** 7 7 * Extract a YouTube ID from a URL if it matches the pattern. ··· 9 9 * @returns A YouTube video ID or undefined if none matched 10 10 */ 11 11 export default function matcher(url: string): string | undefined { 12 - const match = url.match(urlPattern); 13 - return match?.[3]; 12 + const match = url.match(urlPattern); 13 + return match?.[3]; 14 14 }
+1 -1
src/layouts/PostList.astro
··· 1 1 --- 2 - import { SITE_TITLE, SITE_DESCRIPTION } from "../config.json"; 2 + import { SITE_TITLE, SITE_DESCRIPTION } from "../config.ts"; 3 3 4 4 import Footer from "$components/Footer.astro"; 5 5 import Header from "$components/Header.astro";
+1 -1
src/layouts/ProseWrapper.astro
··· 1 1 --- 2 2 import { colorBaseClasses, colorAccentClasses } from "src/colors"; 3 - import { ACCENT_COLOR, BASE_COLOR } from "../config.json"; 3 + import { ACCENT_COLOR, BASE_COLOR } from "../config.ts"; 4 4 5 5 const { class: className } = Astro.props; 6 6 ---
+1 -1
src/pages/about/index.astro
··· 1 1 --- 2 - import { SITE_TITLE, SITE_DESCRIPTION } from "../../config.json"; 2 + import { SITE_TITLE, SITE_DESCRIPTION } from "../../config.ts"; 3 3 4 4 import Footer from "$components/Footer.astro"; 5 5 import Header from "$components/Header.astro";
+1 -1
src/pages/index.astro
··· 1 1 --- 2 - import { POSTS_PER_PAGE } from "../config.json"; 2 + import { POSTS_PER_PAGE } from "../config.ts"; 3 3 import { getBlogPosts } from "src/utils"; 4 4 import PostList from "$layouts/PostList.astro"; 5 5
+4 -2
src/pages/open-graph/[...route].ts
··· 1 1 import { OGImageRoute } from "astro-og-canvas"; 2 - import { SITE_DESCRIPTION, SITE_TITLE } from "../../config.json"; 2 + import { SITE_DESCRIPTION, SITE_TITLE } from "../../config.ts"; 3 3 import { getBlogPosts } from "src/utils"; 4 4 5 5 const posts = await getBlogPosts(); ··· 47 47 title: page.title, 48 48 description: page.description, 49 49 bgImage: { 50 - path: page.useHero ? "." + page.heroImage : "./src/assets/backgrounds/background.png", 50 + path: page.useHero 51 + ? "." + page.heroImage 52 + : "./src/assets/backgrounds/background.png", 51 53 fit: "cover", 52 54 }, 53 55 font: {
+1 -1
src/pages/pages/[...index].astro
··· 1 1 --- 2 - import { POSTS_PER_PAGE } from "../../config.json"; 2 + import { POSTS_PER_PAGE } from "../../config.ts"; 3 3 import { getBlogPosts } from "src/utils"; 4 4 import PostList from "$layouts/PostList.astro"; 5 5
+1 -1
src/pages/posts/[...slug].astro
··· 9 9 import { getBlogPosts } from "src/utils"; 10 10 import BlueskyLikes from "$components/bluesky/BlueskyLikes.astro"; 11 11 import Tag from "$components/Tag.astro"; 12 - import { SHOW_IMAGES } from "../../config.json"; 12 + import { SHOW_IMAGES } from "../../config.ts"; 13 13 14 14 export async function getStaticPaths() { 15 15 const posts = await getBlogPosts();
+1 -1
src/pages/rss.xml.js
··· 1 1 import rss from "@astrojs/rss"; 2 - import { SITE_TITLE, SITE_DESCRIPTION, BASE } from "../config.json"; 2 + import { SITE_TITLE, SITE_DESCRIPTION, BASE } from "../config.ts"; 3 3 import { getBlogPosts } from "src/utils"; 4 4 5 5 export async function GET(context) {
+1 -1
src/pages/tags/[...tag]/[...index].astro
··· 1 1 --- 2 - import { POSTS_PER_PAGE } from "../../../config.json"; 2 + import { POSTS_PER_PAGE } from "../../../config.ts"; 3 3 4 4 import { getBlogPosts } from "src/utils"; 5 5 import PostList from "$layouts/PostList.astro";
+3 -3
src/style-utils.ts
··· 1 - import { type ClassValue, clsx } from 'clsx'; 2 - import { twMerge } from 'tailwind-merge'; 1 + import { type ClassValue, clsx } from "clsx"; 2 + import { twMerge } from "tailwind-merge"; 3 3 4 4 export function cn(...inputs: ClassValue[]) { 5 - return twMerge(clsx(inputs)); 5 + return twMerge(clsx(inputs)); 6 6 }
+1 -1
tailwind.config.mjs
··· 2 2 import forms from "@tailwindcss/forms"; 3 3 import plugin from "tailwindcss/plugin"; 4 4 import colors from "tailwindcss/colors"; 5 - import { ACCENT_COLOR, BASE_COLOR, MANUAL_DARK_MODE } from "./src/config.json"; 5 + import { ACCENT_COLOR, BASE_COLOR, MANUAL_DARK_MODE } from "./src/config.ts"; 6 6 7 7 /** @type {import('tailwindcss').Config} */ 8 8 export default {