├── .depcheckrc.json ├── .env.example ├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ └── playwright.yml ├── .gitignore ├── .husky └── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── CNAME ├── LICENSE ├── README.md ├── SECURITY.md ├── eslint.config.js.bak ├── eslint.config.mjs ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── polite-pop.d.ts ├── postcss.config.js ├── public ├── _redirects ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts │ ├── Inter-UI-Bold.woff │ ├── Inter-UI-BoldItalic.woff │ ├── Inter-UI-Italic.woff │ ├── Inter-UI-Medium.woff │ ├── Inter-UI-MediumItalic.woff │ ├── Inter-UI-Regular.woff │ ├── TAYDumpling.woff │ └── TAYQuickDraw.woff ├── images │ ├── hello-icon.png │ ├── mike-headshot-square.png │ └── wiggle.svg ├── keybase.txt ├── llms.txt ├── mstile-150x150.png ├── safari-pinned-tab.svg └── site.webmanifest ├── src ├── app │ ├── robots.ts │ └── sitemap.ts ├── components │ ├── Analytics │ │ └── Fathom.tsx │ ├── Avatar │ │ ├── Avatar.tsx │ │ ├── AvatarGroup.tsx │ │ └── index.tsx │ ├── Badge │ │ └── index.tsx │ ├── Breadcrumbs │ │ ├── Breadcrumbs.tsx │ │ └── index.tsx │ ├── Button.tsx │ ├── CarbonAd │ │ ├── CarbonAd.css │ │ ├── CarbonAd.tsx │ │ └── index.ts │ ├── Colophon │ │ ├── Colophon.tsx │ │ ├── Flower.tsx │ │ └── index.ts │ ├── ExternalWork │ │ ├── ExternalWorkItem.tsx │ │ └── index.ts │ ├── Forms │ │ ├── Input.tsx │ │ └── Label.tsx │ ├── Heading.tsx │ ├── Headshot │ │ ├── Headshot.tsx │ │ └── index.ts │ ├── Image │ │ ├── Image.tsx │ │ └── index.ts │ ├── Layouts │ │ ├── DefaultLayout.tsx │ │ └── index.ts │ ├── MdxEmbed │ │ ├── README.md │ │ ├── Threads.tsx │ │ ├── Tweet.tsx │ │ ├── Vimeo.tsx │ │ ├── YouTube.tsx │ │ ├── general-observer.tsx │ │ ├── index.ts │ │ └── utils.ts │ ├── Navbar │ │ └── Navbar.tsx │ ├── NewsletterFeed │ │ ├── NewsletterItem.tsx │ │ └── monstera-1.svg │ ├── NewsletterSignup │ │ ├── NewsletterBannerDetailed.tsx │ │ ├── NewsletterBannerFancy.tsx │ │ ├── NewsletterBannerSimple.tsx │ │ ├── NewsletterHero.tsx │ │ ├── SubscriberCount.tsx │ │ └── index.tsx │ ├── PolitePop │ │ ├── PolitePop.tsx │ │ └── index.ts │ ├── Post │ │ ├── FullPost.tsx │ │ ├── Post.tsx │ │ ├── PostSummary.tsx │ │ ├── TableOfContents.tsx │ │ ├── index.ts │ │ └── mentionsSummary.tsx │ ├── PostFeed │ │ ├── PostFeed.tsx │ │ └── index.ts │ ├── PublishDate │ │ ├── PublishDate.tsx │ │ └── index.ts │ ├── PullQuote │ │ ├── PullQuote.tsx │ │ └── index.ts │ ├── RelatedContent │ │ └── RelatedContentLinksByTag.tsx │ ├── Series │ │ └── SeriesNavigation.tsx │ ├── SiteAnnouncement │ │ ├── SiteAnnouncement.tsx │ │ └── index.ts │ ├── Slider │ │ ├── Slider.tsx │ │ └── index.ts │ ├── SocialLinks │ │ ├── SocialLinks.tsx │ │ └── index.ts │ ├── SponsorCTA │ │ └── SponsorCTA.tsx │ ├── SponsoredSection │ │ ├── SponsoredSection.tsx │ │ └── index.ts │ ├── StructuredData │ │ ├── StructuredData.tsx │ │ └── index.tsx │ ├── SubscriptionForm │ │ ├── SubscriptionForm.tsx │ │ └── index.tsx │ ├── Subtitle │ │ ├── Subtitle.tsx │ │ └── index.ts │ ├── demos │ │ ├── CenteredTextDemo.tsx │ │ └── OrtonEffectImage.tsx │ ├── footer.tsx │ ├── icons │ │ ├── BlueskyIcon.tsx │ │ ├── GitHubIcon.tsx │ │ ├── GmailIcon.tsx │ │ ├── MastodonIcon.tsx │ │ ├── PatreonIcon.tsx │ │ ├── RssIcon.tsx │ │ ├── ThreadsIcon.tsx │ │ ├── TwitchIcon.tsx │ │ ├── TwitterIcon.tsx │ │ ├── YouTubeIcon.tsx │ │ └── index.ts │ ├── seo.tsx │ ├── soldOut.tsx │ ├── tag.tsx │ ├── tagsSummary.tsx │ └── webmentionMetadata.tsx ├── config.ts ├── data │ ├── ConvertKitTags.ts │ ├── ConvertkitTag.yaml │ ├── content-types.ts │ ├── external-references │ │ ├── 50-most-powerful-startups-in-charlotte.mdx │ │ ├── building-the-plane-while-flying-it.mdx │ │ ├── charlotte-30-under-30.mdx │ │ ├── design-matters-zero-day.mdx │ │ ├── designing-windows-apps-from-the-ground-up.mdx │ │ ├── driving-successful-launch-for-conversational-actions.mdx │ │ ├── dynamic-shortcuts-for-assistant.mdx │ │ ├── edx-comprehensive-theming-tutorial.mdx │ │ ├── how-to-create-your-first-app-action.mdx │ │ ├── jamstack-ssgs-role-in-creator-economy.mdx │ │ ├── matter-a-whole-thing-about-design.mdx │ │ ├── new-transaction-features-for-smart-displays.mdx │ │ ├── open-edx-slack-intercom.mdx │ │ ├── publishing-your-first-github-pages-website.mdx │ │ ├── smpl-acquired-by-proximity.mdx │ │ ├── ssml-features-for-google-assistant.mdx │ │ ├── the-perils-of-obedience.mdx │ │ ├── voice-talks-ux-for-voice.mdx │ │ ├── what-the-heck-is-a-fullstack-developer.mdx │ │ └── your-app-is-ugly.mdx │ ├── newsletters │ │ ├── aesthetic-usability-effect.mdx │ │ ├── ai-is-your-intern-not-your-boss.mdx │ │ ├── balance-for-remote-workers.mdx │ │ ├── be-intentional-about-building-your-brand.mdx │ │ ├── beyond-the-walled-garden.mdx │ │ ├── books-that-shaped-my-career.mdx │ │ ├── build-measure-listen-rebuild.mdx │ │ ├── build-something-useful-with-ai.mdx │ │ ├── building-a-startup-in-2023-90-day-report.mdx │ │ ├── building-in-public.mdx │ │ ├── burnout-and-stress.mdx │ │ ├── cognitive-load-ux-and-why-you-should-care.mdx │ │ ├── color-context-and-product-design.mdx │ │ ├── continuous-self-improvement.mdx │ │ ├── contrast-effect-cognitive-psychology.mdx │ │ ├── craftwork-accepted-into-y-combinator.mdx │ │ ├── creators-startups-january-pivot.mdx │ │ ├── crowdsourcing-vs-user-research-ux.mdx │ │ ├── data-driven-decisions-product-analytics.mdx │ │ ├── decoy-effect-and-your-brain.mdx │ │ ├── design-decisions-cafe-tables.mdx │ │ ├── design-rules-everyone-should-know.mdx │ │ ├── developer-product-engineer-tech-stack-2025.mdx │ │ ├── dunbars-number-and-network-effect.mdx │ │ ├── endowment-effect-and-designing-free-trials.mdx │ │ ├── enthusiasts-hidden-superpower.mdx │ │ ├── eraser-or-wrecking-ball.mdx │ │ ├── features-of-fonts-and-typography.mdx │ │ ├── first-principles-for-ux-design.mdx │ │ ├── first-principles-frameworks-for-clarity.mdx │ │ ├── fitts-law-and-the-beauty-of-big-goals.mdx │ │ ├── flow-state-and-getting-things-done.mdx │ │ ├── focus-mode-for-the-defiant.mdx │ │ ├── four-years-already.mdx │ │ ├── freedom-is-a-feature.mdx │ │ ├── gestalt-design-principles-for-developers.mdx │ │ ├── good-ux-is-invisible.mdx │ │ ├── great-inbox-reset-for-founders.mdx │ │ ├── how-clever-engineers-stay-ahead-of-the-curve.mdx │ │ ├── how-to-know-when-to-publish-your-work.mdx │ │ ├── how-to-source-and-utilize-expert-knowledge.mdx │ │ ├── how-to-talk-to-ai-llms.mdx │ │ ├── how-you-do-one-thing-is-how-you-do-everything.mdx │ │ ├── i-hate-tailwind-css-heres-why-i-use-it.mdx │ │ ├── impostor-syndrome-is-a-monster.mdx │ │ ├── intellectual-humility-and-how-to-be-wrong.mdx │ │ ├── interpret-feedback-wisely.mdx │ │ ├── jakobs-law-design-ux.mdx │ │ ├── keep-your-tools-sharp.mdx │ │ ├── learn-from-a-dunning-kruger-expert.mdx │ │ ├── learn-from-llms-leaking-their-prompts.mdx │ │ ├── learning-is-an-infinite-game.mdx │ │ ├── leaving-stripe-going-viral.mdx │ │ ├── lessons-from-the-creator-track-at-vidcon.mdx │ │ ├── lessons-in-growth-from-21-to-1000.mdx │ │ ├── lifecycle-of-a-moonshot-company.mdx │ │ ├── make-yourself-smarter-with-chatgpt.mdx │ │ ├── mental-health-for-startups-founders.mdx │ │ ├── meta-threads-launch-two-truths-and-a-lie.mdx │ │ ├── milestones-and-the-top-20-percent.mdx │ │ ├── mistakes-new-creators-make.mdx │ │ ├── moonshot-design-for-everyone.mdx │ │ ├── multitasking-and-product-design.mdx │ │ ├── naming-your-product-kiki-bouba.mdx │ │ ├── no-right-way-to-do-it.mdx │ │ ├── nobody-wants-a-big-reveal.mdx │ │ ├── on-seasonal-change-open-source-carbon-offsets.mdx │ │ ├── open-source-is-your-secret-weapon.mdx │ │ ├── open-sourcing-my-design-system.mdx │ │ ├── passion-and-the-hand-of-the-creator.mdx │ │ ├── portfolio-as-timeline.mdx │ │ ├── quality-and-the-hype-cycle.mdx │ │ ├── reddit-for-learning.mdx │ │ ├── reflecting-on-one-year-as-startup-cto.mdx │ │ ├── reverse-devrel-as-core-engineering-skill.mdx │ │ ├── serendipity-isnt-an-accident.mdx │ │ ├── shrimps-is-bugs.mdx │ │ ├── side-project-and-a-fresh-start.mdx │ │ ├── simple-habits-for-a-happier-team.mdx │ │ ├── sip-coffee-quickly-act-quietly.mdx │ │ ├── steal-these-ideas-for-your-next-product.mdx │ │ ├── strategies-for-your-product-waitlist.mdx │ │ ├── superfounder-superpowers-sending-introductions.mdx │ │ ├── swear-i-wrote-that-down-somewhere.mdx │ │ ├── take-my-money-tools-i-pay-for.mdx │ │ ├── tech-product-growth-wabi-sabi.mdx │ │ ├── themes-beat-resolutions-every-time.mdx │ │ ├── tiny-improvements-for-big-changes.mdx │ │ ├── tiny-tips-for-better-seo.mdx │ │ ├── today-is-your-day-to-start.mdx │ │ ├── tools-for-building-a-new-company.mdx │ │ ├── tools-i-love-resend-react-email.mdx │ │ ├── typescript-and-learning-hard-things.mdx │ │ ├── unexpected-observations-in-human-cognition.mdx │ │ ├── unlock-productivity-with-networked-note-taking.mdx │ │ ├── unreasonable-hospitality-and-design.mdx │ │ ├── use-annual-pricing-build-loyalty.mdx │ │ ├── use-of-language-and-intent.mdx │ │ ├── voting-and-the-founder-mindset.mdx │ │ ├── what-i-learned-from-using-ai.mdx │ │ ├── what-i-learned-studying-cs.mdx │ │ ├── what-to-do-when-the-bottom-falls-out.mdx │ │ ├── what-to-do-when-your-product-breaks.mdx │ │ ├── why-new-frameworks-make-better-developers.mdx │ │ ├── you-are-far-more-influential-than-you-think.mdx │ │ ├── your-anger-as-inspiration-for-change.mdx │ │ ├── your-mvp-is-too-damn-big.mdx │ │ └── your-resume-sucks.mdx │ └── posts │ │ ├── ab-testing-with-posthog-to-fix-conversions.mdx │ │ ├── acquired-costco-podcast-llm-summaries.mdx │ │ ├── add-fathom-analytics-to-remix.mdx │ │ ├── all-about-ch.mdx │ │ ├── are-you-suddenly-a-remote-worker.mdx │ │ ├── building-in-public-growth-report-400-readers.mdx │ │ ├── building-tiny-products.mdx │ │ ├── chrome-extensions-i-use.mdx │ │ ├── content-creation-workflow-my-writing-process.mdx │ │ ├── crosspost-introducing-pistola.mdx │ │ ├── custom-fonts-with-next-font-and-tailwind.mdx │ │ ├── debugging-a-conversion-problem-on-my-nextjs-site.mdx │ │ ├── deconfusing-javascript-destructuring-syntax.mdx │ │ ├── devs-its-okay-to-use-no-code-tools.mdx │ │ ├── dont-center-paragraph-text.mdx │ │ ├── egg-them-all.mdx │ │ ├── embracing-prettier.mdx │ │ ├── eslint-no-floating-promises.mdx │ │ ├── fixing-my-conversion-problem.mdx │ │ ├── gatsby-dev-to-cross-poster-brainstorm.mdx │ │ ├── gitignore-io-is-great.mdx │ │ ├── how-do-you-choose-the-right-crm-for-your-product.mdx │ │ ├── how-stripe-uses-friction-logs.mdx │ │ ├── i-have-to-tell-you-about-dependabot.mdx │ │ ├── it-was-time.mdx │ │ ├── javascript-filter-boolean.mdx │ │ ├── learn-web3-blockchain-with-buildspace.mdx │ │ ├── live-astro-content-driven-website-rebuild.mdx │ │ ├── live-coding-resend-broadcasts-nextjs.mdx │ │ ├── live-coding-satori-og-images-nextjs.mdx │ │ ├── make-vs-code-load-faster-by-removing-extensions.mdx │ │ ├── make-vs-code-load-faster-mac-apple-silicon.mdx │ │ ├── mdx-auto-link-headings-with-rehype-slug.mdx │ │ ├── migrate-from-next-sitemap-to-app-directory-sitemap.mdx │ │ ├── migrate-gatsby-to-nextjs-apisyouwonthate-com.mdx │ │ ├── moving-to-mdx.mdx │ │ ├── my-favorite-design-problem.mdx │ │ ├── next-js-github-bio-about-page.mdx │ │ ├── nullish-coalescing-javascript.mdx │ │ ├── on-normalcy.mdx │ │ ├── orton-effect-css-react.mdx │ │ ├── own-your-work-with-canonical-tags.mdx │ │ ├── picking-apart-javascript-import.mdx │ │ ├── plan-for-things-to-go-wrong-in-your-web-app.mdx │ │ ├── posthog-ab-test-results-are-in.mdx │ │ ├── posthog-helped-me-find-a-bug.mdx │ │ ├── product-marketing-defy-expectations.mdx │ │ ├── promise-all-settled-pt-2-its-partly-settled.mdx │ │ ├── publish-your-newsletter-with-convertkit-api-next-js.mdx │ │ ├── publish-your-newsletter-with-convertkit-api-remix.mdx │ │ ├── quick-tip-uninstall-postgres-from-your-mac.mdx │ │ ├── reclaimed-10gb-of-disk-space-from-node-modules.mdx │ │ ├── refactoring-typescript-react-components-vscode.mdx │ │ ├── remote-work-and-the-third-place.mdx │ │ ├── reset-your-open-graph-embeds-on-linkedin-twitter-facebook.mdx │ │ ├── run-dependabot-locally.mdx │ │ ├── seed-your-supabase-database.mdx │ │ ├── self-healing-urls-nextjs-seo.mdx │ │ ├── semantic-html-heading-subtitle.mdx │ │ ├── seo-tools-for-new-web-projects.mdx │ │ ├── solve-all-your-problems-with-promise-allsettled.mdx │ │ ├── sticker-update-we-raised-176-nzd.mdx │ │ ├── stop-paying-your-isp-to-rent-a-modem.mdx │ │ ├── structured-data-json-ld-for-next-js-sites.mdx │ │ ├── text-wrap-balance-will-make-your-designs-better.mdx │ │ ├── twitch-streaming-software-development-lessons.mdx │ │ ├── twitter-and-the-perils-of-obedience.mdx │ │ ├── typescript-vscode-error-fix-last-resort.mdx │ │ ├── why-fathom-analytics.mdx │ │ ├── working-in-public.mdx │ │ └── your-tastes-will-always-outpace-your-skill.mdx ├── hooks │ ├── useNewsletterStats.ts │ ├── useRouterType.tsx │ └── useWebMentions.ts ├── instrumentation.ts ├── lib │ ├── blog.ts │ ├── content-loaders │ │ ├── getAllContentFromDirectory.ts │ │ ├── getContentBySlug.ts │ │ └── processMDXFileContent.ts │ ├── external-references.ts │ ├── newsletters.ts │ ├── series.ts │ └── tags │ │ ├── TagRegistry.ts │ │ ├── getTagRegistryForAllContent.ts │ │ ├── index.ts │ │ ├── loadContentFromDirectory.ts │ │ └── utils.ts ├── pages │ ├── 404.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── about │ │ ├── cover.png │ │ └── index.tsx │ ├── api │ │ ├── inbound_webhooks │ │ │ ├── convertKit.ts │ │ │ └── resend.ts │ │ └── trpc │ │ │ └── [trpc].ts │ ├── index.tsx │ ├── integrity │ │ └── index.tsx │ ├── newsletter │ │ ├── [slug].tsx │ │ └── index.tsx │ ├── podcast │ │ └── index.tsx │ ├── posts │ │ └── [slug].tsx │ ├── series │ │ └── [slug].tsx │ ├── shop.tsx │ ├── tags │ │ ├── [tag].tsx │ │ └── index.tsx │ └── work.tsx ├── server │ ├── context.ts │ ├── routers │ │ ├── _app.ts │ │ ├── mailingList.ts │ │ └── webMentions.ts │ └── trpc.ts ├── styles │ ├── globals.css │ ├── header.module.scss │ ├── layout.module.scss │ └── tagsPage.module.scss └── utils │ ├── MDXProviderWrapper.tsx │ ├── clsxm.ts │ ├── email │ ├── sendSubscriberNotificationEmail.tsx │ ├── sendWelcomeEmail.tsx │ ├── tags.ts │ └── templates │ │ └── WelcomeEmail.tsx │ ├── env.ts │ ├── format-date.ts │ ├── generateStructuredData.tsx │ ├── images.ts │ ├── mdx.ts │ ├── resend.ts │ ├── rss.ts │ ├── trpc.ts │ └── webmentions.ts ├── tailwind.config.js ├── tests └── smoke.spec.ts ├── tsconfig.json ├── turbo.json └── twttr.d.ts /.depcheckrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [], 3 | "ignores": [ 4 | "@eslint/js", 5 | "@ianvs/prettier-plugin-sort-imports", 6 | "@next/bundle-analyzer", 7 | "@next/eslint-plugin-next", 8 | "@prettier/plugin-ruby", 9 | "@tailwindcss/forms", 10 | "@tailwindcss/typography", 11 | "@tailwindcss/postcss", 12 | "@types/eslint", 13 | "@typescript-eslint", 14 | "@typescript-eslint/eslint-plugin", 15 | "@typescript-eslint/parser", 16 | "depcheck", 17 | "eslint-*", 18 | "eslint", 19 | "jiti", 20 | "playwright", 21 | "postcss-import", 22 | "postcss", 23 | "prettier-plugin-tailwindcss", 24 | "prettier", 25 | "react-email", 26 | "sharp", 27 | "tailwindcss", 28 | "typescript-eslint", 29 | 30 | "@/config", 31 | "@components/*", 32 | "@data/*", 33 | "@hooks/*", 34 | "@layouts/*", 35 | "@lib/*", 36 | "@server/*", 37 | "@server/*", 38 | "@utils/*", 39 | 40 | "mdx", 41 | "src" 42 | ], 43 | "parsers": { 44 | "**/*.{ts,tsx}": ["typescript"] 45 | }, 46 | "aliases": { 47 | "@components": "./src/components", 48 | "@data": "./src/data", 49 | "@hooks": "./src/hooks", 50 | "@layouts": "./src/components/layouts", 51 | "@lib": "./src/lib", 52 | "@utils": "./src/utils", 53 | "@server": "./src/server", 54 | "@/config": "./src/config.ts" 55 | }, 56 | "specials": ["depcheck.special.eslint", "depcheck.special.webpack"] 57 | } 58 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SITE_URL=https://mikebifulco.com 2 | 3 | # Find this at https://cloudinary.com/console/settings/account 4 | NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=mikebifulco-com 5 | 6 | CONVERTKIT_API_SECRET=abc123 7 | 8 | # fathom analytics 9 | NEXT_PUBLIC_FATHOM_ID=PAVJGIYJ 10 | 11 | # posthog 12 | NEXT_PUBLIC_POSTHOG_KEY=test 13 | 14 | # transistor 15 | TRANSISTOR_API_KEY=test 16 | 17 | RESEND_API_KEY=test 18 | RESEND_NEWSLETTER_AUDIENCE_ID=test 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | day: 'saturday' 8 | time: '05:00' 9 | - package-ecosystem: 'npm' 10 | directory: '/' 11 | schedule: 12 | interval: 'weekly' 13 | day: 'saturday' 14 | time: '05:00' 15 | groups: 16 | dependencies: 17 | applies-to: 'version-updates' 18 | update-types: 19 | - 'minor' 20 | - 'patch' 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Dependency Checks 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | jobs: 9 | lint: 10 | timeout-minutes: 7 11 | runs-on: ubuntu-latest 12 | env: 13 | CONVERTKIT_API_SECRET: ${{ secrets.CONVERTKIT_API_SECRET }} 14 | CONVERTKIT_WEBHOOK_SECRET: ${{ secrets.CONVERTKIT_WEBHOOK_SECRET }} 15 | NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: ${{ secrets.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME }} 16 | NEXT_PUBLIC_FATHOM_ID: ${{ secrets.NEXT_PUBLIC_FATHOM_ID }} 17 | NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} 18 | RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} 19 | RESEND_NEWSLETTER_AUDIENCE_ID: ${{ secrets.RESEND_NEWSLETTER_AUDIENCE_ID }} 20 | RESEND_SIGNING_SECRET: ${{ secrets.RESEND_SIGNING_SECRET }} 21 | steps: 22 | - name: Checkout repo 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup pnpm 26 | uses: pnpm/action-setup@v4 27 | 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: lts/* 31 | 32 | - name: Get pnpm store directory 33 | id: pnpm-cache 34 | run: | 35 | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT 36 | 37 | - name: Setup pnpm cache 38 | uses: actions/cache@v4 39 | with: 40 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 41 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 42 | restore-keys: | 43 | ${{ runner.os }}-pnpm-store- 44 | 45 | - name: Install deps (with cache) 46 | run: pnpm install 47 | 48 | - name: Run linting 49 | run: pnpm lint 50 | 51 | - name: Check dependencies 52 | run: pnpm depcheck 53 | 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | jobs: 9 | playwright: 10 | timeout-minutes: 7 11 | runs-on: ubuntu-latest 12 | env: 13 | CONVERTKIT_API_SECRET: ${{ secrets.CONVERTKIT_API_SECRET }} 14 | CONVERTKIT_WEBHOOK_SECRET: ${{ secrets.CONVERTKIT_WEBHOOK_SECRET }} 15 | NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: ${{ secrets.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME }} 16 | NEXT_PUBLIC_FATHOM_ID: ${{ secrets.NEXT_PUBLIC_FATHOM_ID }} 17 | NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} 18 | RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} 19 | RESEND_NEWSLETTER_AUDIENCE_ID: ${{ secrets.RESEND_NEWSLETTER_AUDIENCE_ID }} 20 | RESEND_SIGNING_SECRET: ${{ secrets.RESEND_SIGNING_SECRET }} 21 | steps: 22 | - name: Checkout repo 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup pnpm 26 | uses: pnpm/action-setup@v4 27 | 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: lts/* 31 | 32 | - name: Get pnpm store directory 33 | id: pnpm-cache 34 | run: | 35 | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT 36 | 37 | - name: Setup pnpm cache 38 | uses: actions/cache@v4 39 | with: 40 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 41 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 42 | restore-keys: | 43 | ${{ runner.os }}-pnpm-store- 44 | 45 | - name: Install deps (with cache) 46 | run: pnpm install 47 | 48 | - name: Install Playwright Browsers 49 | run: pnpm exec playwright install --with-deps 50 | 51 | - name: Run Playwright tests 52 | run: pnpm exec playwright test 53 | 54 | - uses: actions/upload-artifact@v4 55 | if: always() 56 | with: 57 | name: playwright-report 58 | path: playwright-report/ 59 | retention-days: 30 60 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": true, 4 | "singleQuote": true, 5 | "jsxSingleQuote": false, 6 | "tabWidth": 2, 7 | "plugins": [ 8 | "@ianvs/prettier-plugin-sort-imports", 9 | "prettier-plugin-tailwindcss" 10 | ], 11 | "tailwindConfig": "./tailwind.config.js", 12 | "importOrder": [ 13 | "^(react/(.*)$)|^(react$)|^(react-native(.*)$)", 14 | "^(next/(.*)$)|^(next$)", 15 | "^(expo(.*)$)|^(expo$)", 16 | "", 17 | "", 18 | "^@craftwork/(.*)$", 19 | "^@components/(.*)$", 20 | "^@data/(.*)$", 21 | "^@layouts/(.*)$", 22 | "^@lib/(.*)$", 23 | "^@ui/(.*)$", 24 | "^@utils/(.*)$", 25 | "^@styles/(.*)$", 26 | "^@/(.*)$", 27 | "^[./]" 28 | ], 29 | "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Attach to application", 8 | "skipFiles": ["/**"], 9 | "port": 9229 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnPaste": true, 3 | "editor.formatOnType": true, 4 | "debug.node.autoAttach": "off", 5 | "cSpell.words": ["chakra", "convertkit", "deduped", "liquidjs", "Webmention"], 6 | "dotenv.enableAutocloaking": false, 7 | "typescript.tsdk": "node_modules/typescript/lib" 8 | } 9 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | mike.biful.co -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 panr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Issues 2 | 3 | If you find any issues with the content on my site, drop by [the issues page](/issues) 4 | 5 | # The stack 6 | 7 | This is a [NextJS](https://nextjs.org) site... migrated from a Gatsby site. 8 | 9 | # Site feature checklist (please add tests for these one day, Mike) 10 | 11 | - [x] Publishing with MDX 12 | - [x] image hosting on cloudinary 13 | - [x] analytics with Fathom 14 | - [x] RSS feed (/rss.xml) 15 | - [x] Podcast page (/podcast) 16 | - [x] Resend newsletter 17 | - [x] web monetization 18 | - [x] web mentions 19 | - [x] Opengraph images + data 20 | - [x] CI with GitHub Actions 21 | - [x] automated build / deploy with Vercel 22 | - [x] TailwindCSS 23 | - [x] TypeScript 24 | - [x] JSON-LD structured data 25 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you discover a vulnerability in this repo, please consider the impact of public disclosure before either sending me an email [hello@mikebifulco.com](mailto:hello@mikebifulco.com) or [opening an issue](https://github.com/mbifulco/blog/issues/new). Thank you! 6 | -------------------------------------------------------------------------------- /eslint.config.js.bak: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | const config = { 3 | extends: [ 4 | 'next/core-web-vitals', 5 | 'plugin:@typescript-eslint/recommended-type-checked', 6 | 'plugin:@typescript-eslint/stylistic-type-checked', 7 | 'prettier', 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | project: true, 12 | }, 13 | plugins: ['@typescript-eslint', 'import'], 14 | rules: { 15 | 'react/prop-types': 'off', 16 | '@typescript-eslint/no-unused-vars': [ 17 | 'error', 18 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, 19 | ], 20 | '@typescript-eslint/consistent-type-definitions': ['error', 'type'], 21 | '@typescript-eslint/consistent-type-imports': [ 22 | 'warn', 23 | { prefer: 'type-imports', fixStyle: 'separate-type-imports' }, 24 | ], 25 | '@typescript-eslint/no-misused-promises': [ 26 | 2, 27 | { checksVoidReturn: { attributes: false } }, 28 | ], 29 | 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 30 | }, 31 | ignorePatterns: [ 32 | '**/.eslintrc.cjs', 33 | '**/*.config.js', 34 | '**/*.config.cjs', 35 | '**/*.config.mjs', 36 | '.next', 37 | 'dist', 38 | 'pnpm-lock.yaml', 39 | ], 40 | reportUnusedDisableDirectives: true, 41 | settings: { 42 | react: { 43 | version: 'detect', 44 | }, 45 | }, 46 | env: { 47 | browser: true, 48 | }, 49 | }; 50 | 51 | module.exports = config; 52 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { FlatCompat } from '@eslint/eslintrc'; 4 | import eslint from '@eslint/js'; 5 | import tsParser from '@typescript-eslint/parser'; 6 | import reactCompiler from 'eslint-plugin-react-compiler'; 7 | import unusedImports from 'eslint-plugin-unused-imports'; 8 | import { configs } from 'typescript-eslint'; 9 | 10 | const compat = new FlatCompat({ 11 | baseDirectory: import.meta.dirname, 12 | }); 13 | 14 | // Import Next.js config via compatibility layer 15 | const nextEslintConfig = compat.extends('next/core-web-vitals'); 16 | 17 | export default [ 18 | ...nextEslintConfig, 19 | eslint.configs.recommended, 20 | ...configs.recommended, 21 | { 22 | ignores: [ 23 | '**/**/node_modules', 24 | '**/**/.next', 25 | '**/**/public', 26 | 'components/ui', 27 | 'env.js', 28 | '**/.eslintrc.cjs', 29 | '**/*.config.js', 30 | '**/*.config.cjs', 31 | '**/*.config.mjs', 32 | '.next', 33 | 'dist', 34 | 'pnpm-lock.yaml', 35 | ], 36 | }, 37 | { 38 | plugins: { 39 | 'unused-imports': unusedImports, 40 | 'react-compiler': reactCompiler, 41 | }, 42 | languageOptions: { 43 | parser: tsParser, 44 | ecmaVersion: 'latest', 45 | sourceType: 'module', 46 | parserOptions: { 47 | project: ['./tsconfig.json'], 48 | }, 49 | }, 50 | }, 51 | { 52 | rules: { 53 | 'react/prop-types': 'off', 54 | '@typescript-eslint/no-unused-vars': [ 55 | 'error', 56 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, 57 | ], 58 | '@typescript-eslint/consistent-type-definitions': ['error', 'type'], 59 | '@typescript-eslint/consistent-type-imports': [ 60 | 'warn', 61 | { prefer: 'type-imports', fixStyle: 'separate-type-imports' }, 62 | ], 63 | '@typescript-eslint/no-misused-promises': [ 64 | 2, 65 | { checksVoidReturn: { attributes: false } }, 66 | ], 67 | 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 68 | }, 69 | }, 70 | ]; 71 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 7 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | export default defineConfig({ 13 | testDir: './tests', 14 | /* Run tests in files in parallel */ 15 | fullyParallel: true, 16 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 17 | forbidOnly: !!process.env.CI, 18 | /* Retry on CI only */ 19 | retries: process.env.CI ? 2 : 0, 20 | /* Opt out of parallel tests on CI. */ 21 | workers: process.env.CI ? 1 : undefined, 22 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 23 | reporter: 'html', 24 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 25 | use: { 26 | /* Base URL to use in actions like `await page.goto('/')`. */ 27 | baseURL: 'http://127.0.0.1:3000', 28 | 29 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 30 | trace: 'on-first-retry', 31 | }, 32 | 33 | /* Configure projects for major browsers */ 34 | projects: [ 35 | { 36 | name: 'chromium', 37 | use: { ...devices['Desktop Chrome'] }, 38 | }, 39 | 40 | // { 41 | // name: 'firefox', 42 | // use: { ...devices['Desktop Firefox'] }, 43 | // }, 44 | 45 | // { 46 | // name: 'webkit', 47 | // use: { ...devices['Desktop Safari'] }, 48 | // }, 49 | 50 | /* Test against mobile viewports. */ 51 | // { 52 | // name: 'Mobile Chrome', 53 | // use: { ...devices['Pixel 7'] }, 54 | // }, 55 | // { 56 | // name: 'Mobile Safari', 57 | // use: { ...devices['iPhone 12'] }, 58 | // }, 59 | 60 | /* Test against branded browsers. */ 61 | // { 62 | // name: 'Microsoft Edge', 63 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 64 | // }, 65 | // { 66 | // name: 'Google Chrome', 67 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 68 | // }, 69 | ], 70 | 71 | /* Run your local dev server before starting the tests */ 72 | webServer: { 73 | command: 'pnpm build && pnpm start', 74 | url: 'http://127.0.0.1:3000', 75 | reuseExistingServer: !process.env.CI, 76 | timeout: 10 * 60 * 1000, // 10 minutes in milliseconds 77 | }, 78 | }); 79 | -------------------------------------------------------------------------------- /polite-pop.d.ts: -------------------------------------------------------------------------------- 1 | declare function PolitePop(config: unknown): unknown; 2 | declare namespace PolitePop { 3 | function onNewEmailSignup(callback: unknown): void; 4 | } 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | plugins: { 5 | '@tailwindcss/postcss': {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /why-fathom-analytics /posts/why-fathom-analytics 2 | /on-normalcy /posts/on-normalcy 3 | /plan-for-things-to-go-wrong-in-your-web-app /posts/plan-for-things-to-go-wrong-in-your-web-app 4 | /are-you-suddenly-a-remote-worker /posts/are-you-suddenly-a-remote-worker 5 | /crosspost-introducing-pistola /posts/crosspost-introducing-pistola 6 | /gatsby-dev-to-cross-poster-brainstorm /posts/gatsby-dev-to-cross-poster-brainstorm 7 | /i-have-to-tell-you-about-dependabot /posts/i-have-to-tell-you-about-dependabot 8 | /all-about-ch /posts/all-about-ch 9 | /promise-all-settled-pt-2-its-partly-settled /posts/promise-all-settled-pt-2-its-partly-settled 10 | /picking-apart-javascript-import /posts/picking-apart-javascript-import 11 | /solve-all-your-problems-with-promise-allsettled /posts/solve-all-your-problems-with-promise-allsettled 12 | /reclaimed-10gb-of-disk-space-from-node-modules /posts/reclaimed-10gb-of-disk-space-from-node-modules 13 | /sticker-update-we-raised-176-nzd /posts/sticker-update-we-raised-176-nzd 14 | /deconfusing-javascript-destructuring-syntax /posts/deconfusing-javascript-destructuring-syntax 15 | /quick-tip-uninstall-postgres-from-your-mac /posts/quick-tip-uninstall-postgres-from-your-mac 16 | /egg-them-all /posts/egg-them-all 17 | /chrome-extensions-i-use /posts/chrome-extensions-i-use 18 | /my-favorite-design-problem-microphones /posts/my-favorite-design-problem-microphones 19 | /embracing-prettier /posts/embracing-prettier 20 | /it-was-time /posts/it-was-time 21 | 22 | /posts / 23 | 24 | /meet https://getchrome.withgoogle.com/schedule/mbifulco 25 | 26 | /talks/11ty-creator-economy https://docs.google.com/presentation/d/1XFJ_nQfPnX0oPOUezOEwVKLZFtptTKemtGg41RJPisQ/edit?usp=sharing 27 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/Inter-UI-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/fonts/Inter-UI-Bold.woff -------------------------------------------------------------------------------- /public/fonts/Inter-UI-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/fonts/Inter-UI-BoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-UI-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/fonts/Inter-UI-Italic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-UI-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/fonts/Inter-UI-Medium.woff -------------------------------------------------------------------------------- /public/fonts/Inter-UI-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/fonts/Inter-UI-MediumItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-UI-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/fonts/Inter-UI-Regular.woff -------------------------------------------------------------------------------- /public/fonts/TAYDumpling.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/fonts/TAYDumpling.woff -------------------------------------------------------------------------------- /public/fonts/TAYQuickDraw.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/fonts/TAYQuickDraw.woff -------------------------------------------------------------------------------- /public/images/hello-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/images/hello-icon.png -------------------------------------------------------------------------------- /public/images/mike-headshot-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/images/mike-headshot-square.png -------------------------------------------------------------------------------- /public/images/wiggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/keybase.txt: -------------------------------------------------------------------------------- 1 | ================================================================== 2 | https://keybase.io/mbifulco 3 | -------------------------------------------------------------------- 4 | 5 | I hereby claim: 6 | 7 | * I am an admin of https://mikebifulco.com 8 | * I am mbifulco (https://keybase.io/mbifulco) on keybase. 9 | * I have a public key ASCxIBuFof9X-NadbMytkqSyGWcHCu0fbIqhDISAy2xwBQo 10 | 11 | To do so, I am signing this object: 12 | 13 | { 14 | "body": { 15 | "key": { 16 | "eldest_kid": "010105a91cc846d25507f8a585d4489e436505971fe230540a3e439161e757ae4cc10a", 17 | "host": "keybase.io", 18 | "kid": "0120b1201b85a1ff57f8d69d6cccad92a4b21967070aed1f6c8aa10c8480cb6c70050a", 19 | "uid": "a330de0f78ed9e82c1a72b5da022be19", 20 | "username": "mbifulco" 21 | }, 22 | "merkle_root": { 23 | "ctime": 1588896933, 24 | "hash": "7101b4c870c44a905a759fb2bb6e5b7b02e0130770c4e79203d7e2546245c0da27cdd9710bf670d44ae3d855df7c2bc33103d41d39cb52af2702e4796220d65b", 25 | "hash_meta": "aab9ef4e07593c92520cc210912f61bcbd5f91e00eb82219bff40f1115c7a2c1", 26 | "seqno": 16204803 27 | }, 28 | "service": { 29 | "entropy": "OEpyHiofWaW01+UXxpAYQ2ug", 30 | "hostname": "mikebifulco.com", 31 | "protocol": "https:" 32 | }, 33 | "type": "web_service_binding", 34 | "version": 2 35 | }, 36 | "client": { 37 | "name": "keybase.io go client", 38 | "version": "5.4.2" 39 | }, 40 | "ctime": 1588896974, 41 | "expire_in": 504576000, 42 | "prev": "0d74ebc65c38edfca8350ab794c0b4e45dd209a001da6424237130a1cc1ec697", 43 | "seqno": 44, 44 | "tag": "signature" 45 | } 46 | 47 | which yields the signature: 48 | 49 | hKRib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEgsSAbhaH/V/jWnWzMrZKkshlnBwrtH2yKoQyEgMtscAUKp3BheWxvYWTESpcCLMQgDXTrxlw47fyoNQq3lMC05F3SCaAB2mQkI3EwocwexpfEIKSDWDRdUtq1e6CpUsqiYaMRMAmisSY1XdI0znasaMlyAgHCo3NpZ8RAH56l2YawXwlWYHv/v8YuoHoAYfIsp5ruwXug6ssU6+LvLn0pUGLcnQSv+IYb/3CUKvXN386K/zLOBlEzHEmmCKhzaWdfdHlwZSCkaGFzaIKkdHlwZQildmFsdWXEILJuBDk3ztSqLE+5hK5NPJJ68xIUYppZOcigAaTKNwI3o3RhZ80CAqd2ZXJzaW9uAQ== 50 | 51 | And finally, I am proving ownership of this host by posting or 52 | appending to this document. 53 | 54 | View my publicly-auditable identity here: https://keybase.io/mbifulco 55 | 56 | ================================================================== -------------------------------------------------------------------------------- /public/llms.txt: -------------------------------------------------------------------------------- 1 | * : index-only 2 | -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbifulco/blog/4e10891f5ef26b9e1e38a34561d3667bcf9f53c2/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from 'next'; 2 | 3 | import { env } from '@utils/env'; 4 | import { BASE_SITE_URL } from '@/config'; 5 | 6 | const noIndexPaths = [ 7 | '/ingest', // posthog's reverse proxy 8 | '/ingest/*', // posthog's reverse proxy 9 | ]; 10 | 11 | export default function robots(): MetadataRoute.Robots { 12 | // if this is not a production environment, disallow all requests 13 | if ( 14 | env.VERCEL_ENV !== 'production' || 15 | process.env.NODE_ENV !== 'production' 16 | ) { 17 | return { 18 | rules: [ 19 | { 20 | userAgent: '*', 21 | disallow: '*', 22 | }, 23 | ], 24 | }; 25 | } 26 | 27 | return { 28 | rules: [ 29 | { 30 | userAgent: '*', 31 | disallow: '/api/', 32 | }, 33 | { 34 | userAgent: '*', 35 | disallow: '/_next/', 36 | }, 37 | { 38 | userAgent: '*', 39 | disallow: '/public/', 40 | }, 41 | ...noIndexPaths.map((path) => ({ 42 | userAgent: '*', 43 | disallow: path, 44 | })), 45 | ], 46 | sitemap: `${BASE_SITE_URL}/sitemap.xml`, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from 'next'; 2 | 3 | import { getAllPosts } from '@lib/blog'; 4 | import { getAllNewsletters } from '@lib/newsletters'; 5 | import { getAllSeries } from '@lib/series'; 6 | import { getAllTags } from '@lib/tags'; 7 | import { BASE_SITE_URL } from '@/config'; 8 | 9 | export default async function sitemap(): Promise { 10 | const baseUrl = BASE_SITE_URL; 11 | 12 | const newsletters = await getAllNewsletters(); 13 | const newslettersSitemap = newsletters.map((newsletter) => ({ 14 | url: `${baseUrl}/newsletter/${newsletter.slug}`, 15 | lastModified: new Date(newsletter.frontmatter.date), 16 | changeFrequency: 'weekly' as const, 17 | priority: 0.8, 18 | })); 19 | 20 | const posts = await getAllPosts(); 21 | const postsSitemap = posts.map((post) => ({ 22 | url: `${baseUrl}/blog/${post.slug}`, 23 | lastModified: new Date(post.frontmatter.date), 24 | changeFrequency: 'weekly' as const, 25 | priority: 0.8, 26 | })); 27 | 28 | const allTags = await getAllTags(); 29 | const tagsSitemap = allTags.map((tag) => ({ 30 | url: `${baseUrl}/tags/${tag}`, 31 | lastModified: new Date(), 32 | changeFrequency: 'weekly' as const, 33 | priority: 0.5, 34 | })); 35 | 36 | const series = await getAllSeries(); 37 | const seriesSitemap = series.map((series) => ({ 38 | url: `${baseUrl}/series/${series.slug}`, 39 | lastModified: new Date(), 40 | changeFrequency: 'weekly' as const, 41 | priority: 0.5, 42 | })); 43 | 44 | // Define your static routes 45 | const routes: string[] = [ 46 | '', // home page 47 | '/about', 48 | '/integrity', 49 | '/newsletter', 50 | '/podcast', 51 | // '/posts', // this is just the home page, /posts does not exist 52 | '/tags', 53 | '/work', 54 | '/shop', 55 | ]; 56 | 57 | // Create sitemap entries for static routes 58 | const staticRoutesSitemap = routes.map((route) => ({ 59 | url: `${baseUrl}${route}`, 60 | lastModified: new Date(), 61 | changeFrequency: 'weekly' as const, 62 | priority: route === '' ? 1 : 0.8, 63 | })); 64 | 65 | return [ 66 | ...staticRoutesSitemap, 67 | ...newslettersSitemap, 68 | ...postsSitemap, 69 | ...seriesSitemap, 70 | ...tagsSitemap, 71 | ]; 72 | } 73 | -------------------------------------------------------------------------------- /src/components/Analytics/Fathom.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Suspense, useEffect } from 'react'; 4 | import { usePathname, useSearchParams } from 'next/navigation'; 5 | import { useRouter } from 'next/router'; 6 | import { useRouterType } from '@hooks/useRouterType'; 7 | import * as Fathom from 'fathom-client'; 8 | import posthog from 'posthog-js'; 9 | 10 | const FATHOM_DOMAINS = ['mikebifulco.com', 'www.mikebifulco.com']; 11 | 12 | const FathomPagesRouter = ({ siteId }: { siteId: string }) => { 13 | const router = useRouter(); 14 | 15 | useEffect(() => { 16 | if (process.env.NODE_ENV !== 'production') return; 17 | Fathom.load(siteId, { 18 | includedDomains: FATHOM_DOMAINS, 19 | url: 'https://cdn.usefathom.com/script.js', 20 | }); 21 | function onRouteChangeComplete() { 22 | Fathom.trackPageview(); 23 | posthog?.capture('$pageview'); 24 | } 25 | // Record a pageview when route changes 26 | router.events.on('routeChangeComplete', onRouteChangeComplete); 27 | 28 | // Unassign event listener 29 | return () => { 30 | router.events.off('routeChangeComplete', onRouteChangeComplete); 31 | }; 32 | }); 33 | 34 | return null; 35 | }; 36 | 37 | export const TrackPageView = ({ siteId }: { siteId: string }) => { 38 | const pathname = usePathname(); 39 | const searchParams = useSearchParams(); 40 | 41 | // Load the Fathom script on mount 42 | useEffect(() => { 43 | Fathom.load(siteId, { 44 | auto: false, 45 | }); 46 | }); 47 | 48 | // Record a pageview when route changes 49 | useEffect(() => { 50 | if (!pathname) return; 51 | 52 | Fathom.trackPageview({ 53 | url: pathname + searchParams?.toString(), 54 | referrer: document.referrer, 55 | }); 56 | posthog?.capture('$pageview', { 57 | pathname, 58 | searchParams: searchParams?.toString(), 59 | }); 60 | }, [pathname, searchParams]); 61 | 62 | return null; 63 | }; 64 | 65 | const FathomAppRouter = ({ siteId }: { siteId: string }) => { 66 | return ( 67 | 68 | 69 | 70 | ); 71 | }; 72 | 73 | type FathomAnalyticsProps = { 74 | siteId: string; 75 | }; 76 | export const FathomAnalytics = ({ siteId }: FathomAnalyticsProps) => { 77 | const routerType = useRouterType(); 78 | if (routerType === 'pages') { 79 | return ; 80 | } 81 | return ; 82 | }; 83 | 84 | export default FathomAnalytics; 85 | -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | import clsxm from '@utils/clsxm'; 4 | 5 | export type AvatarBaseProps = { 6 | name?: string; 7 | src?: string; 8 | } & React.HTMLAttributes; 9 | 10 | export type AvatarSizeVariant = 'sm' | 'md' | 'lg' | 'xl'; 11 | 12 | type AvatarProps = AvatarBaseProps & { 13 | variant: AvatarSizeVariant; 14 | className?: string; 15 | }; 16 | 17 | const Avatar: React.FC = ({ 18 | children, 19 | className, 20 | name, 21 | variant = 'md', 22 | src, 23 | }) => { 24 | return ( 25 |
34 | {src ? ( 35 | {name 55 | ) : ( 56 |
66 | {children} 67 |
68 | )} 69 |
70 | ); 71 | }; 72 | 73 | export default Avatar; 74 | -------------------------------------------------------------------------------- /src/components/Avatar/AvatarGroup.tsx: -------------------------------------------------------------------------------- 1 | import clsxm from '@utils/clsxm'; 2 | import Avatar from './Avatar'; 3 | import type { AvatarBaseProps, AvatarSizeVariant } from './Avatar'; 4 | 5 | type AvatarGroupProps = { 6 | people: AvatarBaseProps[]; 7 | variant: AvatarSizeVariant; 8 | }; 9 | 10 | const AvatarGroup: React.FC = ({ people, variant }) => { 11 | return ( 12 |
13 | {people.map((person, idx) => { 14 | return ( 15 | 3 && 'z-50' 23 | )} 24 | name={person.name} 25 | src={person.src} 26 | variant={variant} 27 | /> 28 | ); 29 | })} 30 |
31 | ); 32 | }; 33 | 34 | export default AvatarGroup; 35 | -------------------------------------------------------------------------------- /src/components/Avatar/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Avatar } from './Avatar'; 2 | export { default as AvatarGroup } from './AvatarGroup'; 3 | -------------------------------------------------------------------------------- /src/components/Badge/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import clsxm from '@utils/clsxm'; 4 | 5 | const badgeVariants = (variant?: BadgeVariant) => 6 | clsxm( 7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2', 8 | !variant || 9 | (variant === 'default' && 10 | 'border-transparent bg-pink-600 text-white shadow-sm hover:bg-primary/80'), 11 | variant === 'secondary' && 12 | 'border-transparent bg-gray-200 text-gray-600 hover:bg-gray-200/80', 13 | variant === 'brand' && 'bg-pink-400 text-white border-0', 14 | variant === 'destructive' && 15 | 'border-transparent bg-red-600 text-white shadow-sm hover:bg-red-700', 16 | variant === 'outline' && 'text-foreground' 17 | ); 18 | 19 | type BadgeVariant = 20 | | 'default' 21 | | 'secondary' 22 | | 'destructive' 23 | | 'outline' 24 | | 'brand'; 25 | 26 | type BadgeProps = { 27 | variant?: BadgeVariant; 28 | className?: string; 29 | } & React.HTMLAttributes; 30 | 31 | const Badge: React.FC = ({ className, variant, ...props }) => { 32 | return ( 33 |
34 | ); 35 | }; 36 | 37 | export { Badge, type BadgeVariant }; 38 | -------------------------------------------------------------------------------- /src/components/Breadcrumbs/Breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { FaChevronRight } from 'react-icons/fa'; 3 | 4 | type BreadcrumbsProps = { 5 | crumbs: { 6 | name: string; 7 | href: string; 8 | }[]; 9 | }; 10 | 11 | const Breadcrumbs: React.FC = ({ crumbs }) => { 12 | return ( 13 | 38 | ); 39 | }; 40 | 41 | export default Breadcrumbs; 42 | -------------------------------------------------------------------------------- /src/components/Breadcrumbs/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as BreadCrumbs } from './Breadcrumbs'; 2 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import clsxm from '@utils/clsxm'; 2 | 3 | type ButtonProps = { 4 | children: React.ReactNode; 5 | className?: string; 6 | type?: 'button' | 'submit' | 'reset'; 7 | } & React.ButtonHTMLAttributes; 8 | 9 | const Button: React.FC = ({ children, className, ...props }) => ( 10 | 19 | ); 20 | 21 | export default Button; 22 | -------------------------------------------------------------------------------- /src/components/CarbonAd/CarbonAd.css: -------------------------------------------------------------------------------- 1 | #carbonads * { 2 | margin: initial; 3 | padding: initial; 4 | } 5 | #carbonads { 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 7 | Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, 8 | sans-serif; 9 | } 10 | 11 | @media screen and (min-width: 750px) { 12 | #carbonads { 13 | float: right; 14 | margin-left: 4em; 15 | max-width: 330px; 16 | } 17 | } 18 | #carbonads { 19 | display: flex; 20 | background-color: hsl(0, 0%, 98%); 21 | box-shadow: 0 1px 4px 1px hsla(0, 0%, 0%, 0.1); 22 | z-index: 100; 23 | } 24 | #carbonads a { 25 | color: inherit; 26 | text-decoration: none; 27 | } 28 | #carbonads a:hover { 29 | color: inherit; 30 | } 31 | #carbonads span { 32 | position: relative; 33 | display: block; 34 | overflow: hidden; 35 | width: 100%; 36 | } 37 | #carbonads .carbon-wrap { 38 | display: flex; 39 | } 40 | #carbonads .carbon-img { 41 | display: block; 42 | margin: 0; 43 | line-height: 1; 44 | } 45 | #carbonads .carbon-img img { 46 | display: block; 47 | } 48 | #carbonads .carbon-text { 49 | font-size: 13px; 50 | padding: 10px; 51 | margin-bottom: 16px; 52 | line-height: 1.5; 53 | text-align: left; 54 | } 55 | #carbonads .carbon-poweredby { 56 | display: block; 57 | padding: 6px 8px; 58 | background: #f1f1f2; 59 | text-align: center; 60 | text-transform: uppercase; 61 | letter-spacing: 0.5px; 62 | font-weight: 600; 63 | font-size: 8px; 64 | line-height: 1; 65 | border-top-left-radius: 3px; 66 | position: absolute; 67 | bottom: 0; 68 | right: 0; 69 | } 70 | -------------------------------------------------------------------------------- /src/components/CarbonAd/CarbonAd.tsx: -------------------------------------------------------------------------------- 1 | import Script from 'next/script'; 2 | 3 | const CarbonAd = () => { 4 | return ( 5 |